From 3df0cd5e9751043968691c1b92e0057f22982a84 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 21 Feb 2024 12:05:02 +0300 Subject: [PATCH 001/153] Add carto apikey name to api conf and default dotenv --- api/config/custom-environment-variables.json | 3 +++ api/config/default.json | 3 +++ env.default | 1 + 3 files changed, 7 insertions(+) diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 7c713ec1f..18c688e75 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -72,5 +72,8 @@ "email": { "sendGridApiKey": "SENDGRID_API_KEY" } + }, + "carto": { + "apiKey": "CARTO_API_KEY" } } diff --git a/api/config/default.json b/api/config/default.json index ed6d80b7f..b5668f804 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -81,5 +81,8 @@ "email": { "sendGridApiKey": null } + }, + "carto": { + "apuKey": null } } diff --git a/env.default b/env.default index ca1d04ac7..b3469eea9 100644 --- a/env.default +++ b/env.default @@ -12,6 +12,7 @@ API_AUTH_JWT_SECRET= API_REDIS_SERVICE_HOST= API_REDIS_SERVICE_PORT=6379 REDIS_COMMANDER_PORT=8081 +CARTO_API_KEY= #CLIENT ENV VARS: CLIENT_SERVICE_PORT= From ff89243bce6157a35f5bb08057569a9a1bf9aa21 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 23 Feb 2024 13:32:50 +0300 Subject: [PATCH 002/153] Add eudr module scaffold --- api/src/app.module.ts | 2 + api/src/modules/eudr/carto/carto.connector.ts | 38 +++++++++++++++ api/src/modules/eudr/carto/carto.module.ts | 0 .../modules/eudr/carto/cartodb.repository.ts | 10 ++++ api/src/modules/eudr/dto/eudr.dto.ts | 17 +++++++ api/src/modules/eudr/dto/eudr.output.dto.ts | 0 api/src/modules/eudr/eudr.controller.ts | 14 ++++++ api/src/modules/eudr/eudr.entity.ts | 47 +++++++++++++++++++ api/src/modules/eudr/eudr.module.ts | 12 +++++ .../modules/eudr/eudr.repositoty.interface.ts | 3 ++ api/src/modules/eudr/eudr.service.ts | 4 ++ 11 files changed, 147 insertions(+) create mode 100644 api/src/modules/eudr/carto/carto.connector.ts create mode 100644 api/src/modules/eudr/carto/carto.module.ts create mode 100644 api/src/modules/eudr/carto/cartodb.repository.ts create mode 100644 api/src/modules/eudr/dto/eudr.dto.ts create mode 100644 api/src/modules/eudr/dto/eudr.output.dto.ts create mode 100644 api/src/modules/eudr/eudr.controller.ts create mode 100644 api/src/modules/eudr/eudr.entity.ts create mode 100644 api/src/modules/eudr/eudr.module.ts create mode 100644 api/src/modules/eudr/eudr.repositoty.interface.ts create mode 100644 api/src/modules/eudr/eudr.service.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 4b014f13a..64f771dc3 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -39,6 +39,7 @@ import { AuthorizationService } from 'modules/authorization/authorization.servic import { TasksService } from 'modules/tasks/tasks.service'; import { NotificationsModule } from 'modules/notifications/notifications.module'; import { ReportsModule } from 'modules/reports/reports.module'; +import { EudrModule } from 'modules/eudr/eudr.module'; const queueConfig: any = config.get('queue'); @@ -84,6 +85,7 @@ const queueConfig: any = config.get('queue'); AuthorizationModule, NotificationsModule, ReportsModule, + EudrModule, ], providers: [ { diff --git a/api/src/modules/eudr/carto/carto.connector.ts b/api/src/modules/eudr/carto/carto.connector.ts new file mode 100644 index 000000000..f558dc729 --- /dev/null +++ b/api/src/modules/eudr/carto/carto.connector.ts @@ -0,0 +1,38 @@ +import { HttpService } from '@nestjs/axios'; +import { + Injectable, + Logger, + ServiceUnavailableException, +} from '@nestjs/common'; +import { AppConfig } from 'utils/app.config'; + +type CartoConfig = { + apiKey: string; + baseUrl: string; + connection: string; +}; + +@Injectable() +export class CartoConnector { + cartoApiKey: string; + cartoBaseUrl: string; + cartoConnection: string; + logger: Logger = new Logger(CartoConnector.name); + + constructor(private readonly httpService: HttpService) { + const { apiKey, baseUrl, connection } = AppConfig.get('carto'); + this.cartoApiKey = apiKey; + this.cartoBaseUrl = baseUrl; + this.cartoConnection = connection; + if (!this.cartoApiKey || !this.cartoBaseUrl || !this.cartoConnection) { + this.logger.error('Carto configuration is missing'); + } + } + + private handleConnectionError(error: typeof Error): void { + this.logger.error('Carto connection error', error); + throw new ServiceUnavailableException( + 'Unable to connect to Carto. Please contact your administrator.', + ); + } +} diff --git a/api/src/modules/eudr/carto/carto.module.ts b/api/src/modules/eudr/carto/carto.module.ts new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/modules/eudr/carto/cartodb.repository.ts b/api/src/modules/eudr/carto/cartodb.repository.ts new file mode 100644 index 000000000..bf89b34f3 --- /dev/null +++ b/api/src/modules/eudr/carto/cartodb.repository.ts @@ -0,0 +1,10 @@ +import { HttpService } from '@nestjs/axios'; +import { IEudrRepository } from 'modules/eudr/eudr.repositoty.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class CartodbRepository implements IEudrRepository { + constructor(private readonly http: HttpService) {} + + async select(): Promise {} +} diff --git a/api/src/modules/eudr/dto/eudr.dto.ts b/api/src/modules/eudr/dto/eudr.dto.ts new file mode 100644 index 000000000..180850000 --- /dev/null +++ b/api/src/modules/eudr/dto/eudr.dto.ts @@ -0,0 +1,17 @@ +// Input DTO to be ingested by Carto + +export class EudrInputDTO { + supplierId: string; + geoRegionId: string; + geom: JSON; + year: number; +} + +export class EudrOutputDTO { + geoRegionId: string; + supplierId: string; + hasEUDRAlerts: boolean; + alertsNumber: number; +} + +export type EudrDTO = EudrInputDTO & EudrOutputDTO; diff --git a/api/src/modules/eudr/dto/eudr.output.dto.ts b/api/src/modules/eudr/dto/eudr.output.dto.ts new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/modules/eudr/eudr.controller.ts b/api/src/modules/eudr/eudr.controller.ts new file mode 100644 index 000000000..fc151a0f6 --- /dev/null +++ b/api/src/modules/eudr/eudr.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get } from '@nestjs/common'; +import { Public } from 'decorators/public.decorator'; +import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; + +@Controller('/api/v1/eudr') +export class EudrController { + constructor(private readonly eudr: CartodbRepository) {} + + @Public() + @Get() + async select(): Promise { + return this.eudr.select(); + } +} diff --git a/api/src/modules/eudr/eudr.entity.ts b/api/src/modules/eudr/eudr.entity.ts new file mode 100644 index 000000000..c716caef7 --- /dev/null +++ b/api/src/modules/eudr/eudr.entity.ts @@ -0,0 +1,47 @@ +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { + BaseEntity, + Column, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +// Base initial entity for EUDR, we might end up storing this data in our side or not +// The initial implementation will build the DTO on the fly and return it to the client +// But we will keep this for reference and future storage in our side + +export class EUDR extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + // The CSV references a PLOT_ID, but this could effectively be a geoRegionId? + // create a one to one realtion with GeoRegion + @OneToOne(() => GeoRegion) + @JoinColumn({ name: 'geoRegionId' }) + geoRegion: GeoRegion; + @Column() + geoRegionId: string; + + // Create a Many to one realtion with Suppliers + @ManyToOne(() => Supplier, (supplier: Supplier) => supplier.id) + @JoinColumn({ name: 'supplierId' }) + supplier: Supplier; + + @Column() + supplierId: string; + + // This might correspond / be related to the sourcing year (sourcing record entity) by one to one relation + @Column({ type: 'int' }) + year: number; + + @Column({ type: 'boolean', default: false }) + hasEUDRAlerts: boolean; + + @Column({ type: 'int' }) + alertsNumber: number; + + // TODO: Clarify if a relation with material is necessary +} diff --git a/api/src/modules/eudr/eudr.module.ts b/api/src/modules/eudr/eudr.module.ts new file mode 100644 index 000000000..945fe703f --- /dev/null +++ b/api/src/modules/eudr/eudr.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { EudrService } from 'modules/eudr/eudr.service'; +import { EudrController } from 'modules/eudr/eudr.controller'; +import { CartodbRepository } from 'modules/eudr/cartodb.repository'; + +@Module({ + imports: [HttpModule], + providers: [EudrService, CartodbRepository], + controllers: [EudrController], +}) +export class EudrModule {} diff --git a/api/src/modules/eudr/eudr.repositoty.interface.ts b/api/src/modules/eudr/eudr.repositoty.interface.ts new file mode 100644 index 000000000..5281989f2 --- /dev/null +++ b/api/src/modules/eudr/eudr.repositoty.interface.ts @@ -0,0 +1,3 @@ +export interface IEudrRepository { + select(): Promise; +} diff --git a/api/src/modules/eudr/eudr.service.ts b/api/src/modules/eudr/eudr.service.ts new file mode 100644 index 000000000..1dd5973bc --- /dev/null +++ b/api/src/modules/eudr/eudr.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class EudrService {} From 04271291d17a6ba8e8df980e4f0ede01b6235fcd Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 26 Feb 2024 10:46:41 +0300 Subject: [PATCH 003/153] Queue for eudr import --- api/src/modules/eudr/eudr.module.ts | 2 +- .../import-data/eudr/eudr.import.service.ts | 314 ++++++++++++++++++ .../import-data/import-data.controller.ts | 31 ++ .../modules/import-data/import-data.module.ts | 5 + .../import-data/import-data.service.ts | 40 ++- .../import-data/workers/eudr.consumer.ts | 56 ++++ .../workers/import-data.producer.ts | 16 + 7 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 api/src/modules/import-data/eudr/eudr.import.service.ts create mode 100644 api/src/modules/import-data/workers/eudr.consumer.ts diff --git a/api/src/modules/eudr/eudr.module.ts b/api/src/modules/eudr/eudr.module.ts index 945fe703f..2529deda8 100644 --- a/api/src/modules/eudr/eudr.module.ts +++ b/api/src/modules/eudr/eudr.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { EudrService } from 'modules/eudr/eudr.service'; import { EudrController } from 'modules/eudr/eudr.controller'; -import { CartodbRepository } from 'modules/eudr/cartodb.repository'; +import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; @Module({ imports: [HttpModule], diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts new file mode 100644 index 000000000..763982566 --- /dev/null +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -0,0 +1,314 @@ +import { + BadRequestException, + Injectable, + Logger, + ServiceUnavailableException, +} from '@nestjs/common'; +import { SourcingLocationGroup } from '../../sourcing-location-groups/sourcing-location-group.entity'; +import { + SourcingData, + SourcingRecordsDtoProcessorService, + SourcingRecordsDtos, +} from '../sourcing-data/dto-processor.service'; +import { Material } from '../../materials/material.entity'; +import { Indicator } from '../../indicators/indicator.entity'; +import { BusinessUnit } from '../../business-units/business-unit.entity'; +import { Supplier } from '../../suppliers/supplier.entity'; +import { SourcingRecordsSheets } from '../sourcing-data/sourcing-data-import.service'; +import { FileService } from '../file.service'; +import { SourcingLocationGroupsService } from '../../sourcing-location-groups/sourcing-location-groups.service'; +import { MaterialsService } from '../../materials/materials.service'; +import { BusinessUnitsService } from '../../business-units/business-units.service'; +import { SuppliersService } from '../../suppliers/suppliers.service'; +import { AdminRegionsService } from '../../admin-regions/admin-regions.service'; +import { GeoRegionsService } from '../../geo-regions/geo-regions.service'; +import { SourcingLocationsService } from '../../sourcing-locations/sourcing-locations.service'; +import { SourcingRecordsService } from '../../sourcing-records/sourcing-records.service'; +import { GeoCodingAbstractClass } from '../../geo-coding/geo-coding-abstract-class'; +import { TasksService } from '../../tasks/tasks.service'; +import { ScenariosService } from '../../scenarios/scenarios.service'; +import { IndicatorsService } from '../../indicators/indicators.service'; +import { IndicatorRecordsService } from '../../indicator-records/indicator-records.service'; +import { ImpactService } from '../../impact/impact.service'; +import { ImpactCalculator } from '../../indicator-records/services/impact-calculator.service'; +import { validateOrReject } from 'class-validator'; + +const EUDR_SHEET_MAP: Record<'data', 'Data'> = { + data: 'Data', +}; + +@Injectable() +export class EudrImportService { + logger: Logger = new Logger(EudrImportService.name); + + constructor( + protected readonly materialService: MaterialsService, + protected readonly businessUnitService: BusinessUnitsService, + protected readonly supplierService: SuppliersService, + protected readonly adminRegionService: AdminRegionsService, + protected readonly geoRegionsService: GeoRegionsService, + protected readonly sourcingLocationService: SourcingLocationsService, + protected readonly sourcingRecordService: SourcingRecordsService, + protected readonly sourcingLocationGroupService: SourcingLocationGroupsService, + protected readonly fileService: FileService, + protected readonly dtoProcessor: SourcingRecordsDtoProcessorService, + protected readonly geoCodingService: GeoCodingAbstractClass, + protected readonly tasksService: TasksService, + protected readonly scenarioService: ScenariosService, + protected readonly indicatorService: IndicatorsService, + protected readonly indicatorRecordService: IndicatorRecordsService, + protected readonly impactService: ImpactService, + protected readonly impactCalculator: ImpactCalculator, + ) {} + + async importEudr(filePath: string, taskId: string): Promise { + this.logger.log(`Starting eudr import process`); + await this.fileService.isFilePresentInFs(filePath); + try { + const parsedEudrData: SourcingRecordsSheets = + await this.fileService.transformToJson(filePath, EUDR_SHEET_MAP); + + const sourcingLocationGroup: SourcingLocationGroup = + await this.sourcingLocationGroupService.create({ + title: 'Sourcing Records import from EUDR input file', + }); + + await this.cleanDataBeforeImport(); + + return parsedEudrData; + + // const dtoMatchedData: SourcingRecordsDtos = + // await this.validateAndCreateDTOs( + // parsedXLSXDataset, + // sourcingLocationGroup.id, + // ).catch(async (err: any) => { + // await this.tasksService.updateImportTask({ + // taskId, + // newErrors: err.message, + // }); + // throw new BadRequestException( + // 'Import failed. There are constraint errors present in the file', + // ); + // }); + + //TODO: Implement transactional import. Move geocoding to first step + // + // const materials: Material[] = + // await this.materialService.findAllUnpaginated(); + // if (!materials.length) { + // throw new ServiceUnavailableException( + // 'No Materials found present in the DB. Please check the LandGriffon installation manual', + // ); + // } + // this.logger.log('Activating Indicators...'); + // const activeIndicators: Indicator[] = + // await this.indicatorService.activateIndicators( + // dtoMatchedData.indicators, + // ); + // this.logger.log('Activating Materials...'); + // const activeMaterials: Material[] = + // await this.materialService.activateMaterials(dtoMatchedData.materials); + // + // await this.tasksService.updateImportTask({ + // taskId, + // newLogs: [ + // `Activated indicators: ${activeIndicators + // .map((i: Indicator) => i.name) + // .join(', ')}`, + // `Activated materials: ${activeMaterials + // .map((i: Material) => i.hsCodeId) + // .join(', ')}`, + // ], + // }); + // + // const businessUnits: BusinessUnit[] = + // await this.businessUnitService.createTree(dtoMatchedData.businessUnits); + // + // const suppliers: Supplier[] = await this.supplierService.createTree( + // dtoMatchedData.suppliers, + // ); + // + // const { geoCodedSourcingData, errors } = + // await this.geoCodingService.geoCodeLocations( + // dtoMatchedData.sourcingData, + // ); + // if (errors.length) { + // await this.tasksService.updateImportTask({ taskId, newErrors: errors }); + // throw new BadRequestException( + // 'Import failed. There are GeoCoding errors present in the file', + // ); + // } + // const warnings: string[] = []; + // geoCodedSourcingData.forEach((elem: SourcingData) => { + // if (elem.locationWarning) warnings.push(elem.locationWarning); + // }); + // warnings.length > 0 && + // (await this.tasksService.updateImportTask({ + // taskId, + // newLogs: warnings, + // })); + // + // const sourcingDataWithOrganizationalEntities: any = + // await this.relateSourcingDataWithOrganizationalEntities( + // suppliers, + // businessUnits, + // materials, + // geoCodedSourcingData, + // ); + // + // await this.sourcingLocationService.save( + // sourcingDataWithOrganizationalEntities, + // ); + // + // this.logger.log('Generating Indicator Records...'); + // + // // TODO: Current approach calculates Impact for all Sourcing Records present in the DB + // // Getting H3 data for calculations is done within DB so we need to improve the error handling + // // TBD: What to do when there is no H3 for a Material + // + // try { + // await this.impactCalculator.calculateImpactForAllSourcingRecords( + // activeIndicators, + // ); + // this.logger.log('Indicator Records generated'); + // await this.impactService.updateImpactView(); + // } catch (err: any) { + // throw new ServiceUnavailableException( + // 'Could not calculate Impact for current data. Please contact with the administrator', + // ); + // } + // } finally { + // await this.fileService.deleteDataFromFS(filePath); + // } + } finally { + await this.fileService.deleteDataFromFS(filePath); + } + } + + private async validateDTOs( + dtoLists: SourcingRecordsDtos, + ): Promise> { + const validationErrorArray: { + line: number; + property: string; + message: any; + }[] = []; + for (const parsedSheet in dtoLists) { + if (dtoLists.hasOwnProperty(parsedSheet)) { + for (const [i, dto] of dtoLists[ + parsedSheet as keyof SourcingRecordsDtos + ].entries()) { + try { + await validateOrReject(dto); + } catch (err: any) { + validationErrorArray.push({ + line: i + 5, + property: err[0].property, + message: err[0].constraints, + }); + } + } + } + } + + /** + * @note If errors are thrown, we should bypass all-exceptions.exception.filter.ts + * in order to return the array containing errors in a more readable way + * Or add a function per entity to validate + */ + if (validationErrorArray.length) + throw new BadRequestException(validationErrorArray); + } + + /** + * @note: Deletes DB content from required entities + * to ensure DB is prune prior loading a XLSX dataset + */ + async cleanDataBeforeImport(): Promise { + this.logger.log('Cleaning database before import...'); + try { + await this.indicatorService.deactivateAllIndicators(); + await this.materialService.deactivateAllMaterials(); + await this.scenarioService.clearTable(); + await this.indicatorRecordService.clearTable(); + await this.businessUnitService.clearTable(); + await this.supplierService.clearTable(); + await this.sourcingLocationService.clearTable(); + await this.sourcingRecordService.clearTable(); + await this.geoRegionsService.deleteGeoRegionsCreatedByUser(); + } catch ({ message }) { + throw new Error( + `Database could not been cleaned before loading new dataset: ${message}`, + ); + } + } + + /** + * @note: Type hack as mpath property does not exist on Materials and BusinessUnits, but its created + * by typeorm when using @Tree('materialized-path)'. + * It's what we can use to know which material/business unit relates to which sourcing-location + * in a synchronous way avoiding hitting the DB + */ + async relateSourcingDataWithOrganizationalEntities( + suppliers: Supplier[], + businessUnits: Record[], + materials: Material[], + sourcingData: SourcingData[], + ): Promise { + this.logger.log(`Relating sourcing data with organizational entities`); + this.logger.log(`Supplier count: ${suppliers.length}`); + this.logger.log(`Business Units count: ${businessUnits.length}`); + this.logger.log(`Materials count: ${materials.length}`); + this.logger.log(`Sourcing Data count: ${sourcingData.length}`); + + const materialMap: Record = {}; + materials.forEach((material: Material) => { + if (!material.hsCodeId) { + return; + } + materialMap[material.hsCodeId] = material.id; + }); + + for (const sourcingLocation of sourcingData) { + for (const supplier of suppliers) { + if (sourcingLocation.producerId === supplier.mpath) { + sourcingLocation.producerId = supplier.id; + } + if (sourcingLocation.t1SupplierId === supplier.mpath) { + sourcingLocation.t1SupplierId = supplier.id; + } + } + for (const businessUnit of businessUnits) { + if (sourcingLocation.businessUnitId === businessUnit.mpath) { + sourcingLocation.businessUnitId = businessUnit.id; + } + } + if (typeof sourcingLocation.materialId === 'undefined') { + return; + } + const sourcingLocationMaterialId: string = sourcingLocation.materialId; + + if (!(sourcingLocationMaterialId in materialMap)) { + throw new Error( + `Could not import sourcing location - material code ${sourcingLocationMaterialId} not found`, + ); + } + sourcingLocation.materialId = materialMap[sourcingLocationMaterialId]; + } + return sourcingData; + } + + async validateAndCreateDTOs( + parsedXLSXDataset: SourcingRecordsSheets, + sourcingLocationGroupId: string, + ): Promise { + const dtoMatchedData: SourcingRecordsDtos = + await this.dtoProcessor.createDTOsFromSourcingRecordsSheets( + parsedXLSXDataset, + sourcingLocationGroupId, + ); + + await this.validateDTOs(dtoMatchedData); + return dtoMatchedData; + } +} diff --git a/api/src/modules/import-data/import-data.controller.ts b/api/src/modules/import-data/import-data.controller.ts index adf9c25dc..8feb35b25 100644 --- a/api/src/modules/import-data/import-data.controller.ts +++ b/api/src/modules/import-data/import-data.controller.ts @@ -60,4 +60,35 @@ export class ImportDataController { }, }; } + + @ApiConsumesXLSX() + @ApiBadRequestResponse({ + description: + 'Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data', + }) + @ApiForbiddenResponse() + @UseInterceptors(FileInterceptor('file'), XlsxPayloadInterceptor) + @RequiredRoles(ROLES.ADMIN) + @Post('/eudr') + async importEudr( + @UploadedFile() xlsxFile: Express.Multer.File, + @GetUser() user: User, + ): Promise> { + if (!user) { + throw new UnauthorizedException(); + } + const userId: string = user.id; + const task: Task = await this.importDataService.loadXlsxFile( + userId, + xlsxFile, + ); + return { + data: { + id: task.id, + createdAt: task.createdAt, + status: task.status, + createdBy: task.userId, + }, + }; + } } diff --git a/api/src/modules/import-data/import-data.module.ts b/api/src/modules/import-data/import-data.module.ts index 0f0a88082..0cb6735d5 100644 --- a/api/src/modules/import-data/import-data.module.ts +++ b/api/src/modules/import-data/import-data.module.ts @@ -25,6 +25,7 @@ import { MulterModule } from '@nestjs/platform-express'; import * as config from 'config'; import MulterConfigService from 'modules/import-data/multer-config.service'; import { ImpactModule } from 'modules/impact/impact.module'; +import { EudrImportService } from './eudr/eudr.import.service'; @Module({ imports: [ @@ -35,6 +36,9 @@ import { ImpactModule } from 'modules/impact/impact.module'; BullModule.registerQueue({ name: importQueueName, }), + BullModule.registerQueue({ + name: 'eudr', + }), MaterialsModule, BusinessUnitsModule, SuppliersModule, @@ -58,6 +62,7 @@ import { ImpactModule } from 'modules/impact/impact.module'; ImportDataProducer, ImportDataConsumer, ImportDataService, + EudrImportService, { provide: 'FILE_UPLOAD_SIZE_LIMIT', useValue: config.get('fileUploads.sizeLimit'), diff --git a/api/src/modules/import-data/import-data.service.ts b/api/src/modules/import-data/import-data.service.ts index e7dfdb48b..9d6af6b5b 100644 --- a/api/src/modules/import-data/import-data.service.ts +++ b/api/src/modules/import-data/import-data.service.ts @@ -3,12 +3,16 @@ import { Logger, ServiceUnavailableException, } from '@nestjs/common'; -import { ImportDataProducer } from 'modules/import-data/workers/import-data.producer'; +import { + EudrImportJob, + ImportDataProducer, +} from 'modules/import-data/workers/import-data.producer'; import { Job } from 'bull'; import { ExcelImportJob } from 'modules/import-data/workers/import-data.producer'; import { SourcingDataImportService } from 'modules/import-data/sourcing-data/sourcing-data-import.service'; import { TasksService } from 'modules/tasks/tasks.service'; import { Task } from 'modules/tasks/task.entity'; +import { EudrImportService } from './eudr/eudr.import.service'; @Injectable() export class ImportDataService { @@ -17,6 +21,7 @@ export class ImportDataService { constructor( private readonly importDataProducer: ImportDataProducer, private readonly sourcingDataImportService: SourcingDataImportService, + private readonly eudrImport: EudrImportService, private readonly tasksService: TasksService, ) {} @@ -46,10 +51,43 @@ export class ImportDataService { } } + async loadEudrFile( + userId: string, + xlsxFileData: Express.Multer.File, + ): Promise { + const { filename, path } = xlsxFileData; + const task: Task = await this.tasksService.createTask({ + data: { filename, path }, + userId, + }); + try { + await this.importDataProducer.addEudrImportJob(xlsxFileData, task.id); + return task; + } catch (error: any) { + this.logger.error( + `Job for file: ${ + xlsxFileData.filename + } sent by user: ${userId} could not been added to queue: ${error.toString()}`, + ); + + await this.tasksService.remove(task.id); + throw new ServiceUnavailableException( + `File: ${xlsxFileData.filename} could not have been loaded. Please try again later or contact the administrator`, + ); + } + } + async processImportJob(job: Job): Promise { await this.sourcingDataImportService.importSourcingData( job.data.xlsxFileData.path, job.data.taskId, ); } + + async processEudrJob(job: Job): Promise { + await this.eudrImport.importEudr( + job.data.xlsxFileData.path, + job.data.taskId, + ); + } } diff --git a/api/src/modules/import-data/workers/eudr.consumer.ts b/api/src/modules/import-data/workers/eudr.consumer.ts new file mode 100644 index 000000000..fd25f547f --- /dev/null +++ b/api/src/modules/import-data/workers/eudr.consumer.ts @@ -0,0 +1,56 @@ +import { + OnQueueCompleted, + OnQueueError, + OnQueueFailed, + Process, + Processor, +} from '@nestjs/bull'; +import { Job } from 'bull'; +import { Logger, ServiceUnavailableException } from '@nestjs/common'; +import { ImportDataService } from 'modules/import-data/import-data.service'; +import { ExcelImportJob } from 'modules/import-data/workers/import-data.producer'; +import { TasksService } from 'modules/tasks/tasks.service'; + +export interface EudrImportJob { + xlsxFileData: Express.Multer.File; + taskId: string; +} + +@Processor('eudr') +export class ImportDataConsumer { + logger: Logger = new Logger(ImportDataService.name); + + constructor( + public readonly importDataService: ImportDataService, + public readonly tasksService: TasksService, + ) {} + + @OnQueueError() + async onQueueError(error: Error): Promise { + throw new ServiceUnavailableException( + `Could not connect to Redis through BullMQ: ${error.message}`, + ); + } + + @OnQueueFailed() + async onJobFailed(job: Job, err: Error): Promise { + // TODO: Handle eudr import errors, updating async tgasks + const { taskId } = job.data; + this.logger.error( + `Import Failed for file: ${job.data.xlsxFileData.filename} for task: ${taskId}: ${err}`, + ); + } + + @OnQueueCompleted() + async onJobComplete(job: Job): Promise { + this.logger.log( + `Import XLSX with TASK ID: ${job.data.taskId} completed successfully`, + ); + // TODO: Handle eudr import completion, updating async tasks + } + + @Process('eudr') + async readImportDataJob(job: Job): Promise { + await this.importDataService.processEudrJob(job); + } +} diff --git a/api/src/modules/import-data/workers/import-data.producer.ts b/api/src/modules/import-data/workers/import-data.producer.ts index 56f1852f4..d6ec85b25 100644 --- a/api/src/modules/import-data/workers/import-data.producer.ts +++ b/api/src/modules/import-data/workers/import-data.producer.ts @@ -8,10 +8,16 @@ export interface ExcelImportJob { taskId: string; } +export interface EudrImportJob { + xlsxFileData: Express.Multer.File; + taskId: string; +} + @Injectable() export class ImportDataProducer { constructor( @InjectQueue(importQueueName) private readonly importQueue: Queue, + @InjectQueue('eudr') private readonly eudrQueue: Queue, ) {} async addExcelImportJob( @@ -23,4 +29,14 @@ export class ImportDataProducer { taskId, }); } + + async addEudrImportJob( + xlsxFileData: Express.Multer.File, + taskId: string, + ): Promise { + await this.eudrQueue.add('eudr', { + xlsxFileData, + taskId, + }); + } } From fae866c6147f3059b4232343d3fc81631c42ed63 Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 26 Feb 2024 11:14:23 +0300 Subject: [PATCH 004/153] Bypass queue and parse file --- .../import-data/eudr/eudr.import.service.ts | 17 +++---- .../import-data/import-data.controller.ts | 44 ++++++++++++------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index 763982566..660d9903a 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -1,18 +1,11 @@ -import { - BadRequestException, - Injectable, - Logger, - ServiceUnavailableException, -} from '@nestjs/common'; -import { SourcingLocationGroup } from '../../sourcing-location-groups/sourcing-location-group.entity'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { SourcingLocationGroup } from 'modules/sourcing-location-groups/sourcing-location-group.entity'; import { SourcingData, SourcingRecordsDtoProcessorService, SourcingRecordsDtos, } from '../sourcing-data/dto-processor.service'; -import { Material } from '../../materials/material.entity'; -import { Indicator } from '../../indicators/indicator.entity'; -import { BusinessUnit } from '../../business-units/business-unit.entity'; +import { Material } from 'modules/materials/material.entity'; import { Supplier } from '../../suppliers/supplier.entity'; import { SourcingRecordsSheets } from '../sourcing-data/sourcing-data-import.service'; import { FileService } from '../file.service'; @@ -33,8 +26,8 @@ import { ImpactService } from '../../impact/impact.service'; import { ImpactCalculator } from '../../indicator-records/services/impact-calculator.service'; import { validateOrReject } from 'class-validator'; -const EUDR_SHEET_MAP: Record<'data', 'Data'> = { - data: 'Data', +const EUDR_SHEET_MAP: Record<'Data', 'Data'> = { + Data: 'Data', }; @Injectable() diff --git a/api/src/modules/import-data/import-data.controller.ts b/api/src/modules/import-data/import-data.controller.ts index 8feb35b25..695f574c1 100644 --- a/api/src/modules/import-data/import-data.controller.ts +++ b/api/src/modules/import-data/import-data.controller.ts @@ -22,13 +22,17 @@ import { User } from 'modules/users/user.entity'; import { ROLES } from 'modules/authorization/roles/roles.enum'; import { RequiredRoles } from 'decorators/roles.decorator'; import { RolesGuard } from 'guards/roles.guard'; +import { EudrImportService } from './eudr/eudr.import.service'; @ApiTags('Import Data') @Controller(`/api/v1/import`) @UseGuards(RolesGuard) @ApiBearerAuth() export class ImportDataController { - constructor(public readonly importDataService: ImportDataService) {} + constructor( + public readonly importDataService: ImportDataService, + private readonly eudr: EudrImportService, + ) {} @ApiConsumesXLSX() @ApiBadRequestResponse({ @@ -74,21 +78,27 @@ export class ImportDataController { @UploadedFile() xlsxFile: Express.Multer.File, @GetUser() user: User, ): Promise> { - if (!user) { - throw new UnauthorizedException(); - } - const userId: string = user.id; - const task: Task = await this.importDataService.loadXlsxFile( - userId, - xlsxFile, - ); - return { - data: { - id: task.id, - createdAt: task.createdAt, - status: task.status, - createdBy: task.userId, - }, - }; + const { path } = xlsxFile; + const taskId: string = 'fa02307f-70f1-4c8a-a117-2a7cfd6f0be5'; + + return this.eudr.importEudr(path, taskId); } + + // if (!user) { + // throw new UnauthorizedException(); + // } + // const userId: string = user.id; + // const task: Task = await this.importDataService.loadXlsxFile( + // userId, + // xlsxFile, + // ); + // return { + // data: { + // id: task.id, + // createdAt: task.createdAt, + // status: task.status, + // createdBy: task.userId, + // }, + // }; + // } } From 5783f2a2bfd8b654c4940f3181f0d1300776113a Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:02:51 +0300 Subject: [PATCH 005/153] Add wellknown to parse WKT to Geom --- api/package.json | 1 + api/yarn.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/api/package.json b/api/package.json index 2b82db8c7..1c30378ec 100644 --- a/api/package.json +++ b/api/package.json @@ -69,6 +69,7 @@ "swagger-ui-express": "~4.6.0", "typeorm": "0.3.11", "uuid": "~9.0.0", + "wellknown": "^0.5.0", "xlsx": "~0.18.5", "yargs": "^17.3.1" }, diff --git a/api/yarn.lock b/api/yarn.lock index 06f097711..b673ca4de 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -2550,6 +2550,15 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@~1.5.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + integrity sha512-H6xsIBfQ94aESBG8jGHXQ7i5AEpy5ZeVaLDOisDICiTCKpqEfr34/KmTrspKQNoLKNu9gTkovlpQcUi630AKiQ== + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + config@~3.3.6: version "3.3.8" resolved "https://registry.yarnpkg.com/config/-/config-3.3.8.tgz#14ef7aef22af25877fdaee696ec64d761feb7be0" @@ -3708,7 +3717,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4746,6 +4755,11 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@~1.2.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^3.0.0: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" @@ -5429,6 +5443,11 @@ pretty-format@^29.6.1: ansi-styles "^5.0.0" react-is "^18.0.0" +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5561,6 +5580,18 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -6017,6 +6048,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -6372,6 +6408,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typedarray@~0.0.5: + version "0.0.7" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.7.tgz#799207136a37f3b3efb8c66c40010d032714dc73" + integrity sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ== + typeorm@0.3.11: version "0.3.11" resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.11.tgz#09b6ab0b0574bf33c1faf7344bab6c363cf28921" @@ -6553,6 +6594,14 @@ webpack@5.82.1: watchpack "^2.4.0" webpack-sources "^3.2.3" +wellknown@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101" + integrity sha512-za5vTLuPF9nmrVOovYQwNEWE/PwJCM+yHMAj4xN1WWUvtq9OElsvKiPL0CR9rO8xhrYqL7NpI7IknqR8r6eYOg== + dependencies: + concat-stream "~1.5.0" + minimist "~1.2.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" From 111bddb9870ba451375b88f4f60aae11be26bee3 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:03:16 +0300 Subject: [PATCH 006/153] Add companyId to supplier entity --- api/src/modules/suppliers/supplier.entity.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/modules/suppliers/supplier.entity.ts b/api/src/modules/suppliers/supplier.entity.ts index 900334d37..2865ff64e 100644 --- a/api/src/modules/suppliers/supplier.entity.ts +++ b/api/src/modules/suppliers/supplier.entity.ts @@ -65,6 +65,9 @@ export class Supplier extends TimestampedBaseEntity { @Column({ nullable: true }) description?: string; + @Column({ type: 'varchar' }) + companyId: string; + @ApiProperty() @Column({ type: 'enum', From 376a3de38689e3a112317cce870fc437f12f3a4f Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:03:33 +0300 Subject: [PATCH 007/153] Add totalArea to georegion entity --- api/src/modules/geo-regions/geo-region.entity.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index b44e19958..04f873f7e 100644 --- a/api/src/modules/geo-regions/geo-region.entity.ts +++ b/api/src/modules/geo-regions/geo-region.entity.ts @@ -49,6 +49,9 @@ export class GeoRegion extends BaseEntity { @ApiPropertyOptional() theGeom?: JSON; + @Column({ type: 'decimal', nullable: true }) + totalArea?: number; + @Column({ type: 'boolean', default: true }) isCreatedByUser: boolean; From c9e7e8e338da244a8564d8dbe34196a3cc43f56d Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:06:34 +0300 Subject: [PATCH 008/153] Adds EUDR location type --- api/src/modules/sourcing-locations/sourcing-location.entity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/modules/sourcing-locations/sourcing-location.entity.ts b/api/src/modules/sourcing-locations/sourcing-location.entity.ts index 79a3b0cca..22489ab58 100644 --- a/api/src/modules/sourcing-locations/sourcing-location.entity.ts +++ b/api/src/modules/sourcing-locations/sourcing-location.entity.ts @@ -28,6 +28,7 @@ export enum LOCATION_TYPES { COUNTRY_OF_PRODUCTION = 'country-of-production', ADMINISTRATIVE_REGION_OF_PRODUCTION = 'administrative-region-of-production', COUNTRY_OF_DELIVERY = 'country-of-delivery', + EUDR = 'eudr', } export enum LOCATION_ACCURACY { From e5d3fac9b323aeb87c8f4fa46ab7848411e04a8d Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:31:38 +0300 Subject: [PATCH 009/153] First approach saving eudr ingested data --- .../eudr/eudr.dto-processor.service.ts | 173 +++++++++++++ .../import-data/eudr/eudr.import.service.ts | 229 +++--------------- .../modules/import-data/import-data.module.ts | 6 +- 3 files changed, 205 insertions(+), 203 deletions(-) create mode 100644 api/src/modules/import-data/eudr/eudr.dto-processor.service.ts diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts new file mode 100644 index 000000000..52923bb0b --- /dev/null +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -0,0 +1,173 @@ +import { + BadRequestException, + Injectable, + Logger, + ValidationError, +} from '@nestjs/common'; +import { CreateSourcingLocationDto } from 'modules/sourcing-locations/dto/create.sourcing-location.dto'; +import { SourcingRecord } from 'modules/sourcing-records/sourcing-record.entity'; +import { SourcingDataExcelValidator } from 'modules/import-data/sourcing-data/validators/sourcing-data.class.validator'; +import { validateOrReject } from 'class-validator'; +import { plainToClass } from 'class-transformer'; +import { + LOCATION_TYPES, + SourcingLocation, +} from 'modules/sourcing-locations/sourcing-location.entity'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +// @ts-ignore +import * as wellknown from 'wellknown'; +import { DataSource, Repository } from 'typeorm'; + +/** + * @debt: Define a more accurate DTO / Interface / Class for API-DB trades + * and spread through typing + */ +export interface SourcingData extends CreateSourcingLocationDto { + sourcingRecords: SourcingRecord[] | { year: number; tonnage: number }[]; + geoRegionId?: string; + adminRegionId?: string; +} + +export interface EudrInputShape { + plot_id: string; + plot_name: string; + company_id: string; + company_name: string; + total_area_ha: number; + sourcing_country: string; + sourcing_district: string; + path_id: string; + material_id: string; + geometry: string; + + [key: string]: string | number | undefined; +} + +@Injectable() +export class EUDRDTOProcessor { + protected readonly logger: Logger = new Logger(EUDRDTOProcessor.name); + + constructor(private readonly dataSource: DataSource) {} + + async save( + importData: EudrInputShape[], + sourcingLocationGroupId?: string, + ): Promise<{ + sourcingLocations: SourcingLocation[]; + }> { + this.logger.debug(`Creating DTOs from sourcing records sheets`); + const sourcingLocations: SourcingLocation[] = []; + const supplierRepository: Repository = + this.dataSource.getRepository(Supplier); + const geoRegionRepository: Repository = + this.dataSource.getRepository(GeoRegion); + for (const row of importData) { + const supplier: Supplier = new Supplier(); + let savedSupplier: Supplier; + supplier.name = row.company_name; + supplier.description = row.company_name; + supplier.companyId = row.company_id; + const foundSupplier: Supplier | null = await supplierRepository.findOne({ + where: { name: supplier.name }, + }); + if (!foundSupplier) { + savedSupplier = await supplierRepository.save(supplier); + } else { + savedSupplier = foundSupplier; + } + const geoRegion: GeoRegion = new GeoRegion(); + let savedGeoRegion: GeoRegion; + geoRegion.totalArea = row.total_area_ha; + geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; + geoRegion.isCreatedByUser = true; + const foundGeoRegion: GeoRegion | null = + await geoRegionRepository.findOne({ + where: { name: geoRegion.name }, + }); + if (!foundGeoRegion) { + savedGeoRegion = await geoRegionRepository.save(geoRegion); + } else { + savedGeoRegion = foundGeoRegion; + } + const sourcingLocation: SourcingLocation = new SourcingLocation(); + sourcingLocation.locationType = LOCATION_TYPES.EUDR; + sourcingLocation.locationCountryInput = row.sourcing_country; + sourcingLocation.locationAddressInput = row.sourcing_district; + // TODO: materialId is coming like mpath, this is an error in the input file + sourcingLocation.materialId = row.material_id + .split('.') + .filter(Boolean) + .pop() as string; + sourcingLocation.producer = savedSupplier; + sourcingLocation.geoRegion = savedGeoRegion; + sourcingLocation.sourcingRecords = []; + + for (const key in row) { + const sourcingRecord: SourcingRecord = new SourcingRecord(); + if (row.hasOwnProperty(key)) { + const match: RegExpMatchArray | null = key.match(/^(\d{4})_t$/); + if (match) { + sourcingRecord.year = parseInt(match[1]); + sourcingRecord.tonnage = row[key] as number; + sourcingLocation.sourcingRecords.push(sourcingRecord); + } + } + } + sourcingLocations.push(sourcingLocation); + } + + const saved: SourcingLocation[] = await this.dataSource + .getRepository(SourcingLocation) + .save(sourcingLocations); + + /** + * Validating parsed and cleaned from Sourcing Data sheet + */ + + return { + sourcingLocations: saved, + }; + } + + private async validateCleanData(nonEmptyData: SourcingData[]): Promise { + const excelErrors: { + line: number; + column: string; + errors: { [type: string]: string } | undefined; + }[] = []; + + for (const [index, dto] of nonEmptyData.entries()) { + const objectToValidate: SourcingDataExcelValidator = plainToClass( + SourcingDataExcelValidator, + dto, + ); + + try { + await validateOrReject(objectToValidate); + } catch (errors: any) { + errors.forEach((error: ValidationError) => { + if (error.children?.length) { + error.children.forEach((nestedError: ValidationError) => { + excelErrors.push({ + line: index + 5, + column: nestedError.value.year, + errors: nestedError.children?.[0].constraints, + }); + }); + } else { + excelErrors.push({ + line: index + 5, + column: error?.property, + errors: error?.constraints, + }); + } + }); + } + } + + if (excelErrors.length) { + throw new BadRequestException(excelErrors); + } + } +} diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index 660d9903a..d8d7bde16 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -1,30 +1,24 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { SourcingLocationGroup } from 'modules/sourcing-location-groups/sourcing-location-group.entity'; -import { - SourcingData, - SourcingRecordsDtoProcessorService, - SourcingRecordsDtos, -} from '../sourcing-data/dto-processor.service'; -import { Material } from 'modules/materials/material.entity'; -import { Supplier } from '../../suppliers/supplier.entity'; -import { SourcingRecordsSheets } from '../sourcing-data/sourcing-data-import.service'; -import { FileService } from '../file.service'; -import { SourcingLocationGroupsService } from '../../sourcing-location-groups/sourcing-location-groups.service'; -import { MaterialsService } from '../../materials/materials.service'; -import { BusinessUnitsService } from '../../business-units/business-units.service'; -import { SuppliersService } from '../../suppliers/suppliers.service'; -import { AdminRegionsService } from '../../admin-regions/admin-regions.service'; -import { GeoRegionsService } from '../../geo-regions/geo-regions.service'; -import { SourcingLocationsService } from '../../sourcing-locations/sourcing-locations.service'; -import { SourcingRecordsService } from '../../sourcing-records/sourcing-records.service'; -import { GeoCodingAbstractClass } from '../../geo-coding/geo-coding-abstract-class'; -import { TasksService } from '../../tasks/tasks.service'; -import { ScenariosService } from '../../scenarios/scenarios.service'; -import { IndicatorsService } from '../../indicators/indicators.service'; -import { IndicatorRecordsService } from '../../indicator-records/indicator-records.service'; -import { ImpactService } from '../../impact/impact.service'; -import { ImpactCalculator } from '../../indicator-records/services/impact-calculator.service'; +import { SourcingRecordsDtos } from 'modules/import-data/sourcing-data/dto-processor.service'; + +import { SourcingRecordsSheets } from 'modules/import-data/sourcing-data/sourcing-data-import.service'; +import { FileService } from 'modules/import-data/file.service'; +import { SourcingLocationGroupsService } from 'modules/sourcing-location-groups/sourcing-location-groups.service'; +import { MaterialsService } from 'modules/materials/materials.service'; +import { BusinessUnitsService } from 'modules/business-units/business-units.service'; +import { SuppliersService } from 'modules/suppliers/suppliers.service'; +import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { GeoRegionsService } from 'modules/geo-regions/geo-regions.service'; +import { SourcingLocationsService } from 'modules/sourcing-locations/sourcing-locations.service'; +import { SourcingRecordsService } from 'modules/sourcing-records/sourcing-records.service'; +import { GeoCodingAbstractClass } from 'modules/geo-coding/geo-coding-abstract-class'; +import { TasksService } from 'modules/tasks/tasks.service'; +import { ScenariosService } from 'modules/scenarios/scenarios.service'; +import { IndicatorsService } from 'modules/indicators/indicators.service'; +import { IndicatorRecordsService } from 'modules/indicator-records/indicator-records.service'; import { validateOrReject } from 'class-validator'; +import { EUDRDTOProcessor } from 'modules/import-data/eudr/eudr.dto-processor.service'; const EUDR_SHEET_MAP: Record<'Data', 'Data'> = { Data: 'Data', @@ -44,22 +38,22 @@ export class EudrImportService { protected readonly sourcingRecordService: SourcingRecordsService, protected readonly sourcingLocationGroupService: SourcingLocationGroupsService, protected readonly fileService: FileService, - protected readonly dtoProcessor: SourcingRecordsDtoProcessorService, + protected readonly dtoProcessor: EUDRDTOProcessor, protected readonly geoCodingService: GeoCodingAbstractClass, protected readonly tasksService: TasksService, protected readonly scenarioService: ScenariosService, protected readonly indicatorService: IndicatorsService, protected readonly indicatorRecordService: IndicatorRecordsService, - protected readonly impactService: ImpactService, - protected readonly impactCalculator: ImpactCalculator, ) {} async importEudr(filePath: string, taskId: string): Promise { this.logger.log(`Starting eudr import process`); await this.fileService.isFilePresentInFs(filePath); try { - const parsedEudrData: SourcingRecordsSheets = - await this.fileService.transformToJson(filePath, EUDR_SHEET_MAP); + const parsedEudrData: any = await this.fileService.transformToJson( + filePath, + EUDR_SHEET_MAP, + ); const sourcingLocationGroup: SourcingLocationGroup = await this.sourcingLocationGroupService.create({ @@ -68,111 +62,11 @@ export class EudrImportService { await this.cleanDataBeforeImport(); - return parsedEudrData; - - // const dtoMatchedData: SourcingRecordsDtos = - // await this.validateAndCreateDTOs( - // parsedXLSXDataset, - // sourcingLocationGroup.id, - // ).catch(async (err: any) => { - // await this.tasksService.updateImportTask({ - // taskId, - // newErrors: err.message, - // }); - // throw new BadRequestException( - // 'Import failed. There are constraint errors present in the file', - // ); - // }); + const { sourcingLocations } = await this.dtoProcessor.save( + parsedEudrData.Data, + ); - //TODO: Implement transactional import. Move geocoding to first step - // - // const materials: Material[] = - // await this.materialService.findAllUnpaginated(); - // if (!materials.length) { - // throw new ServiceUnavailableException( - // 'No Materials found present in the DB. Please check the LandGriffon installation manual', - // ); - // } - // this.logger.log('Activating Indicators...'); - // const activeIndicators: Indicator[] = - // await this.indicatorService.activateIndicators( - // dtoMatchedData.indicators, - // ); - // this.logger.log('Activating Materials...'); - // const activeMaterials: Material[] = - // await this.materialService.activateMaterials(dtoMatchedData.materials); - // - // await this.tasksService.updateImportTask({ - // taskId, - // newLogs: [ - // `Activated indicators: ${activeIndicators - // .map((i: Indicator) => i.name) - // .join(', ')}`, - // `Activated materials: ${activeMaterials - // .map((i: Material) => i.hsCodeId) - // .join(', ')}`, - // ], - // }); - // - // const businessUnits: BusinessUnit[] = - // await this.businessUnitService.createTree(dtoMatchedData.businessUnits); - // - // const suppliers: Supplier[] = await this.supplierService.createTree( - // dtoMatchedData.suppliers, - // ); - // - // const { geoCodedSourcingData, errors } = - // await this.geoCodingService.geoCodeLocations( - // dtoMatchedData.sourcingData, - // ); - // if (errors.length) { - // await this.tasksService.updateImportTask({ taskId, newErrors: errors }); - // throw new BadRequestException( - // 'Import failed. There are GeoCoding errors present in the file', - // ); - // } - // const warnings: string[] = []; - // geoCodedSourcingData.forEach((elem: SourcingData) => { - // if (elem.locationWarning) warnings.push(elem.locationWarning); - // }); - // warnings.length > 0 && - // (await this.tasksService.updateImportTask({ - // taskId, - // newLogs: warnings, - // })); - // - // const sourcingDataWithOrganizationalEntities: any = - // await this.relateSourcingDataWithOrganizationalEntities( - // suppliers, - // businessUnits, - // materials, - // geoCodedSourcingData, - // ); - // - // await this.sourcingLocationService.save( - // sourcingDataWithOrganizationalEntities, - // ); - // - // this.logger.log('Generating Indicator Records...'); - // - // // TODO: Current approach calculates Impact for all Sourcing Records present in the DB - // // Getting H3 data for calculations is done within DB so we need to improve the error handling - // // TBD: What to do when there is no H3 for a Material - // - // try { - // await this.impactCalculator.calculateImpactForAllSourcingRecords( - // activeIndicators, - // ); - // this.logger.log('Indicator Records generated'); - // await this.impactService.updateImpactView(); - // } catch (err: any) { - // throw new ServiceUnavailableException( - // 'Could not calculate Impact for current data. Please contact with the administrator', - // ); - // } - // } finally { - // await this.fileService.deleteDataFromFS(filePath); - // } + return sourcingLocations; } finally { await this.fileService.deleteDataFromFS(filePath); } @@ -235,73 +129,4 @@ export class EudrImportService { ); } } - - /** - * @note: Type hack as mpath property does not exist on Materials and BusinessUnits, but its created - * by typeorm when using @Tree('materialized-path)'. - * It's what we can use to know which material/business unit relates to which sourcing-location - * in a synchronous way avoiding hitting the DB - */ - async relateSourcingDataWithOrganizationalEntities( - suppliers: Supplier[], - businessUnits: Record[], - materials: Material[], - sourcingData: SourcingData[], - ): Promise { - this.logger.log(`Relating sourcing data with organizational entities`); - this.logger.log(`Supplier count: ${suppliers.length}`); - this.logger.log(`Business Units count: ${businessUnits.length}`); - this.logger.log(`Materials count: ${materials.length}`); - this.logger.log(`Sourcing Data count: ${sourcingData.length}`); - - const materialMap: Record = {}; - materials.forEach((material: Material) => { - if (!material.hsCodeId) { - return; - } - materialMap[material.hsCodeId] = material.id; - }); - - for (const sourcingLocation of sourcingData) { - for (const supplier of suppliers) { - if (sourcingLocation.producerId === supplier.mpath) { - sourcingLocation.producerId = supplier.id; - } - if (sourcingLocation.t1SupplierId === supplier.mpath) { - sourcingLocation.t1SupplierId = supplier.id; - } - } - for (const businessUnit of businessUnits) { - if (sourcingLocation.businessUnitId === businessUnit.mpath) { - sourcingLocation.businessUnitId = businessUnit.id; - } - } - if (typeof sourcingLocation.materialId === 'undefined') { - return; - } - const sourcingLocationMaterialId: string = sourcingLocation.materialId; - - if (!(sourcingLocationMaterialId in materialMap)) { - throw new Error( - `Could not import sourcing location - material code ${sourcingLocationMaterialId} not found`, - ); - } - sourcingLocation.materialId = materialMap[sourcingLocationMaterialId]; - } - return sourcingData; - } - - async validateAndCreateDTOs( - parsedXLSXDataset: SourcingRecordsSheets, - sourcingLocationGroupId: string, - ): Promise { - const dtoMatchedData: SourcingRecordsDtos = - await this.dtoProcessor.createDTOsFromSourcingRecordsSheets( - parsedXLSXDataset, - sourcingLocationGroupId, - ); - - await this.validateDTOs(dtoMatchedData); - return dtoMatchedData; - } } diff --git a/api/src/modules/import-data/import-data.module.ts b/api/src/modules/import-data/import-data.module.ts index 0cb6735d5..c5e913170 100644 --- a/api/src/modules/import-data/import-data.module.ts +++ b/api/src/modules/import-data/import-data.module.ts @@ -25,7 +25,10 @@ import { MulterModule } from '@nestjs/platform-express'; import * as config from 'config'; import MulterConfigService from 'modules/import-data/multer-config.service'; import { ImpactModule } from 'modules/impact/impact.module'; -import { EudrImportService } from './eudr/eudr.import.service'; +import { EudrImportService } from 'modules/import-data/eudr/eudr.import.service'; +import { EUDRDTOProcessor } from 'modules/import-data/eudr/eudr.dto-processor.service'; + +// TODO: Move EUDR related stuff to EUDR modules @Module({ imports: [ @@ -63,6 +66,7 @@ import { EudrImportService } from './eudr/eudr.import.service'; ImportDataConsumer, ImportDataService, EudrImportService, + EUDRDTOProcessor, { provide: 'FILE_UPLOAD_SIZE_LIMIT', useValue: config.get('fileUploads.sizeLimit'), From 1b900978d68c7598c653895c9da4289f416c5e76 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:54:47 +0300 Subject: [PATCH 010/153] transactional save --- .../eudr/eudr.dto-processor.service.ts | 143 +++++++++--------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index 52923bb0b..aa3dde870 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -17,7 +17,7 @@ import { Supplier } from 'modules/suppliers/supplier.entity'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; // @ts-ignore import * as wellknown from 'wellknown'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, QueryRunner, Repository } from 'typeorm'; /** * @debt: Define a more accurate DTO / Interface / Class for API-DB trades @@ -56,78 +56,85 @@ export class EUDRDTOProcessor { ): Promise<{ sourcingLocations: SourcingLocation[]; }> { - this.logger.debug(`Creating DTOs from sourcing records sheets`); - const sourcingLocations: SourcingLocation[] = []; - const supplierRepository: Repository = - this.dataSource.getRepository(Supplier); - const geoRegionRepository: Repository = - this.dataSource.getRepository(GeoRegion); - for (const row of importData) { - const supplier: Supplier = new Supplier(); - let savedSupplier: Supplier; - supplier.name = row.company_name; - supplier.description = row.company_name; - supplier.companyId = row.company_id; - const foundSupplier: Supplier | null = await supplierRepository.findOne({ - where: { name: supplier.name }, - }); - if (!foundSupplier) { - savedSupplier = await supplierRepository.save(supplier); - } else { - savedSupplier = foundSupplier; - } - const geoRegion: GeoRegion = new GeoRegion(); - let savedGeoRegion: GeoRegion; - geoRegion.totalArea = row.total_area_ha; - geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; - geoRegion.isCreatedByUser = true; - const foundGeoRegion: GeoRegion | null = - await geoRegionRepository.findOne({ - where: { name: geoRegion.name }, - }); - if (!foundGeoRegion) { - savedGeoRegion = await geoRegionRepository.save(geoRegion); - } else { - savedGeoRegion = foundGeoRegion; - } - const sourcingLocation: SourcingLocation = new SourcingLocation(); - sourcingLocation.locationType = LOCATION_TYPES.EUDR; - sourcingLocation.locationCountryInput = row.sourcing_country; - sourcingLocation.locationAddressInput = row.sourcing_district; - // TODO: materialId is coming like mpath, this is an error in the input file - sourcingLocation.materialId = row.material_id - .split('.') - .filter(Boolean) - .pop() as string; - sourcingLocation.producer = savedSupplier; - sourcingLocation.geoRegion = savedGeoRegion; - sourcingLocation.sourcingRecords = []; + const queryRunner: QueryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + this.logger.debug(`Creating DTOs from sourcing records sheets`); + const sourcingLocations: SourcingLocation[] = []; + const supplierRepository: Repository = + queryRunner.manager.getRepository(Supplier); + const geoRegionRepository: Repository = + queryRunner.manager.getRepository(GeoRegion); + for (const row of importData) { + const supplier: Supplier = new Supplier(); + let savedSupplier: Supplier; + supplier.name = row.company_name; + supplier.description = row.company_name; + supplier.companyId = row.company_id; + const foundSupplier: Supplier | null = await supplierRepository.findOne( + { + where: { name: supplier.name }, + }, + ); + if (!foundSupplier) { + savedSupplier = await supplierRepository.save(supplier); + } else { + savedSupplier = foundSupplier; + } + const geoRegion: GeoRegion = new GeoRegion(); + let savedGeoRegion: GeoRegion; + geoRegion.totalArea = row.total_area_ha; + geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; + geoRegion.isCreatedByUser = true; + const foundGeoRegion: GeoRegion | null = + await geoRegionRepository.findOne({ + where: { name: geoRegion.name }, + }); + if (!foundGeoRegion) { + savedGeoRegion = await geoRegionRepository.save(geoRegion); + } else { + savedGeoRegion = foundGeoRegion; + } + const sourcingLocation: SourcingLocation = new SourcingLocation(); + sourcingLocation.locationType = LOCATION_TYPES.EUDR; + sourcingLocation.locationCountryInput = row.sourcing_country; + sourcingLocation.locationAddressInput = row.sourcing_district; + // TODO: materialId is coming like mpath, this is an error in the input file + sourcingLocation.materialId = row.material_id + .split('.') + .filter(Boolean) + .pop() as string; + sourcingLocation.producer = savedSupplier; + sourcingLocation.geoRegion = savedGeoRegion; + sourcingLocation.sourcingRecords = []; - for (const key in row) { - const sourcingRecord: SourcingRecord = new SourcingRecord(); - if (row.hasOwnProperty(key)) { - const match: RegExpMatchArray | null = key.match(/^(\d{4})_t$/); - if (match) { - sourcingRecord.year = parseInt(match[1]); - sourcingRecord.tonnage = row[key] as number; - sourcingLocation.sourcingRecords.push(sourcingRecord); + for (const key in row) { + const sourcingRecord: SourcingRecord = new SourcingRecord(); + if (row.hasOwnProperty(key)) { + const match: RegExpMatchArray | null = key.match(/^(\d{4})_t$/); + if (match) { + sourcingRecord.year = parseInt(match[1]); + sourcingRecord.tonnage = row[key] as number; + sourcingLocation.sourcingRecords.push(sourcingRecord); + } } } + sourcingLocations.push(sourcingLocation); } - sourcingLocations.push(sourcingLocation); - } - - const saved: SourcingLocation[] = await this.dataSource - .getRepository(SourcingLocation) - .save(sourcingLocations); - /** - * Validating parsed and cleaned from Sourcing Data sheet - */ - - return { - sourcingLocations: saved, - }; + const saved: SourcingLocation[] = await queryRunner.manager + .getRepository(SourcingLocation) + .save(sourcingLocations); + return { + sourcingLocations: saved, + }; + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } } private async validateCleanData(nonEmptyData: SourcingData[]): Promise { From 1bbe26de156eb9c4203b2e09b141e1ed89a729d8 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 06:57:44 +0300 Subject: [PATCH 011/153] Add address for company --- api/src/modules/suppliers/supplier.entity.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/src/modules/suppliers/supplier.entity.ts b/api/src/modules/suppliers/supplier.entity.ts index 2865ff64e..06766a394 100644 --- a/api/src/modules/suppliers/supplier.entity.ts +++ b/api/src/modules/suppliers/supplier.entity.ts @@ -65,9 +65,14 @@ export class Supplier extends TimestampedBaseEntity { @Column({ nullable: true }) description?: string; - @Column({ type: 'varchar' }) + @ApiPropertyOptional() + @Column({ type: 'varchar', nullable: true }) companyId: string; + @ApiPropertyOptional() + @Column({ type: 'varchar', nullable: true }) + address: string; + @ApiProperty() @Column({ type: 'enum', From 5d6adb0fc5c036005305206f4a4b8170215ae965 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 09:23:46 +0300 Subject: [PATCH 012/153] Fallback to intersection when no adminregion found by district name --- .../eudr/eudr.dto-processor.service.ts | 79 +++++++++++++++++++ .../import-data/eudr/eudr.import.service.ts | 7 +- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index aa3dde870..df36b2adc 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -18,6 +18,8 @@ import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; // @ts-ignore import * as wellknown from 'wellknown'; import { DataSource, QueryRunner, Repository } from 'typeorm'; +import { GeoCodingError } from 'modules/geo-coding/errors/geo-coding.error'; +import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; /** * @debt: Define a more accurate DTO / Interface / Class for API-DB trades @@ -87,6 +89,7 @@ export class EUDRDTOProcessor { geoRegion.totalArea = row.total_area_ha; geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; geoRegion.isCreatedByUser = true; + geoRegion.name = row.plot_name; const foundGeoRegion: GeoRegion | null = await geoRegionRepository.findOne({ where: { name: geoRegion.name }, @@ -108,6 +111,13 @@ export class EUDRDTOProcessor { sourcingLocation.producer = savedSupplier; sourcingLocation.geoRegion = savedGeoRegion; sourcingLocation.sourcingRecords = []; + sourcingLocation.adminRegionId = row.sourcing_district + ? await this.getAdminRegionByAddress( + queryRunner, + row.sourcing_district, + row.geometry, + ) + : (null as unknown as string); for (const key in row) { const sourcingRecord: SourcingRecord = new SourcingRecord(); @@ -126,6 +136,8 @@ export class EUDRDTOProcessor { const saved: SourcingLocation[] = await queryRunner.manager .getRepository(SourcingLocation) .save(sourcingLocations); + + await queryRunner.commitTransaction(); return { sourcingLocations: saved, }; @@ -137,6 +149,73 @@ export class EUDRDTOProcessor { } } + private async getAdminRegionByAddress( + queryRunner: QueryRunner, + name: string, + geom: string, + ): Promise { + const adminRegion: AdminRegion | null = await queryRunner.manager + .getRepository(AdminRegion) + .findOne({ where: { name: name, level: 1 } }); + if (!adminRegion) { + this.logger.warn( + `No admin region found for the provided address: ${name}`, + ); + return this.getAdminRegionByIntersection(queryRunner, geom); + } + return adminRegion.id; + } + + // TODO: temporal method to determine the most accurate admin region. For now we only consider Level 0 + // as Country and Level 1 as district + + private async getAdminRegionByIntersection( + queryRunner: QueryRunner, + geometry: string, + ): Promise { + this.logger.log(`Intersecting EUDR geometry...`); + + const adminRegions: any = await queryRunner.manager.query( + ` + WITH intersections AS ( + SELECT + ar.id, + ar.name, + ar."geoRegionId", + gr."theGeom", + ar.level, + ST_Area(ST_Intersection(gr."theGeom", ST_GeomFromEWKT('SRID=4326;${geometry}'))) AS intersection_area + FROM admin_region ar + JOIN geo_region gr ON ar."geoRegionId" = gr.id + WHERE + ST_Intersects(gr."theGeom", ST_GeomFromEWKT('SRID=4326;${geometry}')) + AND ar.level IN (0, 1) + ), + max_intersection_by_level AS ( + SELECT + level, + MAX(intersection_area) AS max_area + FROM intersections + GROUP BY level + ) + SELECT i.* + FROM intersections i + JOIN max_intersection_by_level m ON i.level = m.level AND i.intersection_area = m.max_area; + `, + ); + if (!adminRegions.length) { + throw new GeoCodingError( + `No admin region found for the provided geometry`, + ); + } + + const level1AdminRegionid: string = adminRegions.find( + (ar: any) => ar.level === 1, + ).id; + this.logger.log('Admin region found'); + return level1AdminRegionid; + } + private async validateCleanData(nonEmptyData: SourcingData[]): Promise { const excelErrors: { line: number; diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index d8d7bde16..61c9ed7ca 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -62,6 +62,9 @@ export class EudrImportService { await this.cleanDataBeforeImport(); + // TODO: Check what do we need to do with indicators and specially materials: + // Do we need to ingest new materials? Activate some through the import? Activate all? + const { sourcingLocations } = await this.dtoProcessor.save( parsedEudrData.Data, ); @@ -123,9 +126,9 @@ export class EudrImportService { await this.sourcingLocationService.clearTable(); await this.sourcingRecordService.clearTable(); await this.geoRegionsService.deleteGeoRegionsCreatedByUser(); - } catch ({ message }) { + } catch (e: any) { throw new Error( - `Database could not been cleaned before loading new dataset: ${message}`, + `Database could not been cleaned before loading new dataset: ${e.message}`, ); } } From ff1d3a056827f3b96e8a58c59bb608efbd1a694f Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 10:46:03 +0300 Subject: [PATCH 013/153] generate fake output csv --- .../import-data/eudr/eudr.import.service.ts | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index 61c9ed7ca..7880ec346 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -19,6 +19,14 @@ import { IndicatorsService } from 'modules/indicators/indicators.service'; import { IndicatorRecordsService } from 'modules/indicator-records/indicator-records.service'; import { validateOrReject } from 'class-validator'; import { EUDRDTOProcessor } from 'modules/import-data/eudr/eudr.dto-processor.service'; +import { DataSource } from 'typeorm'; +import { SourcingLocation } from '../../sourcing-locations/sourcing-location.entity'; +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { Supplier } from '../../suppliers/supplier.entity'; +import { SourcingRecord } from '../../sourcing-records/sourcing-record.entity'; + +const { AsyncParser } = require('@json2csv/node'); +import * as fs from 'fs'; // Para tr const EUDR_SHEET_MAP: Record<'Data', 'Data'> = { Data: 'Data', @@ -44,6 +52,7 @@ export class EudrImportService { protected readonly scenarioService: ScenariosService, protected readonly indicatorService: IndicatorsService, protected readonly indicatorRecordService: IndicatorRecordsService, + protected readonly dataSource: DataSource, ) {} async importEudr(filePath: string, taskId: string): Promise { @@ -69,12 +78,83 @@ export class EudrImportService { parsedEudrData.Data, ); - return sourcingLocations; + // TODO: At this point we should send the data to Carto. For now we will be creating a csv to upload + // and read from there + + const data: { + supplierId: string; + geoRegionId: string; + geometry: string; + year: number; + }[] = await this.dataSource + .createQueryBuilder() + .select('s.id', 'supplierId') + .addSelect('g.id', 'geoRegionId') + .addSelect('g.theGeom', 'geometry') + .addSelect('sr.year', 'year') + .from(SourcingRecord, 'sr') + .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') + .leftJoin(GeoRegion, 'g', 'sl.geoRegionId = g.id') + .leftJoin(Supplier, 's', 'sl.producerId = s.id') + + .execute(); + + const fakedCartoOutput: any[] = data.map((row: any) => + this.generateFakeAlerts(row), + ); + console.log(fakedCartoOutput); + + const parsed: any = await new AsyncParser({ + fields: [ + 'geoRegionId', + 'supplierId', + 'geometry', + 'year', + 'alertDate', + 'alertConfidence', + 'alertCount', + ], + }) + .parse(fakedCartoOutput) + .promise(); + try { + await fs.promises.writeFile('fakedCartoOutput.csv', parsed); + } catch (e: any) { + this.logger.error(`Error writing fakedCartoOutput.csv: ${e.message}`); + } + + return fakedCartoOutput; } finally { await this.fileService.deleteDataFromFS(filePath); } } + private generateFakeAlerts(row: { + geoRegionId: string; + supplierId: string; + geometry: string; + year: number; + }): any { + const { geoRegionId, supplierId, geometry } = row; + const alertConfidence: string = Math.random() > 0.5 ? 'high' : 'low'; + const startDate: Date = new Date(row.year, 0, 1); + const endDate: Date = new Date(row.year, 11, 31); + const timeDiff: number = endDate.getTime() - startDate.getTime(); + const randomDate: Date = new Date( + startDate.getTime() + Math.random() * timeDiff, + ); + const alertDate: string = randomDate.toISOString().split('T')[0]; + const alertCount: number = Math.floor(Math.random() * 20) + 1; + return { + geoRegionId, + supplierId, + geometry, + alertDate, + alertConfidence, + alertCount, + }; + } + private async validateDTOs( dtoLists: SourcingRecordsDtos, ): Promise> { From d51a5e95dadffed70cc0b86c68bcb8683c21f7dd Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 16:33:13 +0300 Subject: [PATCH 014/153] Add carto tmp carto connector --- api/config/custom-environment-variables.json | 3 ++- api/config/default.json | 3 ++- api/src/modules/eudr/carto/carto.connector.ts | 22 +++++++++++++++---- api/src/modules/eudr/eudr.controller.ts | 8 +++---- api/src/modules/eudr/eudr.module.ts | 3 ++- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 18c688e75..dd2bad1dd 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -74,6 +74,7 @@ } }, "carto": { - "apiKey": "CARTO_API_KEY" + "apiKey": "CARTO_API_KEY", + "baseUrl": "CARTO_BASE_URL" } } diff --git a/api/config/default.json b/api/config/default.json index b5668f804..a3075196a 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -83,6 +83,7 @@ } }, "carto": { - "apuKey": null + "apiKey": null, + "baseUrl": "null" } } diff --git a/api/src/modules/eudr/carto/carto.connector.ts b/api/src/modules/eudr/carto/carto.connector.ts index f558dc729..57b54254d 100644 --- a/api/src/modules/eudr/carto/carto.connector.ts +++ b/api/src/modules/eudr/carto/carto.connector.ts @@ -16,15 +16,13 @@ type CartoConfig = { export class CartoConnector { cartoApiKey: string; cartoBaseUrl: string; - cartoConnection: string; logger: Logger = new Logger(CartoConnector.name); constructor(private readonly httpService: HttpService) { - const { apiKey, baseUrl, connection } = AppConfig.get('carto'); + const { apiKey, baseUrl } = AppConfig.get('carto'); this.cartoApiKey = apiKey; this.cartoBaseUrl = baseUrl; - this.cartoConnection = connection; - if (!this.cartoApiKey || !this.cartoBaseUrl || !this.cartoConnection) { + if (!this.cartoApiKey || !this.cartoBaseUrl) { this.logger.error('Carto configuration is missing'); } } @@ -35,4 +33,20 @@ export class CartoConnector { 'Unable to connect to Carto. Please contact your administrator.', ); } + + async select(query: string): Promise { + try { + const response: any = await this.httpService + .get(`${this.cartoBaseUrl}${query}`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cartoApiKey}`, + }, + }) + .toPromise(); + return response.data; + } catch (e: any) { + this.handleConnectionError(e); + } + } } diff --git a/api/src/modules/eudr/eudr.controller.ts b/api/src/modules/eudr/eudr.controller.ts index fc151a0f6..602cbb330 100644 --- a/api/src/modules/eudr/eudr.controller.ts +++ b/api/src/modules/eudr/eudr.controller.ts @@ -1,14 +1,14 @@ import { Controller, Get } from '@nestjs/common'; import { Public } from 'decorators/public.decorator'; -import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; +import { CartoConnector } from 'modules/eudr/carto/carto.connector'; @Controller('/api/v1/eudr') export class EudrController { - constructor(private readonly eudr: CartodbRepository) {} + constructor(private readonly carto: CartoConnector) {} @Public() - @Get() + @Get('test') async select(): Promise { - return this.eudr.select(); + return this.carto.select('select * from cartobq.eudr.mock_data limit 10'); } } diff --git a/api/src/modules/eudr/eudr.module.ts b/api/src/modules/eudr/eudr.module.ts index 2529deda8..179ff1f3d 100644 --- a/api/src/modules/eudr/eudr.module.ts +++ b/api/src/modules/eudr/eudr.module.ts @@ -3,10 +3,11 @@ import { HttpModule } from '@nestjs/axios'; import { EudrService } from 'modules/eudr/eudr.service'; import { EudrController } from 'modules/eudr/eudr.controller'; import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; +import { CartoConnector } from './carto/carto.connector'; @Module({ imports: [HttpModule], - providers: [EudrService, CartodbRepository], + providers: [EudrService, CartodbRepository, CartoConnector], controllers: [EudrController], }) export class EudrModule {} From 57d9892d1f46e36a78db7c1f080b4a7b95767d53 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 16:46:20 +0300 Subject: [PATCH 015/153] retrieve eudr materials --- .../modules/materials/materials.controller.ts | 21 +++++++++++++++++++ .../modules/materials/materials.service.ts | 8 ------- api/src/utils/base.query-builder.ts | 10 +++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/api/src/modules/materials/materials.controller.ts b/api/src/modules/materials/materials.controller.ts index 9fa9e54b8..5c1faf4b4 100644 --- a/api/src/modules/materials/materials.controller.ts +++ b/api/src/modules/materials/materials.controller.ts @@ -99,6 +99,27 @@ export class MaterialsController { return this.materialsService.serialize(results); } + @ApiOperation({ + description: + 'Find all EUDR materials and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: Material, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/trees/eudr') + async getTreesForEudr( + @Query(ValidationPipe) materialTreeOptions: GetMaterialTreeWithOptionsDto, + ): Promise { + const results: Material[] = await this.materialsService.getTrees({ + ...materialTreeOptions, + withSourcingLocations: true, + eudr: true, + }); + return this.materialsService.serialize(results); + } + @ApiOperation({ description: 'Find material by id' }) @ApiNotFoundResponse({ description: 'Material not found' }) @ApiOkResponse({ type: Material }) diff --git a/api/src/modules/materials/materials.service.ts b/api/src/modules/materials/materials.service.ts index 523292a41..40ebb7f8f 100644 --- a/api/src/modules/materials/materials.service.ts +++ b/api/src/modules/materials/materials.service.ts @@ -154,14 +154,6 @@ export class MaterialsService extends AppBaseService< return this.materialRepository.findByIds(ids); } - async saveMany(entityArray: Material[]): Promise { - await this.materialRepository.save(entityArray); - } - - async clearTable(): Promise { - await this.materialRepository.delete({}); - } - async findAllUnpaginated(): Promise { return this.materialRepository.find(); } diff --git a/api/src/utils/base.query-builder.ts b/api/src/utils/base.query-builder.ts index 3c28a62b5..59cb3c691 100644 --- a/api/src/utils/base.query-builder.ts +++ b/api/src/utils/base.query-builder.ts @@ -47,12 +47,16 @@ export class BaseQueryBuilder { originIds: filters.originIds, }); } - - if (filters.locationTypes) { + if (filters.eudr) { + queryBuilder.andWhere('sl.locationType = :eudr', { + eudr: LOCATION_TYPES.EUDR, + }); + } else if (filters.locationTypes) { queryBuilder.andWhere('sl.locationType IN (:...locationTypes)', { locationTypes: filters.locationTypes, }); } + if (filters.scenarioIds) { queryBuilder.leftJoin( ScenarioIntervention, @@ -131,4 +135,6 @@ export class CommonFiltersDto { @IsOptional() @IsUUID('4', { each: true }) scenarioIds?: string[]; + + eudr?: boolean; } From 19abf1ce39003274a94e4bcf8e00e7f6a83c0b5a Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 17:05:40 +0300 Subject: [PATCH 016/153] retrieve eudr suppliers --- .../suppliers/dto/get-supplier-by-type.dto.ts | 4 +++ .../dto/get-supplier-tree-with-options.dto.ts | 2 ++ .../modules/suppliers/suppliers.controller.ts | 32 +++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts index 8f7f34265..2d15d05ac 100644 --- a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts +++ b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts @@ -33,3 +33,7 @@ export class GetSupplierByType extends CommonFiltersDto { }) sort?: 'ASC' | 'DESC'; } + +export class GetSupplierEUDR extends GetSupplierByType { + type: SUPPLIER_TYPES = SUPPLIER_TYPES.PRODUCER; +} diff --git a/api/src/modules/suppliers/dto/get-supplier-tree-with-options.dto.ts b/api/src/modules/suppliers/dto/get-supplier-tree-with-options.dto.ts index 49207bc39..d5f37cbe2 100644 --- a/api/src/modules/suppliers/dto/get-supplier-tree-with-options.dto.ts +++ b/api/src/modules/suppliers/dto/get-supplier-tree-with-options.dto.ts @@ -75,4 +75,6 @@ export class GetSupplierTreeWithOptions { @IsOptional() @IsUUID('4', { each: true }) scenarioIds?: string[]; + + eudr?: boolean; } diff --git a/api/src/modules/suppliers/suppliers.controller.ts b/api/src/modules/suppliers/suppliers.controller.ts index 039a9cf81..9b850de9c 100644 --- a/api/src/modules/suppliers/suppliers.controller.ts +++ b/api/src/modules/suppliers/suppliers.controller.ts @@ -30,13 +30,20 @@ import { FetchSpecification, ProcessFetchSpecification, } from 'nestjs-base-service'; -import { Supplier, supplierResource } from 'modules/suppliers/supplier.entity'; +import { + Supplier, + SUPPLIER_TYPES, + supplierResource, +} from 'modules/suppliers/supplier.entity'; import { CreateSupplierDto } from 'modules/suppliers/dto/create.supplier.dto'; import { UpdateSupplierDto } from 'modules/suppliers/dto/update.supplier.dto'; import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; import { PaginationMeta } from 'utils/app-base.service'; import { GetSupplierTreeWithOptions } from 'modules/suppliers/dto/get-supplier-tree-with-options.dto'; -import { GetSupplierByType } from 'modules/suppliers/dto/get-supplier-by-type.dto'; +import { + GetSupplierByType, + GetSupplierEUDR, +} from 'modules/suppliers/dto/get-supplier-by-type.dto'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; @Controller(`/api/v1/suppliers`) @@ -94,6 +101,27 @@ export class SuppliersController { return this.suppliersService.serialize(results); } + @ApiOperation({ + description: + 'Find all EUDR suppliers and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: Supplier, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('/trees/eudr') + async getTreesForEudr( + @Query(ValidationPipe) dto: GetSupplierEUDR, + ): Promise { + const results: Supplier[] = await this.suppliersService.getSupplierByType({ + ...dto, + eudr: true, + }); + return this.suppliersService.serialize(results); + } + @ApiOperation({ description: 'Find all suppliers by type', }) From edc03a3685a7d99c99dea2f36c340fc0acfb11b6 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 27 Feb 2024 17:13:32 +0300 Subject: [PATCH 017/153] retrieve eudr admin-regions --- .../admin-regions/admin-regions.controller.ts | 23 +++++++++++++++++++ .../base/modules/aws/eks/thumbprint.sh | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/api/src/modules/admin-regions/admin-regions.controller.ts b/api/src/modules/admin-regions/admin-regions.controller.ts index f00b0197e..bd3cde942 100644 --- a/api/src/modules/admin-regions/admin-regions.controller.ts +++ b/api/src/modules/admin-regions/admin-regions.controller.ts @@ -97,6 +97,29 @@ export class AdminRegionsController { return this.adminRegionsService.serialize(results); } + @ApiOperation({ + description: + 'Find all EUDR admin regions and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: AdminRegion, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('/trees/eudr') + async getTreesForEudr( + @Query(ValidationPipe) + adminRegionTreeOptions: GetAdminRegionTreeWithOptionsDto, + ): Promise { + const results: AdminRegion[] = await this.adminRegionsService.getTrees({ + ...adminRegionTreeOptions, + withSourcingLocations: true, + eudr: true, + }); + return this.adminRegionsService.serialize(results); + } + @ApiOperation({ description: 'Find all admin regions given a country and return data in a tree format. Data in the "children" will recursively extend for the full depth of the tree', diff --git a/infrastructure/base/modules/aws/eks/thumbprint.sh b/infrastructure/base/modules/aws/eks/thumbprint.sh index 10772d913..9f565c53c 100755 --- a/infrastructure/base/modules/aws/eks/thumbprint.sh +++ b/infrastructure/base/modules/aws/eks/thumbprint.sh @@ -1,5 +1,5 @@ #!/bin/bash -THUMBPRINT=$(echo | openssl s_client -servername oidc.eks.${1}.amazonaws.com -showcerts -connect oidc.eks.${1}.amazonaws.com:443 2>&- | tac | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' | tac | openssl x509 -fingerprint -noout | sed 's/://g' | awk -F= '{print tolower($2)}') +THUMBPRINT=$(echo | openssl s_client -servername oidc.eks.${1}.amazonaws.com -showcerts -connect oidc.eks.${1}.amazonaws.com:443 2>&- | tail -r | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' | tail -r | openssl x509 -fingerprint -noout | sed 's/://g' | awk -F= '{print tolower($2)}') THUMBPRINT_JSON="{\"thumbprint\": \"${THUMBPRINT}\"}" echo $THUMBPRINT_JSON From 66ab6c4309c6447d2daca95ed97b05a04bb5c0d2 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 28 Feb 2024 08:34:27 +0300 Subject: [PATCH 018/153] Type EUDR dtos --- .../modules/admin-regions/admin-regions.controller.ts | 7 +++++-- .../modules/admin-regions/admin-regions.service.ts | 4 ---- .../dto/get-admin-region-tree-with-options.dto.ts | 11 +++++++++-- .../dto/get-material-tree-with-options.dto.ts | 9 ++++++++- api/src/modules/materials/materials.controller.ts | 7 +++++-- .../modules/suppliers/dto/get-supplier-by-type.dto.ts | 7 +++++-- api/src/modules/suppliers/suppliers.controller.ts | 4 ++-- api/src/utils/base.query-builder.ts | 11 ++++++++++- 8 files changed, 44 insertions(+), 16 deletions(-) diff --git a/api/src/modules/admin-regions/admin-regions.controller.ts b/api/src/modules/admin-regions/admin-regions.controller.ts index bd3cde942..45850e172 100644 --- a/api/src/modules/admin-regions/admin-regions.controller.ts +++ b/api/src/modules/admin-regions/admin-regions.controller.ts @@ -38,7 +38,10 @@ import { CreateAdminRegionDto } from 'modules/admin-regions/dto/create.admin-reg import { UpdateAdminRegionDto } from 'modules/admin-regions/dto/update.admin-region.dto'; import { PaginationMeta } from 'utils/app-base.service'; import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; -import { GetAdminRegionTreeWithOptionsDto } from 'modules/admin-regions/dto/get-admin-region-tree-with-options.dto'; +import { + GetAdminRegionTreeWithOptionsDto, + GetEUDRAdminRegions, +} from 'modules/admin-regions/dto/get-admin-region-tree-with-options.dto'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; @Controller(`/api/v1/admin-regions`) @@ -110,7 +113,7 @@ export class AdminRegionsController { @Get('/trees/eudr') async getTreesForEudr( @Query(ValidationPipe) - adminRegionTreeOptions: GetAdminRegionTreeWithOptionsDto, + adminRegionTreeOptions: GetEUDRAdminRegions, ): Promise { const results: AdminRegion[] = await this.adminRegionsService.getTrees({ ...adminRegionTreeOptions, diff --git a/api/src/modules/admin-regions/admin-regions.service.ts b/api/src/modules/admin-regions/admin-regions.service.ts index 86e6f2f9f..2353a9240 100644 --- a/api/src/modules/admin-regions/admin-regions.service.ts +++ b/api/src/modules/admin-regions/admin-regions.service.ts @@ -217,10 +217,6 @@ export class AdminRegionsService extends AppBaseService< return this.findTreesWithOptions({ depth: adminRegionTreeOptions.depth }); } - async getAdminRegionByIds(ids: string[]): Promise { - return this.adminRegionRepository.findByIds(ids); - } - /** * @description: Returns an array of all children of given Admin Region's Ids with optional parameters * @param {string[]} adminRegionIds - The IDs of the admin regions. diff --git a/api/src/modules/admin-regions/dto/get-admin-region-tree-with-options.dto.ts b/api/src/modules/admin-regions/dto/get-admin-region-tree-with-options.dto.ts index cb05708d4..9ac2f8be3 100644 --- a/api/src/modules/admin-regions/dto/get-admin-region-tree-with-options.dto.ts +++ b/api/src/modules/admin-regions/dto/get-admin-region-tree-with-options.dto.ts @@ -1,6 +1,9 @@ import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { CommonFiltersDto } from 'utils/base.query-builder'; +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; +import { + CommonEUDRFiltersDTO, + CommonFiltersDto, +} from 'utils/base.query-builder'; import { Type } from 'class-transformer'; export class GetAdminRegionTreeWithOptionsDto extends CommonFiltersDto { @@ -24,3 +27,7 @@ export class GetAdminRegionTreeWithOptionsDto extends CommonFiltersDto { @IsUUID('4') scenarioId?: string; } + +export class GetEUDRAdminRegions extends CommonEUDRFiltersDTO { + withSourcingLocations!: boolean; +} diff --git a/api/src/modules/materials/dto/get-material-tree-with-options.dto.ts b/api/src/modules/materials/dto/get-material-tree-with-options.dto.ts index 9fa7d059f..b1fdad9e6 100644 --- a/api/src/modules/materials/dto/get-material-tree-with-options.dto.ts +++ b/api/src/modules/materials/dto/get-material-tree-with-options.dto.ts @@ -1,7 +1,10 @@ import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { CommonFiltersDto } from 'utils/base.query-builder'; +import { + CommonEUDRFiltersDTO, + CommonFiltersDto, +} from 'utils/base.query-builder'; export class GetMaterialTreeWithOptionsDto extends CommonFiltersDto { @ApiPropertyOptional({ @@ -24,3 +27,7 @@ export class GetMaterialTreeWithOptionsDto extends CommonFiltersDto { @IsUUID('4') scenarioId?: string; } + +export class GetEUDRMaterials extends CommonEUDRFiltersDTO { + withSourcingLocations!: boolean; +} diff --git a/api/src/modules/materials/materials.controller.ts b/api/src/modules/materials/materials.controller.ts index 5c1faf4b4..8ddf3247a 100644 --- a/api/src/modules/materials/materials.controller.ts +++ b/api/src/modules/materials/materials.controller.ts @@ -36,7 +36,10 @@ import { CreateMaterialDto } from 'modules/materials/dto/create.material.dto'; import { UpdateMaterialDto } from 'modules/materials/dto/update.material.dto'; import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; import { PaginationMeta } from 'utils/app-base.service'; -import { GetMaterialTreeWithOptionsDto } from 'modules/materials/dto/get-material-tree-with-options.dto'; +import { + GetEUDRMaterials, + GetMaterialTreeWithOptionsDto, +} from 'modules/materials/dto/get-material-tree-with-options.dto'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; import { RolesGuard } from 'guards/roles.guard'; import { RequiredRoles } from 'decorators/roles.decorator'; @@ -110,7 +113,7 @@ export class MaterialsController { @ApiForbiddenResponse() @Get('/trees/eudr') async getTreesForEudr( - @Query(ValidationPipe) materialTreeOptions: GetMaterialTreeWithOptionsDto, + @Query(ValidationPipe) materialTreeOptions: GetEUDRMaterials, ): Promise { const results: Material[] = await this.materialsService.getTrees({ ...materialTreeOptions, diff --git a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts index 2d15d05ac..a1f6cd448 100644 --- a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts +++ b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts @@ -8,7 +8,10 @@ import { } from 'class-validator'; import { SUPPLIER_TYPES } from 'modules/suppliers/supplier.entity'; import { Type } from 'class-transformer'; -import { CommonFiltersDto } from 'utils/base.query-builder'; +import { + CommonEUDRFiltersDTO, + CommonFiltersDto, +} from 'utils/base.query-builder'; export class GetSupplierByType extends CommonFiltersDto { @ApiProperty({ @@ -34,6 +37,6 @@ export class GetSupplierByType extends CommonFiltersDto { sort?: 'ASC' | 'DESC'; } -export class GetSupplierEUDR extends GetSupplierByType { +export class GetSupplierEUDR extends CommonEUDRFiltersDTO { type: SUPPLIER_TYPES = SUPPLIER_TYPES.PRODUCER; } diff --git a/api/src/modules/suppliers/suppliers.controller.ts b/api/src/modules/suppliers/suppliers.controller.ts index 9b850de9c..6e7912631 100644 --- a/api/src/modules/suppliers/suppliers.controller.ts +++ b/api/src/modules/suppliers/suppliers.controller.ts @@ -103,7 +103,7 @@ export class SuppliersController { @ApiOperation({ description: - 'Find all EUDR suppliers and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + 'Find all EUDR suppliers and return them in a flat format. Data in the "children" will recursively extend for the full depth of the tree', }) @ApiOkTreeResponse({ treeNodeType: Supplier, @@ -111,7 +111,7 @@ export class SuppliersController { @ApiUnauthorizedResponse() @ApiForbiddenResponse() @UseInterceptors(SetScenarioIdsInterceptor) - @Get('/trees/eudr') + @Get('/eudr') async getTreesForEudr( @Query(ValidationPipe) dto: GetSupplierEUDR, ): Promise { diff --git a/api/src/utils/base.query-builder.ts b/api/src/utils/base.query-builder.ts index 59cb3c691..b9325ddaf 100644 --- a/api/src/utils/base.query-builder.ts +++ b/api/src/utils/base.query-builder.ts @@ -5,7 +5,7 @@ import { WhereExpressionBuilder, } from 'typeorm'; import { IsEnum, IsOptional, IsUUID } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiPropertyOptional, OmitType } from '@nestjs/swagger'; import { LOCATION_TYPES } from 'modules/sourcing-locations/sourcing-location.entity'; import { Type } from 'class-transformer'; import { @@ -138,3 +138,12 @@ export class CommonFiltersDto { eudr?: boolean; } + +export class CommonEUDRFiltersDTO extends OmitType(CommonFiltersDto, [ + 'scenarioIds', + 't1SupplierIds', + 'locationTypes', + 'businessUnitIds', +]) { + eudr?: boolean; +} From b8c56d168cb2912bb2fedc0f80c4e183972ba022 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 28 Feb 2024 09:10:05 +0300 Subject: [PATCH 019/153] Add initial get eudr georegions --- .../admin-regions/admin-regions.controller.ts | 2 +- .../geo-regions/dto/get-geo-region.dto.ts | 5 ++++ .../modules/geo-regions/geo-region.entity.ts | 2 ++ .../geo-regions/geo-region.repository.ts | 18 +++++++++++ .../geo-regions/geo-regions.controller.ts | 28 +++++++++++++++++ .../modules/geo-regions/geo-regions.module.ts | 10 ++++++- .../geo-regions/geo-regions.service.ts | 30 ++++++++++++++++++- .../modules/suppliers/suppliers.service.ts | 8 ----- .../base/modules/aws/eks/thumbprint.sh | 2 +- 9 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 api/src/modules/geo-regions/dto/get-geo-region.dto.ts diff --git a/api/src/modules/admin-regions/admin-regions.controller.ts b/api/src/modules/admin-regions/admin-regions.controller.ts index 45850e172..031331bdc 100644 --- a/api/src/modules/admin-regions/admin-regions.controller.ts +++ b/api/src/modules/admin-regions/admin-regions.controller.ts @@ -114,7 +114,7 @@ export class AdminRegionsController { async getTreesForEudr( @Query(ValidationPipe) adminRegionTreeOptions: GetEUDRAdminRegions, - ): Promise { + ): Promise { const results: AdminRegion[] = await this.adminRegionsService.getTrees({ ...adminRegionTreeOptions, withSourcingLocations: true, diff --git a/api/src/modules/geo-regions/dto/get-geo-region.dto.ts b/api/src/modules/geo-regions/dto/get-geo-region.dto.ts new file mode 100644 index 000000000..401582738 --- /dev/null +++ b/api/src/modules/geo-regions/dto/get-geo-region.dto.ts @@ -0,0 +1,5 @@ +import { CommonEUDRFiltersDTO } from 'utils/base.query-builder'; + +export class GetEUDRGeoRegions extends CommonEUDRFiltersDTO { + withSourcingLocations!: boolean; +} diff --git a/api/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index 04f873f7e..7c05a4961 100644 --- a/api/src/modules/geo-regions/geo-region.entity.ts +++ b/api/src/modules/geo-regions/geo-region.entity.ts @@ -49,6 +49,8 @@ export class GeoRegion extends BaseEntity { @ApiPropertyOptional() theGeom?: JSON; + // TODO: It might be interesting to add a trigger to calculate the value in case it's not provided. We are considering that EUDR will alwaus provide the value + // but not the regular ingestion @Column({ type: 'decimal', nullable: true }) totalArea?: number; diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index d2ff1049d..d927c1e9d 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -7,6 +7,11 @@ import { import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto'; import { Injectable } from '@nestjs/common'; +import { GetAdminRegionTreeWithOptionsDto } from '../admin-regions/dto/get-admin-region-tree-with-options.dto'; +import { AdminRegion } from '../admin-regions/admin-region.entity'; +import { SourcingLocation } from '../sourcing-locations/sourcing-location.entity'; +import { BaseQueryBuilder } from '../../utils/base.query-builder'; +import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; @Injectable() export class GeoRegionRepository extends Repository { @@ -128,4 +133,17 @@ export class GeoRegionRepository extends Repository { ], ); } + + async getGeoRegionsFromSourcingLocations( + getGeoRegionsDto: GetEUDRGeoRegions, + ): Promise { + const initialQueryBuilder: SelectQueryBuilder = + this.createQueryBuilder('gr') + .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id') + .distinct(true); + const queryBuilder: SelectQueryBuilder = + BaseQueryBuilder.addFilters(initialQueryBuilder, getGeoRegionsDto); + + return queryBuilder.getMany(); + } } diff --git a/api/src/modules/geo-regions/geo-regions.controller.ts b/api/src/modules/geo-regions/geo-regions.controller.ts index 48937c8e6..e91dd228e 100644 --- a/api/src/modules/geo-regions/geo-regions.controller.ts +++ b/api/src/modules/geo-regions/geo-regions.controller.ts @@ -6,6 +6,7 @@ import { Param, Patch, Post, + Query, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -35,6 +36,7 @@ import { import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto'; import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto'; import { PaginationMeta } from 'utils/app-base.service'; +import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; @Controller(`/api/v1/geo-regions`) @ApiTags(geoRegionResource.className) @@ -71,6 +73,32 @@ export class GeoRegionsController { return this.geoRegionsService.serialize(results.data, results.metadata); } + @ApiOperation({ + description: 'Find all EUDR geo regions', + }) + @ApiOkResponse({ + type: GeoRegion, + isArray: true, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @JSONAPIQueryParams({ + availableFilters: geoRegionResource.columnsAllowedAsFilter.map( + (columnName: string) => ({ + name: columnName, + }), + ), + }) + @Get('/eudr') + async findAllEudr( + @Query(ValidationPipe) + dto: GetEUDRGeoRegions, + ): Promise { + const results: GeoRegion[] = + await this.geoRegionsService.getGeoRegionsFromSourcingLocations(dto); + return this.geoRegionsService.serialize(results); + } + @ApiOperation({ description: 'Find geo region by id' }) @ApiOkResponse({ type: GeoRegion }) @ApiNotFoundResponse({ description: 'Geo region not found' }) diff --git a/api/src/modules/geo-regions/geo-regions.module.ts b/api/src/modules/geo-regions/geo-regions.module.ts index 6bcd35a66..4bacd115e 100644 --- a/api/src/modules/geo-regions/geo-regions.module.ts +++ b/api/src/modules/geo-regions/geo-regions.module.ts @@ -4,9 +4,17 @@ import { GeoRegionsController } from 'modules/geo-regions/geo-regions.controller import { GeoRegionsService } from 'modules/geo-regions/geo-regions.service'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { GeoRegionRepository } from 'modules/geo-regions/geo-region.repository'; +import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; +import { MaterialsModule } from 'modules/materials/materials.module'; +import { SuppliersModule } from 'modules/suppliers/suppliers.module'; @Module({ - imports: [TypeOrmModule.forFeature([GeoRegion])], + imports: [ + TypeOrmModule.forFeature([GeoRegion]), + AdminRegionsModule, + MaterialsModule, + SuppliersModule, + ], controllers: [GeoRegionsController], providers: [GeoRegionsService, GeoRegionRepository], exports: [GeoRegionsService, GeoRegionRepository], diff --git a/api/src/modules/geo-regions/geo-regions.service.ts b/api/src/modules/geo-regions/geo-regions.service.ts index 624f96ea8..7f3cc6c40 100644 --- a/api/src/modules/geo-regions/geo-regions.service.ts +++ b/api/src/modules/geo-regions/geo-regions.service.ts @@ -12,6 +12,12 @@ import { GeoRegionRepository } from 'modules/geo-regions/geo-region.repository'; import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto'; import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto'; import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto'; +import { GetSupplierByType } from '../suppliers/dto/get-supplier-by-type.dto'; +import { Supplier } from '../suppliers/supplier.entity'; +import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { MaterialsService } from 'modules/materials/materials.service'; +import { SupplierRepository } from 'modules/suppliers/supplier.repository'; +import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; @Injectable() export class GeoRegionsService extends AppBaseService< @@ -20,7 +26,11 @@ export class GeoRegionsService extends AppBaseService< UpdateGeoRegionDto, AppInfoDTO > { - constructor(protected readonly geoRegionRepository: GeoRegionRepository) { + constructor( + protected readonly geoRegionRepository: GeoRegionRepository, + private readonly adminRegionService: AdminRegionsService, + private readonly materialsService: MaterialsService, + ) { super( geoRegionRepository, geoRegionResource.name.singular, @@ -87,4 +97,22 @@ export class GeoRegionsService extends AppBaseService< async deleteGeoRegionsCreatedByUser(): Promise { await this.geoRegionRepository.delete({ isCreatedByUser: true }); } + + async getGeoRegionsFromSourcingLocations( + options: GetEUDRGeoRegions, + ): Promise { + if (options.originIds) { + options.originIds = + await this.adminRegionService.getAdminRegionDescendants( + options.originIds, + ); + } + if (options.materialIds) { + options.materialIds = await this.materialsService.getMaterialsDescendants( + options.materialIds, + ); + } + + return this.geoRegionRepository.getGeoRegionsFromSourcingLocations(options); + } } diff --git a/api/src/modules/suppliers/suppliers.service.ts b/api/src/modules/suppliers/suppliers.service.ts index 8f4edc7b5..359eaea21 100644 --- a/api/src/modules/suppliers/suppliers.service.ts +++ b/api/src/modules/suppliers/suppliers.service.ts @@ -134,18 +134,10 @@ export class SuppliersService extends AppBaseService< await this.supplierRepository.delete({}); } - async getSuppliersByIds(ids: string[]): Promise { - return this.supplierRepository.findByIds(ids); - } - async findTreesWithOptions(depth?: number): Promise { return this.supplierRepository.findTrees({ depth }); } - async findAllUnpaginated(): Promise { - return this.supplierRepository.find({}); - } - /** * * @description Get a tree of Suppliers that are associated with sourcing locations diff --git a/infrastructure/base/modules/aws/eks/thumbprint.sh b/infrastructure/base/modules/aws/eks/thumbprint.sh index 9f565c53c..10772d913 100755 --- a/infrastructure/base/modules/aws/eks/thumbprint.sh +++ b/infrastructure/base/modules/aws/eks/thumbprint.sh @@ -1,5 +1,5 @@ #!/bin/bash -THUMBPRINT=$(echo | openssl s_client -servername oidc.eks.${1}.amazonaws.com -showcerts -connect oidc.eks.${1}.amazonaws.com:443 2>&- | tail -r | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' | tail -r | openssl x509 -fingerprint -noout | sed 's/://g' | awk -F= '{print tolower($2)}') +THUMBPRINT=$(echo | openssl s_client -servername oidc.eks.${1}.amazonaws.com -showcerts -connect oidc.eks.${1}.amazonaws.com:443 2>&- | tac | sed -n '/-----END CERTIFICATE-----/,/-----BEGIN CERTIFICATE-----/p; /-----BEGIN CERTIFICATE-----/q' | tac | openssl x509 -fingerprint -noout | sed 's/://g' | awk -F= '{print tolower($2)}') THUMBPRINT_JSON="{\"thumbprint\": \"${THUMBPRINT}\"}" echo $THUMBPRINT_JSON From e5a005097c53d8a40e21c2689f49e068ef6344a4 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 28 Feb 2024 15:46:53 +0300 Subject: [PATCH 020/153] Fix pg json equality operator error, add tests --- .../geo-regions/geo-region.repository.ts | 3 +- .../geo-regions/geo-regions.controller.ts | 6 +- .../geo-regions/geo-regions.service.ts | 2 +- api/test/common-steps/sourcing-locations.ts | 168 ++++++++++++++++++ api/test/e2e/geo-regions/fixtures.ts | 63 +++++++ .../geo-regions/geo-regions-filters.spec.ts | 42 +++++ .../impact-reports/impact-reports.spec.ts | 2 +- 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 api/test/common-steps/sourcing-locations.ts create mode 100644 api/test/e2e/geo-regions/fixtures.ts create mode 100644 api/test/e2e/geo-regions/geo-regions-filters.spec.ts diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index d927c1e9d..365365038 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -140,7 +140,8 @@ export class GeoRegionRepository extends Repository { const initialQueryBuilder: SelectQueryBuilder = this.createQueryBuilder('gr') .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id') - .distinct(true); + // Using groupBy instead of distinct to avoid the euality opertor for type json + .groupBy('gr.id'); const queryBuilder: SelectQueryBuilder = BaseQueryBuilder.addFilters(initialQueryBuilder, getGeoRegionsDto); diff --git a/api/src/modules/geo-regions/geo-regions.controller.ts b/api/src/modules/geo-regions/geo-regions.controller.ts index e91dd228e..38194b949 100644 --- a/api/src/modules/geo-regions/geo-regions.controller.ts +++ b/api/src/modules/geo-regions/geo-regions.controller.ts @@ -95,7 +95,11 @@ export class GeoRegionsController { dto: GetEUDRGeoRegions, ): Promise { const results: GeoRegion[] = - await this.geoRegionsService.getGeoRegionsFromSourcingLocations(dto); + await this.geoRegionsService.getGeoRegionsFromSourcingLocations({ + ...dto, + withSourcingLocations: true, + eudr: true, + }); return this.geoRegionsService.serialize(results); } diff --git a/api/src/modules/geo-regions/geo-regions.service.ts b/api/src/modules/geo-regions/geo-regions.service.ts index 7f3cc6c40..4a9125cf4 100644 --- a/api/src/modules/geo-regions/geo-regions.service.ts +++ b/api/src/modules/geo-regions/geo-regions.service.ts @@ -40,7 +40,7 @@ export class GeoRegionsService extends AppBaseService< get serializerConfig(): JSONAPISerializerConfig { return { - attributes: ['name'], + attributes: ['name', 'theGeom'], keyForAttribute: 'camelCase', }; } diff --git a/api/test/common-steps/sourcing-locations.ts b/api/test/common-steps/sourcing-locations.ts new file mode 100644 index 000000000..7c5245a0d --- /dev/null +++ b/api/test/common-steps/sourcing-locations.ts @@ -0,0 +1,168 @@ +import { + LOCATION_TYPES, + SourcingLocation, +} from 'modules/sourcing-locations/sourcing-location.entity'; +import { + createAdminRegion, + createBusinessUnit, + createIndicator, + createIndicatorRecord, + createMaterial, + createSourcingLocation, + createSourcingRecord, + createSupplier, + createUnit, +} from '../entity-mocks'; +import { + Indicator, + INDICATOR_NAME_CODES, +} from '../../src/modules/indicators/indicator.entity'; +import { SourcingRecord } from '../../src/modules/sourcing-records/sourcing-record.entity'; +import { Material } from '../../src/modules/materials/material.entity'; + +type MockSourcingLocations = { + materials: Material[]; + sourcingLocations: SourcingLocation[]; + sourcingRecords: SourcingRecord[]; +}; + +type MockSourcingLocationsWithIndicators = MockSourcingLocations & { + indicators: Indicator[]; +}; + +export const CreateSourcingLocationsWithImpact = + async (): Promise => { + const parentMaterial = await createMaterial({ + name: 'Parent Material', + }); + const childMaterial = await createMaterial({ + parentId: parentMaterial.id, + name: 'Child Material', + }); + const supplier = await createSupplier(); + const businessUnit = await createBusinessUnit(); + const adminRegion = await createAdminRegion(); + const sourcingLocationParentMaterial = await createSourcingLocation({ + materialId: parentMaterial.id, + producerId: supplier.id, + businessUnitId: businessUnit.id, + adminRegionId: adminRegion.id, + }); + + const sourcingLocationChildMaterial = await createSourcingLocation({ + materialId: childMaterial.id, + producerId: supplier.id, + businessUnitId: businessUnit.id, + adminRegionId: adminRegion.id, + }); + const unit = await createUnit(); + const indicators: Indicator[] = []; + for (const indicator of Object.values(INDICATOR_NAME_CODES)) { + indicators.push( + await createIndicator({ + nameCode: indicator, + name: indicator, + unit, + shortName: indicator, + }), + ); + } + const sourcingRecords: SourcingRecord[] = []; + for (const year of [2018, 2019, 2020, 2021, 2022, 2023]) { + sourcingRecords.push( + await createSourcingRecord({ + sourcingLocationId: sourcingLocationParentMaterial.id, + year, + tonnage: 100 * year, + }), + ); + sourcingRecords.push( + await createSourcingRecord({ + sourcingLocationId: sourcingLocationChildMaterial.id, + year, + tonnage: 100 * year, + }), + ); + } + for (const sourcingRecord of sourcingRecords) { + for (const indicator of indicators) { + await createIndicatorRecord({ + sourcingRecordId: sourcingRecord.id, + indicatorId: indicator.id, + value: sourcingRecord.tonnage * 2, + }); + } + } + return { + materials: [parentMaterial, childMaterial], + indicators, + sourcingRecords, + sourcingLocations: [ + sourcingLocationParentMaterial, + sourcingLocationChildMaterial, + ], + }; + }; + +export const CreateEUDRSourcingLocations = + async (): Promise => { + const parentMaterial = await createMaterial({ + name: 'EUDR Parent Material', + }); + const childMaterial = await createMaterial({ + parentId: parentMaterial.id, + name: 'EUDR Child Material', + }); + const supplier = await createSupplier(); + const businessUnit = await createBusinessUnit(); + const adminRegion = await createAdminRegion(); + const sourcingLocationParentMaterial = await createSourcingLocation({ + materialId: parentMaterial.id, + producerId: supplier.id, + businessUnitId: businessUnit.id, + adminRegionId: adminRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + + const sourcingLocationChildMaterial = await createSourcingLocation({ + materialId: childMaterial.id, + producerId: supplier.id, + businessUnitId: businessUnit.id, + adminRegionId: adminRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + const unit = await createUnit(); + for (const indicator of Object.values(INDICATOR_NAME_CODES)) { + await createIndicator({ + nameCode: indicator, + name: indicator, + unit, + shortName: indicator, + }); + } + const sourcingRecords: SourcingRecord[] = []; + for (const year of [2018, 2019, 2020, 2021, 2022, 2023]) { + sourcingRecords.push( + await createSourcingRecord({ + sourcingLocationId: sourcingLocationParentMaterial.id, + year, + tonnage: 100 * year, + }), + ); + sourcingRecords.push( + await createSourcingRecord({ + sourcingLocationId: sourcingLocationChildMaterial.id, + year, + tonnage: 100 * year, + }), + ); + } + return { + materials: [parentMaterial, childMaterial], + sourcingLocations: [ + sourcingLocationParentMaterial, + sourcingLocationChildMaterial, + ], + sourcingRecords, + }; + }; diff --git a/api/test/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts new file mode 100644 index 000000000..3c4745df6 --- /dev/null +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -0,0 +1,63 @@ +import { createGeoRegion, createSourcingLocation } from '../../entity-mocks'; +import { TestApplication } from '../../utils/application-manager'; +import * as request from 'supertest'; +import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; +import { LOCATION_TYPES } from '../../../src/modules/sourcing-locations/sourcing-location.entity'; + +export const geoRegionFixtures = () => ({ + GivenGeoRegionsOfSourcingLocations: async () => { + const geoRegion = await createGeoRegion({ + name: 'Regular GeoRegion', + }); + const geoRegion2 = await createGeoRegion({ + name: 'Regular GeoRegion 2', + }); + await createSourcingLocation({ geoRegionId: geoRegion.id }); + await createSourcingLocation({ geoRegionId: geoRegion2.id }); + return { + geoRegions: [geoRegion, geoRegion2], + }; + }, + GivenEUDRGeoRegions: async () => { + const geoRegion = await createGeoRegion({ + name: 'EUDR GeoRegion', + }); + const geoRegion2 = await createGeoRegion({ + name: 'EUDR GeoRegion 2', + }); + await createSourcingLocation({ + geoRegionId: geoRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + await createSourcingLocation({ + geoRegionId: geoRegion2.id, + locationType: LOCATION_TYPES.EUDR, + }); + return { + eudrGeoRegions: [geoRegion, geoRegion2], + }; + }, + WhenIRequestEUDRGeoRegions: async (options: { + app: TestApplication; + jwtToken: string; + }) => { + return request(options.app.getHttpServer()) + .get(`/api/v1/geo-regions/eudr`) + .set('Authorization', `Bearer ${options.jwtToken}`); + }, + ThenIShouldOnlyReceiveEUDRGeoRegions: ( + response: request.Response, + eudrGeoRegions: GeoRegion[], + ) => { + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(eudrGeoRegions.length); + for (const geoRegion of eudrGeoRegions) { + expect( + response.body.data.find( + (geoRegionResponse: GeoRegion) => + geoRegionResponse.id === geoRegion.id, + ), + ).toBeDefined(); + } + }, +}); diff --git a/api/test/e2e/geo-regions/geo-regions-filters.spec.ts b/api/test/e2e/geo-regions/geo-regions-filters.spec.ts new file mode 100644 index 000000000..58ecc858e --- /dev/null +++ b/api/test/e2e/geo-regions/geo-regions-filters.spec.ts @@ -0,0 +1,42 @@ +import { DataSource } from 'typeorm'; +import ApplicationManager from '../../utils/application-manager'; +import { TestApplication } from '../../utils/application-manager'; +import { clearTestDataFromDatabase } from '../../utils/database-test-helper'; +import { setupTestUser } from '../../utils/userAuth'; + +import { geoRegionFixtures } from './fixtures'; + +describe('GeoRegions Filters (e2e)', () => { + const fixtures = geoRegionFixtures(); + let testApplication: TestApplication; + let jwtToken: string; + let dataSource: DataSource; + + beforeAll(async () => { + testApplication = await ApplicationManager.init(); + + dataSource = testApplication.get(DataSource); + }); + beforeEach(async () => { + ({ jwtToken } = await setupTestUser(testApplication)); + }); + + afterEach(async () => { + await clearTestDataFromDatabase(dataSource); + }); + + afterAll(async () => { + await testApplication.close(); + }); + describe('EUDR Geo Regions Filters', () => { + it('should only get geo-regions that are part of EUDR data', async () => { + await fixtures.GivenGeoRegionsOfSourcingLocations(); + const { eudrGeoRegions } = await fixtures.GivenEUDRGeoRegions(); + const response = await fixtures.WhenIRequestEUDRGeoRegions({ + app: testApplication, + jwtToken, + }); + fixtures.ThenIShouldOnlyReceiveEUDRGeoRegions(response, eudrGeoRegions); + }); + }); +}); diff --git a/api/test/e2e/impact/impact-reports/impact-reports.spec.ts b/api/test/e2e/impact/impact-reports/impact-reports.spec.ts index 748270b53..88102fdfa 100644 --- a/api/test/e2e/impact/impact-reports/impact-reports.spec.ts +++ b/api/test/e2e/impact/impact-reports/impact-reports.spec.ts @@ -38,7 +38,7 @@ describe('Impact Reports', () => { indicatorIds: indicators.map((indicator: Indicator) => indicator.id), }); - await fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { + fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { materials, }); }); From 6867a97e2eea554dd665716bee6949dadbade667 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 29 Feb 2024 07:47:02 +0300 Subject: [PATCH 021/153] eudr admin region filter tests --- .../common-steps/and-associated-materials.ts | 21 ++++ .../common-steps/and-associated-suppliers.ts | 30 +++++ .../admin-regions-eudr-smart-filters.spec.ts | 84 +++++++++++++ api/test/e2e/admin-regions/fixtures.ts | 117 ++++++++++++++++++ ...c.ts => geo-regions-smart-filters.spec.ts} | 0 .../impact-reports/impact-reports.spec.ts | 4 +- 6 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 api/test/common-steps/and-associated-materials.ts create mode 100644 api/test/common-steps/and-associated-suppliers.ts create mode 100644 api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts create mode 100644 api/test/e2e/admin-regions/fixtures.ts rename api/test/e2e/geo-regions/{geo-regions-filters.spec.ts => geo-regions-smart-filters.spec.ts} (100%) diff --git a/api/test/common-steps/and-associated-materials.ts b/api/test/common-steps/and-associated-materials.ts new file mode 100644 index 000000000..ed30fd2fe --- /dev/null +++ b/api/test/common-steps/and-associated-materials.ts @@ -0,0 +1,21 @@ +import { Material } from 'modules/materials/material.entity'; +import { SourcingLocation } from '../../src/modules/sourcing-locations/sourcing-location.entity'; + +/** + * @description Associate materials with sourcing locations for tests + */ + +export const AndAssociatedMaterials = async ( + materials: Material[], + existingSourcingLocations: SourcingLocation[], +): Promise => { + const limitLength = Math.min( + materials.length, + existingSourcingLocations.length, + ); + for (let i = 0; i < limitLength; i++) { + existingSourcingLocations[i].materialId = materials[i].id; + await existingSourcingLocations[i].save(); + } + return existingSourcingLocations; +}; diff --git a/api/test/common-steps/and-associated-suppliers.ts b/api/test/common-steps/and-associated-suppliers.ts new file mode 100644 index 000000000..eb0b5f6a9 --- /dev/null +++ b/api/test/common-steps/and-associated-suppliers.ts @@ -0,0 +1,30 @@ +import { SourcingLocation } from '../../src/modules/sourcing-locations/sourcing-location.entity'; +import { + Supplier, + SUPPLIER_TYPES, +} from '../../src/modules/suppliers/supplier.entity'; + +/** + * @description Associate suppliers with sourcing locations for tests + */ + +export const AndAssociatedSuppliers = async ( + supplier: Supplier[], + existingSourcingLocations: SourcingLocation[], + supplierType?: SUPPLIER_TYPES, +): Promise => { + const limitLength = Math.min( + supplier.length, + existingSourcingLocations.length, + ); + for (let i = 0; i < limitLength; i++) { + if (supplierType === SUPPLIER_TYPES.PRODUCER || !supplierType) { + existingSourcingLocations[i].producerId = supplier[i].id; + } + if (supplierType === SUPPLIER_TYPES.T1SUPPLIER) { + existingSourcingLocations[i].t1SupplierId = supplier[i].id; + } + await existingSourcingLocations[i].save(); + } + return existingSourcingLocations; +}; diff --git a/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts b/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts new file mode 100644 index 000000000..8b3f98bb0 --- /dev/null +++ b/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts @@ -0,0 +1,84 @@ +import { DataSource } from 'typeorm'; +import { createMaterial, createSupplier } from '../../entity-mocks'; +import ApplicationManager from '../../utils/application-manager'; +import { TestApplication } from '../../utils/application-manager'; +import { clearTestDataFromDatabase } from '../../utils/database-test-helper'; +import { setupTestUser } from '../../utils/userAuth'; +import { adminRegionsFixtures } from './fixtures'; + +describe('GeoRegions Filters (e2e)', () => { + const fixtures = adminRegionsFixtures(); + let testApplication: TestApplication; + let jwtToken: string; + let dataSource: DataSource; + + beforeAll(async () => { + testApplication = await ApplicationManager.init(); + + dataSource = testApplication.get(DataSource); + }); + beforeEach(async () => { + ({ jwtToken } = await setupTestUser(testApplication)); + }); + + afterEach(async () => { + await clearTestDataFromDatabase(dataSource); + }); + + afterAll(async () => { + await testApplication.close(); + }); + describe('EUDR Admin Regions Filters', () => { + it('should only get geo-regions that are part of EUDR data', async () => { + await fixtures.GivenAdminRegionsOfSourcingLocations(); + const { eudrAdminRegions } = await fixtures.GivenEUDRAdminRegions(); + const response = await fixtures.WhenIRequestEUDRAdminRegions({ + app: testApplication, + jwtToken, + }); + fixtures.ThenIShouldOnlyReceiveEUDRAdminRegions( + response, + eudrAdminRegions, + ); + }); + it('should only get geo-regions that are part of EUDR data and are filtered', async () => { + const { sourcingLocations } = + await fixtures.GivenAdminRegionsOfSourcingLocations(); + const regularMaterial = await createMaterial({ + name: 'Regular Material', + }); + await fixtures.AndAssociatedMaterials( + [regularMaterial], + sourcingLocations, + ); + const regularSupplier = await createSupplier({ + name: 'Regular Supplier', + }); + await fixtures.AndAssociatedSuppliers( + [regularSupplier], + sourcingLocations, + ); + const { eudrAdminRegions, eudrSourcingLocations } = + await fixtures.GivenEUDRAdminRegions(); + const eudrMaterial = await createMaterial({ name: 'EUDR Material' }); + await fixtures.AndAssociatedMaterials( + [eudrMaterial], + [eudrSourcingLocations[0]], + ); + const eudrSupplier = await createSupplier({ name: 'EUDR Supplier' }); + await fixtures.AndAssociatedSuppliers( + [eudrSupplier], + eudrSourcingLocations, + ); + const response = await fixtures.WhenIRequestEUDRAdminRegionWithFilters({ + app: testApplication, + jwtToken, + materialIds: [eudrMaterial.id], + supplierIds: [eudrSupplier.id], + }); + fixtures.ThenIShouldOnlyReceiveFilteredEUDRAdminRegions(response, [ + eudrAdminRegions[0], + ]); + }); + }); +}); diff --git a/api/test/e2e/admin-regions/fixtures.ts b/api/test/e2e/admin-regions/fixtures.ts new file mode 100644 index 000000000..c0a270f2f --- /dev/null +++ b/api/test/e2e/admin-regions/fixtures.ts @@ -0,0 +1,117 @@ +import * as request from 'supertest'; +import { + LOCATION_TYPES, + SourcingLocation, +} from 'modules/sourcing-locations/sourcing-location.entity'; +import { createAdminRegion, createSourcingLocation } from '../../entity-mocks'; +import { TestApplication } from '../../utils/application-manager'; +import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +import { AndAssociatedMaterials } from '../../common-steps/and-associated-materials'; +import { AndAssociatedSuppliers } from '../../common-steps/and-associated-suppliers'; +import { Material } from 'modules/materials/material.entity'; +import { Supplier } from '../../../src/modules/suppliers/supplier.entity'; + +export const adminRegionsFixtures = () => ({ + GivenAdminRegionsOfSourcingLocations: async () => { + const adminRegion = await createAdminRegion({ + name: 'Regular AdminRegion', + }); + const adminRegion2 = await createAdminRegion({ + name: 'Regular AdminRegion 2', + }); + const sourcingLocation1 = await createSourcingLocation({ + adminRegionId: adminRegion.id, + }); + const sourcingLocation2 = await createSourcingLocation({ + adminRegionId: adminRegion2.id, + }); + return { + adminRegions: [adminRegion, adminRegion2], + sourcingLocations: [sourcingLocation1, sourcingLocation2], + }; + }, + AndAssociatedMaterials: async ( + materials: Material[], + sourcingLocations: SourcingLocation[], + ) => { + return AndAssociatedMaterials(materials, sourcingLocations); + }, + AndAssociatedSuppliers: async ( + suppliers: Supplier[], + sourcingLocations: SourcingLocation[], + ) => { + return AndAssociatedSuppliers(suppliers, sourcingLocations); + }, + GivenEUDRAdminRegions: async () => { + const adminRegion = await createAdminRegion({ + name: 'EUDR AdminRegion', + }); + const adminRegion2 = await createAdminRegion({ + name: 'EUDR AdminRegion 2', + }); + const eudrSourcingLocation1 = await createSourcingLocation({ + adminRegionId: adminRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + const eudrSourcingLocation2 = await createSourcingLocation({ + adminRegionId: adminRegion2.id, + locationType: LOCATION_TYPES.EUDR, + }); + return { + eudrAdminRegions: [adminRegion, adminRegion2], + eudrSourcingLocations: [eudrSourcingLocation1, eudrSourcingLocation2], + }; + }, + WhenIRequestEUDRAdminRegions: async (options: { + app: TestApplication; + jwtToken: string; + }) => { + return request(options.app.getHttpServer()) + .get(`/api/v1/admin-regions/trees/eudr`) + .set('Authorization', `Bearer ${options.jwtToken}`); + }, + WhenIRequestEUDRAdminRegionWithFilters: async (options: { + app: TestApplication; + jwtToken: string; + supplierIds?: string[]; + materialIds?: string[]; + }) => { + return request(options.app.getHttpServer()) + .get(`/api/v1/admin-regions/trees/eudr`) + .set('Authorization', `Bearer ${options.jwtToken}`) + .query({ + 'producerIds[]': options.supplierIds, + 'materialIds[]': options.materialIds, + }); + }, + ThenIShouldOnlyReceiveFilteredEUDRAdminRegions: ( + response: request.Response, + eudrAdminRegions: AdminRegion[], + ) => { + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(eudrAdminRegions.length); + for (const adminRegion of eudrAdminRegions) { + expect( + response.body.data.find( + (adminRegionResponse: AdminRegion) => + adminRegionResponse.id === adminRegion.id, + ), + ).toBeDefined(); + } + }, + ThenIShouldOnlyReceiveEUDRAdminRegions: ( + response: request.Response, + eudrAdminRegions: AdminRegion[], + ) => { + expect(response.status).toBe(200); + expect(response.body.data.length).toBe(eudrAdminRegions.length); + for (const adminRegion of eudrAdminRegions) { + expect( + response.body.data.find( + (adminRegionResponse: AdminRegion) => + adminRegionResponse.id === adminRegion.id, + ), + ).toBeDefined(); + } + }, +}); diff --git a/api/test/e2e/geo-regions/geo-regions-filters.spec.ts b/api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts similarity index 100% rename from api/test/e2e/geo-regions/geo-regions-filters.spec.ts rename to api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts diff --git a/api/test/e2e/impact/impact-reports/impact-reports.spec.ts b/api/test/e2e/impact/impact-reports/impact-reports.spec.ts index 88102fdfa..5202eca5c 100644 --- a/api/test/e2e/impact/impact-reports/impact-reports.spec.ts +++ b/api/test/e2e/impact/impact-reports/impact-reports.spec.ts @@ -52,7 +52,7 @@ describe('Impact Reports', () => { comparedScenarioId: scenarioIntervention.scenarioId, }); - await fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { + fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { indicators: [indicator], isActualVsScenario: true, }); @@ -69,7 +69,7 @@ describe('Impact Reports', () => { indicatorIds: [indicator.id], }, ); - await fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { + fixtures.ThenIShouldGetAnImpactReportAboutProvidedFilters(response, { indicators: [indicator], isScenarioVsScenario: true, }); From 596d9a64b561448fa9168b212e1625529a55004f Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 29 Feb 2024 12:48:24 +0300 Subject: [PATCH 022/153] Improve testing framework, add tests for eudr admin-regions --- .../import-data/eudr/eudr.import.service.ts | 1 - .../admin-regions-eudr-smart-filters.spec.ts | 84 +++++---------- api/test/e2e/admin-regions/fixtures.ts | 100 +++++++----------- api/test/utils/application-manager.ts | 4 +- api/test/utils/random-name.ts | 8 ++ api/test/utils/test-manager.ts | 99 +++++++++++++++++ api/test/utils/userAuth.ts | 36 ++++--- 7 files changed, 196 insertions(+), 136 deletions(-) create mode 100644 api/test/utils/random-name.ts create mode 100644 api/test/utils/test-manager.ts diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index 7880ec346..4183551e3 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -102,7 +102,6 @@ export class EudrImportService { const fakedCartoOutput: any[] = data.map((row: any) => this.generateFakeAlerts(row), ); - console.log(fakedCartoOutput); const parsed: any = await new AsyncParser({ fields: [ diff --git a/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts b/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts index 8b3f98bb0..1c405a93d 100644 --- a/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts +++ b/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts @@ -1,82 +1,50 @@ -import { DataSource } from 'typeorm'; -import { createMaterial, createSupplier } from '../../entity-mocks'; -import ApplicationManager from '../../utils/application-manager'; -import { TestApplication } from '../../utils/application-manager'; -import { clearTestDataFromDatabase } from '../../utils/database-test-helper'; -import { setupTestUser } from '../../utils/userAuth'; -import { adminRegionsFixtures } from './fixtures'; +import { AdminRegionTestManager } from './fixtures'; -describe('GeoRegions Filters (e2e)', () => { - const fixtures = adminRegionsFixtures(); - let testApplication: TestApplication; - let jwtToken: string; - let dataSource: DataSource; +describe('Admin Regions EUDR Filters (e2e)', () => { + let testManager: AdminRegionTestManager; beforeAll(async () => { - testApplication = await ApplicationManager.init(); - - dataSource = testApplication.get(DataSource); + testManager = await AdminRegionTestManager.load(); }); + beforeEach(async () => { - ({ jwtToken } = await setupTestUser(testApplication)); + await testManager.refreshState(); }); afterEach(async () => { - await clearTestDataFromDatabase(dataSource); + await testManager.clearDatabase(); }); afterAll(async () => { - await testApplication.close(); + await testManager.close(); }); describe('EUDR Admin Regions Filters', () => { - it('should only get geo-regions that are part of EUDR data', async () => { - await fixtures.GivenAdminRegionsOfSourcingLocations(); - const { eudrAdminRegions } = await fixtures.GivenEUDRAdminRegions(); - const response = await fixtures.WhenIRequestEUDRAdminRegions({ - app: testApplication, - jwtToken, - }); - fixtures.ThenIShouldOnlyReceiveEUDRAdminRegions( - response, + it('should only get admin-regions that are part of EUDR data', async () => { + await testManager.GivenAdminRegionsOfSourcingLocations(); + const { eudrAdminRegions } = await testManager.GivenEUDRAdminRegions(); + await testManager.WhenIRequestEUDRAdminRegions(); + testManager.ThenIShouldOnlyReceiveCorrespondingAdminRegions( eudrAdminRegions, ); }); - it('should only get geo-regions that are part of EUDR data and are filtered', async () => { + it('should only get admin-regions that are part of EUDR data and are filtered', async () => { const { sourcingLocations } = - await fixtures.GivenAdminRegionsOfSourcingLocations(); - const regularMaterial = await createMaterial({ - name: 'Regular Material', - }); - await fixtures.AndAssociatedMaterials( - [regularMaterial], - sourcingLocations, - ); - const regularSupplier = await createSupplier({ - name: 'Regular Supplier', - }); - await fixtures.AndAssociatedSuppliers( - [regularSupplier], - sourcingLocations, - ); + await testManager.GivenAdminRegionsOfSourcingLocations(); + await testManager.AndAssociatedMaterials(sourcingLocations); + await testManager.AndAssociatedSuppliers([sourcingLocations[0]]); const { eudrAdminRegions, eudrSourcingLocations } = - await fixtures.GivenEUDRAdminRegions(); - const eudrMaterial = await createMaterial({ name: 'EUDR Material' }); - await fixtures.AndAssociatedMaterials( - [eudrMaterial], - [eudrSourcingLocations[0]], - ); - const eudrSupplier = await createSupplier({ name: 'EUDR Supplier' }); - await fixtures.AndAssociatedSuppliers( - [eudrSupplier], + await testManager.GivenEUDRAdminRegions(); + const eudrMaterials = await testManager.AndAssociatedMaterials([ + eudrSourcingLocations[0], + ]); + const eudrSuppliers = await testManager.AndAssociatedSuppliers( eudrSourcingLocations, ); - const response = await fixtures.WhenIRequestEUDRAdminRegionWithFilters({ - app: testApplication, - jwtToken, - materialIds: [eudrMaterial.id], - supplierIds: [eudrSupplier.id], + await testManager.WhenIRequestEUDRAdminRegions({ + 'materialIds[]': [eudrMaterials[0].id], + 'producerIds[]': eudrSuppliers.map((s) => s.id), }); - fixtures.ThenIShouldOnlyReceiveFilteredEUDRAdminRegions(response, [ + testManager.ThenIShouldOnlyReceiveCorrespondingAdminRegions([ eudrAdminRegions[0], ]); }); diff --git a/api/test/e2e/admin-regions/fixtures.ts b/api/test/e2e/admin-regions/fixtures.ts index c0a270f2f..45c5542b5 100644 --- a/api/test/e2e/admin-regions/fixtures.ts +++ b/api/test/e2e/admin-regions/fixtures.ts @@ -1,18 +1,25 @@ -import * as request from 'supertest'; import { LOCATION_TYPES, SourcingLocation, } from 'modules/sourcing-locations/sourcing-location.entity'; import { createAdminRegion, createSourcingLocation } from '../../entity-mocks'; -import { TestApplication } from '../../utils/application-manager'; import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { AndAssociatedMaterials } from '../../common-steps/and-associated-materials'; import { AndAssociatedSuppliers } from '../../common-steps/and-associated-suppliers'; -import { Material } from 'modules/materials/material.entity'; -import { Supplier } from '../../../src/modules/suppliers/supplier.entity'; +import { TestManager } from '../../utils/test-manager'; -export const adminRegionsFixtures = () => ({ - GivenAdminRegionsOfSourcingLocations: async () => { +export class AdminRegionTestManager extends TestManager { + url = '/api/v1/admin-regions/'; + + constructor(manager: TestManager) { + super(manager.testApp, manager.jwtToken, manager.dataSource); + } + + static async load() { + return new AdminRegionTestManager(await this.createManager()); + } + + GivenAdminRegionsOfSourcingLocations = async () => { const adminRegion = await createAdminRegion({ name: 'Regular AdminRegion', }); @@ -29,20 +36,24 @@ export const adminRegionsFixtures = () => ({ adminRegions: [adminRegion, adminRegion2], sourcingLocations: [sourcingLocation1, sourcingLocation2], }; - }, - AndAssociatedMaterials: async ( - materials: Material[], + }; + AndAssociatedMaterials = async ( sourcingLocations: SourcingLocation[], + materialNames?: string[], ) => { - return AndAssociatedMaterials(materials, sourcingLocations); - }, - AndAssociatedSuppliers: async ( - suppliers: Supplier[], + const materials = await this.createMaterials(materialNames); + await AndAssociatedMaterials(materials, sourcingLocations); + return materials; + }; + AndAssociatedSuppliers = async ( sourcingLocations: SourcingLocation[], + supplierNames?: string[], ) => { - return AndAssociatedSuppliers(suppliers, sourcingLocations); - }, - GivenEUDRAdminRegions: async () => { + const suppliers = await this.createSuppliers(supplierNames); + await AndAssociatedSuppliers(suppliers, sourcingLocations); + return suppliers; + }; + GivenEUDRAdminRegions = async () => { const adminRegion = await createAdminRegion({ name: 'EUDR AdminRegion', }); @@ -61,57 +72,26 @@ export const adminRegionsFixtures = () => ({ eudrAdminRegions: [adminRegion, adminRegion2], eudrSourcingLocations: [eudrSourcingLocation1, eudrSourcingLocation2], }; - }, - WhenIRequestEUDRAdminRegions: async (options: { - app: TestApplication; - jwtToken: string; - }) => { - return request(options.app.getHttpServer()) - .get(`/api/v1/admin-regions/trees/eudr`) - .set('Authorization', `Bearer ${options.jwtToken}`); - }, - WhenIRequestEUDRAdminRegionWithFilters: async (options: { - app: TestApplication; - jwtToken: string; - supplierIds?: string[]; - materialIds?: string[]; + }; + WhenIRequestEUDRAdminRegions = async (filters?: { + 'producerIds[]'?: string[]; + 'materialIds[]'?: string[]; }) => { - return request(options.app.getHttpServer()) - .get(`/api/v1/admin-regions/trees/eudr`) - .set('Authorization', `Bearer ${options.jwtToken}`) - .query({ - 'producerIds[]': options.supplierIds, - 'materialIds[]': options.materialIds, - }); - }, - ThenIShouldOnlyReceiveFilteredEUDRAdminRegions: ( - response: request.Response, - eudrAdminRegions: AdminRegion[], - ) => { - expect(response.status).toBe(200); - expect(response.body.data.length).toBe(eudrAdminRegions.length); - for (const adminRegion of eudrAdminRegions) { - expect( - response.body.data.find( - (adminRegionResponse: AdminRegion) => - adminRegionResponse.id === adminRegion.id, - ), - ).toBeDefined(); - } - }, - ThenIShouldOnlyReceiveEUDRAdminRegions: ( - response: request.Response, + return this.GET({ url: `${this.url}trees/eudr`, query: filters }); + }; + + ThenIShouldOnlyReceiveCorrespondingAdminRegions = ( eudrAdminRegions: AdminRegion[], ) => { - expect(response.status).toBe(200); - expect(response.body.data.length).toBe(eudrAdminRegions.length); + expect(this.response!.status).toBe(200); + expect(this.response!.body.data.length).toBe(eudrAdminRegions.length); for (const adminRegion of eudrAdminRegions) { expect( - response.body.data.find( + this.response!.body.data.find( (adminRegionResponse: AdminRegion) => adminRegionResponse.id === adminRegion.id, ), ).toBeDefined(); } - }, -}); + }; +} diff --git a/api/test/utils/application-manager.ts b/api/test/utils/application-manager.ts index 980e8f134..2351df515 100644 --- a/api/test/utils/application-manager.ts +++ b/api/test/utils/application-manager.ts @@ -40,11 +40,11 @@ export default class ApplicationManager { const testingModuleBuilder: TestingModuleBuilder = initTestingModuleBuilder || - (await Test.createTestingModule({ + Test.createTestingModule({ imports: [AppModule], }) .overrideProvider('IEmailService') - .useClass(MockEmailService)); + .useClass(MockEmailService); ApplicationManager.testApplication.moduleFixture = await testingModuleBuilder.compile(); diff --git a/api/test/utils/random-name.ts b/api/test/utils/random-name.ts new file mode 100644 index 000000000..578ed2a64 --- /dev/null +++ b/api/test/utils/random-name.ts @@ -0,0 +1,8 @@ +export const randomName = (length = 10): string => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +}; diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts new file mode 100644 index 000000000..3c6bd70b3 --- /dev/null +++ b/api/test/utils/test-manager.ts @@ -0,0 +1,99 @@ +import { DataSource } from 'typeorm'; +import ApplicationManager, { TestApplication } from './application-manager'; +import { clearTestDataFromDatabase } from './database-test-helper'; +import { setupTestUser } from './userAuth'; +import * as request from 'supertest'; +import { + createGeoRegion, + createMaterial, + createSupplier, +} from '../entity-mocks'; +import { Material } from 'modules/materials/material.entity'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { randomName } from './random-name'; + +export class TestManager { + testApp: TestApplication; + jwtToken: string; + dataSource: DataSource; + response?: request.Response; + materials?: Material[]; + suppliers?: Supplier[]; + geoRegions?: GeoRegion[]; + + constructor(app: TestApplication, jwtToken: string, dataSource: DataSource) { + this.testApp = app; + this.jwtToken = jwtToken; + this.dataSource = dataSource; + } + + static async createManager() { + const testApplication = await ApplicationManager.init(); + const dataSource = testApplication.get(DataSource); + const { jwtToken } = await setupTestUser(testApplication); + return new TestManager(testApplication, jwtToken, dataSource); + } + + async refreshState() { + const { jwtToken } = await setupTestUser(this.testApp); + this.jwtToken = jwtToken; + this.materials = undefined; + this.suppliers = undefined; + this.geoRegions = undefined; + this.response = undefined; + } + + async clearDatabase() { + await clearTestDataFromDatabase(this.dataSource); + } + + async GET(options: { url: string; query?: object | string }) { + this.response = await request(this.testApp.getHttpServer()) + .get(options.url) + .query(options?.query || '') + .set('Authorization', `Bearer ${this.token}`); + return this.response; + } + + get token() { + if (!this.jwtToken) { + throw new Error('TestManager has no token available!'); + } + return this.jwtToken; + } + + async close() { + await this.testApp.close(); + } + + async createMaterials(names?: string[]) { + const namesToCreate = names || [randomName()]; + const createdMaterials: Material[] = []; + for (let i = 0; i < namesToCreate.length; i++) { + createdMaterials.push(await createMaterial({ name: namesToCreate[i] })); + } + this.materials?.push(...createdMaterials); + return createdMaterials; + } + + async createSuppliers(names?: string[]) { + const namesToCreate = names || [randomName()]; + const createdSuppliers: Supplier[] = []; + for (let i = 0; i < namesToCreate.length; i++) { + createdSuppliers.push(await createSupplier({ name: namesToCreate[i] })); + } + this.suppliers?.push(...createdSuppliers); + return createdSuppliers; + } + + async createGeoRegion(names?: string[]) { + const namesToCreate = names || [randomName()]; + const createdGeoRegions: GeoRegion[] = []; + for (let i = 0; i < namesToCreate.length; i++) { + createdGeoRegions.push(await createGeoRegion({ name: namesToCreate[i] })); + } + this.geoRegions?.push(...createdGeoRegions); + return createdGeoRegions; + } +} diff --git a/api/test/utils/userAuth.ts b/api/test/utils/userAuth.ts index ac9950874..0707d9a21 100644 --- a/api/test/utils/userAuth.ts +++ b/api/test/utils/userAuth.ts @@ -6,7 +6,6 @@ import { ROLES } from 'modules/authorization/roles/roles.enum'; import { User } from 'modules/users/user.entity'; import { EntityManager } from 'typeorm'; import { TestApplication } from './application-manager'; -import { faker } from '@faker-js/faker'; import { Permission } from '../../src/modules/authorization/permissions/permissions.entity'; import { PERMISSIONS } from '../../src/modules/authorization/permissions/permissions.enum'; @@ -15,7 +14,7 @@ export type TestUser = { jwtToken: string; user: User; password: string }; export async function setupTestUser( applicationManager: TestApplication, roleName: ROLES = ROLES.ADMIN, - extraData: Partial = {}, + extraData: Partial = { password: 'Password123!' }, ): Promise { const salt = await genSalt(); const role = new Role(); @@ -23,26 +22,33 @@ export async function setupTestUser( const entityManager = applicationManager.get(EntityManager); const userRepository = entityManager.getRepository(User); - const { password: extraDataPassword, ...restOfExtraData } = extraData; - - const password = extraDataPassword ?? faker.internet.password(); + const { password, ...restOfExtraData } = extraData; await setUpRolesAndPermissions(entityManager); - const user = await userRepository.save({ - ...E2E_CONFIG.users.signUp, - salt, - password: await hash(password, salt), - isActive: true, - isDeleted: false, - roles: [role], - ...restOfExtraData, + let existingUser = await userRepository.findOne({ + where: { email: E2E_CONFIG.users.signUp.email }, }); + if (!existingUser) { + existingUser = await userRepository.save({ + ...E2E_CONFIG.users.signUp, + salt, + password: await hash(password!, salt), + isActive: true, + isDeleted: false, + roles: [role], + }); + } + const response = await request(applicationManager.application.getHttpServer()) .post('/auth/sign-in') - .send({ username: user.email, password: password }); + .send({ username: existingUser.email, password: password }); - return { jwtToken: response.body.accessToken, user, password }; + return { + jwtToken: response.body.accessToken, + user: existingUser, + password: password!, + }; } async function setUpRolesAndPermissions( From 294f60e4af015f0356f78ffadb5fb228ed650f7b Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 29 Feb 2024 15:19:31 +0300 Subject: [PATCH 023/153] Refactor eudr related endpoints to eudr controller --- .../admin-regions/admin-regions.controller.ts | 23 --- api/src/modules/eudr/eudr.controller.ts | 132 +++++++++++++++++- api/src/modules/eudr/eudr.module.ts | 12 +- .../geo-regions/geo-regions.controller.ts | 30 ---- .../modules/materials/materials.controller.ts | 21 --- .../modules/suppliers/suppliers.controller.ts | 27 +--- .../eudr-admin-region-filters.spec.ts} | 6 +- .../e2e/eudr/eudr-geo-region-filters.spec.ts | 28 ++++ .../e2e/{admin-regions => eudr}/fixtures.ts | 61 +++++++- api/test/utils/test-manager.ts | 2 +- api/test/utils/userAuth.ts | 1 + 11 files changed, 232 insertions(+), 111 deletions(-) rename api/test/e2e/{admin-regions/admin-regions-eudr-smart-filters.spec.ts => eudr/eudr-admin-region-filters.spec.ts} (91%) create mode 100644 api/test/e2e/eudr/eudr-geo-region-filters.spec.ts rename api/test/e2e/{admin-regions => eudr}/fixtures.ts (62%) diff --git a/api/src/modules/admin-regions/admin-regions.controller.ts b/api/src/modules/admin-regions/admin-regions.controller.ts index 031331bdc..b44cffd83 100644 --- a/api/src/modules/admin-regions/admin-regions.controller.ts +++ b/api/src/modules/admin-regions/admin-regions.controller.ts @@ -100,29 +100,6 @@ export class AdminRegionsController { return this.adminRegionsService.serialize(results); } - @ApiOperation({ - description: - 'Find all EUDR admin regions and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', - }) - @ApiOkTreeResponse({ - treeNodeType: AdminRegion, - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @UseInterceptors(SetScenarioIdsInterceptor) - @Get('/trees/eudr') - async getTreesForEudr( - @Query(ValidationPipe) - adminRegionTreeOptions: GetEUDRAdminRegions, - ): Promise { - const results: AdminRegion[] = await this.adminRegionsService.getTrees({ - ...adminRegionTreeOptions, - withSourcingLocations: true, - eudr: true, - }); - return this.adminRegionsService.serialize(results); - } - @ApiOperation({ description: 'Find all admin regions given a country and return data in a tree format. Data in the "children" will recursively extend for the full depth of the tree', diff --git a/api/src/modules/eudr/eudr.controller.ts b/api/src/modules/eudr/eudr.controller.ts index 602cbb330..b0393ec1a 100644 --- a/api/src/modules/eudr/eudr.controller.ts +++ b/api/src/modules/eudr/eudr.controller.ts @@ -1,10 +1,138 @@ -import { Controller, Get } from '@nestjs/common'; +import { + Controller, + Get, + Query, + UseInterceptors, + ValidationPipe, +} from '@nestjs/common'; import { Public } from 'decorators/public.decorator'; import { CartoConnector } from 'modules/eudr/carto/carto.connector'; +import { + ApiForbiddenResponse, + ApiOkResponse, + ApiOperation, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { ApiOkTreeResponse } from '../../decorators/api-tree-response.decorator'; +import { Supplier } from '../suppliers/supplier.entity'; +import { SetScenarioIdsInterceptor } from '../impact/set-scenario-ids.interceptor'; +import { GetSupplierEUDR } from '../suppliers/dto/get-supplier-by-type.dto'; +import { SuppliersService } from '../suppliers/suppliers.service'; +import { MaterialsService } from 'modules/materials/materials.service'; +import { GeoRegionsService } from 'modules/geo-regions/geo-regions.service'; +import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { Material } from '../materials/material.entity'; +import { GetEUDRMaterials } from '../materials/dto/get-material-tree-with-options.dto'; +import { AdminRegion } from '../admin-regions/admin-region.entity'; +import { GetEUDRAdminRegions } from '../admin-regions/dto/get-admin-region-tree-with-options.dto'; +import { GeoRegion, geoRegionResource } from '../geo-regions/geo-region.entity'; +import { JSONAPIQueryParams } from '../../decorators/json-api-parameters.decorator'; +import { GetEUDRGeoRegions } from '../geo-regions/dto/get-geo-region.dto'; @Controller('/api/v1/eudr') export class EudrController { - constructor(private readonly carto: CartoConnector) {} + constructor( + private readonly carto: CartoConnector, + private readonly suppliersService: SuppliersService, + private readonly materialsService: MaterialsService, + private readonly geoRegionsService: GeoRegionsService, + private readonly adminRegionsService: AdminRegionsService, + ) {} + + @ApiOperation({ + description: + 'Find all EUDR suppliers and return them in a flat format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: Supplier, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('/suppliers') + async getSuppliers( + @Query(ValidationPipe) dto: GetSupplierEUDR, + ): Promise { + const results: Supplier[] = await this.suppliersService.getSupplierByType({ + ...dto, + eudr: true, + }); + return this.suppliersService.serialize(results); + } + + @ApiOperation({ + description: + 'Find all EUDR materials and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: Material, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/eudr') + async getMaterialsTree( + @Query(ValidationPipe) materialTreeOptions: GetEUDRMaterials, + ): Promise { + const results: Material[] = await this.materialsService.getTrees({ + ...materialTreeOptions, + withSourcingLocations: true, + eudr: true, + }); + return this.materialsService.serialize(results); + } + + @ApiOperation({ + description: + 'Find all EUDR admin regions and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', + }) + @ApiOkTreeResponse({ + treeNodeType: AdminRegion, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @UseInterceptors(SetScenarioIdsInterceptor) + @Get('/admin-regions') + async getTreesForEudr( + @Query(ValidationPipe) + adminRegionTreeOptions: GetEUDRAdminRegions, + ): Promise { + const results: AdminRegion[] = await this.adminRegionsService.getTrees({ + ...adminRegionTreeOptions, + withSourcingLocations: true, + eudr: true, + }); + return this.adminRegionsService.serialize(results); + } + + @ApiOperation({ + description: 'Find all EUDR geo regions', + }) + @ApiOkResponse({ + type: GeoRegion, + isArray: true, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @JSONAPIQueryParams({ + availableFilters: geoRegionResource.columnsAllowedAsFilter.map( + (columnName: string) => ({ + name: columnName, + }), + ), + }) + @Get('/geo-regions') + async findAllEudr( + @Query(ValidationPipe) + dto: GetEUDRGeoRegions, + ): Promise { + const results: GeoRegion[] = + await this.geoRegionsService.getGeoRegionsFromSourcingLocations({ + ...dto, + withSourcingLocations: true, + eudr: true, + }); + return this.geoRegionsService.serialize(results); + } @Public() @Get('test') diff --git a/api/src/modules/eudr/eudr.module.ts b/api/src/modules/eudr/eudr.module.ts index 179ff1f3d..7cd086054 100644 --- a/api/src/modules/eudr/eudr.module.ts +++ b/api/src/modules/eudr/eudr.module.ts @@ -4,9 +4,19 @@ import { EudrService } from 'modules/eudr/eudr.service'; import { EudrController } from 'modules/eudr/eudr.controller'; import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; import { CartoConnector } from './carto/carto.connector'; +import { MaterialsModule } from 'modules/materials/materials.module'; +import { SuppliersModule } from 'modules/suppliers/suppliers.module'; +import { GeoRegionsModule } from 'modules/geo-regions/geo-regions.module'; +import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; @Module({ - imports: [HttpModule], + imports: [ + HttpModule, + MaterialsModule, + SuppliersModule, + GeoRegionsModule, + AdminRegionsModule, + ], providers: [EudrService, CartodbRepository, CartoConnector], controllers: [EudrController], }) diff --git a/api/src/modules/geo-regions/geo-regions.controller.ts b/api/src/modules/geo-regions/geo-regions.controller.ts index 38194b949..1ab8d4102 100644 --- a/api/src/modules/geo-regions/geo-regions.controller.ts +++ b/api/src/modules/geo-regions/geo-regions.controller.ts @@ -73,36 +73,6 @@ export class GeoRegionsController { return this.geoRegionsService.serialize(results.data, results.metadata); } - @ApiOperation({ - description: 'Find all EUDR geo regions', - }) - @ApiOkResponse({ - type: GeoRegion, - isArray: true, - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @JSONAPIQueryParams({ - availableFilters: geoRegionResource.columnsAllowedAsFilter.map( - (columnName: string) => ({ - name: columnName, - }), - ), - }) - @Get('/eudr') - async findAllEudr( - @Query(ValidationPipe) - dto: GetEUDRGeoRegions, - ): Promise { - const results: GeoRegion[] = - await this.geoRegionsService.getGeoRegionsFromSourcingLocations({ - ...dto, - withSourcingLocations: true, - eudr: true, - }); - return this.geoRegionsService.serialize(results); - } - @ApiOperation({ description: 'Find geo region by id' }) @ApiOkResponse({ type: GeoRegion }) @ApiNotFoundResponse({ description: 'Geo region not found' }) diff --git a/api/src/modules/materials/materials.controller.ts b/api/src/modules/materials/materials.controller.ts index 8ddf3247a..3f2a83add 100644 --- a/api/src/modules/materials/materials.controller.ts +++ b/api/src/modules/materials/materials.controller.ts @@ -102,27 +102,6 @@ export class MaterialsController { return this.materialsService.serialize(results); } - @ApiOperation({ - description: - 'Find all EUDR materials and return them in a tree format. Data in the "children" will recursively extend for the full depth of the tree', - }) - @ApiOkTreeResponse({ - treeNodeType: Material, - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @Get('/trees/eudr') - async getTreesForEudr( - @Query(ValidationPipe) materialTreeOptions: GetEUDRMaterials, - ): Promise { - const results: Material[] = await this.materialsService.getTrees({ - ...materialTreeOptions, - withSourcingLocations: true, - eudr: true, - }); - return this.materialsService.serialize(results); - } - @ApiOperation({ description: 'Find material by id' }) @ApiNotFoundResponse({ description: 'Material not found' }) @ApiOkResponse({ type: Material }) diff --git a/api/src/modules/suppliers/suppliers.controller.ts b/api/src/modules/suppliers/suppliers.controller.ts index 6e7912631..a1b4e39e5 100644 --- a/api/src/modules/suppliers/suppliers.controller.ts +++ b/api/src/modules/suppliers/suppliers.controller.ts @@ -30,11 +30,7 @@ import { FetchSpecification, ProcessFetchSpecification, } from 'nestjs-base-service'; -import { - Supplier, - SUPPLIER_TYPES, - supplierResource, -} from 'modules/suppliers/supplier.entity'; +import { Supplier, supplierResource } from 'modules/suppliers/supplier.entity'; import { CreateSupplierDto } from 'modules/suppliers/dto/create.supplier.dto'; import { UpdateSupplierDto } from 'modules/suppliers/dto/update.supplier.dto'; import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; @@ -101,27 +97,6 @@ export class SuppliersController { return this.suppliersService.serialize(results); } - @ApiOperation({ - description: - 'Find all EUDR suppliers and return them in a flat format. Data in the "children" will recursively extend for the full depth of the tree', - }) - @ApiOkTreeResponse({ - treeNodeType: Supplier, - }) - @ApiUnauthorizedResponse() - @ApiForbiddenResponse() - @UseInterceptors(SetScenarioIdsInterceptor) - @Get('/eudr') - async getTreesForEudr( - @Query(ValidationPipe) dto: GetSupplierEUDR, - ): Promise { - const results: Supplier[] = await this.suppliersService.getSupplierByType({ - ...dto, - eudr: true, - }); - return this.suppliersService.serialize(results); - } - @ApiOperation({ description: 'Find all suppliers by type', }) diff --git a/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts similarity index 91% rename from api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts rename to api/test/e2e/eudr/eudr-admin-region-filters.spec.ts index 1c405a93d..17e2a74d4 100644 --- a/api/test/e2e/admin-regions/admin-regions-eudr-smart-filters.spec.ts +++ b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts @@ -1,10 +1,10 @@ -import { AdminRegionTestManager } from './fixtures'; +import { EUDRTestManager } from './fixtures'; describe('Admin Regions EUDR Filters (e2e)', () => { - let testManager: AdminRegionTestManager; + let testManager: EUDRTestManager; beforeAll(async () => { - testManager = await AdminRegionTestManager.load(); + testManager = await EUDRTestManager.load(); }); beforeEach(async () => { diff --git a/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts new file mode 100644 index 000000000..844ab0496 --- /dev/null +++ b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts @@ -0,0 +1,28 @@ +import { EUDRTestManager } from './fixtures'; + +describe('GeoRegions Filters (e2e)', () => { + let testManager: EUDRTestManager; + + beforeAll(async () => { + testManager = await EUDRTestManager.load(); + }); + beforeEach(async () => { + await testManager.refreshState(); + }); + + afterEach(async () => { + await testManager.clearDatabase(); + }); + + afterAll(async () => { + await testManager.close(); + }); + describe('EUDR Geo Regions Filters', () => { + it('should only get geo-regions that are part of EUDR data', async () => { + await testManager.GivenGeoRegionsOfSourcingLocations(); + const { eudrGeoRegions } = await testManager.GivenEUDRGeoRegions(); + const response = await testManager.WhenIRequestEUDRGeoRegions(); + testManager.ThenIShouldOnlyReceiveCorrespondingGeoRegions(eudrGeoRegions); + }); + }); +}); diff --git a/api/test/e2e/admin-regions/fixtures.ts b/api/test/e2e/eudr/fixtures.ts similarity index 62% rename from api/test/e2e/admin-regions/fixtures.ts rename to api/test/e2e/eudr/fixtures.ts index 45c5542b5..b0d1f98ac 100644 --- a/api/test/e2e/admin-regions/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -7,16 +7,17 @@ import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { AndAssociatedMaterials } from '../../common-steps/and-associated-materials'; import { AndAssociatedSuppliers } from '../../common-steps/and-associated-suppliers'; import { TestManager } from '../../utils/test-manager'; +import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; -export class AdminRegionTestManager extends TestManager { - url = '/api/v1/admin-regions/'; +export class EUDRTestManager extends TestManager { + url = '/api/v1/eudr/'; constructor(manager: TestManager) { super(manager.testApp, manager.jwtToken, manager.dataSource); } static async load() { - return new AdminRegionTestManager(await this.createManager()); + return new EUDRTestManager(await this.createManager()); } GivenAdminRegionsOfSourcingLocations = async () => { @@ -77,7 +78,7 @@ export class AdminRegionTestManager extends TestManager { 'producerIds[]'?: string[]; 'materialIds[]'?: string[]; }) => { - return this.GET({ url: `${this.url}trees/eudr`, query: filters }); + return this.GET({ url: `${this.url}/admin-regions`, query: filters }); }; ThenIShouldOnlyReceiveCorrespondingAdminRegions = ( @@ -94,4 +95,56 @@ export class AdminRegionTestManager extends TestManager { ).toBeDefined(); } }; + + GivenGeoRegionsOfSourcingLocations = async () => { + const [geoRegion, geoRegion2] = await this.createGeoRegions([ + 'Regular GeoRegion', + 'Regular GeoRegion 2', + ]); + await createSourcingLocation({ geoRegionId: geoRegion.id }); + await createSourcingLocation({ geoRegionId: geoRegion2.id }); + return { + geoRegions: [geoRegion, geoRegion2], + }; + }; + + GivenEUDRGeoRegions = async () => { + const [geoRegion, geoRegion2] = await this.createGeoRegions([ + 'EUDR GeoRegion', + 'EUDR GeoRegion 2', + ]); + await createSourcingLocation({ + geoRegionId: geoRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + await createSourcingLocation({ + geoRegionId: geoRegion2.id, + locationType: LOCATION_TYPES.EUDR, + }); + return { + eudrGeoRegions: [geoRegion, geoRegion2], + }; + }; + + WhenIRequestEUDRGeoRegions = async (filters?: { + 'producerIds[]'?: string[]; + 'materialIds[]'?: string[]; + }) => { + return this.GET({ url: `${this.url}/geo-regions`, query: filters }); + }; + + ThenIShouldOnlyReceiveCorrespondingGeoRegions = ( + eudrGeoRegions: GeoRegion[], + ) => { + expect(this.response!.status).toBe(200); + expect(this.response!.body.data.length).toBe(eudrGeoRegions.length); + for (const geoRegion of eudrGeoRegions) { + expect( + this.response!.body.data.find( + (adminRegionResponse: AdminRegion) => + adminRegionResponse.id === geoRegion.id, + ), + ).toBeDefined(); + } + }; } diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index 3c6bd70b3..d6190f14a 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -87,7 +87,7 @@ export class TestManager { return createdSuppliers; } - async createGeoRegion(names?: string[]) { + async createGeoRegions(names?: string[]) { const namesToCreate = names || [randomName()]; const createdGeoRegions: GeoRegion[] = []; for (let i = 0; i < namesToCreate.length; i++) { diff --git a/api/test/utils/userAuth.ts b/api/test/utils/userAuth.ts index 0707d9a21..f568c0590 100644 --- a/api/test/utils/userAuth.ts +++ b/api/test/utils/userAuth.ts @@ -37,6 +37,7 @@ export async function setupTestUser( isActive: true, isDeleted: false, roles: [role], + ...restOfExtraData, }); } From 22168c0646404fdc9a7c674df300e159625c6e09 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 29 Feb 2024 15:44:33 +0300 Subject: [PATCH 024/153] add EUDR output shape class --- api/package.json | 1 + api/src/modules/eudr/dto/eudr.dto.ts | 15 ++++----------- api/src/modules/eudr/dto/eudr.output.dto.ts | 0 api/src/modules/eudr/eudr.controller.ts | 6 ++++++ api/src/modules/eudr/eudr.service.ts | 5 ++++- api/yarn.lock | 5 +++++ 6 files changed, 20 insertions(+), 12 deletions(-) delete mode 100644 api/src/modules/eudr/dto/eudr.output.dto.ts diff --git a/api/package.json b/api/package.json index 1c30378ec..ad50ac35a 100644 --- a/api/package.json +++ b/api/package.json @@ -86,6 +86,7 @@ "@types/config": "^3.3.0", "@types/express": "^4.17.13", "@types/faker": "^6.6.9", + "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.3", "@types/jsonapi-serializer": "^3.6.5", "@types/lodash": "^4.14.177", diff --git a/api/src/modules/eudr/dto/eudr.dto.ts b/api/src/modules/eudr/dto/eudr.dto.ts index 180850000..4e90a0cb6 100644 --- a/api/src/modules/eudr/dto/eudr.dto.ts +++ b/api/src/modules/eudr/dto/eudr.dto.ts @@ -1,17 +1,10 @@ // Input DTO to be ingested by Carto -export class EudrInputDTO { +import { GeoJSON } from 'geojson'; + +export class EudrInput { supplierId: string; geoRegionId: string; - geom: JSON; + geom: GeoJSON; year: number; } - -export class EudrOutputDTO { - geoRegionId: string; - supplierId: string; - hasEUDRAlerts: boolean; - alertsNumber: number; -} - -export type EudrDTO = EudrInputDTO & EudrOutputDTO; diff --git a/api/src/modules/eudr/dto/eudr.output.dto.ts b/api/src/modules/eudr/dto/eudr.output.dto.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/src/modules/eudr/eudr.controller.ts b/api/src/modules/eudr/eudr.controller.ts index b0393ec1a..94e761b1c 100644 --- a/api/src/modules/eudr/eudr.controller.ts +++ b/api/src/modules/eudr/eudr.controller.ts @@ -28,6 +28,7 @@ import { GetEUDRAdminRegions } from '../admin-regions/dto/get-admin-region-tree- import { GeoRegion, geoRegionResource } from '../geo-regions/geo-region.entity'; import { JSONAPIQueryParams } from '../../decorators/json-api-parameters.decorator'; import { GetEUDRGeoRegions } from '../geo-regions/dto/get-geo-region.dto'; +import { EudrAlerts } from './dto/alerts.dto'; @Controller('/api/v1/eudr') export class EudrController { @@ -134,6 +135,11 @@ export class EudrController { return this.geoRegionsService.serialize(results); } + @Get('/alerts') + async getAlerts(): Promise { + return [] as EudrAlerts[]; + } + @Public() @Get('test') async select(): Promise { diff --git a/api/src/modules/eudr/eudr.service.ts b/api/src/modules/eudr/eudr.service.ts index 1dd5973bc..28bab65a9 100644 --- a/api/src/modules/eudr/eudr.service.ts +++ b/api/src/modules/eudr/eudr.service.ts @@ -1,4 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { IEudrRepository } from './eudr.repositoty.interface'; @Injectable() -export class EudrService {} +export class EudrService { + constructor(private readonly eudrRepository: IEudrRepository) {} +} diff --git a/api/yarn.lock b/api/yarn.lock index b673ca4de..51ce581ec 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -1257,6 +1257,11 @@ dependencies: faker "*" +"@types/geojson@^7946.0.14": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" From 7c2975b0be3d4f2bcdb61b95ba58590dcb25d14b Mon Sep 17 00:00:00 2001 From: alexeh Date: Sat, 2 Mar 2024 11:43:07 +0300 Subject: [PATCH 025/153] Add bigquery client --- api/package.json | 1 + api/yarn.lock | 281 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 4 deletions(-) diff --git a/api/package.json b/api/package.json index ad50ac35a..9ad724b87 100644 --- a/api/package.json +++ b/api/package.json @@ -26,6 +26,7 @@ "node": "^18.16" }, "dependencies": { + "@google-cloud/bigquery": "^7.5.0", "@googlemaps/google-maps-services-js": "~3.3.2", "@json2csv/node": "^7.0.1", "@nestjs/axios": "^3.0.0", diff --git a/api/yarn.lock b/api/yarn.lock index 51ce581ec..48f37afc4 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -385,6 +385,61 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== +"@google-cloud/bigquery@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@google-cloud/bigquery/-/bigquery-7.5.0.tgz#7b3281d36b6823b923185cc2b87e391ec83ec87a" + integrity sha512-xYMy5WkNNqa9xqlHhOhHDHGCSjFiaN7vGHFV1rkbOFKTc4NilOxYO7BD03nYdClVyWN6qKy/qlmhPkAElb7ETw== + dependencies: + "@google-cloud/common" "^5.0.0" + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/precise-date" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" + arrify "^2.0.1" + big.js "^6.0.0" + duplexify "^4.0.0" + extend "^3.0.2" + is "^3.3.0" + stream-events "^1.0.5" + uuid "^9.0.0" + +"@google-cloud/common@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-5.0.1.tgz#762e598b0ef61e28d20e5b627141125ef73df957" + integrity sha512-7NBC5vD0au75nkctVs2vEGpdUPFs1BaHTMpeI+RVEgQSMe5/wEU6dx9p0fmZA0bj4HgdpobMKeegOcLUiEoxng== + dependencies: + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" + arrify "^2.0.1" + duplexify "^4.1.1" + ent "^2.2.0" + extend "^3.0.2" + google-auth-library "^9.0.0" + retry-request "^7.0.0" + teeny-request "^9.0.0" + +"@google-cloud/paginator@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.0.tgz#b8cc62f151685095d11467402cbf417c41bf14e6" + integrity sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/precise-date@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-4.0.0.tgz#e179893a3ad628b17a6fabdfcc9d468753aac11a" + integrity sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA== + +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== + +"@google-cloud/promisify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" + integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== + "@googlemaps/google-maps-services-js@~3.3.2": version "3.3.16" resolved "https://registry.yarnpkg.com/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.3.16.tgz#91ac24bd7ed6b087101188a3118fbfd7622b8813" @@ -1086,6 +1141,11 @@ resolved "https://registry.yarnpkg.com/@streamparser/json/-/json-0.0.15.tgz#405fbe94877ce0cbd3cf650b4d9186a0ec6acd0a" integrity sha512-6oikjkMTYAHGqKmcC9leE4+kY4Ch4eiTImXUN/N4d2bNGBYs0LJ/tfxmpvF5eExSU7iiPlV9jYlADqvj3NWA3Q== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -1174,6 +1234,11 @@ resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-4.0.2.tgz#5e76dd9e7881c23f332c2f48e5f326bd05ba9ac9" integrity sha512-fT5FMdzsiSX0AbgnS5gDvHl2Nco0h5zYyjwDQy4yPC7Ww6DeGMVKPRqIZtg9HOXDV2kkc18SL1B0N8f0BecrCA== +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + "@types/cls-hooked@^4.2.1": version "4.3.3" resolved "https://registry.yarnpkg.com/@types/cls-hooked/-/cls-hooked-4.3.3.tgz#c09e2f8dc62198522eaa18a5b6b873053154bd00" @@ -1412,6 +1477,16 @@ dependencies: "@types/node" "*" +"@types/request@^2.48.8": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -1445,6 +1520,11 @@ dependencies: "@types/superagent" "*" +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/uuid@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" @@ -1731,6 +1811,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + agentkeepalive@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" @@ -1913,6 +2000,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +arrify@^2.0.0, arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2015,7 +2107,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2028,6 +2120,16 @@ bcrypt@~5.1.0: "@mapbox/node-pre-gyp" "^1.0.10" node-addon-api "^5.0.0" +big.js@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" + integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2482,7 +2584,7 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -combined-stream@^1.0.8: +combined-stream@^1.0.6, combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2828,7 +2930,17 @@ dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== -ecdsa-sig-formatter@1.0.11: +duplexify@^4.0.0, duplexify@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.0" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -2867,7 +2979,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2890,6 +3002,11 @@ enhanced-resolve@^5.14.0: graceful-fs "^4.2.4" tapable "^2.2.0" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3187,6 +3304,11 @@ express@4.18.2: utils-merge "1.0.1" vary "~1.1.2" +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -3359,6 +3481,15 @@ fork-ts-checker-webpack-plugin@8.0.0: semver "^7.3.5" tapable "^2.2.1" +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3454,6 +3585,24 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.3.0.tgz#5cd858de47c6560caaf0f99bb5d89c5bdfbe9034" + integrity sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + +gcp-metadata@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" + integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg== + dependencies: + gaxios "^6.0.0" + json-bigint "^1.0.0" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -3560,6 +3709,18 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +google-auth-library@^9.0.0: + version "9.6.3" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.6.3.tgz#add8935bc5b842a8e80f84fef2b5ed9febb41d48" + integrity sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -3570,6 +3731,14 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3641,6 +3810,15 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -3649,6 +3827,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" + integrity sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -3865,6 +4051,11 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" + integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -4396,6 +4587,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -4483,6 +4681,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -4491,6 +4698,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -4965,6 +5180,13 @@ node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.9: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build-optional-packages@5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" @@ -5576,6 +5798,15 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -5719,6 +5950,15 @@ retry-axios@^2.2.1: resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-2.6.0.tgz#d4dc5c8a8e73982e26a705e46a33df99a28723e0" integrity sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ== +retry-request@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" + integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== + dependencies: + "@types/request" "^2.48.8" + extend "^3.0.2" + teeny-request "^9.0.0" + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -6019,6 +6259,18 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -6092,6 +6344,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + superagent@^8.0.5: version "8.0.6" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.6.tgz#e3fb0b3112b79b12acd605c08846253197765bf6" @@ -6181,6 +6438,17 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +teeny-request@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" + integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.9" + stream-events "^1.0.5" + uuid "^9.0.0" + terser-webpack-plugin@^5.3.7: version "5.3.9" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" @@ -6503,6 +6771,11 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" From 39982af506b73511b154100d39ce397f8f017b34 Mon Sep 17 00:00:00 2001 From: alexeh Date: Sun, 3 Mar 2024 10:30:51 +0300 Subject: [PATCH 026/153] Stream alerts response back --- api/src/app.module.ts | 2 +- api/src/guards/sensitive-info.guard.ts | 9 +++ .../alerts.entity.ts} | 0 .../modules/eudr-alerts/alerts.repository.ts | 69 +++++++++++++++++++ .../dto/alerts-input.dto.ts} | 0 .../eudr-alerts/dto/alerts-output.dto.ts | 11 +++ .../modules/eudr-alerts/dto/get-alerts.dto.ts | 6 ++ .../{eudr => eudr-alerts}/eudr.controller.ts | 68 +++++++++++------- .../{eudr => eudr-alerts}/eudr.module.ts | 9 ++- api/src/modules/eudr-alerts/eudr.service.ts | 14 ++++ api/src/modules/eudr/carto/carto.module.ts | 0 api/src/modules/eudr/eudr.service.ts | 7 -- .../import-data/workers/eudr.consumer.ts | 4 +- api/src/modules/tasks/tasks.service.ts | 2 +- 14 files changed, 162 insertions(+), 39 deletions(-) rename api/src/modules/{eudr/eudr.entity.ts => eudr-alerts/alerts.entity.ts} (100%) create mode 100644 api/src/modules/eudr-alerts/alerts.repository.ts rename api/src/modules/{eudr/dto/eudr.dto.ts => eudr-alerts/dto/alerts-input.dto.ts} (100%) create mode 100644 api/src/modules/eudr-alerts/dto/alerts-output.dto.ts create mode 100644 api/src/modules/eudr-alerts/dto/get-alerts.dto.ts rename api/src/modules/{eudr => eudr-alerts}/eudr.controller.ts (63%) rename api/src/modules/{eudr => eudr-alerts}/eudr.module.ts (64%) create mode 100644 api/src/modules/eudr-alerts/eudr.service.ts delete mode 100644 api/src/modules/eudr/carto/carto.module.ts delete mode 100644 api/src/modules/eudr/eudr.service.ts diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 64f771dc3..651897d42 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -39,7 +39,7 @@ import { AuthorizationService } from 'modules/authorization/authorization.servic import { TasksService } from 'modules/tasks/tasks.service'; import { NotificationsModule } from 'modules/notifications/notifications.module'; import { ReportsModule } from 'modules/reports/reports.module'; -import { EudrModule } from 'modules/eudr/eudr.module'; +import { EudrModule } from 'modules/eudr-alerts/eudr.module'; const queueConfig: any = config.get('queue'); diff --git a/api/src/guards/sensitive-info.guard.ts b/api/src/guards/sensitive-info.guard.ts index 9fedbc5d1..7e2476316 100644 --- a/api/src/guards/sensitive-info.guard.ts +++ b/api/src/guards/sensitive-info.guard.ts @@ -18,6 +18,15 @@ export class SensitiveInfoGuard implements NestInterceptor { context: ExecutionContext, next: CallHandler, ): Observable { + const request = context.switchToHttp().getRequest(); + if (this.dataComesFromAStreamEndpoint(request.url)) { + return next.handle(); + } + return next.handle().pipe(map((data: any) => instanceToPlain(data))); } + + dataComesFromAStreamEndpoint(url: string): boolean { + return url.includes('eudr'); + } } diff --git a/api/src/modules/eudr/eudr.entity.ts b/api/src/modules/eudr-alerts/alerts.entity.ts similarity index 100% rename from api/src/modules/eudr/eudr.entity.ts rename to api/src/modules/eudr-alerts/alerts.entity.ts diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts new file mode 100644 index 000000000..ccea70d49 --- /dev/null +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -0,0 +1,69 @@ +import { BigQuery } from '@google-cloud/bigquery'; +import { Injectable } from '@nestjs/common'; +import { DataSource, QueryBuilder, SelectQueryBuilder } from 'typeorm'; +import { AlertsOutput } from './dto/alerts-output.dto'; +import { SA } from './SA'; +import { ResourceStream } from '@google-cloud/paginator'; +import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; + +const projectId: string = 'carto-dw-ac-zk2uhih6'; + +const BASE_QUERY: string = `SELECT * FROM cartobq.eudr.mock_data LIMIT 1`; + +const BASE_DATASET: string = 'cartobq.eudr.dev_mock_data_optimized'; + +const limit: number = 1; + +@Injectable() +export class AlertsRepository { + bigQueryClient: BigQuery; + BASE_DATASET: string = 'cartobq.eudr.dev_mock_data_optimized'; + + constructor(private readonly dataSource: DataSource) { + //TODO: Implement error handling for missing service account file + + this.bigQueryClient = new BigQuery({ + credentials: SA, + projectId, + }); + } + + select(dto?: any): ResourceStream { + const queryBuilder: SelectQueryBuilder = this.dataSource + .createQueryBuilder() + .select('georegionid', 'geoRegionId') + .addSelect('supplierid', 'supplierId') + .addSelect('geometry', 'geometry') + .where('alertcount >= :alertCount', { alertCount: 2 }) + .andWhere('supplierid IN (:...supplierIds)', { + supplierIds: [ + '4132ab95-8b04-4438-b706-a82651f491bd', + '4132ab95-8b04-4438-b706-a82651f491bd', + '4132ab95-8b04-4438-b706-a82651f491bd', + ], + }); + if (limit) { + queryBuilder.limit(limit); + } + // const [rows] = await this.bigQueryClient.query( + // this.buildQuery(queryBuilder), + // ); + return this.bigQueryClient.createQueryStream(this.buildQuery(queryBuilder)); + } + + private buildQuery(queryBuilder: SelectQueryBuilder): { + query: string; + params: any[]; + } { + const [query, params] = queryBuilder + .from(this.BASE_DATASET, 'alerts') + .getQueryAndParameters(); + const queryOptions = { + query: query.replace(/\$\d+|"/g, (match: string) => + match === '"' ? '' : '?', + ), + params, + }; + return queryOptions; + } +} diff --git a/api/src/modules/eudr/dto/eudr.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-input.dto.ts similarity index 100% rename from api/src/modules/eudr/dto/eudr.dto.ts rename to api/src/modules/eudr-alerts/dto/alerts-input.dto.ts diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts new file mode 100644 index 000000000..78241b087 --- /dev/null +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -0,0 +1,11 @@ +import { GeoJSON } from 'geojson'; + +export class AlertsOutput { + geoRegionId: string; + supplierId: string; + alertCount: boolean; + geometry: GeoJSON; + date: Date; + year: number; + alertConfidence: 'low' | 'medium' | 'high' | 'very high'; +} diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts new file mode 100644 index 000000000..5448ee413 --- /dev/null +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -0,0 +1,6 @@ +export class GetEUDRALertsDto { + supplierId: string; + geoRegionId: string; + geom: any; + year: number; +} diff --git a/api/src/modules/eudr/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts similarity index 63% rename from api/src/modules/eudr/eudr.controller.ts rename to api/src/modules/eudr-alerts/eudr.controller.ts index 94e761b1c..d4f44b1ed 100644 --- a/api/src/modules/eudr/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -2,38 +2,45 @@ import { Controller, Get, Query, + Res, UseInterceptors, ValidationPipe, } from '@nestjs/common'; -import { Public } from 'decorators/public.decorator'; -import { CartoConnector } from 'modules/eudr/carto/carto.connector'; import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { ApiOkTreeResponse } from '../../decorators/api-tree-response.decorator'; -import { Supplier } from '../suppliers/supplier.entity'; -import { SetScenarioIdsInterceptor } from '../impact/set-scenario-ids.interceptor'; -import { GetSupplierEUDR } from '../suppliers/dto/get-supplier-by-type.dto'; -import { SuppliersService } from '../suppliers/suppliers.service'; +import { Response } from 'express'; +import { Writable } from 'stream'; +import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; +import { GetSupplierEUDR } from 'modules/suppliers/dto/get-supplier-by-type.dto'; +import { SuppliersService } from 'modules/suppliers/suppliers.service'; import { MaterialsService } from 'modules/materials/materials.service'; import { GeoRegionsService } from 'modules/geo-regions/geo-regions.service'; import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; -import { Material } from '../materials/material.entity'; -import { GetEUDRMaterials } from '../materials/dto/get-material-tree-with-options.dto'; -import { AdminRegion } from '../admin-regions/admin-region.entity'; -import { GetEUDRAdminRegions } from '../admin-regions/dto/get-admin-region-tree-with-options.dto'; -import { GeoRegion, geoRegionResource } from '../geo-regions/geo-region.entity'; -import { JSONAPIQueryParams } from '../../decorators/json-api-parameters.decorator'; -import { GetEUDRGeoRegions } from '../geo-regions/dto/get-geo-region.dto'; -import { EudrAlerts } from './dto/alerts.dto'; +import { Material } from 'modules/materials/material.entity'; +import { GetEUDRMaterials } from 'modules/materials/dto/get-material-tree-with-options.dto'; +import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +import { GetEUDRAdminRegions } from 'modules/admin-regions/dto/get-admin-region-tree-with-options.dto'; +import { + GeoRegion, + geoRegionResource, +} from 'modules/geo-regions/geo-region.entity'; +import { JSONAPIQueryParams } from 'decorators/json-api-parameters.decorator'; +import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { EudrService } from 'modules/eudr-alerts/eudr.service'; +import { ResourceStream } from '@google-cloud/paginator'; +import { GetEUDRALertsDto } from './dto/get-alerts.dto'; @Controller('/api/v1/eudr') export class EudrController { constructor( - private readonly carto: CartoConnector, + private readonly eudrAlertsService: EudrService, private readonly suppliersService: SuppliersService, private readonly materialsService: MaterialsService, private readonly geoRegionsService: GeoRegionsService, @@ -70,7 +77,7 @@ export class EudrController { }) @ApiUnauthorizedResponse() @ApiForbiddenResponse() - @Get('/eudr') + @Get('/materials') async getMaterialsTree( @Query(ValidationPipe) materialTreeOptions: GetEUDRMaterials, ): Promise { @@ -136,13 +143,28 @@ export class EudrController { } @Get('/alerts') - async getAlerts(): Promise { - return [] as EudrAlerts[]; + async getAlerts( + @Res() response: Response, + dto: GetEUDRALertsDto, + ): Promise { + const stream: ResourceStream = + this.eudrAlertsService.getAlerts(); + this.streamResponse(response, stream); } - @Public() - @Get('test') - async select(): Promise { - return this.carto.select('select * from cartobq.eudr.mock_data limit 10'); + streamResponse(response: Response, stream: Writable): any { + stream.on('data', (data: any) => { + const json: string = JSON.stringify(data); + response.write(json + '\n'); + }); + + stream.on('end', () => { + response.end(); + }); + + stream.on('error', (error: any) => { + console.error('Stream error:', error); + response.status(500).send('Error processing stream'); + }); } } diff --git a/api/src/modules/eudr/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts similarity index 64% rename from api/src/modules/eudr/eudr.module.ts rename to api/src/modules/eudr-alerts/eudr.module.ts index 7cd086054..85eccb432 100644 --- a/api/src/modules/eudr/eudr.module.ts +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -1,13 +1,12 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; -import { EudrService } from 'modules/eudr/eudr.service'; -import { EudrController } from 'modules/eudr/eudr.controller'; -import { CartodbRepository } from 'modules/eudr/carto/cartodb.repository'; -import { CartoConnector } from './carto/carto.connector'; +import { EudrService } from 'modules/eudr-alerts/eudr.service'; +import { EudrController } from 'modules/eudr-alerts/eudr.controller'; import { MaterialsModule } from 'modules/materials/materials.module'; import { SuppliersModule } from 'modules/suppliers/suppliers.module'; import { GeoRegionsModule } from 'modules/geo-regions/geo-regions.module'; import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; +import { AlertsRepository } from './alerts.repository'; @Module({ imports: [ @@ -17,7 +16,7 @@ import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; GeoRegionsModule, AdminRegionsModule, ], - providers: [EudrService, CartodbRepository, CartoConnector], + providers: [EudrService, AlertsRepository], controllers: [EudrController], }) export class EudrModule {} diff --git a/api/src/modules/eudr-alerts/eudr.service.ts b/api/src/modules/eudr-alerts/eudr.service.ts new file mode 100644 index 000000000..eaab097cf --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { AlertsRepository } from './alerts.repository'; + +import { ResourceStream } from '@google-cloud/paginator'; +import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; + +@Injectable() +export class EudrService { + constructor(private readonly alertsRepository: AlertsRepository) {} + + getAlerts(): ResourceStream { + return this.alertsRepository.select(); + } +} diff --git a/api/src/modules/eudr/carto/carto.module.ts b/api/src/modules/eudr/carto/carto.module.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/src/modules/eudr/eudr.service.ts b/api/src/modules/eudr/eudr.service.ts deleted file mode 100644 index 28bab65a9..000000000 --- a/api/src/modules/eudr/eudr.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IEudrRepository } from './eudr.repositoty.interface'; - -@Injectable() -export class EudrService { - constructor(private readonly eudrRepository: IEudrRepository) {} -} diff --git a/api/src/modules/import-data/workers/eudr.consumer.ts b/api/src/modules/import-data/workers/eudr.consumer.ts index fd25f547f..6b668cbbd 100644 --- a/api/src/modules/import-data/workers/eudr.consumer.ts +++ b/api/src/modules/import-data/workers/eudr.consumer.ts @@ -34,7 +34,7 @@ export class ImportDataConsumer { @OnQueueFailed() async onJobFailed(job: Job, err: Error): Promise { - // TODO: Handle eudr import errors, updating async tgasks + // TODO: Handle eudr-alerts import errors, updating async tgasks const { taskId } = job.data; this.logger.error( `Import Failed for file: ${job.data.xlsxFileData.filename} for task: ${taskId}: ${err}`, @@ -46,7 +46,7 @@ export class ImportDataConsumer { this.logger.log( `Import XLSX with TASK ID: ${job.data.taskId} completed successfully`, ); - // TODO: Handle eudr import completion, updating async tasks + // TODO: Handle eudr-alerts import completion, updating async tasks } @Process('eudr') diff --git a/api/src/modules/tasks/tasks.service.ts b/api/src/modules/tasks/tasks.service.ts index 6add5ef4b..e30eec2cf 100644 --- a/api/src/modules/tasks/tasks.service.ts +++ b/api/src/modules/tasks/tasks.service.ts @@ -123,7 +123,7 @@ export class TasksService extends AppBaseService< const stalledTask: Task | null = await this.taskRepository .createQueryBuilder('task') .where('task.status = :status', { status: TASK_STATUS.PROCESSING }) - .orderBy('task.createdAt', 'DESC') // assuming you have a createdAt field + .orderBy('task.createdAt', 'DESC') .getOne(); if (stalledTask) { stalledTask.status = TASK_STATUS.FAILED; From a972cdc55897a46917fb8c2d513376b61e1d4366 Mon Sep 17 00:00:00 2001 From: alexeh Date: Sun, 3 Mar 2024 10:46:45 +0300 Subject: [PATCH 027/153] Refactor setupUser test utility --- api/test/utils/userAuth.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/api/test/utils/userAuth.ts b/api/test/utils/userAuth.ts index f568c0590..c3cfbf8a6 100644 --- a/api/test/utils/userAuth.ts +++ b/api/test/utils/userAuth.ts @@ -14,7 +14,8 @@ export type TestUser = { jwtToken: string; user: User; password: string }; export async function setupTestUser( applicationManager: TestApplication, roleName: ROLES = ROLES.ADMIN, - extraData: Partial = { password: 'Password123!' }, + extraData: Partial> = {}, + password: string = 'Password123', ): Promise { const salt = await genSalt(); const role = new Role(); @@ -22,22 +23,20 @@ export async function setupTestUser( const entityManager = applicationManager.get(EntityManager); const userRepository = entityManager.getRepository(User); - const { password, ...restOfExtraData } = extraData; - await setUpRolesAndPermissions(entityManager); let existingUser = await userRepository.findOne({ - where: { email: E2E_CONFIG.users.signUp.email }, + where: { email: extraData.email ?? E2E_CONFIG.users.signUp.email }, }); if (!existingUser) { existingUser = await userRepository.save({ ...E2E_CONFIG.users.signUp, salt, - password: await hash(password!, salt), + password: await hash(password, salt), isActive: true, isDeleted: false, roles: [role], - ...restOfExtraData, + ...extraData, }); } From 3effc68d12d2b84ebcaf9db29447e478be6d5799 Mon Sep 17 00:00:00 2001 From: alexeh Date: Sun, 3 Mar 2024 12:09:55 +0300 Subject: [PATCH 028/153] Mock eudr repository --- api/config/custom-environment-variables.json | 3 ++- api/config/default.json | 3 ++- api/src/modules/eudr-alerts/alerts.repository.ts | 16 ++++++---------- .../carto/carto.connector.ts | 0 api/src/modules/eudr-alerts/eudr.module.ts | 12 ++++++++++-- .../eudr-alerts/eudr.repositoty.interface.ts | 3 +++ api/src/modules/eudr-alerts/eudr.service.ts | 5 ++--- api/src/modules/eudr/carto/cartodb.repository.ts | 10 ---------- .../modules/eudr/eudr.repositoty.interface.ts | 3 --- .../notifications/notifications.module.ts | 6 ++++-- api/test/utils/application-manager.ts | 11 ++++++++--- api/test/utils/service-mocks.ts | 12 ++++++++++++ 12 files changed, 49 insertions(+), 35 deletions(-) rename api/src/modules/{eudr => eudr-alerts}/carto/carto.connector.ts (100%) create mode 100644 api/src/modules/eudr-alerts/eudr.repositoty.interface.ts delete mode 100644 api/src/modules/eudr/carto/cartodb.repository.ts delete mode 100644 api/src/modules/eudr/eudr.repositoty.interface.ts diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index dd2bad1dd..2699a221a 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -75,6 +75,7 @@ }, "carto": { "apiKey": "CARTO_API_KEY", - "baseUrl": "CARTO_BASE_URL" + "baseUrl": "CARTO_BASE_URL", + "credentials": "CARTO_CREDENTIALS" } } diff --git a/api/config/default.json b/api/config/default.json index a3075196a..6069b6d9a 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -84,6 +84,7 @@ }, "carto": { "apiKey": null, - "baseUrl": "null" + "baseUrl": "null", + "credentials": null } } diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index ccea70d49..3dbaaf78f 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -1,29 +1,25 @@ import { BigQuery } from '@google-cloud/bigquery'; import { Injectable } from '@nestjs/common'; -import { DataSource, QueryBuilder, SelectQueryBuilder } from 'typeorm'; +import { DataSource, SelectQueryBuilder } from 'typeorm'; import { AlertsOutput } from './dto/alerts-output.dto'; -import { SA } from './SA'; import { ResourceStream } from '@google-cloud/paginator'; import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; +import { IEUDRAlertsRepository } from './eudr.repositoty.interface'; +import { AppConfig } from '../../utils/app.config'; const projectId: string = 'carto-dw-ac-zk2uhih6'; -const BASE_QUERY: string = `SELECT * FROM cartobq.eudr.mock_data LIMIT 1`; - -const BASE_DATASET: string = 'cartobq.eudr.dev_mock_data_optimized'; - const limit: number = 1; @Injectable() -export class AlertsRepository { +export class AlertsRepository implements IEUDRAlertsRepository { bigQueryClient: BigQuery; BASE_DATASET: string = 'cartobq.eudr.dev_mock_data_optimized'; constructor(private readonly dataSource: DataSource) { - //TODO: Implement error handling for missing service account file - + const { credentials } = AppConfig.get('carto'); this.bigQueryClient = new BigQuery({ - credentials: SA, + credentials: JSON.parse(credentials), projectId, }); } diff --git a/api/src/modules/eudr/carto/carto.connector.ts b/api/src/modules/eudr-alerts/carto/carto.connector.ts similarity index 100% rename from api/src/modules/eudr/carto/carto.connector.ts rename to api/src/modules/eudr-alerts/carto/carto.connector.ts diff --git a/api/src/modules/eudr-alerts/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts index 85eccb432..9e65a3682 100644 --- a/api/src/modules/eudr-alerts/eudr.module.ts +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -6,8 +6,11 @@ import { MaterialsModule } from 'modules/materials/materials.module'; import { SuppliersModule } from 'modules/suppliers/suppliers.module'; import { GeoRegionsModule } from 'modules/geo-regions/geo-regions.module'; import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; -import { AlertsRepository } from './alerts.repository'; +import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; +export const IEUDRAlertsRepositoryToken = Symbol('IEUDRAlertsRepository'); + +// TODO: Use token injection and refer to the interface, right now I am having a dependencv issue @Module({ imports: [ HttpModule, @@ -16,7 +19,12 @@ import { AlertsRepository } from './alerts.repository'; GeoRegionsModule, AdminRegionsModule, ], - providers: [EudrService, AlertsRepository], + providers: [ + EudrService, + AlertsRepository, + { provide: IEUDRAlertsRepositoryToken, useClass: AlertsRepository }, + ], controllers: [EudrController], + // exports: [IEUDRAlertsRepositoryToken], }) export class EudrModule {} diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts new file mode 100644 index 000000000..12f3de272 --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -0,0 +1,3 @@ +export interface IEUDRAlertsRepository { + select(): any; +} diff --git a/api/src/modules/eudr-alerts/eudr.service.ts b/api/src/modules/eudr-alerts/eudr.service.ts index eaab097cf..01fb0686d 100644 --- a/api/src/modules/eudr-alerts/eudr.service.ts +++ b/api/src/modules/eudr-alerts/eudr.service.ts @@ -1,8 +1,7 @@ -import { Injectable } from '@nestjs/common'; -import { AlertsRepository } from './alerts.repository'; - +import { Inject, Injectable } from '@nestjs/common'; import { ResourceStream } from '@google-cloud/paginator'; import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; +import { AlertsRepository } from './alerts.repository'; @Injectable() export class EudrService { diff --git a/api/src/modules/eudr/carto/cartodb.repository.ts b/api/src/modules/eudr/carto/cartodb.repository.ts deleted file mode 100644 index bf89b34f3..000000000 --- a/api/src/modules/eudr/carto/cartodb.repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { IEudrRepository } from 'modules/eudr/eudr.repositoty.interface'; -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class CartodbRepository implements IEudrRepository { - constructor(private readonly http: HttpService) {} - - async select(): Promise {} -} diff --git a/api/src/modules/eudr/eudr.repositoty.interface.ts b/api/src/modules/eudr/eudr.repositoty.interface.ts deleted file mode 100644 index 5281989f2..000000000 --- a/api/src/modules/eudr/eudr.repositoty.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IEudrRepository { - select(): Promise; -} diff --git a/api/src/modules/notifications/notifications.module.ts b/api/src/modules/notifications/notifications.module.ts index 414187097..af3afaeb4 100644 --- a/api/src/modules/notifications/notifications.module.ts +++ b/api/src/modules/notifications/notifications.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { SendgridEmailService } from 'modules/notifications/email/sendgrid.email.service'; +export const IEmailServiceToken: string = 'IEmailService'; + @Module({ - providers: [{ provide: 'IEmailService', useClass: SendgridEmailService }], - exports: ['IEmailService'], + providers: [{ provide: IEmailServiceToken, useClass: SendgridEmailService }], + exports: [IEmailServiceToken], }) export class NotificationsModule {} diff --git a/api/test/utils/application-manager.ts b/api/test/utils/application-manager.ts index 2351df515..964c52b54 100644 --- a/api/test/utils/application-manager.ts +++ b/api/test/utils/application-manager.ts @@ -9,7 +9,10 @@ import { TestingModuleBuilder } from '@nestjs/testing/testing-module.builder'; import { Type } from '@nestjs/common/interfaces'; import { TestingModule } from '@nestjs/testing/testing-module'; import { isUndefined } from 'lodash'; -import { MockEmailService } from './service-mocks'; +import { MockAlertRepository, MockEmailService } from './service-mocks'; +import { IEUDRAlertsRepositoryToken } from '../../src/modules/eudr-alerts/eudr.module'; +import { IEmailServiceToken } from '../../src/modules/notifications/notifications.module'; +import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; export default class ApplicationManager { static readonly regenerateResourcesOnEachTest: boolean = false; @@ -43,8 +46,10 @@ export default class ApplicationManager { Test.createTestingModule({ imports: [AppModule], }) - .overrideProvider('IEmailService') - .useClass(MockEmailService); + .overrideProvider(IEmailServiceToken) + .useClass(MockEmailService) + .overrideProvider(AlertsRepository) + .useClass(MockAlertRepository); ApplicationManager.testApplication.moduleFixture = await testingModuleBuilder.compile(); diff --git a/api/test/utils/service-mocks.ts b/api/test/utils/service-mocks.ts index 1cb4351c0..98626b7b8 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -3,6 +3,7 @@ import { SendMailDTO, } from '../../src/modules/notifications/email/email.service.interface'; import { Logger } from '@nestjs/common'; +import { IEUDRAlertsRepository } from 'modules/eudr-alerts/eudr.repositoty.interface'; export class MockEmailService implements IEmailService { logger: Logger = new Logger(MockEmailService.name); @@ -12,3 +13,14 @@ export class MockEmailService implements IEmailService { return Promise.resolve(); } } + +export class MockAlertRepository implements IEUDRAlertsRepository { + logger: Logger = new Logger(MockAlertRepository.name); + + select(): any { + this.logger.warn(`Alert Repository Mock called... `); + return new Promise((resolve) => { + resolve([]); + }); + } +} From b2c4e89acd9bba7ac5d210b698bcf805a9f64b6f Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 4 Mar 2024 06:59:20 +0300 Subject: [PATCH 029/153] Fix regressions, leave legit failing tests until further decisions about EUDR --- api/test/e2e/eudr/fixtures.ts | 2 +- .../geo-regions-smart-filters.spec.ts | 42 ------------------- 2 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index b0d1f98ac..e776fe852 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -10,7 +10,7 @@ import { TestManager } from '../../utils/test-manager'; import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; export class EUDRTestManager extends TestManager { - url = '/api/v1/eudr/'; + url = '/api/v1/eudr'; constructor(manager: TestManager) { super(manager.testApp, manager.jwtToken, manager.dataSource); diff --git a/api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts b/api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts deleted file mode 100644 index 58ecc858e..000000000 --- a/api/test/e2e/geo-regions/geo-regions-smart-filters.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DataSource } from 'typeorm'; -import ApplicationManager from '../../utils/application-manager'; -import { TestApplication } from '../../utils/application-manager'; -import { clearTestDataFromDatabase } from '../../utils/database-test-helper'; -import { setupTestUser } from '../../utils/userAuth'; - -import { geoRegionFixtures } from './fixtures'; - -describe('GeoRegions Filters (e2e)', () => { - const fixtures = geoRegionFixtures(); - let testApplication: TestApplication; - let jwtToken: string; - let dataSource: DataSource; - - beforeAll(async () => { - testApplication = await ApplicationManager.init(); - - dataSource = testApplication.get(DataSource); - }); - beforeEach(async () => { - ({ jwtToken } = await setupTestUser(testApplication)); - }); - - afterEach(async () => { - await clearTestDataFromDatabase(dataSource); - }); - - afterAll(async () => { - await testApplication.close(); - }); - describe('EUDR Geo Regions Filters', () => { - it('should only get geo-regions that are part of EUDR data', async () => { - await fixtures.GivenGeoRegionsOfSourcingLocations(); - const { eudrGeoRegions } = await fixtures.GivenEUDRGeoRegions(); - const response = await fixtures.WhenIRequestEUDRGeoRegions({ - app: testApplication, - jwtToken, - }); - fixtures.ThenIShouldOnlyReceiveEUDRGeoRegions(response, eudrGeoRegions); - }); - }); -}); From 84185ee50d13d6e4a5380a9cdd2f2177cc28daef Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 4 Mar 2024 09:02:01 +0300 Subject: [PATCH 030/153] Endpoint to retrieve filtered EUDR Alerts --- api/config/custom-environment-variables.json | 5 +- api/config/default.json | 5 +- api/config/test.json | 6 + .../big-query-alerts-query.builder.ts | 118 ++++++++++++++++++ .../modules/eudr-alerts/alerts.repository.ts | 114 ++++++++++------- .../eudr-alerts/dto/alerts-output.dto.ts | 13 +- .../modules/eudr-alerts/dto/get-alerts.dto.ts | 52 +++++++- .../modules/eudr-alerts/eudr.controller.ts | 13 +- api/src/modules/eudr-alerts/eudr.module.ts | 15 ++- .../eudr-alerts/eudr.repositoty.interface.ts | 18 ++- api/src/modules/eudr-alerts/eudr.service.ts | 15 ++- api/test/utils/service-mocks.ts | 14 ++- 12 files changed, 304 insertions(+), 84 deletions(-) create mode 100644 api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 2699a221a..a9b0514db 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -73,9 +73,10 @@ "sendGridApiKey": "SENDGRID_API_KEY" } }, - "carto": { + "eudr": { "apiKey": "CARTO_API_KEY", "baseUrl": "CARTO_BASE_URL", - "credentials": "CARTO_CREDENTIALS" + "credentials": "EUDR_CREDENTIALS", + "dataset": "EUDR_DATASET" } } diff --git a/api/config/default.json b/api/config/default.json index 6069b6d9a..9f83e9a0d 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -82,9 +82,10 @@ "sendGridApiKey": null } }, - "carto": { + "eudr": { "apiKey": null, "baseUrl": "null", - "credentials": null + "credentials": null, + "dataset": null } } diff --git a/api/config/test.json b/api/config/test.json index 718a8f346..823d75b56 100644 --- a/api/config/test.json +++ b/api/config/test.json @@ -46,5 +46,11 @@ "email": { "sendGridApiKey": "SG.forSomeReasonSendGridApiKeysNeedToStartWithSG." } + }, + "eudr": { + "apiKey": null, + "baseUrl": "null", + "credentials": null, + "dataset": "test_dataset" } } diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts new file mode 100644 index 000000000..0c622c9d6 --- /dev/null +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -0,0 +1,118 @@ +import { SelectQueryBuilder } from 'typeorm'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { Query } from '@google-cloud/bigquery'; + +export class BigQueryAlertsQueryBuilder { + queryBuilder: SelectQueryBuilder; + dto?: GetEUDRAlertsDto; + + constructor( + queryBuilder: SelectQueryBuilder, + getAlertsDto?: GetEUDRAlertsDto, + ) { + this.queryBuilder = queryBuilder; + this.dto = getAlertsDto; + } + + buildQuery(): Query { + if (this.dto?.supplierIds) { + this.queryBuilder.andWhere('supplierid IN (:...supplierIds)', { + supplierIds: this.dto.supplierIds, + }); + } + if (this.dto?.geoRegionIds) { + this.queryBuilder.andWhere('georegionid IN (:...geoRegionIds)', { + geoRegionIds: this.dto.geoRegionIds, + }); + } + if (this.dto?.alertConfidence) { + this.queryBuilder.andWhere('alertConfidence = :alertConfidence', { + alertConfidence: this.dto.alertConfidence, + }); + } + + if (this.dto?.startYear && this.dto?.endYear) { + this.addYearRange(); + } else if (this.dto?.startYear) { + this.addYearGreaterThanOrEqual(); + } else if (this.dto?.endYear) { + this.addYearLessThanOrEqual(); + } + + if (this.dto?.startAlertDate && this.dto?.endAlertDate) { + this.addAlertDateRange(); + } else if (this.dto?.startAlertDate) { + this.addAlertDateGreaterThanOrEqual(); + } else if (this.dto?.endAlertDate) { + this.addAlertDateLessThanOrEqual(); + } + + this.queryBuilder.limit(this.dto?.limit); + + const [query, params] = this.queryBuilder.getQueryAndParameters(); + + return this.parseToBigQuery(query, params); + } + + addYearRange(): void { + this.queryBuilder.andWhere('year BETWEEN :startYear AND :endYear', { + startYear: this.dto?.startYear, + endYear: this.dto?.endYear, + }); + } + + addYearGreaterThanOrEqual(): void { + this.queryBuilder.andWhere('year >= :startYear', { + startYear: this.dto?.startYear, + }); + } + + addYearLessThanOrEqual(): void { + this.queryBuilder.andWhere('year <= :endYear', { + endYear: this.dto?.endYear, + }); + } + + addAlertDateRange(): void { + this.queryBuilder.andWhere( + 'DATE(alertdate) BETWEEN :startAlertDate AND :endAlertDate', + { + startAlertDate: this.dto?.startAlertDate, + endAlertDate: this.dto?.endAlertDate, + }, + ); + } + + addAlertDateGreaterThanOrEqual(): void { + this.queryBuilder.andWhere('DATE(alertdate) >= DATE(:startAlertDate)', { + startAlertDate: this.dto?.startAlertDate, + }); + } + + addAlertDateLessThanOrEqual(): void { + this.queryBuilder.andWhere('DATE(alertDate) <= :DATE(endAlertDate)', { + endAlertDate: this.dto?.endAlertDate, + }); + } + + parseToBigQuery(query: string, params: any[]): Query { + return { + query: this.removeDoubleQuotesAndReplacePositionalArguments(query), + params, + }; + } + + /** + * @description: BigQuery does not allow double quotes and the positional argument symbol must be a "?". + * So there is a need to replace the way TypeORM handles the positional arguments, with $1, $2, etc. + */ + + private removeDoubleQuotesAndReplacePositionalArguments( + query: string, + ): string { + return query.replace(/\$\d+|"/g, (match: string) => + match === '"' ? '' : '?', + ); + } +} diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index 3dbaaf78f..ca611ebab 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -1,65 +1,85 @@ -import { BigQuery } from '@google-cloud/bigquery'; -import { Injectable } from '@nestjs/common'; +import { + BigQuery, + Query, + SimpleQueryRowsResponse, +} from '@google-cloud/bigquery'; +import { + Inject, + Injectable, + Logger, + ServiceUnavailableException, +} from '@nestjs/common'; import { DataSource, SelectQueryBuilder } from 'typeorm'; -import { AlertsOutput } from './dto/alerts-output.dto'; -import { ResourceStream } from '@google-cloud/paginator'; -import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; -import { IEUDRAlertsRepository } from './eudr.repositoty.interface'; -import { AppConfig } from '../../utils/app.config'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { + EUDRAlertDates, + GetEUDRAlertDatesDto, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { BigQueryAlertsQueryBuilder } from 'modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; const projectId: string = 'carto-dw-ac-zk2uhih6'; -const limit: number = 1; - @Injectable() export class AlertsRepository implements IEUDRAlertsRepository { + logger: Logger = new Logger(AlertsRepository.name); bigQueryClient: BigQuery; - BASE_DATASET: string = 'cartobq.eudr.dev_mock_data_optimized'; - constructor(private readonly dataSource: DataSource) { - const { credentials } = AppConfig.get('carto'); + constructor( + private readonly dataSource: DataSource, + @Inject('EUDRCredentials') private credentials: string, + @Inject('EUDRDataset') private baseDataset: string, + ) { + // if (!credentials) { + // this.logger.error('BigQuery credentials are missing'); + // throw new ServiceUnavailableException( + // 'EUDR Module not available. Tearing down the application', + // ); + // } this.bigQueryClient = new BigQuery({ - credentials: JSON.parse(credentials), + credentials: JSON.parse(this.credentials), projectId, }); } - select(dto?: any): ResourceStream { - const queryBuilder: SelectQueryBuilder = this.dataSource - .createQueryBuilder() - .select('georegionid', 'geoRegionId') - .addSelect('supplierid', 'supplierId') - .addSelect('geometry', 'geometry') - .where('alertcount >= :alertCount', { alertCount: 2 }) - .andWhere('supplierid IN (:...supplierIds)', { - supplierIds: [ - '4132ab95-8b04-4438-b706-a82651f491bd', - '4132ab95-8b04-4438-b706-a82651f491bd', - '4132ab95-8b04-4438-b706-a82651f491bd', - ], - }); - if (limit) { - queryBuilder.limit(limit); + async getAlerts(dto?: GetEUDRAlertsDto): Promise { + const queryBuilder: SelectQueryBuilder = + this.dataSource.createQueryBuilder(); + // TODO: Make field selection dynamic + queryBuilder.from(this.baseDataset, 'alerts'); + queryBuilder.select('alertdate', 'alertDate'); + queryBuilder.addSelect('alertconfidence', 'alertConfidence'); + queryBuilder.addSelect('year', 'alertYear'); + queryBuilder.addSelect('alertcount', 'alertCount'); + try { + const response: SimpleQueryRowsResponse = await this.bigQueryClient.query( + this.buildQuery(queryBuilder, dto), + ); + if (!response.length || 'error' in response) { + this.logger.error('Error in query', response); + throw new Error(); + } + return response[0]; + } catch (e) { + this.logger.error('Error in query', e); + throw new ServiceUnavailableException( + 'Unable to retrieve EUDR Data. Please contact your administrator.', + ); } - // const [rows] = await this.bigQueryClient.query( - // this.buildQuery(queryBuilder), - // ); - return this.bigQueryClient.createQueryStream(this.buildQuery(queryBuilder)); } - private buildQuery(queryBuilder: SelectQueryBuilder): { - query: string; - params: any[]; - } { - const [query, params] = queryBuilder - .from(this.BASE_DATASET, 'alerts') - .getQueryAndParameters(); - const queryOptions = { - query: query.replace(/\$\d+|"/g, (match: string) => - match === '"' ? '' : '?', - ), - params, - }; - return queryOptions; + getDates(dto: GetEUDRAlertDatesDto): Promise { + return [] as any; + } + + private buildQuery( + queryBuilder: SelectQueryBuilder, + dto?: GetEUDRAlertsDto, + ): Query { + const alertsQueryBuilder: BigQueryAlertsQueryBuilder = + new BigQueryAlertsQueryBuilder(queryBuilder, dto); + + return alertsQueryBuilder.buildQuery(); } } diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts index 78241b087..824e5785a 100644 --- a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -1,11 +1,14 @@ import { GeoJSON } from 'geojson'; -export class AlertsOutput { - geoRegionId: string; - supplierId: string; +export type AlertsOutput = { alertCount: boolean; - geometry: GeoJSON; date: Date; year: number; alertConfidence: 'low' | 'medium' | 'high' | 'very high'; -} +}; + +export type AlertGeometry = { + geometry: { value: string }; +}; + +export type AlertsWithGeom = AlertsOutput & AlertGeometry; diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts index 5448ee413..77f3e295e 100644 --- a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -1,6 +1,48 @@ -export class GetEUDRALertsDto { - supplierId: string; - geoRegionId: string; - geom: any; - year: number; +import { Type } from 'class-transformer'; +import { + IsArray, + IsDate, + IsEnum, + IsInt, + IsNumber, + IsOptional, + IsUUID, +} from 'class-validator'; + +export class GetEUDRAlertsDto { + @IsOptional() + @IsArray() + @IsUUID('4', { each: true }) + supplierIds: string[]; + + @IsOptional() + @IsArray() + @IsUUID('4', { each: true }) + geoRegionIds: string[]; + + @IsOptional() + @IsNumber() + @Type(() => Number) + startYear: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + endYear: number; + + alertConfidence: 'high' | 'medium' | 'low'; + + @IsOptional() + @IsDate() + @Type(() => Date) + startAlertDate: Date; + + @IsOptional() + @IsDate() + @Type(() => Date) + endAlertDate: Date; + + @IsOptional() + @IsInt() + limit: number = 1000; } diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index d4f44b1ed..d45b8eeaa 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -32,10 +32,8 @@ import { } from 'modules/geo-regions/geo-region.entity'; import { JSONAPIQueryParams } from 'decorators/json-api-parameters.decorator'; import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; -import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { EudrService } from 'modules/eudr-alerts/eudr.service'; -import { ResourceStream } from '@google-cloud/paginator'; -import { GetEUDRALertsDto } from './dto/get-alerts.dto'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; @Controller('/api/v1/eudr') export class EudrController { @@ -143,13 +141,8 @@ export class EudrController { } @Get('/alerts') - async getAlerts( - @Res() response: Response, - dto: GetEUDRALertsDto, - ): Promise { - const stream: ResourceStream = - this.eudrAlertsService.getAlerts(); - this.streamResponse(response, stream); + async getAlerts(@Query(ValidationPipe) dto: GetEUDRAlertsDto): Promise { + return this.eudrAlertsService.getAlerts(dto); } streamResponse(response: Response, stream: Writable): any { diff --git a/api/src/modules/eudr-alerts/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts index 9e65a3682..692323e5e 100644 --- a/api/src/modules/eudr-alerts/eudr.module.ts +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -7,8 +7,15 @@ import { SuppliersModule } from 'modules/suppliers/suppliers.module'; import { GeoRegionsModule } from 'modules/geo-regions/geo-regions.module'; import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; +import { AppConfig } from 'utils/app.config'; -export const IEUDRAlertsRepositoryToken = Symbol('IEUDRAlertsRepository'); +export const IEUDRAlertsRepositoryToken: symbol = Symbol( + 'IEUDRAlertsRepository', +); +export const EUDRDataSetToken: symbol = Symbol('EUDRDataSet'); +export const EUDRCredentialsToken: symbol = Symbol('EUDRCredentials'); + +const { credentials, dataset } = AppConfig.get('eudr'); // TODO: Use token injection and refer to the interface, right now I am having a dependencv issue @Module({ @@ -21,10 +28,10 @@ export const IEUDRAlertsRepositoryToken = Symbol('IEUDRAlertsRepository'); ], providers: [ EudrService, - AlertsRepository, - { provide: IEUDRAlertsRepositoryToken, useClass: AlertsRepository }, + { provide: 'IEUDRAlertsRepository', useClass: AlertsRepository }, + { provide: 'EUDRDataset', useValue: dataset }, + { provide: 'EUDRCredentials', useValue: credentials }, ], controllers: [EudrController], - // exports: [IEUDRAlertsRepositoryToken], }) export class EudrModule {} diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index 12f3de272..dfd1c9662 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -1,3 +1,19 @@ +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; + +export class GetEUDRAlertDatesDto { + startDate: string; + endDate: string; +} + +export type EUDRAlertDates = { + alertDate: { + value: Date | string; + }; +}; + export interface IEUDRAlertsRepository { - select(): any; + getAlerts(dto?: GetEUDRAlertsDto): Promise; + + getDates(dto: GetEUDRAlertDatesDto): Promise; } diff --git a/api/src/modules/eudr-alerts/eudr.service.ts b/api/src/modules/eudr-alerts/eudr.service.ts index 01fb0686d..3d0a232f7 100644 --- a/api/src/modules/eudr-alerts/eudr.service.ts +++ b/api/src/modules/eudr-alerts/eudr.service.ts @@ -1,13 +1,16 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ResourceStream } from '@google-cloud/paginator'; -import { RowMetadata } from '@google-cloud/bigquery/build/src/table'; -import { AlertsRepository } from './alerts.repository'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { IEUDRAlertsRepository } from 'modules/eudr-alerts/eudr.repositoty.interface'; @Injectable() export class EudrService { - constructor(private readonly alertsRepository: AlertsRepository) {} + constructor( + @Inject('IEUDRAlertsRepository') + private readonly alertsRepository: IEUDRAlertsRepository, + ) {} - getAlerts(): ResourceStream { - return this.alertsRepository.select(); + async getAlerts(dto: GetEUDRAlertsDto): Promise { + return this.alertsRepository.getAlerts(dto); } } diff --git a/api/test/utils/service-mocks.ts b/api/test/utils/service-mocks.ts index 98626b7b8..1b393ae9b 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -3,7 +3,11 @@ import { SendMailDTO, } from '../../src/modules/notifications/email/email.service.interface'; import { Logger } from '@nestjs/common'; -import { IEUDRAlertsRepository } from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { + EUDRAlertDates, + GetEUDRAlertDatesDto, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; export class MockEmailService implements IEmailService { logger: Logger = new Logger(MockEmailService.name); @@ -17,10 +21,16 @@ export class MockEmailService implements IEmailService { export class MockAlertRepository implements IEUDRAlertsRepository { logger: Logger = new Logger(MockAlertRepository.name); - select(): any { + getAlerts(): any { this.logger.warn(`Alert Repository Mock called... `); return new Promise((resolve) => { resolve([]); }); } + + getDates(dto: GetEUDRAlertDatesDto): Promise { + return new Promise((resolve) => { + resolve([]); + }); + } } From 1d0fe3832154ba4a3ed70b8e01dee2dcee03feae Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 5 Mar 2024 08:52:19 +0300 Subject: [PATCH 031/153] Dates endpoint --- .../big-query-alerts-query.builder.ts | 6 +++-- .../modules/eudr-alerts/alerts.repository.ts | 22 ++++++++++++++----- .../eudr-alerts/dto/alerts-output.dto.ts | 6 ++--- .../modules/eudr-alerts/dto/get-alerts.dto.ts | 9 +++++++- .../modules/eudr-alerts/eudr.controller.ts | 20 ++++++++++++++++- .../eudr-alerts/eudr.repositoty.interface.ts | 18 +++++++-------- api/src/modules/eudr-alerts/eudr.service.ts | 9 +++++++- api/test/utils/application-manager.ts | 3 +-- api/test/utils/service-mocks.ts | 3 +-- 9 files changed, 70 insertions(+), 26 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index 0c622c9d6..b708a09c9 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -1,7 +1,7 @@ import { SelectQueryBuilder } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; -import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { Query } from '@google-cloud/bigquery'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; export class BigQueryAlertsQueryBuilder { queryBuilder: SelectQueryBuilder; @@ -51,6 +51,8 @@ export class BigQueryAlertsQueryBuilder { this.queryBuilder.limit(this.dto?.limit); const [query, params] = this.queryBuilder.getQueryAndParameters(); + console.log('query', query); + console.log('params', params); return this.parseToBigQuery(query, params); } @@ -76,7 +78,7 @@ export class BigQueryAlertsQueryBuilder { addAlertDateRange(): void { this.queryBuilder.andWhere( - 'DATE(alertdate) BETWEEN :startAlertDate AND :endAlertDate', + 'DATE(alertdate) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)', { startAlertDate: this.dto?.startAlertDate, endAlertDate: this.dto?.endAlertDate, diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index ca611ebab..7d6ada2c6 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -13,7 +13,6 @@ import { DataSource, SelectQueryBuilder } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { EUDRAlertDates, - GetEUDRAlertDatesDto, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; @@ -52,6 +51,23 @@ export class AlertsRepository implements IEUDRAlertsRepository { queryBuilder.addSelect('alertconfidence', 'alertConfidence'); queryBuilder.addSelect('year', 'alertYear'); queryBuilder.addSelect('alertcount', 'alertCount'); + return this.query(queryBuilder, dto); + } + + async getDates(dto: GetEUDRAlertsDto): Promise { + const queryBuilder: SelectQueryBuilder = + this.dataSource.createQueryBuilder(); + queryBuilder.from(this.baseDataset, 'alerts'); + queryBuilder.select('alertdate', 'alertDate'); + queryBuilder.orderBy('alertdate', 'ASC'); + queryBuilder.groupBy('alertdate'); + return this.query(queryBuilder, dto); + } + + private async query( + queryBuilder: SelectQueryBuilder, + dto?: GetEUDRAlertsDto, + ): Promise { try { const response: SimpleQueryRowsResponse = await this.bigQueryClient.query( this.buildQuery(queryBuilder, dto), @@ -69,10 +85,6 @@ export class AlertsRepository implements IEUDRAlertsRepository { } } - getDates(dto: GetEUDRAlertDatesDto): Promise { - return [] as any; - } - private buildQuery( queryBuilder: SelectQueryBuilder, dto?: GetEUDRAlertsDto, diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts index 824e5785a..a66b22516 100644 --- a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -1,8 +1,8 @@ -import { GeoJSON } from 'geojson'; - export type AlertsOutput = { alertCount: boolean; - date: Date; + alertDate: { + value: Date | string; + }; year: number; alertConfidence: 'low' | 'medium' | 'high' | 'very high'; }; diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts index 77f3e295e..5b6699caa 100644 --- a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -1,8 +1,8 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsDate, - IsEnum, IsInt, IsNumber, IsOptional, @@ -10,21 +10,25 @@ import { } from 'class-validator'; export class GetEUDRAlertsDto { + @ApiPropertyOptional() @IsOptional() @IsArray() @IsUUID('4', { each: true }) supplierIds: string[]; + @ApiPropertyOptional() @IsOptional() @IsArray() @IsUUID('4', { each: true }) geoRegionIds: string[]; + @ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) startYear: number; + @ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) @@ -32,16 +36,19 @@ export class GetEUDRAlertsDto { alertConfidence: 'high' | 'medium' | 'low'; + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) startAlertDate: Date; + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) endAlertDate: Date; + @ApiPropertyOptional() @IsOptional() @IsInt() limit: number = 1000; diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index d45b8eeaa..01d8e7ec5 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -2,7 +2,6 @@ import { Controller, Get, Query, - Res, UseInterceptors, ValidationPipe, } from '@nestjs/common'; @@ -10,6 +9,7 @@ import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { Response } from 'express'; @@ -34,7 +34,9 @@ import { JSONAPIQueryParams } from 'decorators/json-api-parameters.decorator'; import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; import { EudrService } from 'modules/eudr-alerts/eudr.service'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface'; +@ApiTags('EUDR') @Controller('/api/v1/eudr') export class EudrController { constructor( @@ -140,6 +142,22 @@ export class EudrController { return this.geoRegionsService.serialize(results); } + @ApiOperation({ + description: 'Get EUDR alerts dates', + }) + @ApiOkResponse({ + type: EUDRAlertDates, + isArray: true, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/dates') + async getAlertDates( + @Query(ValidationPipe) dto: GetEUDRAlertsDto, + ): Promise { + return this.eudrAlertsService.getDates(dto); + } + @Get('/alerts') async getAlerts(@Query(ValidationPipe) dto: GetEUDRAlertsDto): Promise { return this.eudrAlertsService.getAlerts(dto); diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index dfd1c9662..eda3a10c6 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -1,19 +1,19 @@ import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { ApiProperty } from '@nestjs/swagger'; -export class GetEUDRAlertDatesDto { - startDate: string; - endDate: string; +class DateValue { + @ApiProperty() + value: Date | string; } -export type EUDRAlertDates = { - alertDate: { - value: Date | string; - }; -}; +export class EUDRAlertDates { + @ApiProperty() + alertDate: DateValue; +} export interface IEUDRAlertsRepository { getAlerts(dto?: GetEUDRAlertsDto): Promise; - getDates(dto: GetEUDRAlertDatesDto): Promise; + getDates(dto: GetEUDRAlertsDto): Promise; } diff --git a/api/src/modules/eudr-alerts/eudr.service.ts b/api/src/modules/eudr-alerts/eudr.service.ts index 3d0a232f7..06e909bf1 100644 --- a/api/src/modules/eudr-alerts/eudr.service.ts +++ b/api/src/modules/eudr-alerts/eudr.service.ts @@ -1,7 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; -import { IEUDRAlertsRepository } from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { + EUDRAlertDates, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; @Injectable() export class EudrService { @@ -13,4 +16,8 @@ export class EudrService { async getAlerts(dto: GetEUDRAlertsDto): Promise { return this.alertsRepository.getAlerts(dto); } + + async getDates(dto: GetEUDRAlertsDto): Promise { + return this.alertsRepository.getDates(dto); + } } diff --git a/api/test/utils/application-manager.ts b/api/test/utils/application-manager.ts index 964c52b54..61dd9fb57 100644 --- a/api/test/utils/application-manager.ts +++ b/api/test/utils/application-manager.ts @@ -10,7 +10,6 @@ import { Type } from '@nestjs/common/interfaces'; import { TestingModule } from '@nestjs/testing/testing-module'; import { isUndefined } from 'lodash'; import { MockAlertRepository, MockEmailService } from './service-mocks'; -import { IEUDRAlertsRepositoryToken } from '../../src/modules/eudr-alerts/eudr.module'; import { IEmailServiceToken } from '../../src/modules/notifications/notifications.module'; import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; @@ -48,7 +47,7 @@ export default class ApplicationManager { }) .overrideProvider(IEmailServiceToken) .useClass(MockEmailService) - .overrideProvider(AlertsRepository) + .overrideProvider('IEUDRAlertsRepository') .useClass(MockAlertRepository); ApplicationManager.testApplication.moduleFixture = diff --git a/api/test/utils/service-mocks.ts b/api/test/utils/service-mocks.ts index 1b393ae9b..45cc6dc31 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -5,7 +5,6 @@ import { import { Logger } from '@nestjs/common'; import { EUDRAlertDates, - GetEUDRAlertDatesDto, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; @@ -28,7 +27,7 @@ export class MockAlertRepository implements IEUDRAlertsRepository { }); } - getDates(dto: GetEUDRAlertDatesDto): Promise { + getDates(): Promise { return new Promise((resolve) => { resolve([]); }); From b4858f2feb5a5675dc10134028724e873cbaa6c8 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 5 Mar 2024 15:51:44 +0300 Subject: [PATCH 032/153] Add swagger spec file generation --- api/src/create-swagger-specification.ts | 34 +++++++++++++++++++++++++ api/src/main.ts | 3 +++ api/swagger-spec.json | 1 + 3 files changed, 38 insertions(+) create mode 100644 api/src/create-swagger-specification.ts create mode 100644 api/swagger-spec.json diff --git a/api/src/create-swagger-specification.ts b/api/src/create-swagger-specification.ts new file mode 100644 index 000000000..d3225745b --- /dev/null +++ b/api/src/create-swagger-specification.ts @@ -0,0 +1,34 @@ +import * as fs from 'fs'; +import * as crypto from 'crypto'; +import * as process from 'process'; + +function hashContent(content: string): string { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +export async function createOrUpdateSwaggerSpec(document: any): Promise { + if (process.env.NODE_ENV !== 'development') { + console.log( + 'Skipping Swagger spec update: Not in development environment.', + ); + return; + } + const documentString: string = JSON.stringify(document); + const currentHash: string = hashContent(documentString); + + const specPath: string = './swagger-spec.json'; + if (fs.existsSync(specPath)) { + const existingSpec: string = fs.readFileSync(specPath, 'utf8'); + const existingHash: string = hashContent(existingSpec); + + if (currentHash !== existingHash) { + console.log('Swagger spec has changed. Updating...'); + fs.writeFileSync(specPath, documentString); + } else { + console.log('No changes in Swagger spec.'); + } + } else { + console.log('Swagger spec does not exist. Creating...'); + fs.writeFileSync(specPath, documentString); + } +} diff --git a/api/src/main.ts b/api/src/main.ts index 3293a6241..eed9240ba 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -8,6 +8,7 @@ import * as compression from 'compression'; import { JwtAuthGuard } from 'guards/jwt-auth.guard'; import { useContainer } from 'class-validator'; import { SensitiveInfoGuard } from 'guards/sensitive-info.guard'; +import { createOrUpdateSwaggerSpec } from 'create-swagger-specification'; async function bootstrap(): Promise { const logger: Logger = new Logger('bootstrap'); @@ -35,6 +36,8 @@ async function bootstrap(): Promise { ); SwaggerModule.setup('/swagger', app, swaggerDocument); + await createOrUpdateSwaggerSpec(swaggerDocument); + app.useGlobalPipes( new ValidationPipe({ forbidUnknownValues: true, diff --git a/api/swagger-spec.json b/api/swagger-spec.json new file mode 100644 index 000000000..72587e740 --- /dev/null +++ b/api/swagger-spec.json @@ -0,0 +1 @@ +{"openapi":"3.0.0","paths":{"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"},"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}}}}},"/api/v1/admin-regions":{"get":{"operationId":"AdminRegionsController_findAll","summary":"","description":"Find all admin regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"AdminRegionsController_create","summary":"","description":"Create a admin region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/trees":{"get":{"operationId":"AdminRegionsController_getTrees","summary":"","description":"Find all admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Admin Regions with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{countryId}/regions":{"get":{"operationId":"AdminRegionsController_findRegionsByCountry","summary":"","description":"Find all admin regions given a country and return data in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"countryId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{id}":{"get":{"operationId":"AdminRegionsController_findOne","summary":"","description":"Find admin region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"AdminRegionsController_update","summary":"","description":"Updates a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"AdminRegionsController_delete","summary":"","description":"Deletes a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/materials":{"get":{"operationId":"MaterialsController_findAll","summary":"","description":"Find all materials and return them in a list format","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `children`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`, `hsCodeId`, `earthstatId`, `mapspamId`, `metadata`, `h3Grid`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]},"post":{"operationId":"MaterialsController_create","summary":"","description":"Create a material","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/trees":{"get":{"operationId":"MaterialsController_getTrees","summary":"","description":"Find all materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Materials with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/{id}":{"get":{"operationId":"MaterialsController_findOne","summary":"","description":"Find material by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"patch":{"operationId":"MaterialsController_update","summary":"","description":"Updates a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"delete":{"operationId":"MaterialsController_delete","summary":"","description":"Deletes a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/suppliers":{"get":{"operationId":"SuppliersController_findAll","summary":"","description":"Find all suppliers","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]},"post":{"operationId":"SuppliersController_create","summary":"","description":"Create a supplier","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/trees":{"get":{"operationId":"SuppliersController_getTrees","summary":"","description":"Find all suppliers and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Suppliers with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the supplier search","schema":{"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/types":{"get":{"operationId":"SuppliersController_getSupplierByType","summary":"","description":"Find all suppliers by type","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"type","required":true,"in":"query","schema":{"enum":["t1supplier","producer"],"type":"string"}},{"name":"sort","required":true,"in":"query","description":"The sort order by Name for the resulting entities. Can be 'ASC' (Ascendant) or 'DESC' (Descendent). Defaults to ASC","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/{id}":{"get":{"operationId":"SuppliersController_findOne","summary":"","description":"Find supplier by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"patch":{"operationId":"SuppliersController_update","summary":"","description":"Updates a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"delete":{"operationId":"SuppliersController_delete","summary":"","description":"Deletes a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/business-units":{"get":{"operationId":"BusinessUnitsController_findAll","summary":"","description":"Find all business units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"post":{"operationId":"BusinessUnitsController_create","summary":"","description":"Create a business unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/trees":{"get":{"operationId":"BusinessUnitsController_getTrees","summary":"","description":"Find all business units with sourcing-locations and return them in a tree format.","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Business Units with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the business unit search","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BusinessUnit"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/BusinessUnit"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/{id}":{"get":{"operationId":"BusinessUnitsController_findOne","summary":"","description":"Find business unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"patch":{"operationId":"BusinessUnitsController_update","summary":"","description":"Updates a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"delete":{"operationId":"BusinessUnitsController_delete","summary":"","description":"Deletes a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations":{"get":{"operationId":"SourcingLocationsController_findAll","summary":"","description":"Find all sourcing locations","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `sourcingLocationGroup`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationsController_create","summary":"","description":"Create a sourcing location","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/materials":{"get":{"operationId":"SourcingLocationsController_findAllMaterials","summary":"","description":"Find all Materials with details for Sourcing Locations","parameters":[{"name":"orderBy","required":false,"in":"query","schema":{"enum":["country","businessUnit","producer","t1Supplier","material","locationType"],"type":"string"}},{"name":"order","required":false,"in":"query","schema":{"enum":["desc","asc"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationsMaterialsResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types":{"get":{"operationId":"SourcingLocationsController_getLocationTypes","summary":"","description":"Gets available location types. Optionally returns all supported location types","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"supported","required":false,"in":"query","description":"Get all supported location types. Setting this to true overrides all other parameters","schema":{"type":"boolean"}},{"name":"sort","required":false,"in":"query","description":"Sorting parameter to order the result. Defaults to ASC ","schema":{"enum":["ASC","DESC"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types/supported":{"get":{"operationId":"SourcingLocationsController_getAllSupportedLocationTypes","summary":"","description":"Get location types supported by the platform","deprecated":true,"parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/{id}":{"get":{"operationId":"SourcingLocationsController_findOne","summary":"","description":"Find sourcing location by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationsController_update","summary":"","description":"Updates a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationsController_delete","summary":"","description":"Deletes a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/auth/sign-in":{"post":{"operationId":"sign-in","summary":"Sign user in","description":"Sign user in, issuing a JWT token.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"201":{"description":"Login successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-account":{"post":{"operationId":"AuthenticationController_validateAccount","summary":"","description":"Confirm an activation for a new user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JSONAPIUserData"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-token":{"get":{"operationId":"AuthenticationController_validateToken","parameters":[],"responses":{"200":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/refresh-token":{"post":{"operationId":"refresh-token","summary":"Refresh JWT token","description":"Request a fresh JWT token, given a still-valid one for the same user; no request payload is required: the user id is read from the JWT token presented.","parameters":[],"responses":{"201":{"description":"Token refreshed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}},"401":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/api/v1/api-events":{"get":{"operationId":"ApiEventsController_findAll","summary":"","description":"Find all API events","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEventResult"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ApiEvents"]},"post":{"operationId":"ApiEventsController_create","summary":"","description":"Create an API event","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiEventDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}/latest":{"get":{"operationId":"ApiEventsController_findLatestEventByKindAndTopic","summary":"","description":"Find latest API event by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}":{"delete":{"operationId":"ApiEventsController_deleteEventSeriesByKindAndTopic","summary":"","description":"Delete API event series by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/users":{"get":{"operationId":"UsersController_findAll","summary":"","description":"Find all users","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"UsersController_createUser","summary":"","description":"Create new user","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password":{"patch":{"operationId":"UsersController_updateOwnPassword","summary":"","description":"Update the password of a user, if they can present the current one.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserPasswordDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me":{"patch":{"operationId":"UsersController_update","summary":"","description":"Update own user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOwnUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"get":{"operationId":"UsersController_userMetadata","summary":"","description":"Retrieve attributes of the current user","parameters":[],"responses":{"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."},"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"delete":{"operationId":"UsersController_deleteOwnUser","summary":"","description":"Mark user as deleted.","parameters":[],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/recover":{"post":{"operationId":"UsersController_recoverPassword","summary":"","description":"Recover password presenting a valid user email","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoverPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/reset":{"post":{"operationId":"UsersController_resetPassword","summary":"","description":"Reset a user password presenting a valid token","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{id}":{"patch":{"operationId":"UsersController_updateUser","summary":"","description":"Update a user as admin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{userId}":{"delete":{"operationId":"UsersController_deleteUser","summary":"","description":"Delete a user. This operation will destroy any resource related to the user and it will be irreversible","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/scenarios":{"get":{"operationId":"ScenariosController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`, `status`, `userId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}},{"name":"search","required":true,"in":"query","description":"Must be provided when searching with partial matching. Each key of the map corresponds to a field that is to be matched partially, and its value, the string that will be partially matched against","schema":{"type":"map"}},{"name":"hasActiveInterventions","required":false,"in":"query","description":"If true, only scenarios with at least one active intervention will be selected.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Scenario"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenariosController_create","summary":"","description":"Create a scenario","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}":{"get":{"operationId":"ScenariosController_findOne","summary":"","description":"Find scenario by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenariosController_update","summary":"","description":"Updates a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenariosController_delete","summary":"","description":"Deletes a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}/interventions":{"get":{"operationId":"ScenariosController_findInterventionsByScenario","summary":"","description":"Find all Interventions that belong to a given Scenario Id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions":{"get":{"operationId":"ScenarioInterventionsController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenarioInterventionsController_create","summary":"","description":"Create a scenario intervention","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions/{id}":{"get":{"operationId":"ScenarioInterventionsController_findOne","summary":"","description":"Find scenario intervention by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenarioInterventionsController_update","summary":"","description":"Update a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenarioInterventionsController_delete","summary":"","description":"Delete a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/impact/table":{"get":{"operationId":"ImpactController_getImpactTable","summary":"","description":"Get data for Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/scenario":{"get":{"operationId":"ImpactController_getTwoScenariosImpactTable","summary":"","description":"Get data for comparing Impacts of 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioVsScenarioPaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/actual":{"get":{"operationId":"ImpactController_getActualVsScenarioImpactTable","summary":"","description":"Get data for comapring Actual data with Scenario in form of Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/ranking":{"get":{"operationId":"ImpactController_getRankedImpactTable","summary":"","description":"Get Ranked Impact Table, up to maxRankingEntities, aggregating the rest of entities, for each indicator ","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"maxRankingEntities","required":true,"in":"query","description":"The maximum number of entities to show in the Impact Table. If the result includes more than that, they will beaggregated into the \"other\" field in the response","schema":{"type":"number"}},{"name":"sort","required":true,"in":"query","description":"The sort order for the resulting entities. Can be 'ASC' (Ascendant) or 'DES' (Descendent), with the default being 'DES'","schema":{"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/table/report":{"get":{"operationId":"ImpactReportController_getImpactReport","summary":"","description":"Get a Impact Table CSV Report","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/actual/report":{"get":{"operationId":"ImpactReportController_getActualVsScenarioImpactReport","summary":"","description":"Get a Actual Vs Scenario Impact Table CSV Report for a given scenario","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/scenario/report":{"get":{"operationId":"ImpactReportController_getTwoScenariosImpacReport","summary":"","description":"Get a Scenario Vs Scenario Impact Table CSV Report for 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/indicators":{"get":{"operationId":"IndicatorsController_findAll","summary":"","description":"Find all indicators","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorsController_create","summary":"","description":"Create a indicator","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/indicators/{id}":{"get":{"operationId":"IndicatorsController_findOne","summary":"","description":"Find indicator by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorsController_update","summary":"","description":"Updates a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorsController_delete","summary":"","description":"Deletes a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records":{"get":{"operationId":"SourcingRecordsController_findAll","summary":"","description":"Find all sourcing record","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `tonnage`, `year`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingRecordsController_create","summary":"","description":"Create a sourcing record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/years":{"get":{"operationId":"SourcingRecordsController_getYears","summary":"","description":"Find years associated with existing sourcing records","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"List of years","type":"array","items":{"type":"integer","example":2021}}}}}}}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/{id}":{"get":{"operationId":"SourcingRecordsController_findOne","summary":"","description":"Find sourcing record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingRecordsController_update","summary":"","description":"Updates a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingRecordsController_delete","summary":"","description":"Deletes a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records":{"get":{"operationId":"IndicatorRecordsController_findAll","summary":"","description":"Find all indicator records","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `status`, `sourcingRecordId`, `indicatorId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorRecordsController_create","summary":"","description":"Create a indicator record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records/{id}":{"get":{"operationId":"IndicatorRecordsController_findOne","summary":"","description":"Find indicator record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorRecordsController_update","summary":"","description":"Updates a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorRecordsController_delete","summary":"","description":"Deletes a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/h3/data/{h3TableName}/{h3ColumnName}":{"get":{"operationId":"H3DataController_getH3ByName","summary":"","description":"Retrieve H3 data providing its name","parameters":[{"name":"h3TableName","required":true,"in":"path","schema":{"type":"string"}},{"name":"h3ColumnName","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3DataResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/years":{"get":{"operationId":"H3DataController_getYearsByLayerType","summary":"","description":"Retrieve years for which there is data, by layer","parameters":[{"name":"layer","required":true,"in":"query","schema":{"type":"string"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"indicatorId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"integer","example":2021}}}}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/material":{"get":{"operationId":"H3DataController_getMaterialMap","summary":"","description":"Get a Material map of h3 indexes by ID in a given resolution","parameters":[{"name":"materialId","required":true,"in":"query","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialId","in":"query","required":true,"schema":{"type":"string"}},{"name":"resolution","in":"query","required":true,"schema":{"type":"number"}},{"name":"year","in":"query","required":true,"schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact":{"get":{"operationId":"H3DataController_getImpactMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"The scenarioID, whose information will be included in the response. That is, the impact of all indicator records related to the interventions of that scenarioId, will be aggregated into the response map data along the actual data.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/actual/vs/scenario":{"get":{"operationId":"H3DataController_getImpactActualVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the actual data against the given Scenario. The resulting map will contain the difference between the actual data and the given scenario data plus actual data","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the actual data will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/scenario/vs/scenario":{"get":{"operationId":"H3DataController_getImpactScenarioVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the given Scenario against another Scenario. The resulting map will contain the difference between actual data and the given base scenario data, minus the actual data and the compared Scenario.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":true,"in":"query","description":"The of the scenario that will be the base for the comparison.","schema":{"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the base Scenario will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions":{"get":{"operationId":"UnitConversionsController_findAll","summary":"","description":"Find all conversion units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `unit1`, `unit2`, `factor`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitConversionsController_create","summary":"","description":"Create a conversion unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions/{id}":{"get":{"operationId":"UnitConversionsController_findOne","summary":"","description":"Find conversion unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitConversionsController_update","summary":"","description":"Updates a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitConversionsController_delete","summary":"","description":"Deletes a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions":{"get":{"operationId":"GeoRegionsController_findAll","summary":"","description":"Find all geo regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"GeoRegionsController_create","summary":"","description":"Create a geo region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions/{id}":{"get":{"operationId":"GeoRegionsController_findOne","summary":"","description":"Find geo region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"GeoRegionsController_update","summary":"","description":"Updates a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"GeoRegionsController_delete","summary":"","description":"Deletes a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/categories":{"get":{"operationId":"ContextualLayersController_getContextualLayersByCategory","summary":"","description":"Get all Contextual Layer info grouped by Category","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContextualLayerByCategory"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/{id}/h3data":{"get":{"operationId":"ContextualLayersController_getContextualLayerH3","summary":"","description":"Returns all the H3 index data for this given contextual layer, resolution and optionally year","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetContextualLayerH3ResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/import/sourcing-data":{"post":{"operationId":"ImportDataController_importSourcingRecords","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/import/eudr":{"post":{"operationId":"ImportDataController_importEudr","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups":{"get":{"operationId":"SourcingLocationGroupsController_findAll","summary":"","description":"Find all sourcing location groups","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationGroupsController_create","summary":"","description":"Create a sourcing location group","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups/{id}":{"get":{"operationId":"SourcingLocationGroupsController_findOne","summary":"","description":"Find sourcing location group by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationGroupsController_update","summary":"","description":"Updates a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationGroupsController_delete","summary":"","description":"Deletes a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/tasks":{"get":{"operationId":"TasksController_findAll","summary":"","description":"Find all tasks","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `status`, `data`, `createdBy`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"post":{"operationId":"TasksController_create","summary":"","description":"Create a Task","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/{id}":{"get":{"operationId":"TasksController_findOne","summary":"","description":"Find task by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"patch":{"operationId":"TasksController_update","summary":"","description":"Updates a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]},"delete":{"operationId":"TasksController_delete","summary":"","description":"Deletes a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/report/errors/{id}":{"get":{"operationId":"TasksController_getErrorsReport","summary":"","description":"Get a CSV report of errors by Task Id and type","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients":{"get":{"operationId":"IndicatorCoefficientsController_findAll","summary":"","description":"Find all indicator coefficients","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `year`, `indicatorSourceId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorCoefficientsController_create","summary":"","description":"Create a indicator coefficient","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients/{id}":{"get":{"operationId":"IndicatorCoefficientsController_findOne","summary":"","description":"Find indicator coefficient by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorCoefficientsController_update","summary":"","description":"Updates a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorCoefficientsController_delete","summary":"","description":"Deletes a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/targets":{"get":{"operationId":"TargetsController_findAll","summary":"","description":"Find all targets","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Target"],"security":[{"bearer":[]}]},"post":{"operationId":"TargetsController_create","summary":"","description":"Create a target","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/targets/{id}":{"get":{"operationId":"TargetsController_findOne","summary":"","description":"Find target by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"patch":{"operationId":"TargetsController_update","summary":"","description":"Updates a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"delete":{"operationId":"TargetsController_delete","summary":"","description":"Deletes a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/units":{"get":{"operationId":"UnitsController_findAll","summary":"","description":"Find all units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `symbol`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Unit"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitsController_create","summary":"","description":"Create a unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/units/{id}":{"get":{"operationId":"UnitsController_findOne","summary":"","description":"Find unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitsController_update","summary":"","description":"Updates a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitsController_delete","summary":"","description":"Deletes a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/url-params/{id}":{"get":{"operationId":"UrlParamsController_findOne","summary":"","description":"Find URL params set by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"404":{"description":"URL params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]},"delete":{"operationId":"UrlParamsController_delete","summary":"","description":"Deletes a set of URL Params","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"URL Params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/url-params":{"post":{"operationId":"UrlParamsController_create","summary":"","description":"Save URL params set","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/eudr/suppliers":{"get":{"operationId":"EudrController_getSuppliers","summary":"","description":"Find all EUDR suppliers and return them in a flat format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/materials":{"get":{"operationId":"EudrController_getMaterialsTree","summary":"","description":"Find all EUDR materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/admin-regions":{"get":{"operationId":"EudrController_getTreesForEudr","summary":"","description":"Find all EUDR admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/geo-regions":{"get":{"operationId":"EudrController_findAllEudr","summary":"","description":"Find all EUDR geo regions","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GeoRegion"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/dates":{"get":{"operationId":"EudrController_getAlertDates","summary":"","description":"Get EUDR alerts dates","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EUDRAlertDates"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/alerts":{"get":{"operationId":"EudrController_getAlerts","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"tags":["EUDR"]}}},"info":{"title":"LandGriffon API","description":"LandGriffon is a conservation planning platform.","version":"0.2.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"GeoRegion":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"theGeom":{"type":"object"},"adminRegions":{"type":"array","items":{"type":"string"}},"sourcingLocations":{"type":"array","items":{"type":"string"}}},"required":["id"]},"AdminRegion":{"type":"object","properties":{"id":{"type":"string"},"parent":{"$ref":"#/components/schemas/AdminRegion"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"sourcingLocations":{"type":"array","items":{"type":"string"}},"geoRegion":{"$ref":"#/components/schemas/GeoRegion"},"geoRegionId":{"type":"string"}},"required":["id","status","geoRegion"]},"CreateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"Material":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}},"required":["name","hsCodeId"]},"UpdateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}}},"Supplier":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"companyId":{"type":"string"},"address":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}},"required":["name"]},"UpdateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}}},"BusinessUnit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["id","name","status"]},"CreateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"SourcingLocation":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationWarning":{"type":"string"},"geoRegionId":{"type":"string"},"metadata":{"type":"object"},"materialId":{"type":"string"},"adminRegionId":{"type":"string"},"businessUnitId":{"type":"string"},"sourcingLocationGroupId":{"type":"string"},"interventionType":{"type":"string"},"scenarioInterventionId":{"type":"string"}},"required":["createdAt","updatedAt","id","locationType","locationAccuracy"]},"SourcingLocationsMaterialsResponseDto":{"type":"object","properties":{"meta":{"type":"object","properties":{"totalItems":{"type":"number","example":45},"totalPages":{"type":"number","example":9},"size":{"type":"number","example":5},"page":{"type":"number","example":1}}},"data":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","example":"sourcing locations"},"id":{"type":"string","example":"a2428cbb-e1b1-4313-ad85-9579b260387f"},"attributes":{"type":"object","properties":{"locationType":{"type":"string","example":"point of production"},"material":{"type":"string","example":"bananas"},"materialId":{"type":"string","example":"cdde28a2-5692-401b-a1a7-6c68ad38010f"},"t1Supplier":{"type":"string","example":"Cargill"},"producer":{"type":"string","example":"Moll"},"businessUnit":{"type":"string","example":"Accessories"},"locationCountryInput":{"type":"string","example":"Japan"},"purchases":{"type":"array","items":{"type":"object","properties":{"year":{"type":"number","example":2010},"tonnage":{"type":"number","example":730}}}}}}}}}},"required":["meta","data"]},"LocationTypesDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}}}}},"required":["data"]},"CreateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}},"required":["title","materialId"]},"UpdateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}}},"LoginDto":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}},"required":["username","password"]},"AccessToken":{"type":"object","properties":{"user":{"type":"object"},"accessToken":{"type":"string"}},"required":["user","accessToken"]},"ResetPasswordDto":{"type":"object","properties":{"password":{"type":"string"}},"required":["password"]},"Permission":{"type":"object","properties":{"action":{"type":"string"}},"required":["action"]},"Role":{"type":"object","properties":{"name":{"type":"string","enum":["admin","user"]},"permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}}},"required":["name","permissions"]},"User":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"string"},"fname":{"type":"string"},"lname":{"type":"string"},"avatarDataUrl":{"type":"string"},"isActive":{"type":"boolean"},"isDeleted":{"type":"boolean"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}},"required":["email","isActive","isDeleted","roles"]},"JSONAPIUserData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/User"}},"required":["type","id","attributes"]},"ApiEvent":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"}},"required":["kind","topic"]},"JSONAPIApiEventData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/ApiEvent"}},"required":["type","id","attributes"]},"ApiEventResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIApiEventData"}},"required":["data"]},"CreateApiEventDTO":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"},"data":{"type":"object"}},"required":["kind","topic"]},"CreateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}},"required":["email","password","roles"]},"UpdateUserPasswordDTO":{"type":"object","properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string"}},"required":["currentPassword","newPassword"]},"UserResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIUserData"}},"required":["data"]},"UpdateOwnUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"avatarDataUrl":{"type":"string"}},"required":["email"]},"RecoverPasswordDto":{"type":"object","properties":{}},"UpdateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}}},"Scenario":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean","description":"Make a Scenario public to all users"},"status":{"type":"string","enum":["active","inactive","deleted"]},"metadata":{"type":"object"},"user":{"$ref":"#/components/schemas/User"},"userId":{"type":"string"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","user","updatedBy"]},"CreateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}},"required":["title"]},"UpdateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}}},"ScenarioIntervention":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"startYear":{"type":"number"},"endYear":{"type":"number"},"percentage":{"type":"number"},"newIndicatorCoefficients":{"type":"object"},"scenario":{"$ref":"#/components/schemas/Scenario"},"newMaterial":{"$ref":"#/components/schemas/Material"},"newBusinessUnit":{"$ref":"#/components/schemas/BusinessUnit"},"newT1Supplier":{"$ref":"#/components/schemas/Supplier"},"newProducer":{"$ref":"#/components/schemas/Supplier"},"newAdminRegion":{"$ref":"#/components/schemas/AdminRegion"},"newLocationType":{"type":"string"},"newLocationCountryInput":{"type":"string"},"newLocationAddressInput":{"type":"string"},"newLocationLatitudeInput":{"type":"number"},"newLocationLongitudeInput":{"type":"number"},"newMaterialTonnageRatio":{"type":"number"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","type","startYear","percentage","newIndicatorCoefficients","scenario","updatedBy"]},"IndicatorCoefficientsDto":{"type":"object","properties":{"LF":{"type":"number"},"DF_SLUC":{"type":"number"},"GHG_DEF_SLUC":{"type":"number"},"UWU":{"type":"number"},"WU":{"type":"number"},"NL":{"type":"number"},"NCE":{"type":"number"},"FLIL":{"type":"number"},"ENL":{"type":"number"},"GHG_FARM":{"type":"number"}}},"CreateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5}},"required":["title","type","startYear","percentage","scenarioId","materialIds","adminRegionIds","newLocationCountryInput","newLocationAdminRegionInput"]},"UpdateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5},"status":{"type":"string","description":"Status of the intervention","enum":["active","inactive","deleted"],"example":"inactive"}}},"ImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"value":{"type":"number"}},"required":["year","isProjected","value"]},"ImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"YearSumData":{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]},"ImpactTableDataAggregationInfo":{"type":"object","properties":{}},"ImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/YearSumData"}},"metadata":{"type":"object"},"others":{"description":"Extra information used for Ranked ImpactTable requests. Missing on normal ImpactTable requests","allOf":[{"$ref":"#/components/schemas/ImpactTableDataAggregationInfo"}]}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"PaginationMeta":{"type":"object","properties":{}},"PaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"ScenarioVsScenarioImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"ScenarioVsScenarioIndicatorSumByYearData":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioIndicatorSumByYearData"}},"metadata":{"type":"object"}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ScenarioVsScenarioImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ScenarioVsScenarioImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"ScenarioVsScenarioPaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"Indicator":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"},"indicatorCoefficients":{"type":"array","items":{"type":"string"}}},"required":["id","name","status"]},"CreateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}},"required":["name","nameCode"]},"UpdateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}}},"SourcingRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"tonnage":{"type":"number"},"year":{"type":"number"},"metadata":{"type":"object"},"updatedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","tonnage","year","updatedBy"]},"CreateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}},"required":["tonnage","year"]},"UpdateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}}},"IndicatorRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["createdAt","updatedAt","id","value","status","statusMsg"]},"CreateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"sourcingRecordId":{"type":"string"},"indicatorId":{"type":"string"},"indicatorCoefficientId":{"type":"string"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["value","indicatorId"]},"UpdateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"},"status":{"type":"string"}}},"H3DataResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"H3MapResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}},"metadata":{"type":"object","properties":{"unit":{"type":"string"},"quantiles":{"type":"array","items":{"type":"number"}},"indicatorDataYear":{"type":"number"},"materialsH3DataYears":{"type":"array","items":{"type":"object","properties":{"materialName":{"type":"string"},"materialDataYear":{"type":"number"},"materialDataType":{"type":"string"}}}}}}},"required":["data","metadata"]},"UnitConversion":{"type":"object","properties":{"id":{"type":"string"},"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}},"required":["id"]},"CreateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"UpdateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"CreateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"UpdateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"ContextualLayerByCategory":{"type":"object","properties":{}},"GetContextualLayerH3ResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"SourcingLocationGroup":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title"]},"CreateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}},"required":["title"]},"UpdateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}}},"Task":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"user":{"$ref":"#/components/schemas/User"},"status":{"type":"string"},"message":{"type":"string"},"data":{"type":"object"},"logs":{"type":"array","items":{"type":"string"}},"errors":{"type":"array","items":{"type":"string"}},"dismissedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","type","user","status","data","logs","errors","dismissedBy"]},"CreateTaskDto":{"type":"object","properties":{"type":{"type":"string"},"status":{"type":"string"},"data":{"type":"object"}},"required":["type","status","data"]},"UpdateTaskDto":{"type":"object","properties":{"status":{"type":"string"},"newData":{"type":"object"},"dismissedBy":{"type":"string"}}},"IndicatorCoefficient":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"year":{"type":"number"},"adminRegion":{"$ref":"#/components/schemas/AdminRegion"},"user":{"$ref":"#/components/schemas/User"},"indicator":{"$ref":"#/components/schemas/Indicator"},"material":{"$ref":"#/components/schemas/Material"}},"required":["createdAt","updatedAt","id","year","user","indicator","material"]},"CreateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}},"required":["year"]},"UpdateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}}},"Target":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","baseLineYear","targetYear","value","indicatorId","updatedById"]},"CreateTargetDto":{"type":"object","properties":{"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"}},"required":["baseLineYear","targetYear","value","indicatorId"]},"UpdateTargetDto":{"type":"object","properties":{"targetYear":{"type":"number"},"value":{"type":"number"}},"required":["targetYear","value"]},"Unit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"number"}},"required":["id","name","symbol"]},"CreateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}},"required":["name"]},"UpdateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}}},"UrlResponseAttributes":{"type":"object","properties":{"params":{"type":"object"}},"required":["params"]},"UrlResponseDto":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/UrlResponseAttributes"}},"required":["type","id","attributes"]},"SerializedUrlResponseDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UrlResponseDto"}},"required":["data"]},"DateValue":{"type":"object","properties":{"value":{"type":"object"}},"required":["value"]},"EUDRAlertDates":{"type":"object","properties":{"alertDate":{"$ref":"#/components/schemas/DateValue"}},"required":["alertDate"]}}}} \ No newline at end of file From 8d4f44ca889abdc2401c8fa56fd333b74e40d74a Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 5 Mar 2024 11:51:41 +0300 Subject: [PATCH 033/153] add eudr credentials k8s secret and eudr dataset api env var --- infrastructure/kubernetes/main.tf | 1 + .../kubernetes/modules/aws/env/main.tf | 16 ++++++++++++---- .../kubernetes/modules/aws/env/variables.tf | 6 ++++++ .../kubernetes/modules/aws/secrets/main.tf | 2 ++ .../kubernetes/modules/aws/secrets/variable.tf | 6 ++++++ infrastructure/kubernetes/variables.tf | 18 ++++++++++++++++++ .../kubernetes/vars/terraform.tfvars | 2 ++ 7 files changed, 47 insertions(+), 4 deletions(-) diff --git a/infrastructure/kubernetes/main.tf b/infrastructure/kubernetes/main.tf index 828b0c9b1..85de0e034 100644 --- a/infrastructure/kubernetes/main.tf +++ b/infrastructure/kubernetes/main.tf @@ -76,6 +76,7 @@ module "aws_environment" { allowed_account_id = var.allowed_account_id gmaps_api_key = var.gmaps_api_key sendgrid_api_key = var.sendgrid_api_key + eudr_credentials = jsonencode(var.eudr_credentials) load_fresh_data = lookup(each.value, "load_fresh_data", false) data_import_arguments = lookup(each.value, "data_import_arguments", ["seed-data"]) image_tag = lookup(each.value, "image_tag", each.key) diff --git a/infrastructure/kubernetes/modules/aws/env/main.tf b/infrastructure/kubernetes/modules/aws/env/main.tf index 2a1fd1ed4..408903efd 100644 --- a/infrastructure/kubernetes/modules/aws/env/main.tf +++ b/infrastructure/kubernetes/modules/aws/env/main.tf @@ -29,13 +29,13 @@ locals { name = "REQUIRE_USER_ACCOUNT_ACTIVATION" value = "true" }, - { - name = "USE_NEW_METHODOLOGY" - value = "true" - }, { name = "FILE_SIZE_LIMIT" value = 31457280 + }, + { + name = "EUDR_DATASET" + value = "cartobq.eudr.mock_data_optimized" } ] : env.name => env.value } @@ -136,8 +136,14 @@ module "k8s_api" { name = "SENDGRID_API_KEY" secret_name = "api" secret_key = "SENDGRID_API_KEY" + }, + { + name = "EUDR_CREDENTIALS" + secret_name = "api" + secret_key = "EUDR_CREDENTIALS" } + ]) env_vars = local.api_env_vars @@ -260,6 +266,7 @@ module "k8s_data_import" { ] } + module "k8s_secrets" { source = "../secrets" tf_state_bucket = var.tf_state_bucket @@ -268,6 +275,7 @@ module "k8s_secrets" { namespace = var.environment gmaps_api_key = var.gmaps_api_key sendgrid_api_key = var.sendgrid_api_key + eudr_credentials = var.eudr_credentials depends_on = [ module.k8s_namespace diff --git a/infrastructure/kubernetes/modules/aws/env/variables.tf b/infrastructure/kubernetes/modules/aws/env/variables.tf index 74c9f5517..2b5f486cc 100644 --- a/infrastructure/kubernetes/modules/aws/env/variables.tf +++ b/infrastructure/kubernetes/modules/aws/env/variables.tf @@ -67,6 +67,12 @@ variable "sendgrid_api_key" { description = "The Sendgrid API key used for sending emails" } +variable "eudr_credentials" { + type = string + sensitive = true + description = "Service Account credentials to access EUDR Data" +} + variable "load_fresh_data" { type = bool default = false diff --git a/infrastructure/kubernetes/modules/aws/secrets/main.tf b/infrastructure/kubernetes/modules/aws/secrets/main.tf index 30b3b3217..5c03e584c 100644 --- a/infrastructure/kubernetes/modules/aws/secrets/main.tf +++ b/infrastructure/kubernetes/modules/aws/secrets/main.tf @@ -11,6 +11,7 @@ locals { jwt_password_reset_secret = random_password.jwt_password_reset_secret_generator.result gmaps_api_key = var.gmaps_api_key sendgrid_api_key = var.sendgrid_api_key + eudr_credentials = var.eudr_credentials } } @@ -52,6 +53,7 @@ resource "kubernetes_secret" "api_secret" { JWT_PASSWORD_RESET_SECRET = local.api_secret_json.jwt_password_reset_secret GMAPS_API_KEY = local.api_secret_json.gmaps_api_key SENDGRID_API_KEY = local.api_secret_json.sendgrid_api_key + EUDR_CREDENTIALS = local.api_secret_json.eudr_credentials } } diff --git a/infrastructure/kubernetes/modules/aws/secrets/variable.tf b/infrastructure/kubernetes/modules/aws/secrets/variable.tf index cf63b9f94..f9dba198a 100644 --- a/infrastructure/kubernetes/modules/aws/secrets/variable.tf +++ b/infrastructure/kubernetes/modules/aws/secrets/variable.tf @@ -29,3 +29,9 @@ variable "sendgrid_api_key" { sensitive = true description = "The SendGrid API key used for sending emails" } + +variable "eudr_credentials" { + type = string + sensitive = true + description = "Service Account credentials to access EUDR Data" +} diff --git a/infrastructure/kubernetes/variables.tf b/infrastructure/kubernetes/variables.tf index 67c2a4203..474394667 100644 --- a/infrastructure/kubernetes/variables.tf +++ b/infrastructure/kubernetes/variables.tf @@ -57,6 +57,24 @@ variable "sendgrid_api_key" { description = "The Sendgrid API key used for sending emails" } +variable "eudr_credentials" { + type = object({ + type = string + project_id = string + private_key = string + private_key_id = string + client_email = string + client_id = string + auth_uri = string + client_x509_cert_url = string + token_uri = string + auth_provider_x509_cert_url = string + universe_domain = string + }) + sensitive = true + description = "Service Account credentials to access EUDR Data" +} + variable "repo_name" { type = string description = "Name of the github repo where the project is hosted" diff --git a/infrastructure/kubernetes/vars/terraform.tfvars b/infrastructure/kubernetes/vars/terraform.tfvars index 001b5c073..c3033dfd5 100644 --- a/infrastructure/kubernetes/vars/terraform.tfvars +++ b/infrastructure/kubernetes/vars/terraform.tfvars @@ -25,4 +25,6 @@ gcp_project_id = "landgriffon" gmaps_api_key = "" mapbox_api_token = "" sendgrid_api_key = "" +eudr_credentials = {} + From ebee8721ad65bb5234d73dd85854bd7fdc07afd4 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 08:42:58 +0300 Subject: [PATCH 034/153] Adds reverse index to sourcing location georegionid column --- api/src/modules/sourcing-locations/sourcing-location.entity.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/modules/sourcing-locations/sourcing-location.entity.ts b/api/src/modules/sourcing-locations/sourcing-location.entity.ts index 22489ab58..863e881f4 100644 --- a/api/src/modules/sourcing-locations/sourcing-location.entity.ts +++ b/api/src/modules/sourcing-locations/sourcing-location.entity.ts @@ -104,8 +104,10 @@ export class SourcingLocation extends TimestampedBaseEntity { (geoRegion: GeoRegion) => geoRegion.sourcingLocations, { eager: true }, ) + @JoinColumn({ name: 'geoRegionId' }) geoRegion: GeoRegion; + @Index() @Column({ nullable: true }) @ApiPropertyOptional() geoRegionId: string; From 51f77b1cd8c7995599798bf70db5b202e749ef76 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 08:45:48 +0300 Subject: [PATCH 035/153] Adds geo-features.service.ts --- .../geo-regions/geo-features.service.ts | 47 +++++++++++++++++++ .../modules/geo-regions/geo-regions.module.ts | 3 +- .../geo-regions/geo-regions.service.ts | 17 +++++-- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 api/src/modules/geo-regions/geo-features.service.ts diff --git a/api/src/modules/geo-regions/geo-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts new file mode 100644 index 000000000..c94e40a6c --- /dev/null +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository, SelectQueryBuilder } from 'typeorm'; +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { FeatureCollection, Feature } from 'geojson'; +import { + GetEUDRFeaturesGeoJSONDto, + GetFeaturesGeoJsonDto, +} from 'modules/geo-regions/dto/get-features-geojson.dto'; +import { + LOCATION_TYPES, + SourcingLocation, +} from 'modules/sourcing-locations/sourcing-location.entity'; + +@Injectable() +export class GeoFeaturesService extends Repository { + constructor(private dataSource: DataSource) { + super(GeoRegion, dataSource.createEntityManager()); + } + + async getGeoFeatures(): Promise { + return null as any; + } + + async getGeoJson( + dto?: GetEUDRFeaturesGeoJSONDto | GetFeaturesGeoJsonDto, + ): Promise { + const queryBuilder: SelectQueryBuilder = + this.createQueryBuilder('gr'); + queryBuilder + .select( + 'json_build_object(type, FeatureCollection, features, json_agg(ST_AsGeoJSON(gr.theGeom)::json))', + 'geojson', + ) + .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); + if (dto?.geoRegionIds) { + queryBuilder.where('gr.id IN (:...ids)', { ids: dto.geoRegionIds }); + } + + if (dto?.isEUDRRequested()) { + queryBuilder.andWhere('sl.locationType = :type', { + type: LOCATION_TYPES.EUDR, + }); + } + const [qury, params] = queryBuilder.getQueryAndParameters(); + return queryBuilder.getRawMany(); + } +} diff --git a/api/src/modules/geo-regions/geo-regions.module.ts b/api/src/modules/geo-regions/geo-regions.module.ts index 4bacd115e..bcf572bcb 100644 --- a/api/src/modules/geo-regions/geo-regions.module.ts +++ b/api/src/modules/geo-regions/geo-regions.module.ts @@ -7,6 +7,7 @@ import { GeoRegionRepository } from 'modules/geo-regions/geo-region.repository'; import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; import { MaterialsModule } from 'modules/materials/materials.module'; import { SuppliersModule } from 'modules/suppliers/suppliers.module'; +import { GeoFeaturesService } from 'modules/geo-regions/geo-features.service'; @Module({ imports: [ @@ -16,7 +17,7 @@ import { SuppliersModule } from 'modules/suppliers/suppliers.module'; SuppliersModule, ], controllers: [GeoRegionsController], - providers: [GeoRegionsService, GeoRegionRepository], + providers: [GeoRegionsService, GeoRegionRepository, GeoFeaturesService], exports: [GeoRegionsService, GeoRegionRepository], }) export class GeoRegionsModule {} diff --git a/api/src/modules/geo-regions/geo-regions.service.ts b/api/src/modules/geo-regions/geo-regions.service.ts index 4a9125cf4..a0f575c0d 100644 --- a/api/src/modules/geo-regions/geo-regions.service.ts +++ b/api/src/modules/geo-regions/geo-regions.service.ts @@ -12,12 +12,14 @@ import { GeoRegionRepository } from 'modules/geo-regions/geo-region.repository'; import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto'; import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto'; import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto'; -import { GetSupplierByType } from '../suppliers/dto/get-supplier-by-type.dto'; -import { Supplier } from '../suppliers/supplier.entity'; import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; import { MaterialsService } from 'modules/materials/materials.service'; -import { SupplierRepository } from 'modules/suppliers/supplier.repository'; -import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; +import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; +import { + GetEUDRFeaturesGeoJSONDto, + GetFeaturesGeoJsonDto, +} from 'modules/geo-regions/dto/get-features-geojson.dto'; +import { GeoFeaturesService } from 'modules/geo-regions/geo-features.service'; @Injectable() export class GeoRegionsService extends AppBaseService< @@ -30,6 +32,7 @@ export class GeoRegionsService extends AppBaseService< protected readonly geoRegionRepository: GeoRegionRepository, private readonly adminRegionService: AdminRegionsService, private readonly materialsService: MaterialsService, + private readonly geoFeatures: GeoFeaturesService, ) { super( geoRegionRepository, @@ -115,4 +118,10 @@ export class GeoRegionsService extends AppBaseService< return this.geoRegionRepository.getGeoRegionsFromSourcingLocations(options); } + + async getGeoJson( + dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, + ): Promise { + return this.geoFeatures.getGeoJson(dto); + } } From af397c1e711a605bb433b7bf8174c86cc8d4005c Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 10:30:04 +0300 Subject: [PATCH 036/153] Get Features and FeatureCollection, update swagger spec --- api/src/create-swagger-specification.ts | 37 ++++---- .../modules/eudr-alerts/eudr.controller.ts | 49 +++++++---- .../dto/geo-feature-response.dto.ts | 33 ++++++++ .../dto/get-features-geojson.dto.ts | 25 ++++++ .../geo-regions/geo-features.service.ts | 84 ++++++++++++++----- .../modules/geo-regions/geo-region.entity.ts | 3 +- .../geo-regions/geo-region.repository.ts | 8 +- .../geo-regions/geo-regions.controller.ts | 2 - .../geo-regions/geo-regions.service.ts | 4 +- .../eudr/eudr.dto-processor.service.ts | 3 +- api/swagger-spec.json | 2 +- 11 files changed, 183 insertions(+), 67 deletions(-) create mode 100644 api/src/modules/geo-regions/dto/geo-feature-response.dto.ts create mode 100644 api/src/modules/geo-regions/dto/get-features-geojson.dto.ts diff --git a/api/src/create-swagger-specification.ts b/api/src/create-swagger-specification.ts index d3225745b..536d9f133 100644 --- a/api/src/create-swagger-specification.ts +++ b/api/src/create-swagger-specification.ts @@ -6,29 +6,30 @@ function hashContent(content: string): string { return crypto.createHash('sha256').update(content).digest('hex'); } +const active: boolean = false; + export async function createOrUpdateSwaggerSpec(document: any): Promise { - if (process.env.NODE_ENV !== 'development') { - console.log( - 'Skipping Swagger spec update: Not in development environment.', - ); - return; - } - const documentString: string = JSON.stringify(document); - const currentHash: string = hashContent(documentString); + if (active && process.env.NODE_ENV == 'development') { + const documentString: string = JSON.stringify(document); + const currentHash: string = hashContent(documentString); - const specPath: string = './swagger-spec.json'; - if (fs.existsSync(specPath)) { - const existingSpec: string = fs.readFileSync(specPath, 'utf8'); - const existingHash: string = hashContent(existingSpec); + const specPath: string = './swagger-spec.json'; + if (fs.existsSync(specPath)) { + const existingSpec: string = fs.readFileSync(specPath, 'utf8'); + const existingHash: string = hashContent(existingSpec); - if (currentHash !== existingHash) { - console.log('Swagger spec has changed. Updating...'); - fs.writeFileSync(specPath, documentString); + if (currentHash !== existingHash) { + console.log('Swagger spec has changed. Updating...'); + fs.writeFileSync(specPath, documentString); + } else { + console.log('No changes in Swagger spec.'); + } } else { - console.log('No changes in Swagger spec.'); + console.log('Swagger spec does not exist. Creating...'); + fs.writeFileSync(specPath, documentString); } } else { - console.log('Swagger spec does not exist. Creating...'); - fs.writeFileSync(specPath, documentString); + console.log('Swagger spec update is not active.'); + return; } } diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 01d8e7ec5..0e0236545 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -6,14 +6,15 @@ import { ValidationPipe, } from '@nestjs/common'; import { + ApiExtraModels, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, + refs, } from '@nestjs/swagger'; -import { Response } from 'express'; -import { Writable } from 'stream'; + import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; import { Supplier } from 'modules/suppliers/supplier.entity'; import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor'; @@ -35,6 +36,12 @@ import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; import { EudrService } from 'modules/eudr-alerts/eudr.service'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { GetEUDRFeaturesGeoJSONDto } from 'modules/geo-regions/dto/get-features-geojson.dto'; +import { Feature, FeatureCollection } from 'geojson'; +import { + GeoFeatureCollectionResponse, + GeoFeatureResponse, +} from '../geo-regions/dto/geo-feature-response.dto'; @ApiTags('EUDR') @Controller('/api/v1/eudr') @@ -163,19 +170,31 @@ export class EudrController { return this.eudrAlertsService.getAlerts(dto); } - streamResponse(response: Response, stream: Writable): any { - stream.on('data', (data: any) => { - const json: string = JSON.stringify(data); - response.write(json + '\n'); - }); - - stream.on('end', () => { - response.end(); - }); + @ApiOperation({ + description: 'Get a Feature List GeoRegion Ids', + }) + @ApiOkResponse({ type: GeoFeatureResponse, isArray: true }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/geo-features') + async getGeoFeatureList( + @Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto, + ): Promise { + return this.geoRegionsService.getGeoJson(dto); + } - stream.on('error', (error: any) => { - console.error('Stream error:', error); - response.status(500).send('Error processing stream'); - }); + @ApiOperation({ + description: 'Get a Feature Collection by GeoRegion Ids', + }) + @ApiOkResponse({ type: GeoFeatureCollectionResponse }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/geo-features/collection') + async getGeoFeatureCollection( + @Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto, + ): Promise { + return this.geoRegionsService.getGeoJson( + Object.assign(dto, { collection: true }), + ); } } diff --git a/api/src/modules/geo-regions/dto/geo-feature-response.dto.ts b/api/src/modules/geo-regions/dto/geo-feature-response.dto.ts new file mode 100644 index 000000000..256d96324 --- /dev/null +++ b/api/src/modules/geo-regions/dto/geo-feature-response.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + Feature, + FeatureCollection, + GeoJsonProperties, + Geometry, +} from 'geojson'; + +class FeatureClass implements Feature { + @ApiProperty() + geometry: Geometry; + @ApiProperty() + properties: GeoJsonProperties; + @ApiProperty() + type: 'Feature'; +} + +class FeatureCollectionClass implements FeatureCollection { + @ApiProperty() + features: Feature[]; + @ApiProperty() + type: 'FeatureCollection'; +} + +export class GeoFeatureResponse { + @ApiProperty() + geojson: FeatureClass; +} + +export class GeoFeatureCollectionResponse { + @ApiProperty() + geojson: FeatureCollectionClass; +} diff --git a/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts new file mode 100644 index 000000000..2bd593067 --- /dev/null +++ b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts @@ -0,0 +1,25 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; + +export class GetFeaturesGeoJsonDto { + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + geoRegionIds!: string[]; + + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + collection: boolean = false; + + isEUDRRequested(): boolean { + return 'eudr' in this; + } +} + +export class GetEUDRFeaturesGeoJSONDto extends GetFeaturesGeoJsonDto { + @IsOptional() + @IsBoolean() + eudr: boolean = true; +} diff --git a/api/src/modules/geo-regions/geo-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts index c94e40a6c..231978169 100644 --- a/api/src/modules/geo-regions/geo-features.service.ts +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { DataSource, Repository, SelectQueryBuilder } from 'typeorm'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { FeatureCollection, Feature } from 'geojson'; @@ -13,35 +13,75 @@ import { @Injectable() export class GeoFeaturesService extends Repository { + logger: Logger = new Logger(GeoFeaturesService.name); + constructor(private dataSource: DataSource) { super(GeoRegion, dataSource.createEntityManager()); } - async getGeoFeatures(): Promise { - return null as any; - } - - async getGeoJson( - dto?: GetEUDRFeaturesGeoJSONDto | GetFeaturesGeoJsonDto, - ): Promise { + async getGeoFeatures( + dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, + ): Promise { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder('gr'); - queryBuilder - .select( - 'json_build_object(type, FeatureCollection, features, json_agg(ST_AsGeoJSON(gr.theGeom)::json))', - 'geojson', - ) - .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); - if (dto?.geoRegionIds) { - queryBuilder.where('gr.id IN (:...ids)', { ids: dto.geoRegionIds }); + queryBuilder.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); + if (dto.isEUDRRequested()) { + queryBuilder.where('sl.locationType = :locationType', { + locationType: LOCATION_TYPES.EUDR, + }); } - - if (dto?.isEUDRRequested()) { - queryBuilder.andWhere('sl.locationType = :type', { - type: LOCATION_TYPES.EUDR, + if (dto.geoRegionIds) { + queryBuilder.andWhere('gr.id IN (:...geoRegionIds)', { + geoRegionIds: dto.geoRegionIds, }); } - const [qury, params] = queryBuilder.getQueryAndParameters(); - return queryBuilder.getRawMany(); + if (dto?.collection) { + return this.selectAsFeatureCollection(queryBuilder); + } + return this.selectAsFeatures(queryBuilder); + } + + private async selectAsFeatures( + queryBuilder: SelectQueryBuilder, + ): Promise { + queryBuilder.select( + ` + json_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(gr.theGeom)::json, + 'properties', json_build_object('id', gr.id) + )`, + 'geojson', + ); + const result: Feature[] | undefined = await queryBuilder.getRawMany(); + if (!result.length) { + throw new NotFoundException(`Could not retrieve geo features`); + } + return result; + } + + private async selectAsFeatureCollection( + queryBuilder: SelectQueryBuilder, + ): Promise { + queryBuilder.select( + ` + json_build_object( + 'type', 'FeatureCollection', + 'features', json_agg( + json_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(gr.theGeom)::json, + 'properties', json_build_object('id', gr.id) + ) + ) + )`, + 'geojson', + ); + const result: FeatureCollection | undefined = + await queryBuilder.getRawOne(); + if (!result) { + throw new NotFoundException(`Could not retrieve geo features`); + } + return result; } } diff --git a/api/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index 7c05a4961..6c13c6df2 100644 --- a/api/src/modules/geo-regions/geo-region.entity.ts +++ b/api/src/modules/geo-regions/geo-region.entity.ts @@ -10,6 +10,7 @@ import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { BaseServiceResource } from 'types/resource.interface'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; +import { Geometry } from 'geojson'; export const geoRegionResource: BaseServiceResource = { className: 'GeoRegion', @@ -47,7 +48,7 @@ export class GeoRegion extends BaseEntity { nullable: true, }) @ApiPropertyOptional() - theGeom?: JSON; + theGeom?: Geometry; // TODO: It might be interesting to add a trigger to calculate the value in case it's not provided. We are considering that EUDR will alwaus provide the value // but not the regular ingestion diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index 365365038..5bd49584c 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -7,11 +7,9 @@ import { import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { LocationGeoRegionDto } from 'modules/geo-regions/dto/location.geo-region.dto'; import { Injectable } from '@nestjs/common'; -import { GetAdminRegionTreeWithOptionsDto } from '../admin-regions/dto/get-admin-region-tree-with-options.dto'; -import { AdminRegion } from '../admin-regions/admin-region.entity'; -import { SourcingLocation } from '../sourcing-locations/sourcing-location.entity'; -import { BaseQueryBuilder } from '../../utils/base.query-builder'; -import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; +import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; +import { BaseQueryBuilder } from 'utils/base.query-builder'; +import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; @Injectable() export class GeoRegionRepository extends Repository { diff --git a/api/src/modules/geo-regions/geo-regions.controller.ts b/api/src/modules/geo-regions/geo-regions.controller.ts index 1ab8d4102..48937c8e6 100644 --- a/api/src/modules/geo-regions/geo-regions.controller.ts +++ b/api/src/modules/geo-regions/geo-regions.controller.ts @@ -6,7 +6,6 @@ import { Param, Patch, Post, - Query, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -36,7 +35,6 @@ import { import { CreateGeoRegionDto } from 'modules/geo-regions/dto/create.geo-region.dto'; import { UpdateGeoRegionDto } from 'modules/geo-regions/dto/update.geo-region.dto'; import { PaginationMeta } from 'utils/app-base.service'; -import { GetEUDRGeoRegions } from './dto/get-geo-region.dto'; @Controller(`/api/v1/geo-regions`) @ApiTags(geoRegionResource.className) diff --git a/api/src/modules/geo-regions/geo-regions.service.ts b/api/src/modules/geo-regions/geo-regions.service.ts index a0f575c0d..d4483dd36 100644 --- a/api/src/modules/geo-regions/geo-regions.service.ts +++ b/api/src/modules/geo-regions/geo-regions.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { AppBaseService, JSONAPISerializerConfig, @@ -122,6 +122,6 @@ export class GeoRegionsService extends AppBaseService< async getGeoJson( dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, ): Promise { - return this.geoFeatures.getGeoJson(dto); + return this.geoFeatures.getGeoFeatures(dto); } } diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index df36b2adc..f587ebcdc 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -20,6 +20,7 @@ import * as wellknown from 'wellknown'; import { DataSource, QueryRunner, Repository } from 'typeorm'; import { GeoCodingError } from 'modules/geo-coding/errors/geo-coding.error'; import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +import { Geometry } from 'geojson'; /** * @debt: Define a more accurate DTO / Interface / Class for API-DB trades @@ -87,7 +88,7 @@ export class EUDRDTOProcessor { const geoRegion: GeoRegion = new GeoRegion(); let savedGeoRegion: GeoRegion; geoRegion.totalArea = row.total_area_ha; - geoRegion.theGeom = wellknown.parse(row.geometry) as unknown as JSON; + geoRegion.theGeom = wellknown.parse(row.geometry) as Geometry; geoRegion.isCreatedByUser = true; geoRegion.name = row.plot_name; const foundGeoRegion: GeoRegion | null = diff --git a/api/swagger-spec.json b/api/swagger-spec.json index 72587e740..3f7146426 100644 --- a/api/swagger-spec.json +++ b/api/swagger-spec.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"},"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}}}}},"/api/v1/admin-regions":{"get":{"operationId":"AdminRegionsController_findAll","summary":"","description":"Find all admin regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"AdminRegionsController_create","summary":"","description":"Create a admin region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/trees":{"get":{"operationId":"AdminRegionsController_getTrees","summary":"","description":"Find all admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Admin Regions with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{countryId}/regions":{"get":{"operationId":"AdminRegionsController_findRegionsByCountry","summary":"","description":"Find all admin regions given a country and return data in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"countryId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{id}":{"get":{"operationId":"AdminRegionsController_findOne","summary":"","description":"Find admin region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"AdminRegionsController_update","summary":"","description":"Updates a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"AdminRegionsController_delete","summary":"","description":"Deletes a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/materials":{"get":{"operationId":"MaterialsController_findAll","summary":"","description":"Find all materials and return them in a list format","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `children`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`, `hsCodeId`, `earthstatId`, `mapspamId`, `metadata`, `h3Grid`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]},"post":{"operationId":"MaterialsController_create","summary":"","description":"Create a material","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/trees":{"get":{"operationId":"MaterialsController_getTrees","summary":"","description":"Find all materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Materials with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/{id}":{"get":{"operationId":"MaterialsController_findOne","summary":"","description":"Find material by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"patch":{"operationId":"MaterialsController_update","summary":"","description":"Updates a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"delete":{"operationId":"MaterialsController_delete","summary":"","description":"Deletes a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/suppliers":{"get":{"operationId":"SuppliersController_findAll","summary":"","description":"Find all suppliers","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]},"post":{"operationId":"SuppliersController_create","summary":"","description":"Create a supplier","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/trees":{"get":{"operationId":"SuppliersController_getTrees","summary":"","description":"Find all suppliers and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Suppliers with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the supplier search","schema":{"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/types":{"get":{"operationId":"SuppliersController_getSupplierByType","summary":"","description":"Find all suppliers by type","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"type","required":true,"in":"query","schema":{"enum":["t1supplier","producer"],"type":"string"}},{"name":"sort","required":true,"in":"query","description":"The sort order by Name for the resulting entities. Can be 'ASC' (Ascendant) or 'DESC' (Descendent). Defaults to ASC","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/{id}":{"get":{"operationId":"SuppliersController_findOne","summary":"","description":"Find supplier by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"patch":{"operationId":"SuppliersController_update","summary":"","description":"Updates a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"delete":{"operationId":"SuppliersController_delete","summary":"","description":"Deletes a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/business-units":{"get":{"operationId":"BusinessUnitsController_findAll","summary":"","description":"Find all business units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"post":{"operationId":"BusinessUnitsController_create","summary":"","description":"Create a business unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/trees":{"get":{"operationId":"BusinessUnitsController_getTrees","summary":"","description":"Find all business units with sourcing-locations and return them in a tree format.","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Business Units with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the business unit search","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BusinessUnit"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/BusinessUnit"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/{id}":{"get":{"operationId":"BusinessUnitsController_findOne","summary":"","description":"Find business unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"patch":{"operationId":"BusinessUnitsController_update","summary":"","description":"Updates a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"delete":{"operationId":"BusinessUnitsController_delete","summary":"","description":"Deletes a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations":{"get":{"operationId":"SourcingLocationsController_findAll","summary":"","description":"Find all sourcing locations","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `sourcingLocationGroup`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationsController_create","summary":"","description":"Create a sourcing location","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/materials":{"get":{"operationId":"SourcingLocationsController_findAllMaterials","summary":"","description":"Find all Materials with details for Sourcing Locations","parameters":[{"name":"orderBy","required":false,"in":"query","schema":{"enum":["country","businessUnit","producer","t1Supplier","material","locationType"],"type":"string"}},{"name":"order","required":false,"in":"query","schema":{"enum":["desc","asc"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationsMaterialsResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types":{"get":{"operationId":"SourcingLocationsController_getLocationTypes","summary":"","description":"Gets available location types. Optionally returns all supported location types","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"supported","required":false,"in":"query","description":"Get all supported location types. Setting this to true overrides all other parameters","schema":{"type":"boolean"}},{"name":"sort","required":false,"in":"query","description":"Sorting parameter to order the result. Defaults to ASC ","schema":{"enum":["ASC","DESC"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types/supported":{"get":{"operationId":"SourcingLocationsController_getAllSupportedLocationTypes","summary":"","description":"Get location types supported by the platform","deprecated":true,"parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/{id}":{"get":{"operationId":"SourcingLocationsController_findOne","summary":"","description":"Find sourcing location by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationsController_update","summary":"","description":"Updates a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationsController_delete","summary":"","description":"Deletes a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/auth/sign-in":{"post":{"operationId":"sign-in","summary":"Sign user in","description":"Sign user in, issuing a JWT token.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"201":{"description":"Login successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-account":{"post":{"operationId":"AuthenticationController_validateAccount","summary":"","description":"Confirm an activation for a new user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JSONAPIUserData"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-token":{"get":{"operationId":"AuthenticationController_validateToken","parameters":[],"responses":{"200":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/refresh-token":{"post":{"operationId":"refresh-token","summary":"Refresh JWT token","description":"Request a fresh JWT token, given a still-valid one for the same user; no request payload is required: the user id is read from the JWT token presented.","parameters":[],"responses":{"201":{"description":"Token refreshed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}},"401":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/api/v1/api-events":{"get":{"operationId":"ApiEventsController_findAll","summary":"","description":"Find all API events","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEventResult"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ApiEvents"]},"post":{"operationId":"ApiEventsController_create","summary":"","description":"Create an API event","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiEventDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}/latest":{"get":{"operationId":"ApiEventsController_findLatestEventByKindAndTopic","summary":"","description":"Find latest API event by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}":{"delete":{"operationId":"ApiEventsController_deleteEventSeriesByKindAndTopic","summary":"","description":"Delete API event series by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/users":{"get":{"operationId":"UsersController_findAll","summary":"","description":"Find all users","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"UsersController_createUser","summary":"","description":"Create new user","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password":{"patch":{"operationId":"UsersController_updateOwnPassword","summary":"","description":"Update the password of a user, if they can present the current one.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserPasswordDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me":{"patch":{"operationId":"UsersController_update","summary":"","description":"Update own user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOwnUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"get":{"operationId":"UsersController_userMetadata","summary":"","description":"Retrieve attributes of the current user","parameters":[],"responses":{"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."},"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"delete":{"operationId":"UsersController_deleteOwnUser","summary":"","description":"Mark user as deleted.","parameters":[],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/recover":{"post":{"operationId":"UsersController_recoverPassword","summary":"","description":"Recover password presenting a valid user email","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoverPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/reset":{"post":{"operationId":"UsersController_resetPassword","summary":"","description":"Reset a user password presenting a valid token","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{id}":{"patch":{"operationId":"UsersController_updateUser","summary":"","description":"Update a user as admin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{userId}":{"delete":{"operationId":"UsersController_deleteUser","summary":"","description":"Delete a user. This operation will destroy any resource related to the user and it will be irreversible","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/scenarios":{"get":{"operationId":"ScenariosController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`, `status`, `userId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}},{"name":"search","required":true,"in":"query","description":"Must be provided when searching with partial matching. Each key of the map corresponds to a field that is to be matched partially, and its value, the string that will be partially matched against","schema":{"type":"map"}},{"name":"hasActiveInterventions","required":false,"in":"query","description":"If true, only scenarios with at least one active intervention will be selected.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Scenario"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenariosController_create","summary":"","description":"Create a scenario","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}":{"get":{"operationId":"ScenariosController_findOne","summary":"","description":"Find scenario by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenariosController_update","summary":"","description":"Updates a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenariosController_delete","summary":"","description":"Deletes a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}/interventions":{"get":{"operationId":"ScenariosController_findInterventionsByScenario","summary":"","description":"Find all Interventions that belong to a given Scenario Id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions":{"get":{"operationId":"ScenarioInterventionsController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenarioInterventionsController_create","summary":"","description":"Create a scenario intervention","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions/{id}":{"get":{"operationId":"ScenarioInterventionsController_findOne","summary":"","description":"Find scenario intervention by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenarioInterventionsController_update","summary":"","description":"Update a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenarioInterventionsController_delete","summary":"","description":"Delete a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/impact/table":{"get":{"operationId":"ImpactController_getImpactTable","summary":"","description":"Get data for Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/scenario":{"get":{"operationId":"ImpactController_getTwoScenariosImpactTable","summary":"","description":"Get data for comparing Impacts of 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioVsScenarioPaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/actual":{"get":{"operationId":"ImpactController_getActualVsScenarioImpactTable","summary":"","description":"Get data for comapring Actual data with Scenario in form of Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/ranking":{"get":{"operationId":"ImpactController_getRankedImpactTable","summary":"","description":"Get Ranked Impact Table, up to maxRankingEntities, aggregating the rest of entities, for each indicator ","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"maxRankingEntities","required":true,"in":"query","description":"The maximum number of entities to show in the Impact Table. If the result includes more than that, they will beaggregated into the \"other\" field in the response","schema":{"type":"number"}},{"name":"sort","required":true,"in":"query","description":"The sort order for the resulting entities. Can be 'ASC' (Ascendant) or 'DES' (Descendent), with the default being 'DES'","schema":{"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/table/report":{"get":{"operationId":"ImpactReportController_getImpactReport","summary":"","description":"Get a Impact Table CSV Report","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/actual/report":{"get":{"operationId":"ImpactReportController_getActualVsScenarioImpactReport","summary":"","description":"Get a Actual Vs Scenario Impact Table CSV Report for a given scenario","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/scenario/report":{"get":{"operationId":"ImpactReportController_getTwoScenariosImpacReport","summary":"","description":"Get a Scenario Vs Scenario Impact Table CSV Report for 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/indicators":{"get":{"operationId":"IndicatorsController_findAll","summary":"","description":"Find all indicators","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorsController_create","summary":"","description":"Create a indicator","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/indicators/{id}":{"get":{"operationId":"IndicatorsController_findOne","summary":"","description":"Find indicator by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorsController_update","summary":"","description":"Updates a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorsController_delete","summary":"","description":"Deletes a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records":{"get":{"operationId":"SourcingRecordsController_findAll","summary":"","description":"Find all sourcing record","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `tonnage`, `year`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingRecordsController_create","summary":"","description":"Create a sourcing record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/years":{"get":{"operationId":"SourcingRecordsController_getYears","summary":"","description":"Find years associated with existing sourcing records","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"List of years","type":"array","items":{"type":"integer","example":2021}}}}}}}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/{id}":{"get":{"operationId":"SourcingRecordsController_findOne","summary":"","description":"Find sourcing record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingRecordsController_update","summary":"","description":"Updates a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingRecordsController_delete","summary":"","description":"Deletes a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records":{"get":{"operationId":"IndicatorRecordsController_findAll","summary":"","description":"Find all indicator records","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `status`, `sourcingRecordId`, `indicatorId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorRecordsController_create","summary":"","description":"Create a indicator record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records/{id}":{"get":{"operationId":"IndicatorRecordsController_findOne","summary":"","description":"Find indicator record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorRecordsController_update","summary":"","description":"Updates a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorRecordsController_delete","summary":"","description":"Deletes a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/h3/data/{h3TableName}/{h3ColumnName}":{"get":{"operationId":"H3DataController_getH3ByName","summary":"","description":"Retrieve H3 data providing its name","parameters":[{"name":"h3TableName","required":true,"in":"path","schema":{"type":"string"}},{"name":"h3ColumnName","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3DataResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/years":{"get":{"operationId":"H3DataController_getYearsByLayerType","summary":"","description":"Retrieve years for which there is data, by layer","parameters":[{"name":"layer","required":true,"in":"query","schema":{"type":"string"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"indicatorId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"integer","example":2021}}}}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/material":{"get":{"operationId":"H3DataController_getMaterialMap","summary":"","description":"Get a Material map of h3 indexes by ID in a given resolution","parameters":[{"name":"materialId","required":true,"in":"query","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialId","in":"query","required":true,"schema":{"type":"string"}},{"name":"resolution","in":"query","required":true,"schema":{"type":"number"}},{"name":"year","in":"query","required":true,"schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact":{"get":{"operationId":"H3DataController_getImpactMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"The scenarioID, whose information will be included in the response. That is, the impact of all indicator records related to the interventions of that scenarioId, will be aggregated into the response map data along the actual data.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/actual/vs/scenario":{"get":{"operationId":"H3DataController_getImpactActualVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the actual data against the given Scenario. The resulting map will contain the difference between the actual data and the given scenario data plus actual data","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the actual data will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/scenario/vs/scenario":{"get":{"operationId":"H3DataController_getImpactScenarioVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the given Scenario against another Scenario. The resulting map will contain the difference between actual data and the given base scenario data, minus the actual data and the compared Scenario.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":true,"in":"query","description":"The of the scenario that will be the base for the comparison.","schema":{"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the base Scenario will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions":{"get":{"operationId":"UnitConversionsController_findAll","summary":"","description":"Find all conversion units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `unit1`, `unit2`, `factor`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitConversionsController_create","summary":"","description":"Create a conversion unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions/{id}":{"get":{"operationId":"UnitConversionsController_findOne","summary":"","description":"Find conversion unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitConversionsController_update","summary":"","description":"Updates a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitConversionsController_delete","summary":"","description":"Deletes a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions":{"get":{"operationId":"GeoRegionsController_findAll","summary":"","description":"Find all geo regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"GeoRegionsController_create","summary":"","description":"Create a geo region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions/{id}":{"get":{"operationId":"GeoRegionsController_findOne","summary":"","description":"Find geo region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"GeoRegionsController_update","summary":"","description":"Updates a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"GeoRegionsController_delete","summary":"","description":"Deletes a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/categories":{"get":{"operationId":"ContextualLayersController_getContextualLayersByCategory","summary":"","description":"Get all Contextual Layer info grouped by Category","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContextualLayerByCategory"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/{id}/h3data":{"get":{"operationId":"ContextualLayersController_getContextualLayerH3","summary":"","description":"Returns all the H3 index data for this given contextual layer, resolution and optionally year","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetContextualLayerH3ResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/import/sourcing-data":{"post":{"operationId":"ImportDataController_importSourcingRecords","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/import/eudr":{"post":{"operationId":"ImportDataController_importEudr","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups":{"get":{"operationId":"SourcingLocationGroupsController_findAll","summary":"","description":"Find all sourcing location groups","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationGroupsController_create","summary":"","description":"Create a sourcing location group","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups/{id}":{"get":{"operationId":"SourcingLocationGroupsController_findOne","summary":"","description":"Find sourcing location group by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationGroupsController_update","summary":"","description":"Updates a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationGroupsController_delete","summary":"","description":"Deletes a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/tasks":{"get":{"operationId":"TasksController_findAll","summary":"","description":"Find all tasks","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `status`, `data`, `createdBy`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"post":{"operationId":"TasksController_create","summary":"","description":"Create a Task","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/{id}":{"get":{"operationId":"TasksController_findOne","summary":"","description":"Find task by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"patch":{"operationId":"TasksController_update","summary":"","description":"Updates a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]},"delete":{"operationId":"TasksController_delete","summary":"","description":"Deletes a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/report/errors/{id}":{"get":{"operationId":"TasksController_getErrorsReport","summary":"","description":"Get a CSV report of errors by Task Id and type","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients":{"get":{"operationId":"IndicatorCoefficientsController_findAll","summary":"","description":"Find all indicator coefficients","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `year`, `indicatorSourceId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorCoefficientsController_create","summary":"","description":"Create a indicator coefficient","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients/{id}":{"get":{"operationId":"IndicatorCoefficientsController_findOne","summary":"","description":"Find indicator coefficient by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorCoefficientsController_update","summary":"","description":"Updates a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorCoefficientsController_delete","summary":"","description":"Deletes a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/targets":{"get":{"operationId":"TargetsController_findAll","summary":"","description":"Find all targets","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Target"],"security":[{"bearer":[]}]},"post":{"operationId":"TargetsController_create","summary":"","description":"Create a target","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/targets/{id}":{"get":{"operationId":"TargetsController_findOne","summary":"","description":"Find target by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"patch":{"operationId":"TargetsController_update","summary":"","description":"Updates a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"delete":{"operationId":"TargetsController_delete","summary":"","description":"Deletes a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/units":{"get":{"operationId":"UnitsController_findAll","summary":"","description":"Find all units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `symbol`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Unit"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitsController_create","summary":"","description":"Create a unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/units/{id}":{"get":{"operationId":"UnitsController_findOne","summary":"","description":"Find unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitsController_update","summary":"","description":"Updates a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitsController_delete","summary":"","description":"Deletes a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/url-params/{id}":{"get":{"operationId":"UrlParamsController_findOne","summary":"","description":"Find URL params set by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"404":{"description":"URL params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]},"delete":{"operationId":"UrlParamsController_delete","summary":"","description":"Deletes a set of URL Params","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"URL Params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/url-params":{"post":{"operationId":"UrlParamsController_create","summary":"","description":"Save URL params set","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/eudr/suppliers":{"get":{"operationId":"EudrController_getSuppliers","summary":"","description":"Find all EUDR suppliers and return them in a flat format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/materials":{"get":{"operationId":"EudrController_getMaterialsTree","summary":"","description":"Find all EUDR materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/admin-regions":{"get":{"operationId":"EudrController_getTreesForEudr","summary":"","description":"Find all EUDR admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/geo-regions":{"get":{"operationId":"EudrController_findAllEudr","summary":"","description":"Find all EUDR geo regions","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GeoRegion"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/dates":{"get":{"operationId":"EudrController_getAlertDates","summary":"","description":"Get EUDR alerts dates","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EUDRAlertDates"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/alerts":{"get":{"operationId":"EudrController_getAlerts","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"tags":["EUDR"]}}},"info":{"title":"LandGriffon API","description":"LandGriffon is a conservation planning platform.","version":"0.2.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"GeoRegion":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"theGeom":{"type":"object"},"adminRegions":{"type":"array","items":{"type":"string"}},"sourcingLocations":{"type":"array","items":{"type":"string"}}},"required":["id"]},"AdminRegion":{"type":"object","properties":{"id":{"type":"string"},"parent":{"$ref":"#/components/schemas/AdminRegion"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"sourcingLocations":{"type":"array","items":{"type":"string"}},"geoRegion":{"$ref":"#/components/schemas/GeoRegion"},"geoRegionId":{"type":"string"}},"required":["id","status","geoRegion"]},"CreateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"Material":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}},"required":["name","hsCodeId"]},"UpdateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}}},"Supplier":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"companyId":{"type":"string"},"address":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}},"required":["name"]},"UpdateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}}},"BusinessUnit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["id","name","status"]},"CreateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"SourcingLocation":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationWarning":{"type":"string"},"geoRegionId":{"type":"string"},"metadata":{"type":"object"},"materialId":{"type":"string"},"adminRegionId":{"type":"string"},"businessUnitId":{"type":"string"},"sourcingLocationGroupId":{"type":"string"},"interventionType":{"type":"string"},"scenarioInterventionId":{"type":"string"}},"required":["createdAt","updatedAt","id","locationType","locationAccuracy"]},"SourcingLocationsMaterialsResponseDto":{"type":"object","properties":{"meta":{"type":"object","properties":{"totalItems":{"type":"number","example":45},"totalPages":{"type":"number","example":9},"size":{"type":"number","example":5},"page":{"type":"number","example":1}}},"data":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","example":"sourcing locations"},"id":{"type":"string","example":"a2428cbb-e1b1-4313-ad85-9579b260387f"},"attributes":{"type":"object","properties":{"locationType":{"type":"string","example":"point of production"},"material":{"type":"string","example":"bananas"},"materialId":{"type":"string","example":"cdde28a2-5692-401b-a1a7-6c68ad38010f"},"t1Supplier":{"type":"string","example":"Cargill"},"producer":{"type":"string","example":"Moll"},"businessUnit":{"type":"string","example":"Accessories"},"locationCountryInput":{"type":"string","example":"Japan"},"purchases":{"type":"array","items":{"type":"object","properties":{"year":{"type":"number","example":2010},"tonnage":{"type":"number","example":730}}}}}}}}}},"required":["meta","data"]},"LocationTypesDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}}}}},"required":["data"]},"CreateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}},"required":["title","materialId"]},"UpdateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}}},"LoginDto":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}},"required":["username","password"]},"AccessToken":{"type":"object","properties":{"user":{"type":"object"},"accessToken":{"type":"string"}},"required":["user","accessToken"]},"ResetPasswordDto":{"type":"object","properties":{"password":{"type":"string"}},"required":["password"]},"Permission":{"type":"object","properties":{"action":{"type":"string"}},"required":["action"]},"Role":{"type":"object","properties":{"name":{"type":"string","enum":["admin","user"]},"permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}}},"required":["name","permissions"]},"User":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"string"},"fname":{"type":"string"},"lname":{"type":"string"},"avatarDataUrl":{"type":"string"},"isActive":{"type":"boolean"},"isDeleted":{"type":"boolean"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}},"required":["email","isActive","isDeleted","roles"]},"JSONAPIUserData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/User"}},"required":["type","id","attributes"]},"ApiEvent":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"}},"required":["kind","topic"]},"JSONAPIApiEventData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/ApiEvent"}},"required":["type","id","attributes"]},"ApiEventResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIApiEventData"}},"required":["data"]},"CreateApiEventDTO":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"},"data":{"type":"object"}},"required":["kind","topic"]},"CreateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}},"required":["email","password","roles"]},"UpdateUserPasswordDTO":{"type":"object","properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string"}},"required":["currentPassword","newPassword"]},"UserResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIUserData"}},"required":["data"]},"UpdateOwnUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"avatarDataUrl":{"type":"string"}},"required":["email"]},"RecoverPasswordDto":{"type":"object","properties":{}},"UpdateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}}},"Scenario":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean","description":"Make a Scenario public to all users"},"status":{"type":"string","enum":["active","inactive","deleted"]},"metadata":{"type":"object"},"user":{"$ref":"#/components/schemas/User"},"userId":{"type":"string"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","user","updatedBy"]},"CreateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}},"required":["title"]},"UpdateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}}},"ScenarioIntervention":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"startYear":{"type":"number"},"endYear":{"type":"number"},"percentage":{"type":"number"},"newIndicatorCoefficients":{"type":"object"},"scenario":{"$ref":"#/components/schemas/Scenario"},"newMaterial":{"$ref":"#/components/schemas/Material"},"newBusinessUnit":{"$ref":"#/components/schemas/BusinessUnit"},"newT1Supplier":{"$ref":"#/components/schemas/Supplier"},"newProducer":{"$ref":"#/components/schemas/Supplier"},"newAdminRegion":{"$ref":"#/components/schemas/AdminRegion"},"newLocationType":{"type":"string"},"newLocationCountryInput":{"type":"string"},"newLocationAddressInput":{"type":"string"},"newLocationLatitudeInput":{"type":"number"},"newLocationLongitudeInput":{"type":"number"},"newMaterialTonnageRatio":{"type":"number"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","type","startYear","percentage","newIndicatorCoefficients","scenario","updatedBy"]},"IndicatorCoefficientsDto":{"type":"object","properties":{"LF":{"type":"number"},"DF_SLUC":{"type":"number"},"GHG_DEF_SLUC":{"type":"number"},"UWU":{"type":"number"},"WU":{"type":"number"},"NL":{"type":"number"},"NCE":{"type":"number"},"FLIL":{"type":"number"},"ENL":{"type":"number"},"GHG_FARM":{"type":"number"}}},"CreateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5}},"required":["title","type","startYear","percentage","scenarioId","materialIds","adminRegionIds","newLocationCountryInput","newLocationAdminRegionInput"]},"UpdateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5},"status":{"type":"string","description":"Status of the intervention","enum":["active","inactive","deleted"],"example":"inactive"}}},"ImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"value":{"type":"number"}},"required":["year","isProjected","value"]},"ImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"YearSumData":{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]},"ImpactTableDataAggregationInfo":{"type":"object","properties":{}},"ImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/YearSumData"}},"metadata":{"type":"object"},"others":{"description":"Extra information used for Ranked ImpactTable requests. Missing on normal ImpactTable requests","allOf":[{"$ref":"#/components/schemas/ImpactTableDataAggregationInfo"}]}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"PaginationMeta":{"type":"object","properties":{}},"PaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"ScenarioVsScenarioImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"ScenarioVsScenarioIndicatorSumByYearData":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioIndicatorSumByYearData"}},"metadata":{"type":"object"}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ScenarioVsScenarioImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ScenarioVsScenarioImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"ScenarioVsScenarioPaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"Indicator":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"},"indicatorCoefficients":{"type":"array","items":{"type":"string"}}},"required":["id","name","status"]},"CreateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}},"required":["name","nameCode"]},"UpdateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}}},"SourcingRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"tonnage":{"type":"number"},"year":{"type":"number"},"metadata":{"type":"object"},"updatedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","tonnage","year","updatedBy"]},"CreateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}},"required":["tonnage","year"]},"UpdateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}}},"IndicatorRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["createdAt","updatedAt","id","value","status","statusMsg"]},"CreateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"sourcingRecordId":{"type":"string"},"indicatorId":{"type":"string"},"indicatorCoefficientId":{"type":"string"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["value","indicatorId"]},"UpdateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"},"status":{"type":"string"}}},"H3DataResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"H3MapResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}},"metadata":{"type":"object","properties":{"unit":{"type":"string"},"quantiles":{"type":"array","items":{"type":"number"}},"indicatorDataYear":{"type":"number"},"materialsH3DataYears":{"type":"array","items":{"type":"object","properties":{"materialName":{"type":"string"},"materialDataYear":{"type":"number"},"materialDataType":{"type":"string"}}}}}}},"required":["data","metadata"]},"UnitConversion":{"type":"object","properties":{"id":{"type":"string"},"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}},"required":["id"]},"CreateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"UpdateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"CreateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"UpdateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"ContextualLayerByCategory":{"type":"object","properties":{}},"GetContextualLayerH3ResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"SourcingLocationGroup":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title"]},"CreateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}},"required":["title"]},"UpdateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}}},"Task":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"user":{"$ref":"#/components/schemas/User"},"status":{"type":"string"},"message":{"type":"string"},"data":{"type":"object"},"logs":{"type":"array","items":{"type":"string"}},"errors":{"type":"array","items":{"type":"string"}},"dismissedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","type","user","status","data","logs","errors","dismissedBy"]},"CreateTaskDto":{"type":"object","properties":{"type":{"type":"string"},"status":{"type":"string"},"data":{"type":"object"}},"required":["type","status","data"]},"UpdateTaskDto":{"type":"object","properties":{"status":{"type":"string"},"newData":{"type":"object"},"dismissedBy":{"type":"string"}}},"IndicatorCoefficient":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"year":{"type":"number"},"adminRegion":{"$ref":"#/components/schemas/AdminRegion"},"user":{"$ref":"#/components/schemas/User"},"indicator":{"$ref":"#/components/schemas/Indicator"},"material":{"$ref":"#/components/schemas/Material"}},"required":["createdAt","updatedAt","id","year","user","indicator","material"]},"CreateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}},"required":["year"]},"UpdateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}}},"Target":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","baseLineYear","targetYear","value","indicatorId","updatedById"]},"CreateTargetDto":{"type":"object","properties":{"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"}},"required":["baseLineYear","targetYear","value","indicatorId"]},"UpdateTargetDto":{"type":"object","properties":{"targetYear":{"type":"number"},"value":{"type":"number"}},"required":["targetYear","value"]},"Unit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"number"}},"required":["id","name","symbol"]},"CreateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}},"required":["name"]},"UpdateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}}},"UrlResponseAttributes":{"type":"object","properties":{"params":{"type":"object"}},"required":["params"]},"UrlResponseDto":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/UrlResponseAttributes"}},"required":["type","id","attributes"]},"SerializedUrlResponseDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UrlResponseDto"}},"required":["data"]},"DateValue":{"type":"object","properties":{"value":{"type":"object"}},"required":["value"]},"EUDRAlertDates":{"type":"object","properties":{"alertDate":{"$ref":"#/components/schemas/DateValue"}},"required":["alertDate"]}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/health":{"get":{"operationId":"HealthController_check","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}},"503":{"description":"The Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"error":{"type":"object","example":{"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"},"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":{"type":"string"}}}}}}}}}}},"/api/v1/admin-regions":{"get":{"operationId":"AdminRegionsController_findAll","summary":"","description":"Find all admin regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"AdminRegionsController_create","summary":"","description":"Create a admin region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/trees":{"get":{"operationId":"AdminRegionsController_getTrees","summary":"","description":"Find all admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Admin Regions with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{countryId}/regions":{"get":{"operationId":"AdminRegionsController_findRegionsByCountry","summary":"","description":"Find all admin regions given a country and return data in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"countryId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/admin-regions/{id}":{"get":{"operationId":"AdminRegionsController_findOne","summary":"","description":"Find admin region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"AdminRegionsController_update","summary":"","description":"Updates a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminRegion"}}}},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"AdminRegionsController_delete","summary":"","description":"Deletes a admin region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Admin region not found"}},"tags":["AdminRegion"],"security":[{"bearer":[]}]}},"/api/v1/materials":{"get":{"operationId":"MaterialsController_findAll","summary":"","description":"Find all materials and return them in a list format","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `children`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`, `hsCodeId`, `earthstatId`, `mapspamId`, `metadata`, `h3Grid`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]},"post":{"operationId":"MaterialsController_create","summary":"","description":"Create a material","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/trees":{"get":{"operationId":"MaterialsController_getTrees","summary":"","description":"Find all materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Materials with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/materials/{id}":{"get":{"operationId":"MaterialsController_findOne","summary":"","description":"Find material by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"patch":{"operationId":"MaterialsController_update","summary":"","description":"Updates a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaterialDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]},"delete":{"operationId":"MaterialsController_delete","summary":"","description":"Deletes a material","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Material not found"}},"tags":["Material"],"security":[{"bearer":[]}]}},"/api/v1/suppliers":{"get":{"operationId":"SuppliersController_findAll","summary":"","description":"Find all suppliers","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]},"post":{"operationId":"SuppliersController_create","summary":"","description":"Create a supplier","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/trees":{"get":{"operationId":"SuppliersController_getTrees","summary":"","description":"Find all suppliers and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Suppliers with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the supplier search","schema":{"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/types":{"get":{"operationId":"SuppliersController_getSupplierByType","summary":"","description":"Find all suppliers by type","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"type","required":true,"in":"query","schema":{"enum":["t1supplier","producer"],"type":"string"}},{"name":"sort","required":true,"in":"query","description":"The sort order by Name for the resulting entities. Can be 'ASC' (Ascendant) or 'DESC' (Descendent). Defaults to ASC","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/suppliers/{id}":{"get":{"operationId":"SuppliersController_findOne","summary":"","description":"Find supplier by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"patch":{"operationId":"SuppliersController_update","summary":"","description":"Updates a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Supplier"}}}},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]},"delete":{"operationId":"SuppliersController_delete","summary":"","description":"Deletes a supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Supplier not found"}},"tags":["Supplier"],"security":[{"bearer":[]}]}},"/api/v1/business-units":{"get":{"operationId":"BusinessUnitsController_findAll","summary":"","description":"Find all business units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"post":{"operationId":"BusinessUnitsController_create","summary":"","description":"Create a business unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/trees":{"get":{"operationId":"BusinessUnitsController_getTrees","summary":"","description":"Find all business units with sourcing-locations and return them in a tree format.","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"withSourcingLocations","required":false,"in":"query","description":"Return Business Units with related Sourcing Locations. Setting this to true will override depth param","schema":{"type":"boolean"}},{"name":"depth","required":false,"in":"query","schema":{"type":"number"}},{"name":"scenarioId","required":false,"in":"query","description":"Array of Scenario Ids to include in the business unit search","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/BusinessUnit"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/BusinessUnit"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/business-units/{id}":{"get":{"operationId":"BusinessUnitsController_findOne","summary":"","description":"Find business unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"patch":{"operationId":"BusinessUnitsController_update","summary":"","description":"Updates a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBusinessUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessUnit"}}}},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]},"delete":{"operationId":"BusinessUnitsController_delete","summary":"","description":"Deletes a business unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Business unit not found"}},"tags":["BusinessUnit"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations":{"get":{"operationId":"SourcingLocationsController_findAll","summary":"","description":"Find all sourcing locations","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `sourcingLocationGroup`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationsController_create","summary":"","description":"Create a sourcing location","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/materials":{"get":{"operationId":"SourcingLocationsController_findAllMaterials","summary":"","description":"Find all Materials with details for Sourcing Locations","parameters":[{"name":"orderBy","required":false,"in":"query","schema":{"enum":["country","businessUnit","producer","t1Supplier","material","locationType"],"type":"string"}},{"name":"order","required":false,"in":"query","schema":{"enum":["desc","asc"],"type":"string"}},{"name":"search","required":false,"in":"query","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationsMaterialsResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types":{"get":{"operationId":"SourcingLocationsController_getLocationTypes","summary":"","description":"Gets available location types. Optionally returns all supported location types","parameters":[{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioIds","required":false,"in":"query","description":"Array of Scenario Ids to include entities present in them","schema":{"type":"array","items":{"type":"string"}}},{"name":"scenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"supported","required":false,"in":"query","description":"Get all supported location types. Setting this to true overrides all other parameters","schema":{"type":"boolean"}},{"name":"sort","required":false,"in":"query","description":"Sorting parameter to order the result. Defaults to ASC ","schema":{"enum":["ASC","DESC"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/location-types/supported":{"get":{"operationId":"SourcingLocationsController_getAllSupportedLocationTypes","summary":"","description":"Get location types supported by the platform","deprecated":true,"parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationTypesDto"}}}}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-locations/{id}":{"get":{"operationId":"SourcingLocationsController_findOne","summary":"","description":"Find sourcing location by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationsController_update","summary":"","description":"Updates a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocation"}}}},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationsController_delete","summary":"","description":"Deletes a sourcing location","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location not found"}},"tags":["SourcingLocation"],"security":[{"bearer":[]}]}},"/auth/sign-in":{"post":{"operationId":"sign-in","summary":"Sign user in","description":"Sign user in, issuing a JWT token.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"201":{"description":"Login successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-account":{"post":{"operationId":"AuthenticationController_validateAccount","summary":"","description":"Confirm an activation for a new user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JSONAPIUserData"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/validate-token":{"get":{"operationId":"AuthenticationController_validateToken","parameters":[],"responses":{"200":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/refresh-token":{"post":{"operationId":"refresh-token","summary":"Refresh JWT token","description":"Request a fresh JWT token, given a still-valid one for the same user; no request payload is required: the user id is read from the JWT token presented.","parameters":[],"responses":{"201":{"description":"Token refreshed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessToken"}}}},"401":{"description":""}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/api/v1/api-events":{"get":{"operationId":"ApiEventsController_findAll","summary":"","description":"Find all API events","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEventResult"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ApiEvents"]},"post":{"operationId":"ApiEventsController_create","summary":"","description":"Create an API event","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiEventDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}/latest":{"get":{"operationId":"ApiEventsController_findLatestEventByKindAndTopic","summary":"","description":"Find latest API event by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/api-events/kind/{kind}/topic/{topic}":{"delete":{"operationId":"ApiEventsController_deleteEventSeriesByKindAndTopic","summary":"","description":"Delete API event series by kind for a given topic","parameters":[{"name":"kind","required":true,"in":"path","schema":{"type":"string"}},{"name":"topic","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEvent"}}}}},"tags":["ApiEvents"]}},"/api/v1/users":{"get":{"operationId":"UsersController_findAll","summary":"","description":"Find all users","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `projects`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"UsersController_createUser","summary":"","description":"Create new user","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password":{"patch":{"operationId":"UsersController_updateOwnPassword","summary":"","description":"Update the password of a user, if they can present the current one.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserPasswordDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me":{"patch":{"operationId":"UsersController_update","summary":"","description":"Update own user.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOwnUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"get":{"operationId":"UsersController_userMetadata","summary":"","description":"Retrieve attributes of the current user","parameters":[],"responses":{"401":{"description":"Unauthorized."},"403":{"description":"The current user does not have suitable permissions for this request."},"default":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResult"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"delete":{"operationId":"UsersController_deleteOwnUser","summary":"","description":"Mark user as deleted.","parameters":[],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/recover":{"post":{"operationId":"UsersController_recoverPassword","summary":"","description":"Recover password presenting a valid user email","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecoverPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/me/password/reset":{"post":{"operationId":"UsersController_resetPassword","summary":"","description":"Reset a user password presenting a valid token","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{id}":{"patch":{"operationId":"UsersController_updateUser","summary":"","description":"Update a user as admin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDTO"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"201":{"description":"User created successfully"},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/users/{userId}":{"delete":{"operationId":"UsersController_deleteUser","summary":"","description":"Delete a user. This operation will destroy any resource related to the user and it will be irreversible","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["User"],"security":[{"bearer":[]}]}},"/api/v1/scenarios":{"get":{"operationId":"ScenariosController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`, `status`, `userId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}},{"name":"search","required":true,"in":"query","description":"Must be provided when searching with partial matching. Each key of the map corresponds to a field that is to be matched partially, and its value, the string that will be partially matched against","schema":{"type":"map"}},{"name":"hasActiveInterventions","required":false,"in":"query","description":"If true, only scenarios with at least one active intervention will be selected.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Scenario"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenariosController_create","summary":"","description":"Create a scenario","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}":{"get":{"operationId":"ScenariosController_findOne","summary":"","description":"Find scenario by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenariosController_update","summary":"","description":"Updates a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenariosController_delete","summary":"","description":"Deletes a scenario","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenarios/{id}/interventions":{"get":{"operationId":"ScenariosController_findInterventionsByScenario","summary":"","description":"Find all Interventions that belong to a given Scenario Id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scenario"}}}},"404":{"description":"Scenario not found"}},"tags":["Scenario"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions":{"get":{"operationId":"ScenarioInterventionsController_findAll","summary":"","description":"Find all scenarios","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"post":{"operationId":"ScenarioInterventionsController_create","summary":"","description":"Create a scenario intervention","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/scenario-interventions/{id}":{"get":{"operationId":"ScenarioInterventionsController_findOne","summary":"","description":"Find scenario intervention by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"patch":{"operationId":"ScenarioInterventionsController_update","summary":"","description":"Update a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScenarioInterventionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioIntervention"}}}},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]},"delete":{"operationId":"ScenarioInterventionsController_delete","summary":"","description":"Delete a scenario intervention","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Scenario intervention not found"}},"tags":["ScenarioIntervention"],"security":[{"bearer":[]}]}},"/api/v1/impact/table":{"get":{"operationId":"ImpactController_getImpactTable","summary":"","description":"Get data for Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/scenario":{"get":{"operationId":"ImpactController_getTwoScenariosImpactTable","summary":"","description":"Get data for comparing Impacts of 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScenarioVsScenarioPaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/compare/scenario/vs/actual":{"get":{"operationId":"ImpactController_getActualVsScenarioImpactTable","summary":"","description":"Get data for comapring Actual data with Scenario in form of Impact Table","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/ranking":{"get":{"operationId":"ImpactController_getRankedImpactTable","summary":"","description":"Get Ranked Impact Table, up to maxRankingEntities, aggregating the rest of entities, for each indicator ","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"maxRankingEntities","required":true,"in":"query","description":"The maximum number of entities to show in the Impact Table. If the result includes more than that, they will beaggregated into the \"other\" field in the response","schema":{"type":"number"}},{"name":"sort","required":true,"in":"query","description":"The sort order for the resulting entities. Can be 'ASC' (Ascendant) or 'DES' (Descendent), with the default being 'DES'","schema":{"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImpactTable"}}}}},"tags":["Impact"],"security":[{"bearer":[]}]}},"/api/v1/impact/table/report":{"get":{"operationId":"ImpactReportController_getImpactReport","summary":"","description":"Get a Impact Table CSV Report","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"Include in the response elements that are being intervened in a Scenario,","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the impact value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/actual/report":{"get":{"operationId":"ImpactReportController_getActualVsScenarioImpactReport","summary":"","description":"Get a Actual Vs Scenario Impact Table CSV Report for a given scenario","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/impact/compare/scenario/vs/scenario/report":{"get":{"operationId":"ImpactReportController_getTwoScenariosImpacReport","summary":"","description":"Get a Scenario Vs Scenario Impact Table CSV Report for 2 Scenarios","parameters":[{"name":"indicatorIds[]","required":true,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":true,"in":"query","schema":{"type":"number"}},{"name":"groupBy","required":true,"in":"query","schema":{"enum":["material","business-unit","region","t1Supplier","producer","location-type"],"type":"string"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1SupplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"comparedScenarioId","required":false,"in":"query","schema":{"type":"string"}},{"name":"sortingYear","required":false,"in":"query","description":"Sort all the entities recursively by the absolute difference value corresponding to the sortingYear","schema":{"type":"number"}},{"name":"sortingOrder","required":false,"in":"query","description":"Indicates the order by which the entities will be sorted","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Impact"]}},"/api/v1/indicators":{"get":{"operationId":"IndicatorsController_findAll","summary":"","description":"Find all indicators","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `status`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorsController_create","summary":"","description":"Create a indicator","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"},"403":{"description":""}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/indicators/{id}":{"get":{"operationId":"IndicatorsController_findOne","summary":"","description":"Find indicator by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorsController_update","summary":"","description":"Updates a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Indicator"}}}},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorsController_delete","summary":"","description":"Deletes a indicator","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"403":{"description":""},"404":{"description":"Indicator not found"}},"tags":["Indicator"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records":{"get":{"operationId":"SourcingRecordsController_findAll","summary":"","description":"Find all sourcing record","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `tonnage`, `year`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingRecordsController_create","summary":"","description":"Create a sourcing record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/years":{"get":{"operationId":"SourcingRecordsController_getYears","summary":"","description":"Find years associated with existing sourcing records","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"List of years","type":"array","items":{"type":"integer","example":2021}}}}}}}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-records/{id}":{"get":{"operationId":"SourcingRecordsController_findOne","summary":"","description":"Find sourcing record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingRecordsController_update","summary":"","description":"Updates a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingRecord"}}}},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingRecordsController_delete","summary":"","description":"Deletes a sourcing record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing record not found"}},"tags":["SourcingRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records":{"get":{"operationId":"IndicatorRecordsController_findAll","summary":"","description":"Find all indicator records","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `status`, `sourcingRecordId`, `indicatorId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorRecordsController_create","summary":"","description":"Create a indicator record","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/indicator-records/{id}":{"get":{"operationId":"IndicatorRecordsController_findOne","summary":"","description":"Find indicator record by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorRecordsController_update","summary":"","description":"Updates a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorRecordDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorRecord"}}}},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorRecordsController_delete","summary":"","description":"Deletes a indicator record","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator record not found"}},"tags":["IndicatorRecord"],"security":[{"bearer":[]}]}},"/api/v1/h3/data/{h3TableName}/{h3ColumnName}":{"get":{"operationId":"H3DataController_getH3ByName","summary":"","description":"Retrieve H3 data providing its name","parameters":[{"name":"h3TableName","required":true,"in":"path","schema":{"type":"string"}},{"name":"h3ColumnName","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3DataResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/years":{"get":{"operationId":"H3DataController_getYearsByLayerType","summary":"","description":"Retrieve years for which there is data, by layer","parameters":[{"name":"layer","required":true,"in":"query","schema":{"type":"string"}},{"name":"materialIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"indicatorId","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"integer","example":2021}}}}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/material":{"get":{"operationId":"H3DataController_getMaterialMap","summary":"","description":"Get a Material map of h3 indexes by ID in a given resolution","parameters":[{"name":"materialId","required":true,"in":"query","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialId","in":"query","required":true,"schema":{"type":"string"}},{"name":"resolution","in":"query","required":true,"schema":{"type":"number"}},{"name":"year","in":"query","required":true,"schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact":{"get":{"operationId":"H3DataController_getImpactMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"scenarioId","required":false,"in":"query","description":"The scenarioID, whose information will be included in the response. That is, the impact of all indicator records related to the interventions of that scenarioId, will be aggregated into the response map data along the actual data.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/actual/vs/scenario":{"get":{"operationId":"H3DataController_getImpactActualVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the actual data against the given Scenario. The resulting map will contain the difference between the actual data and the given scenario data plus actual data","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the actual data will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/h3/map/impact/compare/scenario/vs/scenario":{"get":{"operationId":"H3DataController_getImpactScenarioVsScenarioComparisonMap","summary":"","description":"Get a calculated H3 impact map given an Indicator, Year and Resolution comparing the given Scenario against another Scenario. The resulting map will contain the difference between actual data and the given base scenario data, minus the actual data and the compared Scenario.","parameters":[{"name":"indicatorId","required":true,"in":"query","schema":{"type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"number"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"t1supplierIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"businessUnitIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"locationTypes[]","required":false,"in":"query","description":"Types of Sourcing Locations, written with hyphens","schema":{"enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"type":"string"}},{"name":"baseScenarioId","required":true,"in":"query","description":"The of the scenario that will be the base for the comparison.","schema":{"type":"string"}},{"name":"comparedScenarioId","required":true,"in":"query","description":"The id of the scenario against which the base Scenario will be compared to.","schema":{"type":"string"}},{"name":"relative","required":true,"in":"query","description":"Indicates whether the result will be absolute difference values (false) or relative values in percentages (true)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/H3MapResponse"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["H3Data"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions":{"get":{"operationId":"UnitConversionsController_findAll","summary":"","description":"Find all conversion units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `unit1`, `unit2`, `factor`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitConversionsController_create","summary":"","description":"Create a conversion unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/unit-conversions/{id}":{"get":{"operationId":"UnitConversionsController_findOne","summary":"","description":"Find conversion unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitConversionsController_update","summary":"","description":"Updates a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitConversionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitConversion"}}}},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitConversionsController_delete","summary":"","description":"Deletes a conversion unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Conversion unit not found"}},"tags":["UnitConversion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions":{"get":{"operationId":"GeoRegionsController_findAll","summary":"","description":"Find all geo regions","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"401":{"description":""},"403":{"description":""}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"post":{"operationId":"GeoRegionsController_create","summary":"","description":"Create a geo region","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/geo-regions/{id}":{"get":{"operationId":"GeoRegionsController_findOne","summary":"","description":"Find geo region by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"patch":{"operationId":"GeoRegionsController_update","summary":"","description":"Updates a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGeoRegionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoRegion"}}}},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]},"delete":{"operationId":"GeoRegionsController_delete","summary":"","description":"Deletes a geo region","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Geo region not found"}},"tags":["GeoRegion"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/categories":{"get":{"operationId":"ContextualLayersController_getContextualLayersByCategory","summary":"","description":"Get all Contextual Layer info grouped by Category","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ContextualLayerByCategory"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/contextual-layers/{id}/h3data":{"get":{"operationId":"ContextualLayersController_getContextualLayerH3","summary":"","description":"Returns all the H3 index data for this given contextual layer, resolution and optionally year","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"resolution","required":true,"in":"query","schema":{"type":"number"}},{"name":"year","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetContextualLayerH3ResponseDto"}}}},"401":{"description":""},"403":{"description":""}},"tags":["ContextualLayer"],"security":[{"bearer":[]}]}},"/api/v1/import/sourcing-data":{"post":{"operationId":"ImportDataController_importSourcingRecords","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/import/eudr":{"post":{"operationId":"ImportDataController_importEudr","summary":"","description":"Upload XLSX dataset","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"XLSX File","format":"binary"}}}}}},"responses":{"201":{"description":""},"400":{"description":"Bad Request. A .XLSX file not provided as payload or contains missing or incorrect data"},"403":{"description":""}},"tags":["Import Data"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups":{"get":{"operationId":"SourcingLocationGroupsController_findAll","summary":"","description":"Find all sourcing location groups","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `title`, `description`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"401":{"description":""},"403":{"description":""}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"post":{"operationId":"SourcingLocationGroupsController_create","summary":"","description":"Create a sourcing location group","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/sourcing-location-groups/{id}":{"get":{"operationId":"SourcingLocationGroupsController_findOne","summary":"","description":"Find sourcing location group by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"patch":{"operationId":"SourcingLocationGroupsController_update","summary":"","description":"Updates a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourcingLocationGroupDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcingLocationGroup"}}}},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]},"delete":{"operationId":"SourcingLocationGroupsController_delete","summary":"","description":"Deletes a sourcing location group","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Sourcing location group not found"}},"tags":["SourcingLocationGroup"],"security":[{"bearer":[]}]}},"/api/v1/tasks":{"get":{"operationId":"TasksController_findAll","summary":"","description":"Find all tasks","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `status`, `data`, `createdBy`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"post":{"operationId":"TasksController_create","summary":"","description":"Create a Task","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/{id}":{"get":{"operationId":"TasksController_findOne","summary":"","description":"Find task by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned. Allowed values are: `user`.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Task"],"security":[{"bearer":[]}]},"patch":{"operationId":"TasksController_update","summary":"","description":"Updates a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTaskDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Task"}}}},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]},"delete":{"operationId":"TasksController_delete","summary":"","description":"Deletes a task","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/tasks/report/errors/{id}":{"get":{"operationId":"TasksController_getErrorsReport","summary":"","description":"Get a CSV report of errors by Task Id and type","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Task not found"}},"tags":["Task"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients":{"get":{"operationId":"IndicatorCoefficientsController_findAll","summary":"","description":"Find all indicator coefficients","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `value`, `year`, `indicatorSourceId`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"401":{"description":""},"403":{"description":""}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"post":{"operationId":"IndicatorCoefficientsController_create","summary":"","description":"Create a indicator coefficient","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/indicator-coefficients/{id}":{"get":{"operationId":"IndicatorCoefficientsController_findOne","summary":"","description":"Find indicator coefficient by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"patch":{"operationId":"IndicatorCoefficientsController_update","summary":"","description":"Updates a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIndicatorCoefficientDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicatorCoefficient"}}}},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]},"delete":{"operationId":"IndicatorCoefficientsController_delete","summary":"","description":"Deletes a indicator coefficient","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Indicator coefficient not found"}},"tags":["IndicatorCoefficient"],"security":[{"bearer":[]}]}},"/api/v1/targets":{"get":{"operationId":"TargetsController_findAll","summary":"","description":"Find all targets","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":""},"401":{"description":""},"403":{"description":""}},"tags":["Target"],"security":[{"bearer":[]}]},"post":{"operationId":"TargetsController_create","summary":"","description":"Create a target","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/targets/{id}":{"get":{"operationId":"TargetsController_findOne","summary":"","description":"Find target by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"patch":{"operationId":"TargetsController_update","summary":"","description":"Updates a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTargetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Target"}}}},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]},"delete":{"operationId":"TargetsController_delete","summary":"","description":"Deletes a target","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Target not found"}},"tags":["Target"],"security":[{"bearer":[]}]}},"/api/v1/units":{"get":{"operationId":"UnitsController_findAll","summary":"","description":"Find all units","parameters":[{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`, `description`, `symbol`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"401":{"description":""},"403":{"description":""}},"tags":["Unit"],"security":[{"bearer":[]}]},"post":{"operationId":"UnitsController_create","summary":"","description":"Create a unit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/units/{id}":{"get":{"operationId":"UnitsController_findOne","summary":"","description":"Find unit by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"patch":{"operationId":"UnitsController_update","summary":"","description":"Updates a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUnitDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Unit"}}}},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]},"delete":{"operationId":"UnitsController_delete","summary":"","description":"Deletes a unit","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"Unit not found"}},"tags":["Unit"],"security":[{"bearer":[]}]}},"/api/v1/url-params/{id}":{"get":{"operationId":"UrlParamsController_findOne","summary":"","description":"Find URL params set by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"404":{"description":"URL params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]},"delete":{"operationId":"UrlParamsController_delete","summary":"","description":"Deletes a set of URL Params","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""},"404":{"description":"URL Params not found"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/url-params":{"post":{"operationId":"UrlParamsController_create","summary":"","description":"Save URL params set","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SerializedUrlResponseDto"}}}},"400":{"description":"Bad Request. Incorrect or missing parameters"}},"tags":["UrlParam"],"security":[{"bearer":[]}]}},"/api/v1/eudr/suppliers":{"get":{"operationId":"EudrController_getSuppliers","summary":"","description":"Find all EUDR suppliers and return them in a flat format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Supplier"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Supplier"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/materials":{"get":{"operationId":"EudrController_getMaterialsTree","summary":"","description":"Find all EUDR materials and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Material"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/Material"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/admin-regions":{"get":{"operationId":"EudrController_getTreesForEudr","summary":"","description":"Find all EUDR admin regions and return them in a tree format. Data in the \"children\" will recursively extend for the full depth of the tree","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminRegion"},{"properties":{"children":{"type":"array","items":{"$ref":"#/components/schemas/AdminRegion"}}}}]}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/geo-regions":{"get":{"operationId":"EudrController_findAllEudr","summary":"","description":"Find all EUDR geo regions","parameters":[{"name":"producerIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"originIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"materialIds[]","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"include","required":false,"in":"query","description":"A comma-separated list of relationship paths. Allows the client to customize which related resources should be returned.","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"An array of filters (e.g. `filter[keyA]=&filter[keyB]=,...`). Allows the client to request for specific filtering criteria to be applied to the request. Semantics of each set of filter key/values and of the set of filters as a whole depend on the specific request. Available filters: `name`.","schema":{"type":"array","items":{"type":"string"}}},{"name":"fields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of the fields to be returned. An empty value indicates that all fields will be returned (less any fields specified as `omitFields`).","schema":{"type":"string"}},{"name":"omitFields","required":false,"in":"query","description":"A comma-separated list that refers to the name(s) of fields to be omitted from the results. This could be useful as a shortcut when a specific field such as large geometry fields should be omitted, but it is not practical or not desirable to explicitly whitelist fields individually. An empty value indicates that no fields will be omitted (although they may still not be present in the result if an explicit choice of fields was provided via `fields`).","schema":{"type":"string"}},{"name":"sort","required":false,"in":"query","description":"A comma-separated list of fields of the primary data according to which the results should be sorted. Sort order is ascending unless the field name is prefixed with a minus (for descending order).","schema":{"type":"string"}},{"name":"page[size]","required":false,"in":"query","description":"Page size for pagination. If not supplied, pagination with default page size of 25 elements will be applied.","schema":{"type":"number"}},{"name":"page[number]","required":false,"in":"query","description":"Page number for pagination. If not supplied, the first page of results will be returned.","schema":{"type":"number"}},{"name":"disablePagination","required":false,"in":"query","description":"If set to `true`, pagination will be disabled. This overrides any other pagination query parameters, if supplied.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GeoRegion"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/dates":{"get":{"operationId":"EudrController_getAlertDates","summary":"","description":"Get EUDR alerts dates","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EUDRAlertDates"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/alerts":{"get":{"operationId":"EudrController_getAlerts","parameters":[{"name":"supplierIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}},{"name":"startYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"endYear","required":false,"in":"query","schema":{"type":"number"}},{"name":"startAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"endAlertDate","required":false,"in":"query","schema":{"format":"date-time","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/geo-features":{"get":{"operationId":"EudrController_getGeoFeatureList","summary":"","description":"Get a Feature List GeoRegion Ids","parameters":[{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GeoFeatureResponse"}}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}},"/api/v1/eudr/geo-features/collection":{"get":{"operationId":"EudrController_getGeoFeatureCollection","summary":"","description":"Get a Feature Collection by GeoRegion Ids","parameters":[{"name":"geoRegionIds","required":false,"in":"query","schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoFeatureCollectionResponse"}}}},"401":{"description":""},"403":{"description":""}},"tags":["EUDR"]}}},"info":{"title":"LandGriffon API","description":"LandGriffon is a conservation planning platform.","version":"0.2.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"GeoRegion":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"theGeom":{"type":"object"},"adminRegions":{"type":"array","items":{"type":"string"}},"sourcingLocations":{"type":"array","items":{"type":"string"}}},"required":["id"]},"AdminRegion":{"type":"object","properties":{"id":{"type":"string"},"parent":{"$ref":"#/components/schemas/AdminRegion"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"sourcingLocations":{"type":"array","items":{"type":"string"}},"geoRegion":{"$ref":"#/components/schemas/GeoRegion"},"geoRegionId":{"type":"string"}},"required":["id","status","geoRegion"]},"CreateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateAdminRegionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isoA2":{"type":"string"},"isoA3":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"Material":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}},"required":["name","hsCodeId"]},"UpdateMaterialDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"},"hsCodeId":{"type":"string"},"earthstatId":{"type":"string"},"mapspamId":{"type":"string"}}},"Supplier":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"companyId":{"type":"string"},"address":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["createdAt","updatedAt","id","name","status"]},"CreateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}},"required":["name"]},"UpdateSupplierDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"},"parentId":{"type":"string"}}},"BusinessUnit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"}},"required":["id","name","status"]},"CreateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}},"required":["name"]},"UpdateBusinessUnitDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"string"}}},"SourcingLocation":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationWarning":{"type":"string"},"geoRegionId":{"type":"string"},"metadata":{"type":"object"},"materialId":{"type":"string"},"adminRegionId":{"type":"string"},"businessUnitId":{"type":"string"},"sourcingLocationGroupId":{"type":"string"},"interventionType":{"type":"string"},"scenarioInterventionId":{"type":"string"}},"required":["createdAt","updatedAt","id","locationType","locationAccuracy"]},"SourcingLocationsMaterialsResponseDto":{"type":"object","properties":{"meta":{"type":"object","properties":{"totalItems":{"type":"number","example":45},"totalPages":{"type":"number","example":9},"size":{"type":"number","example":5},"page":{"type":"number","example":1}}},"data":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","example":"sourcing locations"},"id":{"type":"string","example":"a2428cbb-e1b1-4313-ad85-9579b260387f"},"attributes":{"type":"object","properties":{"locationType":{"type":"string","example":"point of production"},"material":{"type":"string","example":"bananas"},"materialId":{"type":"string","example":"cdde28a2-5692-401b-a1a7-6c68ad38010f"},"t1Supplier":{"type":"string","example":"Cargill"},"producer":{"type":"string","example":"Moll"},"businessUnit":{"type":"string","example":"Accessories"},"locationCountryInput":{"type":"string","example":"Japan"},"purchases":{"type":"array","items":{"type":"object","properties":{"year":{"type":"number","example":2010},"tonnage":{"type":"number","example":730}}}}}}}}}},"required":["meta","data"]},"LocationTypesDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}}}}},"required":["data"]},"CreateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}},"required":["title","materialId"]},"UpdateSourcingLocationDto":{"type":"object","properties":{"title":{"type":"string"},"businessUnitId":{"type":"string"},"materialId":{"type":"string"},"t1SupplierId":{"type":"string"},"producerId":{"type":"string"},"locationType":{"type":"string"},"locationAddressInput":{"type":"string"},"locationCountryInput":{"type":"string"},"locationAccuracy":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"metadata":{"type":"object"},"sourcingLocationGroupId":{"type":"string"}}},"LoginDto":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}},"required":["username","password"]},"AccessToken":{"type":"object","properties":{"user":{"type":"object"},"accessToken":{"type":"string"}},"required":["user","accessToken"]},"ResetPasswordDto":{"type":"object","properties":{"password":{"type":"string"}},"required":["password"]},"Permission":{"type":"object","properties":{"action":{"type":"string"}},"required":["action"]},"Role":{"type":"object","properties":{"name":{"type":"string","enum":["admin","user"]},"permissions":{"type":"array","items":{"$ref":"#/components/schemas/Permission"}}},"required":["name","permissions"]},"User":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"string"},"fname":{"type":"string"},"lname":{"type":"string"},"avatarDataUrl":{"type":"string"},"isActive":{"type":"boolean"},"isDeleted":{"type":"boolean"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}},"required":["email","isActive","isDeleted","roles"]},"JSONAPIUserData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/User"}},"required":["type","id","attributes"]},"ApiEvent":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"}},"required":["kind","topic"]},"JSONAPIApiEventData":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/ApiEvent"}},"required":["type","id","attributes"]},"ApiEventResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIApiEventData"}},"required":["data"]},"CreateApiEventDTO":{"type":"object","properties":{"kind":{"type":"string"},"topic":{"type":"string"},"data":{"type":"object"}},"required":["kind","topic"]},"CreateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}},"required":["email","password","roles"]},"UpdateUserPasswordDTO":{"type":"object","properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string"}},"required":["currentPassword","newPassword"]},"UserResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/JSONAPIUserData"}},"required":["data"]},"UpdateOwnUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"avatarDataUrl":{"type":"string"}},"required":["email"]},"RecoverPasswordDto":{"type":"object","properties":{}},"UpdateUserDTO":{"type":"object","properties":{"email":{"type":"string"},"title":{"type":"object"},"fname":{"type":"object"},"lname":{"type":"object"},"password":{"type":"string"},"avatarDataUrl":{"type":"string"},"roles":{"type":"array","example":["admin","user"],"items":{"type":"string","enum":["admin","user"]}}}},"Scenario":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean","description":"Make a Scenario public to all users"},"status":{"type":"string","enum":["active","inactive","deleted"]},"metadata":{"type":"object"},"user":{"$ref":"#/components/schemas/User"},"userId":{"type":"string"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","user","updatedBy"]},"CreateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}},"required":["title"]},"UpdateScenarioDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"isPublic":{"type":"boolean"},"metadata":{"type":"string"}}},"ScenarioIntervention":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"startYear":{"type":"number"},"endYear":{"type":"number"},"percentage":{"type":"number"},"newIndicatorCoefficients":{"type":"object"},"scenario":{"$ref":"#/components/schemas/Scenario"},"newMaterial":{"$ref":"#/components/schemas/Material"},"newBusinessUnit":{"$ref":"#/components/schemas/BusinessUnit"},"newT1Supplier":{"$ref":"#/components/schemas/Supplier"},"newProducer":{"$ref":"#/components/schemas/Supplier"},"newAdminRegion":{"$ref":"#/components/schemas/AdminRegion"},"newLocationType":{"type":"string"},"newLocationCountryInput":{"type":"string"},"newLocationAddressInput":{"type":"string"},"newLocationLatitudeInput":{"type":"number"},"newLocationLongitudeInput":{"type":"number"},"newMaterialTonnageRatio":{"type":"number"},"updatedBy":{"$ref":"#/components/schemas/User"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title","status","type","startYear","percentage","newIndicatorCoefficients","scenario","updatedBy"]},"IndicatorCoefficientsDto":{"type":"object","properties":{"LF":{"type":"number"},"DF_SLUC":{"type":"number"},"GHG_DEF_SLUC":{"type":"number"},"UWU":{"type":"number"},"WU":{"type":"number"},"NL":{"type":"number"},"NCE":{"type":"number"},"FLIL":{"type":"number"},"ENL":{"type":"number"},"GHG_FARM":{"type":"number"}}},"CreateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5}},"required":["title","type","startYear","percentage","scenarioId","materialIds","adminRegionIds","newLocationCountryInput","newLocationAdminRegionInput"]},"UpdateScenarioInterventionDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the Intervention","example":"Replace cotton"},"description":{"type":"string","description":"Brief description of the Intervention","example":"This intervention will replace cotton for wool"},"type":{"type":"string","description":"Type of the Intervention","enum":["default","Source from new supplier or location","Change production efficiency","Switch to a new material"],"example":"Switch to a new material"},"startYear":{"type":"number","description":"Start year of the Intervention","example":2022},"endYear":{"type":"number","description":"End year of the Intervention","example":2025},"percentage":{"type":"number","description":"Percentage of the chosen sourcing records affected by intervention","example":50},"scenarioId":{"type":"uuid","description":"Id of Scenario the intervention belongs to","example":"a15e4933-cd9a-4afc-bd53-56941b816ef3"},"materialIds":{"description":"Ids of Materials that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b816ef3","type":"array","items":{"type":"string"}},"businessUnitIds":{"description":"Ids of Business Units that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b812345","type":"array","items":{"type":"string"}},"t1SupplierIds":{"description":"Ids of T1 Suppliers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"producerIds":{"description":"Ids of Producers that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b865432","type":"array","items":{"type":"string"}},"adminRegionIds":{"description":"Ids of Admin Regions that will be affected by intervention","example":"bc5e4933-cd9a-4afc-bd53-56941b8adca3","type":"array","items":{"type":"string"}},"newIndicatorCoefficients":{"$ref":"#/components/schemas/IndicatorCoefficientsDto"},"newT1SupplierId":{"type":"string","description":"Id of the New Supplier","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc111"},"newProducerId":{"type":"string","description":"Id of the New Producer","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc222"},"newLocationType":{"type":"string","description":"Type of new Supplier Location, is required for Intervention types: Switch to a new material and Source from new supplier or location","enum":["unknown","production-aggregation-point","point-of-production","country-of-production","administrative-region-of-production","country-of-delivery","eudr"],"example":"point-of-production"},"newLocationCountryInput":{"type":"string","description":"New Supplier Location country, is required for Intervention types: Switch to a new material, Source from new supplier or location","example":"Spain"},"newLocationAdminRegionInput":{"type":"string","description":"New Administrative Region, is required for Intervention types: Switch to a new material, Source from new supplier or location\n for Location Type: administrative-region-of-production","example":"Murcia"},"newLocationAddressInput":{"type":"string","description":"\n New Supplier Location address, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no coordintaes were provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if coordinates are provided for the relevant location types","example":"Main Street, 1"},"newLocationLatitude":{"type":"number","description":"\n New Supplier Location latitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of types: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-90,"maximum":90,"example":30.123},"newLocationLongitude":{"type":"number","description":"\n New Supplier Location longitude, is required for Intervention types: Switch to a new material, Source from new supplier or location\n and New Supplier Locations of types: point-of-production and production-aggregation-point in case no address was provided.\n Address OR coordinates must be provided.\n\n Must be NULL for New Supplier Locations of type: unknown and country-of-production\n or if address is provided for the relevant location types.","minimum":-180,"maximum":180,"example":100.123},"newMaterialId":{"type":"string","description":"Id of the New Material, is required if Intervention type is Switch to a new material","example":"bc5e4933-cd9a-4afc-bd53-56941b8adc444"},"newMaterialTonnageRatio":{"type":"number","description":"New Material tonnage ratio","example":0.5},"status":{"type":"string","description":"Status of the intervention","enum":["active","inactive","deleted"],"example":"inactive"}}},"ImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"value":{"type":"number"}},"required":["year","isProjected","value"]},"ImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"YearSumData":{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]},"ImpactTableDataAggregationInfo":{"type":"object","properties":{}},"ImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/YearSumData"}},"metadata":{"type":"object"},"others":{"description":"Extra information used for Ranked ImpactTable requests. Missing on normal ImpactTable requests","allOf":[{"$ref":"#/components/schemas/ImpactTableDataAggregationInfo"}]}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"PaginationMeta":{"type":"object","properties":{}},"PaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"ScenarioVsScenarioImpactTableRowsValues":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableRows":{"type":"object","properties":{"name":{"type":"string"},"values":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRowsValues"}},"children":{"type":"array","items":{"type":"object"}}},"required":["name","values","children"]},"ScenarioVsScenarioIndicatorSumByYearData":{"type":"object","properties":{"year":{"type":"number"},"isProjected":{"type":"boolean"},"baseScenarioValue":{"type":"number"},"comparedScenarioValue":{"type":"number"},"absoluteDifference":{"type":"number"},"percentageDifference":{"type":"number"}},"required":["year","isProjected","baseScenarioValue","comparedScenarioValue","absoluteDifference","percentageDifference"]},"ScenarioVsScenarioImpactTableDataByIndicator":{"type":"object","properties":{"indicatorShortName":{"type":"string"},"indicatorId":{"type":"string"},"groupBy":{"type":"string"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableRows"}},"yearSum":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioIndicatorSumByYearData"}},"metadata":{"type":"object"}},"required":["indicatorShortName","indicatorId","groupBy","rows","yearSum","metadata"]},"ScenarioVsScenarioImpactTablePurchasedTonnes":{"type":"object","properties":{"year":{"type":"number"},"value":{"type":"number"},"isProjected":{"type":"boolean"}},"required":["year","value","isProjected"]},"ScenarioVsScenarioImpactTable":{"type":"object","properties":{"impactTable":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTableDataByIndicator"}},"purchasedTonnes":{"type":"array","items":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTablePurchasedTonnes"}}},"required":["impactTable","purchasedTonnes"]},"ScenarioVsScenarioPaginatedImpactTable":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ScenarioVsScenarioImpactTable"},"metadata":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["data","metadata"]},"Indicator":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"metadata":{"type":"object"},"indicatorCoefficients":{"type":"array","items":{"type":"string"}}},"required":["id","name","status"]},"CreateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}},"required":["name","nameCode"]},"UpdateIndicatorDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"nameCode":{"type":"string"},"metadata":{"type":"string"}}},"SourcingRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"tonnage":{"type":"number"},"year":{"type":"number"},"metadata":{"type":"object"},"updatedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","tonnage","year","updatedBy"]},"CreateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}},"required":["tonnage","year"]},"UpdateSourcingRecordDto":{"type":"object","properties":{"tonnage":{"type":"number"},"year":{"type":"number"},"sourcingLocationsId":{"type":"string"}}},"IndicatorRecord":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["createdAt","updatedAt","id","value","status","statusMsg"]},"CreateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"sourcingRecordId":{"type":"string"},"indicatorId":{"type":"string"},"indicatorCoefficientId":{"type":"string"},"status":{"type":"string"},"statusMsg":{"type":"string"}},"required":["value","indicatorId"]},"UpdateIndicatorRecordDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"},"status":{"type":"string"}}},"H3DataResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"H3MapResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}},"metadata":{"type":"object","properties":{"unit":{"type":"string"},"quantiles":{"type":"array","items":{"type":"number"}},"indicatorDataYear":{"type":"number"},"materialsH3DataYears":{"type":"array","items":{"type":"object","properties":{"materialName":{"type":"string"},"materialDataYear":{"type":"number"},"materialDataType":{"type":"string"}}}}}}},"required":["data","metadata"]},"UnitConversion":{"type":"object","properties":{"id":{"type":"string"},"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}},"required":["id"]},"CreateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"UpdateUnitConversionDto":{"type":"object","properties":{"unit1":{"type":"number"},"unit2":{"type":"number"},"factor":{"type":"number"}}},"CreateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"UpdateGeoRegionDto":{"type":"object","properties":{"name":{"type":"string"},"h3Compact":{"type":"array","items":{"type":"string"}},"theGeom":{"type":"string"}}},"ContextualLayerByCategory":{"type":"object","properties":{}},"GetContextualLayerH3ResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"h":{"type":"string"},"v":{"type":"number"}}}}},"required":["data"]},"SourcingLocationGroup":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","title"]},"CreateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}},"required":["title"]},"UpdateSourcingLocationGroupDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"metadata":{"type":"object"}}},"Task":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"type":{"type":"string"},"user":{"$ref":"#/components/schemas/User"},"status":{"type":"string"},"message":{"type":"string"},"data":{"type":"object"},"logs":{"type":"array","items":{"type":"string"}},"errors":{"type":"array","items":{"type":"string"}},"dismissedBy":{"type":"string"}},"required":["createdAt","updatedAt","id","type","user","status","data","logs","errors","dismissedBy"]},"CreateTaskDto":{"type":"object","properties":{"type":{"type":"string"},"status":{"type":"string"},"data":{"type":"object"}},"required":["type","status","data"]},"UpdateTaskDto":{"type":"object","properties":{"status":{"type":"string"},"newData":{"type":"object"},"dismissedBy":{"type":"string"}}},"IndicatorCoefficient":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"value":{"type":"number"},"year":{"type":"number"},"adminRegion":{"$ref":"#/components/schemas/AdminRegion"},"user":{"$ref":"#/components/schemas/User"},"indicator":{"$ref":"#/components/schemas/Indicator"},"material":{"$ref":"#/components/schemas/Material"}},"required":["createdAt","updatedAt","id","year","user","indicator","material"]},"CreateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}},"required":["year"]},"UpdateIndicatorCoefficientDto":{"type":"object","properties":{"value":{"type":"number"},"year":{"type":"number"}}},"Target":{"type":"object","properties":{"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"id":{"type":"string"},"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"},"updatedById":{"type":"string"}},"required":["createdAt","updatedAt","id","baseLineYear","targetYear","value","indicatorId","updatedById"]},"CreateTargetDto":{"type":"object","properties":{"baseLineYear":{"type":"number"},"targetYear":{"type":"number"},"value":{"type":"number"},"indicatorId":{"type":"string"}},"required":["baseLineYear","targetYear","value","indicatorId"]},"UpdateTargetDto":{"type":"object","properties":{"targetYear":{"type":"number"},"value":{"type":"number"}},"required":["targetYear","value"]},"Unit":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"number"}},"required":["id","name","symbol"]},"CreateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}},"required":["name"]},"UpdateUnitDto":{"type":"object","properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}}},"UrlResponseAttributes":{"type":"object","properties":{"params":{"type":"object"}},"required":["params"]},"UrlResponseDto":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"attributes":{"$ref":"#/components/schemas/UrlResponseAttributes"}},"required":["type","id","attributes"]},"SerializedUrlResponseDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UrlResponseDto"}},"required":["data"]},"DateValue":{"type":"object","properties":{"value":{"type":"object"}},"required":["value"]},"EUDRAlertDates":{"type":"object","properties":{"alertDate":{"$ref":"#/components/schemas/DateValue"}},"required":["alertDate"]},"FeatureClass":{"type":"object","properties":{"geometry":{"type":"object"},"properties":{"type":"object"},"type":{"type":"string"}},"required":["geometry","properties","type"]},"GeoFeatureResponse":{"type":"object","properties":{"geojson":{"$ref":"#/components/schemas/FeatureClass"}},"required":["geojson"]},"FeatureCollectionClass":{"type":"object","properties":{"features":{"type":"array","items":{"type":"string"}},"type":{"type":"string"}},"required":["features","type"]},"GeoFeatureCollectionResponse":{"type":"object","properties":{"geojson":{"$ref":"#/components/schemas/FeatureCollectionClass"}},"required":["geojson"]}}}} \ No newline at end of file From d3850a0579665e7b79aaae16263a0ee671ddb242 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 12:21:08 +0300 Subject: [PATCH 037/153] geo-features tests --- api/test/common-steps/given-geo-region.ts | 18 +++ .../eudr/eudr-admin-region-filters.spec.ts | 3 +- .../e2e/eudr/eudr-geo-region-filters.spec.ts | 3 +- api/test/e2e/eudr/fixtures.ts | 4 - api/test/e2e/geo-regions/fixtures.ts | 111 +++++++++++------- api/test/e2e/geo-regions/geo-features.spec.ts | 56 +++++++++ api/test/entity-mocks.ts | 1 + ...random-name.ts => generate-random-name.ts} | 2 +- api/test/utils/test-manager.ts | 12 +- 9 files changed, 157 insertions(+), 53 deletions(-) create mode 100644 api/test/common-steps/given-geo-region.ts create mode 100644 api/test/e2e/geo-regions/geo-features.spec.ts rename api/test/utils/{random-name.ts => generate-random-name.ts} (79%) diff --git a/api/test/common-steps/given-geo-region.ts b/api/test/common-steps/given-geo-region.ts new file mode 100644 index 000000000..06deed3ce --- /dev/null +++ b/api/test/common-steps/given-geo-region.ts @@ -0,0 +1,18 @@ +import { createGeoRegion } from '../entity-mocks'; +import { GeoRegion } from '../../src/modules/geo-regions/geo-region.entity'; +import { generateRandomName } from '../utils/generate-random-name'; + +const wkt = require('wellknown'); + +const geomstring = + 'MULTIPOLYGON (((-74.94038756755259 -8.789917036555973, -74.9404292564128 -8.78987345274758, -74.94037947090003 -8.789320280383404, -74.94007769168027 -8.78832128710426, -74.94014586742746 -8.788218619928239, -74.94016386858105 -8.78780352397087, -74.94010104117281 -8.787799975977926, -74.94010288471793 -8.78778799293464, -74.9395829026956 -8.787693953632717, -74.93871995380748 -8.787450557792479, -74.93826369227841 -8.787672292367361, -74.93588217957925 -8.78758885088354, -74.93581959017885 -8.787667135197673, -74.9357715871026 -8.788277919312772, -74.93494257105218 -8.78845185941805, -74.93498553672873 -8.788177110353425, -74.93374345713029 -8.788556626292976, -74.9326813890679 -8.788562556226449, -74.93185593961496 -8.788775235806332, -74.93064363729047 -8.789016035583105, -74.92945984843114 -8.789701969314692, -74.92933888309716 -8.79004576552706, -74.92900915373343 -8.790418620740047, -74.92907515796331 -8.791877374183946, -74.92920716642304 -8.792695696771696, -74.92975920180011 -8.793276823308746, -74.9315893190828 -8.793092997666013, -74.93331142944389 -8.792891382340077, -74.93551357056765 -8.792594889014318, -74.93895179090532 -8.79218572783506, -74.9395578297432 -8.79241699290495, -74.9406559001128 -8.79224502658469, -74.94093191780134 -8.79210863944635, -74.94088391472505 -8.79196039249988, -74.94060189665201 -8.791729127144945, -74.94034388011707 -8.791438562776397, -74.94015186781199 -8.791088699254734, -74.94014586742746 -8.790721045706444, -74.94026587511813 -8.790288162835186, -74.9404098843469 -8.78997980760475, -74.94038756755259 -8.789917036555973), (-74.9373868084098 -8.7894253831326, -74.9374531890935 -8.789707501038329, -74.93732595944972 -8.789840262405733, -74.93722085670055 -8.789856857576657, -74.93708256360951 -8.789541549329078, -74.9373868084098 -8.7894253831326)))'; + +export const GivenGeoRegionWithGeometry = async ( + additionalData?: Partial, +): Promise => { + return await createGeoRegion({ + theGeom: wkt.parse(geomstring), + name: generateRandomName(), + ...additionalData, + }); +}; diff --git a/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts index 17e2a74d4..39d6ca42f 100644 --- a/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts +++ b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts @@ -1,10 +1,11 @@ import { EUDRTestManager } from './fixtures'; +import { TestManager } from '../../utils/test-manager'; describe('Admin Regions EUDR Filters (e2e)', () => { let testManager: EUDRTestManager; beforeAll(async () => { - testManager = await EUDRTestManager.load(); + testManager = await TestManager.load(EUDRTestManager); }); beforeEach(async () => { diff --git a/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts index 844ab0496..ed209a296 100644 --- a/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts +++ b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts @@ -1,10 +1,11 @@ import { EUDRTestManager } from './fixtures'; +import { TestManager } from '../../utils/test-manager'; describe('GeoRegions Filters (e2e)', () => { let testManager: EUDRTestManager; beforeAll(async () => { - testManager = await EUDRTestManager.load(); + testManager = await TestManager.load(EUDRTestManager); }); beforeEach(async () => { await testManager.refreshState(); diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index e776fe852..de379245c 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -16,10 +16,6 @@ export class EUDRTestManager extends TestManager { super(manager.testApp, manager.jwtToken, manager.dataSource); } - static async load() { - return new EUDRTestManager(await this.createManager()); - } - GivenAdminRegionsOfSourcingLocations = async () => { const adminRegion = await createAdminRegion({ name: 'Regular AdminRegion', diff --git a/api/test/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts index 3c4745df6..cc57ff2ff 100644 --- a/api/test/e2e/geo-regions/fixtures.ts +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -1,63 +1,90 @@ -import { createGeoRegion, createSourcingLocation } from '../../entity-mocks'; -import { TestApplication } from '../../utils/application-manager'; +import { createSourcingLocation } from '../../entity-mocks'; import * as request from 'supertest'; import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; import { LOCATION_TYPES } from '../../../src/modules/sourcing-locations/sourcing-location.entity'; +import { TestManager } from '../../utils/test-manager'; +import { Feature, Geometry } from 'geojson'; +import { GivenGeoRegionWithGeometry } from '../../common-steps/given-geo-region'; -export const geoRegionFixtures = () => ({ - GivenGeoRegionsOfSourcingLocations: async () => { - const geoRegion = await createGeoRegion({ - name: 'Regular GeoRegion', +export class GeoRegionsTestManager extends TestManager { + constructor(manager: TestManager) { + super(manager.testApp, manager.jwtToken, manager.dataSource); + } + + GivenRegularSourcingLocationsWithGeoRegions = async () => { + const geoRegion = await GivenGeoRegionWithGeometry(); + const geoRegion2 = await GivenGeoRegionWithGeometry(); + const sourcingLocation1 = await createSourcingLocation({ + geoRegionId: geoRegion.id, + locationType: LOCATION_TYPES.ADMINISTRATIVE_REGION_OF_PRODUCTION, }); - const geoRegion2 = await createGeoRegion({ - name: 'Regular GeoRegion 2', + const sourcingLocation2 = await createSourcingLocation({ + geoRegionId: geoRegion2.id, + locationType: LOCATION_TYPES.PRODUCTION_AGGREGATION_POINT, }); - await createSourcingLocation({ geoRegionId: geoRegion.id }); - await createSourcingLocation({ geoRegionId: geoRegion2.id }); return { + sourcingLocations: [sourcingLocation1, sourcingLocation2], geoRegions: [geoRegion, geoRegion2], }; - }, - GivenEUDRGeoRegions: async () => { - const geoRegion = await createGeoRegion({ - name: 'EUDR GeoRegion', - }); - const geoRegion2 = await createGeoRegion({ - name: 'EUDR GeoRegion 2', - }); - await createSourcingLocation({ + }; + + GivenEUDRSourcingLocationsWithGeoRegions = async () => { + const geoRegion = await GivenGeoRegionWithGeometry(); + const geoRegion2 = await GivenGeoRegionWithGeometry(); + const sourcingLocation1 = await createSourcingLocation({ geoRegionId: geoRegion.id, locationType: LOCATION_TYPES.EUDR, }); - await createSourcingLocation({ + const sourcingLocation2 = await createSourcingLocation({ geoRegionId: geoRegion2.id, locationType: LOCATION_TYPES.EUDR, }); return { eudrGeoRegions: [geoRegion, geoRegion2], + eudrSourcingLocations: [sourcingLocation1, sourcingLocation2], }; - }, - WhenIRequestEUDRGeoRegions: async (options: { - app: TestApplication; - jwtToken: string; - }) => { - return request(options.app.getHttpServer()) - .get(`/api/v1/geo-regions/eudr`) - .set('Authorization', `Bearer ${options.jwtToken}`); - }, - ThenIShouldOnlyReceiveEUDRGeoRegions: ( - response: request.Response, + }; + + WhenIRequestEUDRGeoFeatures = async (filters: { + 'geoRegionIds[]': string[]; + collection?: boolean; + }): Promise => { + this.response = await request(this.testApp.getHttpServer()) + .get('/api/v1/eudr/geo-features') + .query(filters) + .set('Authorization', `Bearer ${this.jwtToken}`); + }; + + WhenIRequestEUDRGeoFeatureCollection = async (filters: { + 'geoRegionIds[]': string[]; + }): Promise => { + this.response = await request(this.testApp.getHttpServer()) + .get('/api/v1/eudr/geo-features/collection') + .query(filters) + .set('Authorization', `Bearer ${this.jwtToken}`); + }; + + ThenIShouldOnlyRecieveCorrespondingGeoFeatures = ( eudrGeoRegions: GeoRegion[], + collection?: boolean, ) => { - expect(response.status).toBe(200); - expect(response.body.data.length).toBe(eudrGeoRegions.length); - for (const geoRegion of eudrGeoRegions) { - expect( - response.body.data.find( - (geoRegionResponse: GeoRegion) => - geoRegionResponse.id === geoRegion.id, - ), - ).toBeDefined(); + expect(this.response!.status).toBe(200); + if (collection) { + expect(this.response!.body.geojson.type).toEqual('FeatureCollection'); + expect(this.response!.body.geojson.features.length).toBe( + eudrGeoRegions.length, + ); + } else { + expect(this.response!.body[0].geojson.type).toEqual('Feature'); + expect(this.response!.body.length).toBe(eudrGeoRegions.length); + for (const geoRegion of eudrGeoRegions) { + expect( + this.response!.body.find( + (geoRegionResponse: { geojson: Feature }) => + geoRegionResponse.geojson.properties?.id === geoRegion.id, + ), + ).toBeDefined(); + } } - }, -}); + }; +} diff --git a/api/test/e2e/geo-regions/geo-features.spec.ts b/api/test/e2e/geo-regions/geo-features.spec.ts new file mode 100644 index 000000000..e9ebe3e82 --- /dev/null +++ b/api/test/e2e/geo-regions/geo-features.spec.ts @@ -0,0 +1,56 @@ +import { GeoRegionsTestManager } from './fixtures'; +import { TestManager } from '../../utils/test-manager'; + +describe('Admin Regions EUDR Filters (e2e)', () => { + let testManager: GeoRegionsTestManager; + + beforeAll(async () => { + testManager = await TestManager.load(GeoRegionsTestManager); + }); + + beforeEach(async () => { + await testManager.refreshState(); + }); + + afterEach(async () => { + await testManager.clearDatabase(); + }); + + afterAll(async () => { + await testManager.close(); + }); + + test('should only get geo-features that are part of EUDR data', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatures({ + 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures(eudrGeoRegions); + }); + + test('should only get geo-features that are part of EUDR data and are filtered by geo region id', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatures({ + 'geoRegionIds[]': [eudrGeoRegions[0].id], + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures([ + eudrGeoRegions[0], + ]); + }); + test('sould only get EUDR geo-features as a FeatureCollection', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatureCollection({ + 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( + eudrGeoRegions, + true, + ); + }); +}); diff --git a/api/test/entity-mocks.ts b/api/test/entity-mocks.ts index 4bd5670bf..49958651d 100644 --- a/api/test/entity-mocks.ts +++ b/api/test/entity-mocks.ts @@ -39,6 +39,7 @@ import { User } from '../src/modules/users/user.entity'; import { faker } from '@faker-js/faker'; import { genSalt, hash } from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; +import { generateRandomName } from './utils/generate-random-name'; async function createAdminRegion( additionalData: Partial = {}, diff --git a/api/test/utils/random-name.ts b/api/test/utils/generate-random-name.ts similarity index 79% rename from api/test/utils/random-name.ts rename to api/test/utils/generate-random-name.ts index 578ed2a64..690749949 100644 --- a/api/test/utils/random-name.ts +++ b/api/test/utils/generate-random-name.ts @@ -1,4 +1,4 @@ -export const randomName = (length = 10): string => { +export const generateRandomName = (length = 10): string => { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let result = ''; for (let i = 0; i < length; i++) { diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index d6190f14a..b6530cbe3 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -11,7 +11,7 @@ import { import { Material } from 'modules/materials/material.entity'; import { Supplier } from 'modules/suppliers/supplier.entity'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; -import { randomName } from './random-name'; +import { generateRandomName } from './generate-random-name'; export class TestManager { testApp: TestApplication; @@ -28,6 +28,10 @@ export class TestManager { this.dataSource = dataSource; } + static async load(manager: any) { + return new manager(await this.createManager()); + } + static async createManager() { const testApplication = await ApplicationManager.init(); const dataSource = testApplication.get(DataSource); @@ -68,7 +72,7 @@ export class TestManager { } async createMaterials(names?: string[]) { - const namesToCreate = names || [randomName()]; + const namesToCreate = names || [generateRandomName()]; const createdMaterials: Material[] = []; for (let i = 0; i < namesToCreate.length; i++) { createdMaterials.push(await createMaterial({ name: namesToCreate[i] })); @@ -78,7 +82,7 @@ export class TestManager { } async createSuppliers(names?: string[]) { - const namesToCreate = names || [randomName()]; + const namesToCreate = names || [generateRandomName()]; const createdSuppliers: Supplier[] = []; for (let i = 0; i < namesToCreate.length; i++) { createdSuppliers.push(await createSupplier({ name: namesToCreate[i] })); @@ -88,7 +92,7 @@ export class TestManager { } async createGeoRegions(names?: string[]) { - const namesToCreate = names || [randomName()]; + const namesToCreate = names || [generateRandomName()]; const createdGeoRegions: GeoRegion[] = []; for (let i = 0; i < namesToCreate.length; i++) { createdGeoRegions.push(await createGeoRegion({ name: namesToCreate[i] })); From bc53ccb8527635132466055173841b8041ee9dc9 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 15:12:21 +0300 Subject: [PATCH 038/153] clean-up testing flow --- .../common-steps/and-associated-materials.ts | 21 ------- .../common-steps/and-associated-suppliers.ts | 30 ---------- api/test/common-steps/given-geo-region.ts | 18 ------ api/test/e2e/eudr/fixtures.ts | 55 ++++++++++++------- api/test/e2e/geo-regions/fixtures.ts | 21 ++++--- api/test/entity-mocks.ts | 7 ++- api/test/utils/test-manager.ts | 35 +----------- 7 files changed, 58 insertions(+), 129 deletions(-) delete mode 100644 api/test/common-steps/and-associated-materials.ts delete mode 100644 api/test/common-steps/and-associated-suppliers.ts delete mode 100644 api/test/common-steps/given-geo-region.ts diff --git a/api/test/common-steps/and-associated-materials.ts b/api/test/common-steps/and-associated-materials.ts deleted file mode 100644 index ed30fd2fe..000000000 --- a/api/test/common-steps/and-associated-materials.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Material } from 'modules/materials/material.entity'; -import { SourcingLocation } from '../../src/modules/sourcing-locations/sourcing-location.entity'; - -/** - * @description Associate materials with sourcing locations for tests - */ - -export const AndAssociatedMaterials = async ( - materials: Material[], - existingSourcingLocations: SourcingLocation[], -): Promise => { - const limitLength = Math.min( - materials.length, - existingSourcingLocations.length, - ); - for (let i = 0; i < limitLength; i++) { - existingSourcingLocations[i].materialId = materials[i].id; - await existingSourcingLocations[i].save(); - } - return existingSourcingLocations; -}; diff --git a/api/test/common-steps/and-associated-suppliers.ts b/api/test/common-steps/and-associated-suppliers.ts deleted file mode 100644 index eb0b5f6a9..000000000 --- a/api/test/common-steps/and-associated-suppliers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SourcingLocation } from '../../src/modules/sourcing-locations/sourcing-location.entity'; -import { - Supplier, - SUPPLIER_TYPES, -} from '../../src/modules/suppliers/supplier.entity'; - -/** - * @description Associate suppliers with sourcing locations for tests - */ - -export const AndAssociatedSuppliers = async ( - supplier: Supplier[], - existingSourcingLocations: SourcingLocation[], - supplierType?: SUPPLIER_TYPES, -): Promise => { - const limitLength = Math.min( - supplier.length, - existingSourcingLocations.length, - ); - for (let i = 0; i < limitLength; i++) { - if (supplierType === SUPPLIER_TYPES.PRODUCER || !supplierType) { - existingSourcingLocations[i].producerId = supplier[i].id; - } - if (supplierType === SUPPLIER_TYPES.T1SUPPLIER) { - existingSourcingLocations[i].t1SupplierId = supplier[i].id; - } - await existingSourcingLocations[i].save(); - } - return existingSourcingLocations; -}; diff --git a/api/test/common-steps/given-geo-region.ts b/api/test/common-steps/given-geo-region.ts deleted file mode 100644 index 06deed3ce..000000000 --- a/api/test/common-steps/given-geo-region.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createGeoRegion } from '../entity-mocks'; -import { GeoRegion } from '../../src/modules/geo-regions/geo-region.entity'; -import { generateRandomName } from '../utils/generate-random-name'; - -const wkt = require('wellknown'); - -const geomstring = - 'MULTIPOLYGON (((-74.94038756755259 -8.789917036555973, -74.9404292564128 -8.78987345274758, -74.94037947090003 -8.789320280383404, -74.94007769168027 -8.78832128710426, -74.94014586742746 -8.788218619928239, -74.94016386858105 -8.78780352397087, -74.94010104117281 -8.787799975977926, -74.94010288471793 -8.78778799293464, -74.9395829026956 -8.787693953632717, -74.93871995380748 -8.787450557792479, -74.93826369227841 -8.787672292367361, -74.93588217957925 -8.78758885088354, -74.93581959017885 -8.787667135197673, -74.9357715871026 -8.788277919312772, -74.93494257105218 -8.78845185941805, -74.93498553672873 -8.788177110353425, -74.93374345713029 -8.788556626292976, -74.9326813890679 -8.788562556226449, -74.93185593961496 -8.788775235806332, -74.93064363729047 -8.789016035583105, -74.92945984843114 -8.789701969314692, -74.92933888309716 -8.79004576552706, -74.92900915373343 -8.790418620740047, -74.92907515796331 -8.791877374183946, -74.92920716642304 -8.792695696771696, -74.92975920180011 -8.793276823308746, -74.9315893190828 -8.793092997666013, -74.93331142944389 -8.792891382340077, -74.93551357056765 -8.792594889014318, -74.93895179090532 -8.79218572783506, -74.9395578297432 -8.79241699290495, -74.9406559001128 -8.79224502658469, -74.94093191780134 -8.79210863944635, -74.94088391472505 -8.79196039249988, -74.94060189665201 -8.791729127144945, -74.94034388011707 -8.791438562776397, -74.94015186781199 -8.791088699254734, -74.94014586742746 -8.790721045706444, -74.94026587511813 -8.790288162835186, -74.9404098843469 -8.78997980760475, -74.94038756755259 -8.789917036555973), (-74.9373868084098 -8.7894253831326, -74.9374531890935 -8.789707501038329, -74.93732595944972 -8.789840262405733, -74.93722085670055 -8.789856857576657, -74.93708256360951 -8.789541549329078, -74.9373868084098 -8.7894253831326)))'; - -export const GivenGeoRegionWithGeometry = async ( - additionalData?: Partial, -): Promise => { - return await createGeoRegion({ - theGeom: wkt.parse(geomstring), - name: generateRandomName(), - ...additionalData, - }); -}; diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index de379245c..671658e8d 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -2,12 +2,19 @@ import { LOCATION_TYPES, SourcingLocation, } from 'modules/sourcing-locations/sourcing-location.entity'; -import { createAdminRegion, createSourcingLocation } from '../../entity-mocks'; +import { + createAdminRegion, + createGeoRegion, + createMaterial, + createSourcingLocation, + createSupplier, +} from '../../entity-mocks'; import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; -import { AndAssociatedMaterials } from '../../common-steps/and-associated-materials'; -import { AndAssociatedSuppliers } from '../../common-steps/and-associated-suppliers'; import { TestManager } from '../../utils/test-manager'; import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; +import { Material } from 'modules/materials/material.entity'; +import { generateRandomName } from '../../utils/generate-random-name'; +import { Supplier } from '../../../src/modules/suppliers/supplier.entity'; export class EUDRTestManager extends TestManager { url = '/api/v1/eudr'; @@ -38,17 +45,29 @@ export class EUDRTestManager extends TestManager { sourcingLocations: SourcingLocation[], materialNames?: string[], ) => { - const materials = await this.createMaterials(materialNames); - await AndAssociatedMaterials(materials, sourcingLocations); - return materials; + const materials: Material[] = []; + for (const name of materialNames || [generateRandomName()]) { + materials.push(await createMaterial({ name })); + } + const limitLength = Math.min(materials.length, sourcingLocations.length); + for (let i = 0; i < limitLength; i++) { + sourcingLocations[i].materialId = materials[i].id; + await sourcingLocations[i].save(); + } }; AndAssociatedSuppliers = async ( sourcingLocations: SourcingLocation[], supplierNames?: string[], ) => { - const suppliers = await this.createSuppliers(supplierNames); - await AndAssociatedSuppliers(suppliers, sourcingLocations); - return suppliers; + const suppliers: Supplier[] = []; + for (const name of supplierNames || [generateRandomName()]) { + suppliers.push(await createSupplier({ name })); + } + const limitLength = Math.min(suppliers.length, sourcingLocations.length); + for (let i = 0; i < limitLength; i++) { + sourcingLocations[i].materialId = suppliers[i].id; + await sourcingLocations[i].save(); + } }; GivenEUDRAdminRegions = async () => { const adminRegion = await createAdminRegion({ @@ -80,8 +99,8 @@ export class EUDRTestManager extends TestManager { ThenIShouldOnlyReceiveCorrespondingAdminRegions = ( eudrAdminRegions: AdminRegion[], ) => { - expect(this.response!.status).toBe(200); - expect(this.response!.body.data.length).toBe(eudrAdminRegions.length); + expect(this.response?.status).toBe(200); + expect(this.response?.body.data.length).toBe(eudrAdminRegions.length); for (const adminRegion of eudrAdminRegions) { expect( this.response!.body.data.find( @@ -93,10 +112,9 @@ export class EUDRTestManager extends TestManager { }; GivenGeoRegionsOfSourcingLocations = async () => { - const [geoRegion, geoRegion2] = await this.createGeoRegions([ - 'Regular GeoRegion', - 'Regular GeoRegion 2', - ]); + const geoRegion = await createGeoRegion({ name: 'Regular GeoRegion' }); + const geoRegion2 = await createGeoRegion({ name: 'Regular GeoRegion 2' }); + await createSourcingLocation({ geoRegionId: geoRegion.id }); await createSourcingLocation({ geoRegionId: geoRegion2.id }); return { @@ -105,10 +123,9 @@ export class EUDRTestManager extends TestManager { }; GivenEUDRGeoRegions = async () => { - const [geoRegion, geoRegion2] = await this.createGeoRegions([ - 'EUDR GeoRegion', - 'EUDR GeoRegion 2', - ]); + const geoRegion = await createGeoRegion({ name: 'EUDR GeoRegion' }); + const geoRegion2 = await createGeoRegion({ name: 'EUDR GeoRegion 2' }); + await createSourcingLocation({ geoRegionId: geoRegion.id, locationType: LOCATION_TYPES.EUDR, diff --git a/api/test/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts index cc57ff2ff..86539d2b8 100644 --- a/api/test/e2e/geo-regions/fixtures.ts +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -1,10 +1,9 @@ -import { createSourcingLocation } from '../../entity-mocks'; +import { createGeoRegion, createSourcingLocation } from '../../entity-mocks'; import * as request from 'supertest'; import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; import { LOCATION_TYPES } from '../../../src/modules/sourcing-locations/sourcing-location.entity'; import { TestManager } from '../../utils/test-manager'; -import { Feature, Geometry } from 'geojson'; -import { GivenGeoRegionWithGeometry } from '../../common-steps/given-geo-region'; +import { Feature } from 'geojson'; export class GeoRegionsTestManager extends TestManager { constructor(manager: TestManager) { @@ -12,8 +11,12 @@ export class GeoRegionsTestManager extends TestManager { } GivenRegularSourcingLocationsWithGeoRegions = async () => { - const geoRegion = await GivenGeoRegionWithGeometry(); - const geoRegion2 = await GivenGeoRegionWithGeometry(); + const geoRegion = await createGeoRegion({ + name: this.generateRandomName(), + }); + const geoRegion2 = await createGeoRegion({ + name: this.generateRandomName(), + }); const sourcingLocation1 = await createSourcingLocation({ geoRegionId: geoRegion.id, locationType: LOCATION_TYPES.ADMINISTRATIVE_REGION_OF_PRODUCTION, @@ -29,8 +32,12 @@ export class GeoRegionsTestManager extends TestManager { }; GivenEUDRSourcingLocationsWithGeoRegions = async () => { - const geoRegion = await GivenGeoRegionWithGeometry(); - const geoRegion2 = await GivenGeoRegionWithGeometry(); + const geoRegion = await createGeoRegion({ + name: this.generateRandomName(), + }); + const geoRegion2 = await createGeoRegion({ + name: this.generateRandomName(), + }); const sourcingLocation1 = await createSourcingLocation({ geoRegionId: geoRegion.id, locationType: LOCATION_TYPES.EUDR, diff --git a/api/test/entity-mocks.ts b/api/test/entity-mocks.ts index 49958651d..2afb385ed 100644 --- a/api/test/entity-mocks.ts +++ b/api/test/entity-mocks.ts @@ -39,7 +39,8 @@ import { User } from '../src/modules/users/user.entity'; import { faker } from '@faker-js/faker'; import { genSalt, hash } from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; -import { generateRandomName } from './utils/generate-random-name'; + +const wkt = require('wellknown'); async function createAdminRegion( additionalData: Partial = {}, @@ -286,6 +287,9 @@ async function createMaterial( async function createGeoRegion( additionalData: Partial = {}, ): Promise { + const geomstring = + 'MULTIPOLYGON (((-74.94038756755259 -8.789917036555973, -74.9404292564128 -8.78987345274758, -74.94037947090003 -8.789320280383404, -74.94007769168027 -8.78832128710426, -74.94014586742746 -8.788218619928239, -74.94016386858105 -8.78780352397087, -74.94010104117281 -8.787799975977926, -74.94010288471793 -8.78778799293464, -74.9395829026956 -8.787693953632717, -74.93871995380748 -8.787450557792479, -74.93826369227841 -8.787672292367361, -74.93588217957925 -8.78758885088354, -74.93581959017885 -8.787667135197673, -74.9357715871026 -8.788277919312772, -74.93494257105218 -8.78845185941805, -74.93498553672873 -8.788177110353425, -74.93374345713029 -8.788556626292976, -74.9326813890679 -8.788562556226449, -74.93185593961496 -8.788775235806332, -74.93064363729047 -8.789016035583105, -74.92945984843114 -8.789701969314692, -74.92933888309716 -8.79004576552706, -74.92900915373343 -8.790418620740047, -74.92907515796331 -8.791877374183946, -74.92920716642304 -8.792695696771696, -74.92975920180011 -8.793276823308746, -74.9315893190828 -8.793092997666013, -74.93331142944389 -8.792891382340077, -74.93551357056765 -8.792594889014318, -74.93895179090532 -8.79218572783506, -74.9395578297432 -8.79241699290495, -74.9406559001128 -8.79224502658469, -74.94093191780134 -8.79210863944635, -74.94088391472505 -8.79196039249988, -74.94060189665201 -8.791729127144945, -74.94034388011707 -8.791438562776397, -74.94015186781199 -8.791088699254734, -74.94014586742746 -8.790721045706444, -74.94026587511813 -8.790288162835186, -74.9404098843469 -8.78997980760475, -74.94038756755259 -8.789917036555973), (-74.9373868084098 -8.7894253831326, -74.9374531890935 -8.789707501038329, -74.93732595944972 -8.789840262405733, -74.93722085670055 -8.789856857576657, -74.93708256360951 -8.789541549329078, -74.9373868084098 -8.7894253831326)))'; + const defaultData: DeepPartial = { h3Compact: [ '8667737afffffff', @@ -307,6 +311,7 @@ async function createGeoRegion( ], h3FlatLength: 7, name: 'ABC', + theGeom: wkt.parse(geomstring), }; const geoRegion = GeoRegion.merge( diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index b6530cbe3..bfb0a4cc0 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -3,11 +3,6 @@ import ApplicationManager, { TestApplication } from './application-manager'; import { clearTestDataFromDatabase } from './database-test-helper'; import { setupTestUser } from './userAuth'; import * as request from 'supertest'; -import { - createGeoRegion, - createMaterial, - createSupplier, -} from '../entity-mocks'; import { Material } from 'modules/materials/material.entity'; import { Supplier } from 'modules/suppliers/supplier.entity'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; @@ -71,33 +66,7 @@ export class TestManager { await this.testApp.close(); } - async createMaterials(names?: string[]) { - const namesToCreate = names || [generateRandomName()]; - const createdMaterials: Material[] = []; - for (let i = 0; i < namesToCreate.length; i++) { - createdMaterials.push(await createMaterial({ name: namesToCreate[i] })); - } - this.materials?.push(...createdMaterials); - return createdMaterials; - } - - async createSuppliers(names?: string[]) { - const namesToCreate = names || [generateRandomName()]; - const createdSuppliers: Supplier[] = []; - for (let i = 0; i < namesToCreate.length; i++) { - createdSuppliers.push(await createSupplier({ name: namesToCreate[i] })); - } - this.suppliers?.push(...createdSuppliers); - return createdSuppliers; - } - - async createGeoRegions(names?: string[]) { - const namesToCreate = names || [generateRandomName()]; - const createdGeoRegions: GeoRegion[] = []; - for (let i = 0; i < namesToCreate.length; i++) { - createdGeoRegions.push(await createGeoRegion({ name: namesToCreate[i] })); - } - this.geoRegions?.push(...createdGeoRegions); - return createdGeoRegions; + generateRandomName(): string { + return generateRandomName(); } } From 145b15dfab9c40a2d4f848acdc2a1295b40190bb Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 6 Mar 2024 15:38:33 +0300 Subject: [PATCH 039/153] Add entity filters for geo-features --- .../modules/eudr-alerts/eudr.controller.ts | 15 ++++--- api/src/modules/eudr-alerts/eudr.module.ts | 5 ++- .../dto/get-features-geojson.dto.ts | 23 +++++------ .../geo-regions/geo-features.service.ts | 24 ++++-------- api/src/utils/base.query-builder.ts | 12 ++++++ .../eudr/eudr-admin-region-filters.spec.ts | 3 +- api/test/e2e/eudr/fixtures.ts | 4 +- api/test/e2e/geo-regions/fixtures.ts | 39 +++++++++++++++++-- api/test/e2e/geo-regions/geo-features.spec.ts | 16 +++++++- 9 files changed, 96 insertions(+), 45 deletions(-) diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 0e0236545..8c0cbfcf7 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -6,13 +6,11 @@ import { ValidationPipe, } from '@nestjs/common'; import { - ApiExtraModels, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, - refs, } from '@nestjs/swagger'; import { ApiOkTreeResponse } from 'decorators/api-tree-response.decorator'; @@ -37,11 +35,10 @@ import { EudrService } from 'modules/eudr-alerts/eudr.service'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRFeaturesGeoJSONDto } from 'modules/geo-regions/dto/get-features-geojson.dto'; -import { Feature, FeatureCollection } from 'geojson'; import { GeoFeatureCollectionResponse, GeoFeatureResponse, -} from '../geo-regions/dto/geo-feature-response.dto'; +} from 'modules/geo-regions/dto/geo-feature-response.dto'; @ApiTags('EUDR') @Controller('/api/v1/eudr') @@ -180,7 +177,7 @@ export class EudrController { async getGeoFeatureList( @Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto, ): Promise { - return this.geoRegionsService.getGeoJson(dto); + return this.geoRegionsService.getGeoJson({ ...dto, eudr: true }); } @ApiOperation({ @@ -193,8 +190,10 @@ export class EudrController { async getGeoFeatureCollection( @Query(ValidationPipe) dto: GetEUDRFeaturesGeoJSONDto, ): Promise { - return this.geoRegionsService.getGeoJson( - Object.assign(dto, { collection: true }), - ); + return this.geoRegionsService.getGeoJson({ + ...dto, + eudr: true, + collection: true, + }); } } diff --git a/api/src/modules/eudr-alerts/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts index 692323e5e..9c7692615 100644 --- a/api/src/modules/eudr-alerts/eudr.module.ts +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -15,7 +15,10 @@ export const IEUDRAlertsRepositoryToken: symbol = Symbol( export const EUDRDataSetToken: symbol = Symbol('EUDRDataSet'); export const EUDRCredentialsToken: symbol = Symbol('EUDRCredentials'); -const { credentials, dataset } = AppConfig.get('eudr'); +const { credentials, dataset } = AppConfig.get<{ + credentials: string; + dataset: string; +}>('eudr'); // TODO: Use token injection and refer to the interface, right now I am having a dependencv issue @Module({ diff --git a/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts index 2bd593067..f96f48567 100644 --- a/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts +++ b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts @@ -1,25 +1,20 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; - -export class GetFeaturesGeoJsonDto { - @ApiPropertyOptional() - @IsOptional() - @IsUUID('4', { each: true }) - geoRegionIds!: string[]; +import { IsBoolean, IsOptional } from 'class-validator'; +import { + CommonEUDRFiltersDTO, + CommonFiltersDto, +} from 'utils/base.query-builder'; +export class GetFeaturesGeoJsonDto extends CommonFiltersDto { @IsOptional() @IsBoolean() @Type(() => Boolean) collection: boolean = false; - - isEUDRRequested(): boolean { - return 'eudr' in this; - } } -export class GetEUDRFeaturesGeoJSONDto extends GetFeaturesGeoJsonDto { +export class GetEUDRFeaturesGeoJSONDto extends CommonEUDRFiltersDTO { @IsOptional() @IsBoolean() - eudr: boolean = true; + @Type(() => Boolean) + collection: boolean = false; } diff --git a/api/src/modules/geo-regions/geo-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts index 231978169..08a2f8ace 100644 --- a/api/src/modules/geo-regions/geo-features.service.ts +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -6,10 +6,8 @@ import { GetEUDRFeaturesGeoJSONDto, GetFeaturesGeoJsonDto, } from 'modules/geo-regions/dto/get-features-geojson.dto'; -import { - LOCATION_TYPES, - SourcingLocation, -} from 'modules/sourcing-locations/sourcing-location.entity'; +import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; +import { BaseQueryBuilder } from 'utils/base.query-builder'; @Injectable() export class GeoFeaturesService extends Repository { @@ -25,20 +23,14 @@ export class GeoFeaturesService extends Repository { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder('gr'); queryBuilder.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); - if (dto.isEUDRRequested()) { - queryBuilder.where('sl.locationType = :locationType', { - locationType: LOCATION_TYPES.EUDR, - }); - } - if (dto.geoRegionIds) { - queryBuilder.andWhere('gr.id IN (:...geoRegionIds)', { - geoRegionIds: dto.geoRegionIds, - }); - } + + const filteredQueryBuilder: SelectQueryBuilder = + BaseQueryBuilder.addFilters(queryBuilder, dto); + if (dto?.collection) { - return this.selectAsFeatureCollection(queryBuilder); + return this.selectAsFeatureCollection(filteredQueryBuilder); } - return this.selectAsFeatures(queryBuilder); + return this.selectAsFeatures(filteredQueryBuilder); } private async selectAsFeatures( diff --git a/api/src/utils/base.query-builder.ts b/api/src/utils/base.query-builder.ts index b9325ddaf..6e8fb0ee4 100644 --- a/api/src/utils/base.query-builder.ts +++ b/api/src/utils/base.query-builder.ts @@ -47,6 +47,11 @@ export class BaseQueryBuilder { originIds: filters.originIds, }); } + if (filters.geoRegionIds) { + queryBuilder.andWhere('sl.geoRegionId IN (:...geoRegionIds)', { + geoRegionIds: filters.geoRegionIds, + }); + } if (filters.eudr) { queryBuilder.andWhere('sl.locationType = :eudr', { eudr: LOCATION_TYPES.EUDR, @@ -137,6 +142,8 @@ export class CommonFiltersDto { scenarioIds?: string[]; eudr?: boolean; + + geoRegionIds?: string[]; } export class CommonEUDRFiltersDTO extends OmitType(CommonFiltersDto, [ @@ -146,4 +153,9 @@ export class CommonEUDRFiltersDTO extends OmitType(CommonFiltersDto, [ 'businessUnitIds', ]) { eudr?: boolean; + + @ApiPropertyOptional({ name: 'geoRegionIds[]' }) + @IsOptional() + @IsUUID('4', { each: true }) + geoRegionIds?: string[]; } diff --git a/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts index 39d6ca42f..4c18421d6 100644 --- a/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts +++ b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts @@ -1,5 +1,6 @@ import { EUDRTestManager } from './fixtures'; import { TestManager } from '../../utils/test-manager'; +import { Supplier } from '../../../src/modules/suppliers/supplier.entity'; describe('Admin Regions EUDR Filters (e2e)', () => { let testManager: EUDRTestManager; @@ -43,7 +44,7 @@ describe('Admin Regions EUDR Filters (e2e)', () => { ); await testManager.WhenIRequestEUDRAdminRegions({ 'materialIds[]': [eudrMaterials[0].id], - 'producerIds[]': eudrSuppliers.map((s) => s.id), + 'producerIds[]': eudrSuppliers.map((s: Supplier) => s.id), }); testManager.ThenIShouldOnlyReceiveCorrespondingAdminRegions([ eudrAdminRegions[0], diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index 671658e8d..81951deb1 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -54,6 +54,7 @@ export class EUDRTestManager extends TestManager { sourcingLocations[i].materialId = materials[i].id; await sourcingLocations[i].save(); } + return materials; }; AndAssociatedSuppliers = async ( sourcingLocations: SourcingLocation[], @@ -65,9 +66,10 @@ export class EUDRTestManager extends TestManager { } const limitLength = Math.min(suppliers.length, sourcingLocations.length); for (let i = 0; i < limitLength; i++) { - sourcingLocations[i].materialId = suppliers[i].id; + sourcingLocations[i].producerId = suppliers[i].id; await sourcingLocations[i].save(); } + return suppliers; }; GivenEUDRAdminRegions = async () => { const adminRegion = await createAdminRegion({ diff --git a/api/test/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts index 86539d2b8..f13b4820c 100644 --- a/api/test/e2e/geo-regions/fixtures.ts +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -1,9 +1,19 @@ -import { createGeoRegion, createSourcingLocation } from '../../entity-mocks'; +import { + createAdminRegion, + createGeoRegion, + createMaterial, + createSourcingLocation, + createSupplier, +} from '../../entity-mocks'; import * as request from 'supertest'; import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; -import { LOCATION_TYPES } from '../../../src/modules/sourcing-locations/sourcing-location.entity'; +import { + LOCATION_TYPES, + SourcingLocation, +} from '../../../src/modules/sourcing-locations/sourcing-location.entity'; import { TestManager } from '../../utils/test-manager'; import { Feature } from 'geojson'; +import { SUPPLIER_TYPES } from 'modules/suppliers/supplier.entity'; export class GeoRegionsTestManager extends TestManager { constructor(manager: TestManager) { @@ -38,13 +48,33 @@ export class GeoRegionsTestManager extends TestManager { const geoRegion2 = await createGeoRegion({ name: this.generateRandomName(), }); + + const adminRegion = await createAdminRegion({ name: 'EUDR AdminRegion' }); + const adminRegion2 = await createAdminRegion({ + name: 'EUDR AdminRegion 2', + }); + + const supplier = await createSupplier({ + name: 'EUDR Supplier', + }); + + const material = await createMaterial({ name: 'EUDR Material' }); + const material2 = await createMaterial({ name: 'EUDR Material 2' }); + + const supplier2 = await createSupplier({ name: 'EUDR Supplier 2' }); const sourcingLocation1 = await createSourcingLocation({ geoRegionId: geoRegion.id, locationType: LOCATION_TYPES.EUDR, + adminRegionId: adminRegion.id, + producerId: supplier.id, + materialId: material.id, }); const sourcingLocation2 = await createSourcingLocation({ geoRegionId: geoRegion2.id, locationType: LOCATION_TYPES.EUDR, + adminRegionId: adminRegion2.id, + producerId: supplier2.id, + materialId: material2.id, }); return { eudrGeoRegions: [geoRegion, geoRegion2], @@ -63,7 +93,10 @@ export class GeoRegionsTestManager extends TestManager { }; WhenIRequestEUDRGeoFeatureCollection = async (filters: { - 'geoRegionIds[]': string[]; + 'geoRegionIds[]'?: string[]; + 'producerIds[]'?: string[]; + 'materialIds[]'?: string[]; + 'originIds[]'?: string[]; }): Promise => { this.response = await request(this.testApp.getHttpServer()) .get('/api/v1/eudr/geo-features/collection') diff --git a/api/test/e2e/geo-regions/geo-features.spec.ts b/api/test/e2e/geo-regions/geo-features.spec.ts index e9ebe3e82..b7de75f96 100644 --- a/api/test/e2e/geo-regions/geo-features.spec.ts +++ b/api/test/e2e/geo-regions/geo-features.spec.ts @@ -41,7 +41,21 @@ describe('Admin Regions EUDR Filters (e2e)', () => { eudrGeoRegions[0], ]); }); - test('sould only get EUDR geo-features as a FeatureCollection', async () => { + test('should only get EUDR geo-features filtered by materials, suppliers and admin regions', async () => { + const {} = await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrSourcingLocations, eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatureCollection({ + 'materialIds[]': [eudrSourcingLocations[0].materialId], + 'producerIds[]': [eudrSourcingLocations[0].producerId as string], + 'originIds[]': [eudrSourcingLocations[0].adminRegionId], + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( + [eudrGeoRegions[0]], + true, + ); + }); + test('sould only get EUDR geo-features as a FeatureCollection and filtered by geo regions', async () => { await testManager.GivenRegularSourcingLocationsWithGeoRegions(); const { eudrGeoRegions } = await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); From 2b41f81bc89118623bbde3ff7549dd76c88a9d3f Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 7 Mar 2024 09:45:26 +0300 Subject: [PATCH 040/153] Add metadata to geo features --- .../geo-regions/geo-features.service.ts | 25 ++++- api/test/e2e/eudr/fixtures.ts | 7 +- api/test/e2e/geo-regions/fixtures.ts | 68 +++++++++++-- api/test/e2e/geo-regions/geo-features.spec.ts | 97 +++++++++++-------- api/test/utils/test-manager.ts | 2 +- 5 files changed, 142 insertions(+), 57 deletions(-) diff --git a/api/src/modules/geo-regions/geo-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts index 08a2f8ace..727a6deb2 100644 --- a/api/src/modules/geo-regions/geo-features.service.ts +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -8,6 +8,7 @@ import { } from 'modules/geo-regions/dto/get-features-geojson.dto'; import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; import { BaseQueryBuilder } from 'utils/base.query-builder'; +import { Supplier } from '../suppliers/supplier.entity'; @Injectable() export class GeoFeaturesService extends Repository { @@ -23,6 +24,7 @@ export class GeoFeaturesService extends Repository { const queryBuilder: SelectQueryBuilder = this.createQueryBuilder('gr'); queryBuilder.innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id'); + queryBuilder.innerJoin(Supplier, 's', 's.id = sl."producerId"'); const filteredQueryBuilder: SelectQueryBuilder = BaseQueryBuilder.addFilters(queryBuilder, dto); @@ -41,7 +43,7 @@ export class GeoFeaturesService extends Repository { json_build_object( 'type', 'Feature', 'geometry', ST_AsGeoJSON(gr.theGeom)::json, - 'properties', json_build_object('id', gr.id) + 'properties', json_build_object(${this.injectMetaDataQuery()}) )`, 'geojson', ); @@ -63,7 +65,7 @@ export class GeoFeaturesService extends Repository { json_build_object( 'type', 'Feature', 'geometry', ST_AsGeoJSON(gr.theGeom)::json, - 'properties', json_build_object('id', gr.id) + 'properties', json_build_object(${this.injectMetaDataQuery()}) ) ) )`, @@ -76,4 +78,23 @@ export class GeoFeaturesService extends Repository { } return result; } + + /** + * @description: The Baseline Volume is the purchase volume of a supplier for a specific year (2020) which is the cut-off date for the EUDR + * This is very specific to EUDR and not a dynamic thing, so for now we will be hardcoding it + */ + private injectMetaDataQuery(): string { + const baselineVolumeYear: number = 2020; + return ` + 'id', gr.id, + 'supplierName', s.name, + 'plotName', gr.name, + 'baselineVolume', ( + SELECT SUM(sr.tonnage) + FROM sourcing_records sr + WHERE sr."sourcingLocationId" = sl.id + AND sr.year = ${baselineVolumeYear} + ) + `; + } } diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index 81951deb1..07ea1c727 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -95,7 +95,10 @@ export class EUDRTestManager extends TestManager { 'producerIds[]'?: string[]; 'materialIds[]'?: string[]; }) => { - return this.GET({ url: `${this.url}/admin-regions`, query: filters }); + return this.getRequest({ + url: `${this.url}/admin-regions`, + query: filters, + }); }; ThenIShouldOnlyReceiveCorrespondingAdminRegions = ( @@ -145,7 +148,7 @@ export class EUDRTestManager extends TestManager { 'producerIds[]'?: string[]; 'materialIds[]'?: string[]; }) => { - return this.GET({ url: `${this.url}/geo-regions`, query: filters }); + return this.getRequest({ url: `${this.url}/geo-regions`, query: filters }); }; ThenIShouldOnlyReceiveCorrespondingGeoRegions = ( diff --git a/api/test/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts index f13b4820c..1a69e235d 100644 --- a/api/test/e2e/geo-regions/fixtures.ts +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -3,6 +3,7 @@ import { createGeoRegion, createMaterial, createSourcingLocation, + createSourcingRecord, createSupplier, } from '../../entity-mocks'; import * as request from 'supertest'; @@ -13,7 +14,8 @@ import { } from '../../../src/modules/sourcing-locations/sourcing-location.entity'; import { TestManager } from '../../utils/test-manager'; import { Feature } from 'geojson'; -import { SUPPLIER_TYPES } from 'modules/suppliers/supplier.entity'; +import { SourcingRecord } from '../../../src/modules/sourcing-records/sourcing-record.entity'; +import { Supplier } from '../../../src/modules/suppliers/supplier.entity'; export class GeoRegionsTestManager extends TestManager { constructor(manager: TestManager) { @@ -76,6 +78,18 @@ export class GeoRegionsTestManager extends TestManager { producerId: supplier2.id, materialId: material2.id, }); + for (const year of [2018, 2019, 2020, 2021, 2022, 2023]) { + await createSourcingRecord({ + sourcingLocationId: sourcingLocation1.id, + year, + tonnage: 100 * year, + }); + await createSourcingRecord({ + sourcingLocationId: sourcingLocation2.id, + year, + tonnage: 100 * year, + }); + } return { eudrGeoRegions: [geoRegion, geoRegion2], eudrSourcingLocations: [sourcingLocation1, sourcingLocation2], @@ -86,22 +100,22 @@ export class GeoRegionsTestManager extends TestManager { 'geoRegionIds[]': string[]; collection?: boolean; }): Promise => { - this.response = await request(this.testApp.getHttpServer()) - .get('/api/v1/eudr/geo-features') - .query(filters) - .set('Authorization', `Bearer ${this.jwtToken}`); + this.response = await this.getRequest({ + url: '/api/v1/eudr/geo-features', + query: filters, + }); }; - WhenIRequestEUDRGeoFeatureCollection = async (filters: { + WhenIRequestEUDRGeoFeatureCollection = async (filters?: { 'geoRegionIds[]'?: string[]; 'producerIds[]'?: string[]; 'materialIds[]'?: string[]; 'originIds[]'?: string[]; }): Promise => { - this.response = await request(this.testApp.getHttpServer()) - .get('/api/v1/eudr/geo-features/collection') - .query(filters) - .set('Authorization', `Bearer ${this.jwtToken}`); + this.response = await this.getRequest({ + url: '/api/v1/eudr/geo-features/collection', + query: filters, + }); }; ThenIShouldOnlyRecieveCorrespondingGeoFeatures = ( @@ -127,4 +141,38 @@ export class GeoRegionsTestManager extends TestManager { } } }; + + ThenTheGeoFeaturesShouldHaveCorrectMetadata = async ( + sourceLocations: SourcingLocation[], + ) => { + for (const feature of this.response!.body.geojson.features) { + const sourcingLocation = sourceLocations.find( + (r: any) => r.geoRegionId === feature.properties.id, + ); + const expectedMetadataContentForEachFeature = await this.dataSource + .createQueryBuilder() + .from(SourcingRecord, 'sr') + .select('sr.tonnage', 'baselineVolume') + .addSelect('gr.id', 'id') + .addSelect('gr.name', 'plotName') + .addSelect('s.name', 'supplierName') + .innerJoin(SourcingLocation, 'sl', 'sr."sourcingLocationId" = sl.id') + .innerJoin(Supplier, 's', 'sl."producerId" = s.id') + .innerJoin(GeoRegion, 'gr', 'sl."geoRegionId" = gr.id') + .where('sr."sourcingLocationId" = :sourcingLocationId', { + sourcingLocationId: sourcingLocation!.id, + }) + .andWhere('sr.year = :year', { year: 2020 }) + .getRawOne(); + expect(sourcingLocation).toBeDefined(); + expect(feature.properties).toEqual({ + id: expectedMetadataContentForEachFeature.id, + supplierName: expectedMetadataContentForEachFeature.supplierName, + plotName: expectedMetadataContentForEachFeature.plotName, + baselineVolume: parseInt( + expectedMetadataContentForEachFeature.baselineVolume, + ), + }); + } + }; } diff --git a/api/test/e2e/geo-regions/geo-features.spec.ts b/api/test/e2e/geo-regions/geo-features.spec.ts index b7de75f96..152ede171 100644 --- a/api/test/e2e/geo-regions/geo-features.spec.ts +++ b/api/test/e2e/geo-regions/geo-features.spec.ts @@ -1,7 +1,7 @@ import { GeoRegionsTestManager } from './fixtures'; import { TestManager } from '../../utils/test-manager'; -describe('Admin Regions EUDR Filters (e2e)', () => { +describe('Geo Features tests (e2e)', () => { let testManager: GeoRegionsTestManager; beforeAll(async () => { @@ -20,51 +20,64 @@ describe('Admin Regions EUDR Filters (e2e)', () => { await testManager.close(); }); - test('should only get geo-features that are part of EUDR data', async () => { - await testManager.GivenRegularSourcingLocationsWithGeoRegions(); - const { eudrGeoRegions } = - await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); - await testManager.WhenIRequestEUDRGeoFeatures({ - 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + describe('EUDR Geo Features', () => { + test('should only get geo-features that are part of EUDR data', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatures({ + 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( + eudrGeoRegions, + ); }); - testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures(eudrGeoRegions); - }); - test('should only get geo-features that are part of EUDR data and are filtered by geo region id', async () => { - await testManager.GivenRegularSourcingLocationsWithGeoRegions(); - const { eudrGeoRegions } = - await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); - await testManager.WhenIRequestEUDRGeoFeatures({ - 'geoRegionIds[]': [eudrGeoRegions[0].id], + test('should only get geo-features that are part of EUDR data and are filtered by geo region id', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatures({ + 'geoRegionIds[]': [eudrGeoRegions[0].id], + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures([ + eudrGeoRegions[0], + ]); }); - testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures([ - eudrGeoRegions[0], - ]); - }); - test('should only get EUDR geo-features filtered by materials, suppliers and admin regions', async () => { - const {} = await testManager.GivenRegularSourcingLocationsWithGeoRegions(); - const { eudrSourcingLocations, eudrGeoRegions } = - await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); - await testManager.WhenIRequestEUDRGeoFeatureCollection({ - 'materialIds[]': [eudrSourcingLocations[0].materialId], - 'producerIds[]': [eudrSourcingLocations[0].producerId as string], - 'originIds[]': [eudrSourcingLocations[0].adminRegionId], + test('should only get EUDR geo-features filtered by materials, suppliers and admin regions', async () => { + const {} = + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrSourcingLocations, eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatureCollection({ + 'materialIds[]': [eudrSourcingLocations[0].materialId], + 'producerIds[]': [eudrSourcingLocations[0].producerId as string], + 'originIds[]': [eudrSourcingLocations[0].adminRegionId], + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( + [eudrGeoRegions[0]], + true, + ); }); - testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( - [eudrGeoRegions[0]], - true, - ); - }); - test('sould only get EUDR geo-features as a FeatureCollection and filtered by geo regions', async () => { - await testManager.GivenRegularSourcingLocationsWithGeoRegions(); - const { eudrGeoRegions } = - await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); - await testManager.WhenIRequestEUDRGeoFeatureCollection({ - 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + test('sould only get EUDR geo-features as a FeatureCollection and filtered by geo regions', async () => { + await testManager.GivenRegularSourcingLocationsWithGeoRegions(); + const { eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatureCollection({ + 'geoRegionIds[]': eudrGeoRegions.map((r) => r.id), + }); + testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( + eudrGeoRegions, + true, + ); + }); + test('each feature should include the corresponding metadata', async () => { + const { eudrSourcingLocations, eudrGeoRegions } = + await testManager.GivenEUDRSourcingLocationsWithGeoRegions(); + await testManager.WhenIRequestEUDRGeoFeatureCollection(); + await testManager.ThenTheGeoFeaturesShouldHaveCorrectMetadata( + eudrSourcingLocations, + ); }); - testManager.ThenIShouldOnlyRecieveCorrespondingGeoFeatures( - eudrGeoRegions, - true, - ); }); }); diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index bfb0a4cc0..c780046a9 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -47,7 +47,7 @@ export class TestManager { await clearTestDataFromDatabase(this.dataSource); } - async GET(options: { url: string; query?: object | string }) { + async getRequest(options: { url: string; query?: object | string }) { this.response = await request(this.testApp.getHttpServer()) .get(options.url) .query(options?.query || '') From c8d07bc86f72d049f2ee2e358dac807c3ea7b5b4 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 7 Mar 2024 14:41:52 +0300 Subject: [PATCH 041/153] internal param supplier type optional --- api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts index a1f6cd448..e255feb37 100644 --- a/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts +++ b/api/src/modules/suppliers/dto/get-supplier-by-type.dto.ts @@ -38,5 +38,6 @@ export class GetSupplierByType extends CommonFiltersDto { } export class GetSupplierEUDR extends CommonEUDRFiltersDTO { + @IsOptional() type: SUPPLIER_TYPES = SUPPLIER_TYPES.PRODUCER; } From 316a4030b3ec35729660f1eafacaffa11b38fd26 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 7 Mar 2024 17:34:57 +0300 Subject: [PATCH 042/153] wrap eudr alert dates in data prop --- api/src/modules/eudr-alerts/eudr.controller.ts | 7 +++++-- api/src/modules/eudr-alerts/eudr.repositoty.interface.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 8c0cbfcf7..9724d0885 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -158,8 +158,11 @@ export class EudrController { @Get('/dates') async getAlertDates( @Query(ValidationPipe) dto: GetEUDRAlertsDto, - ): Promise { - return this.eudrAlertsService.getDates(dto); + ): Promise<{ data: EUDRAlertDates[] }> { + const dates: EUDRAlertDates[] = await this.eudrAlertsService.getDates(dto); + return { + data: dates, + }; } @Get('/alerts') diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index eda3a10c6..9dc9f08e4 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger'; class DateValue { @ApiProperty() - value: Date | string; + value: Date; } export class EUDRAlertDates { From 817ac38854f3eea8c4d6e963244b68a092b63f39 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 8 Mar 2024 06:50:55 +0300 Subject: [PATCH 043/153] update kubectl install gh workflow --- .github/workflows/deploy-to-kubernetes.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-to-kubernetes.yml b/.github/workflows/deploy-to-kubernetes.yml index 491245a88..4d3d8e568 100644 --- a/.github/workflows/deploy-to-kubernetes.yml +++ b/.github/workflows/deploy-to-kubernetes.yml @@ -70,10 +70,10 @@ jobs: - name: Install kubectl run: | - curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/kubernetes-archive-keyring.gpg - echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list + curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o --no-tty /etc/apt/keyrings/kubernetes-apt-keyring.gpg + echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update - sudo apt-get install -y kubectl + sudo apt install -y kubectl - name: Config kubectl run: | From c378dd3d71ed95d1922f94fe32160a4578b2dd98 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 8 Mar 2024 08:09:43 +0300 Subject: [PATCH 044/153] eudr dashboard first version --- .../big-query-alerts-query.builder.ts | 58 +++++++- .../modules/eudr-alerts/alerts.repository.ts | 62 +++++++- .../dashboard/eudr-dashboard.service.ts | 132 ++++++++++++++++++ .../modules/eudr-alerts/dashboard/types.ts | 82 +++++++++++ .../modules/eudr-alerts/eudr.controller.ts | 38 +++++ api/src/modules/eudr-alerts/eudr.module.ts | 2 + .../eudr-alerts/eudr.repositoty.interface.ts | 15 ++ api/test/utils/service-mocks.ts | 6 + 8 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts create mode 100644 api/src/modules/eudr-alerts/dashboard/types.ts diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index b708a09c9..398459886 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -1,7 +1,8 @@ -import { SelectQueryBuilder } from 'typeorm'; +import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { Query } from '@google-cloud/bigquery'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { EUDRAlertsFields } from 'modules/eudr-alerts/alerts.repository'; export class BigQueryAlertsQueryBuilder { queryBuilder: SelectQueryBuilder; @@ -15,16 +16,59 @@ export class BigQueryAlertsQueryBuilder { this.dto = getAlertsDto; } + getQuery(): string { + return this.queryBuilder.getQuery(); + } + + getParameters(): ObjectLiteral { + return this.queryBuilder.getParameters(); + } + + setParameters(parameters: ObjectLiteral): this { + this.queryBuilder.setParameters(parameters); + return this; + } + + select(field: string, alias?: string): this { + this.queryBuilder.select(field, alias); + return this; + } + + getQueryBuilder(): SelectQueryBuilder { + return this.queryBuilder; + } + + groupBy(fields: string): this { + this.queryBuilder.groupBy(fields); + return this; + } + + from(table: string, alias: string): this { + this.queryBuilder.from(table, alias); + return this; + } + + addSelect(fields: string, alias?: string): this { + this.queryBuilder.addSelect(fields, alias); + return this; + } + buildQuery(): Query { if (this.dto?.supplierIds) { - this.queryBuilder.andWhere('supplierid IN (:...supplierIds)', { - supplierIds: this.dto.supplierIds, - }); + this.queryBuilder.andWhere( + `${EUDRAlertsFields.supplierId} IN (:...supplierIds)`, + { + supplierIds: this.dto.supplierIds, + }, + ); } if (this.dto?.geoRegionIds) { - this.queryBuilder.andWhere('georegionid IN (:...geoRegionIds)', { - geoRegionIds: this.dto.geoRegionIds, - }); + this.queryBuilder.andWhere( + `${EUDRAlertsFields.geoRegionId} IN (:...geoRegionIds)`, + { + geoRegionIds: this.dto.geoRegionIds, + }, + ); } if (this.dto?.alertConfidence) { this.queryBuilder.andWhere('alertConfidence = :alertConfidence', { diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index 7d6ada2c6..ed5a13d28 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -12,7 +12,9 @@ import { import { DataSource, SelectQueryBuilder } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { + EUDRAlertDatabaseResult, EUDRAlertDates, + GetAlertSummary, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; @@ -20,6 +22,15 @@ import { BigQueryAlertsQueryBuilder } from 'modules/eudr-alerts/alerts-query-bui const projectId: string = 'carto-dw-ac-zk2uhih6'; +export enum EUDRAlertsFields { + alertDate = 'alertdate', + alertConfidence = 'alertconfidence', + year = 'year', + alertCount = 'alertcount', + geoRegionId = 'georegionid', + supplierId = 'supplierid', +} + @Injectable() export class AlertsRepository implements IEUDRAlertsRepository { logger: Logger = new Logger(AlertsRepository.name); @@ -64,8 +75,52 @@ export class AlertsRepository implements IEUDRAlertsRepository { return this.query(queryBuilder, dto); } + async getAlertSummary(dto: any): Promise { + const bigQueryBuilder: BigQueryAlertsQueryBuilder = + new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); + bigQueryBuilder + .from(this.baseDataset, 'alerts') + .select('supplierid', 'supplierId') + .addSelect( + 'SUM(CASE WHEN georegionid IS NULL THEN 1 ELSE 0 END)', + 'null_geo_regions_count', + ) + .addSelect( + 'SUM(CASE WHEN alertcount = 0 THEN 1 ELSE 0 END)', + 'zero_alerts', + ) + .addSelect( + 'SUM(CASE WHEN alertcount > 0 THEN 1 ELSE 0 END)', + 'nonzero_alerts', + ) + .addSelect('COUNT(*)', 'total_geo_regions') + + .groupBy('supplierid'); + const mainQueryBuilder: BigQueryAlertsQueryBuilder = + new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder()); + + mainQueryBuilder + .select('supplierid') + .addSelect( + '(CAST(zero_alerts AS FLOAT64) / NULLIF(total_geo_regions, 0)) * 100', + 'dfs', + ) + .addSelect( + '(CAST(nonzero_alerts AS FLOAT64) / NULLIF(total_geo_regions, 0)) * 100', + 'sda', + ) + .addSelect( + '(CAST(null_geo_regions_count AS FLOAT64) / NULLIF(total_geo_regions, 0)) * 100', + 'tpl', + ) + .from('(' + bigQueryBuilder.getQuery() + ')', 'alerts_summary') + .setParameters(bigQueryBuilder.getParameters()); + + return this.query(mainQueryBuilder); + } + private async query( - queryBuilder: SelectQueryBuilder, + queryBuilder: SelectQueryBuilder | BigQueryAlertsQueryBuilder, dto?: GetEUDRAlertsDto, ): Promise { try { @@ -86,9 +141,12 @@ export class AlertsRepository implements IEUDRAlertsRepository { } private buildQuery( - queryBuilder: SelectQueryBuilder, + queryBuilder: SelectQueryBuilder | BigQueryAlertsQueryBuilder, dto?: GetEUDRAlertsDto, ): Query { + if (queryBuilder instanceof BigQueryAlertsQueryBuilder) { + return queryBuilder.buildQuery(); + } const alertsQueryBuilder: BigQueryAlertsQueryBuilder = new BigQueryAlertsQueryBuilder(queryBuilder, dto); diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts new file mode 100644 index 000000000..6df217c85 --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -0,0 +1,132 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, SelectQueryBuilder } from 'typeorm'; +import { + EUDRAlertDatabaseResult, + IEUDRAlertsRepository, +} from '../eudr.repositoty.interface'; +import { SourcingLocation } from '../../sourcing-locations/sourcing-location.entity'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +import { SourcingRecord } from 'modules/sourcing-records/sourcing-record.entity'; +import { Material } from 'modules/materials/material.entity'; +import { GetDashBoardDTO } from '../eudr.controller'; + +type Entities = { + supplierId: string; + supplierName: string; + companyId: string; + adminRegionId: string; + adminRegionName: string; + materialId: string; + materialName: string; + totalBaselineVolume: number; +}; + +@Injectable() +export class EudrDashboardService { + constructor( + @Inject('IEUDRAlertsRepository') + private readonly eudrRepository: IEUDRAlertsRepository, + private readonly datasource: DataSource, + ) {} + + async buildDashboard(dto: GetDashBoardDTO): Promise { + const alertSummary: EUDRAlertDatabaseResult[] = + await this.eudrRepository.getAlertSummary({ + alertStartDate: dto.startAlertDate, + alertEnDate: dto.endAlertDate, + supplierIds: dto.producerIds, + }); + + const transformed: any = alertSummary.reduce( + (acc: any, cur: EUDRAlertDatabaseResult) => { + acc[cur.supplierid] = { + supplierid: cur.supplierid, + dfs: cur.dfs, + tpl: cur.tpl, + sda: cur.sda, + }; + return acc; + }, + {}, + ); + + const entities: Entities[] = await this.getEntities(dto); + + const transformedEntities: any = entities.reduce( + (acc: any, cur: Entities) => { + acc[cur.supplierId] = { ...cur }; + return acc; + }, + {}, + ); + + const materialsBySupplier = new Map(); + const originsBySupplier = new Map(); + + entities.forEach((entity) => { + if (!materialsBySupplier.has(entity.supplierId)) { + materialsBySupplier.set(entity.supplierId, []); + originsBySupplier.set(entity.supplierId, []); + } + + materialsBySupplier.get(entity.supplierId).push({ + materialName: entity.materialName, + id: entity.materialId, + }); + + originsBySupplier.get(entity.supplierId).push({ + originName: entity.adminRegionName, + id: entity.adminRegionId, + }); + }); + + const result: any = Object.keys(transformed).map((key: string) => { + return { + supplierId: key, + supplierName: transformedEntities[key].supplierName, + companyId: transformedEntities[key].companyId, + baselineVolume: transformedEntities[key].totalBaselineVolume, + dfs: transformed[key].dfs, + sda: transformed[key].sda, + tpl: transformed[key].tpl, + materials: materialsBySupplier.get(key) || [], + origins: originsBySupplier.get(key) || [], + }; + }); + return { + table: result, + breakDown: {}, + }; + } + + async getEntities(dto: any): Promise { + const queryBuilder: SelectQueryBuilder = + this.datasource.createQueryBuilder(); + queryBuilder + .select('s.id', 'supplierId') + .addSelect('s.name', 'supplierName') + .addSelect('s.companyId', 'companyId') + .addSelect('m.id', 'materialId') + .addSelect('m.name', 'materialName') + .addSelect('ar.id', 'adminRegionId') + .addSelect('ar.name', 'adminRegionName') + .addSelect('SUM(sr.tonnage)', 'totalBaselineVolume') + .from(SourcingLocation, 'sl') + .leftJoin(Supplier, 's', 's.id = sl.producerId') + .leftJoin(AdminRegion, 'ar', 'ar.id = sl.adminRegionId') + .leftJoin(Material, 'm', 'm.id = sl.materialId') + .leftJoin(SourcingRecord, 'sr', 'sr.sourcingLocationId = sl.id') + .where('sr.year = :year', { year: 2020 }) + .groupBy('s.id') + .addGroupBy('m.id') + .addGroupBy('ar.id'); + if (dto.producerIds) { + queryBuilder.andWhere('s.id IN (:...producerIds)', { + producerIds: dto.producerIds, + }); + } + + return queryBuilder.getRawMany(); + } +} diff --git a/api/src/modules/eudr-alerts/dashboard/types.ts b/api/src/modules/eudr-alerts/dashboard/types.ts new file mode 100644 index 000000000..f9e5cc609 --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/types.ts @@ -0,0 +1,82 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class EUDRDashboard { + @ApiProperty({ type: () => DashBoardTableElements, isArray: true }) + table: DashBoardTableElements[]; + + @ApiProperty({ description: 'Not available yet' }) + breakDown: any; +} + +export class DashBoardTableElements { + @ApiProperty() + supplierName: string; + + // rename to companyCode? + @ApiProperty() + companyId: string; + + // EUDR baseline volume, purchase of 2020, accumulate of all the comodditie + @ApiProperty() + baselineVolume: number; + + // percentage of deforestation free plots for each supplier. + @ApiProperty() + dfs: number; + + @ApiProperty() + sda: number; + + @ApiProperty() + tpl: number; + + @ApiProperty({ type: () => EUDRAlertMaterial, isArray: true }) + materials: EUDRAlertMaterial[]; + + @ApiProperty({ type: () => EUDRAlertOrigin, isArray: true }) + origins: EUDRAlertOrigin[]; +} + +export class EUDRAlertMaterial { + @ApiProperty() + materialName: string; + + @ApiProperty() + id: string; +} + +export class EUDRAlertOrigin { + @ApiProperty() + originName: string; + + @ApiProperty() + id: string; +} + +export class EUDRBreakDown { + materials: BreakDownMaterialCategory; + + origins: any; +} + +export class BreakDownMaterialCategory { + category: 'material'; + + detail: BreakDownSupplierDetail[]; +} + +export class BreakDownSupplierDetail { + // Deforestation-free, partial-deforestation, etc + name: string; + value: string; + + data: BreakDownByMaterial[]; +} + +export class BreakDownByMaterial { + // material or origin name + name: string; + + // percentaje + value: number; +} diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 9724d0885..a1b758a50 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -9,6 +9,7 @@ import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, + ApiPropertyOptional, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; @@ -39,12 +40,37 @@ import { GeoFeatureCollectionResponse, GeoFeatureResponse, } from 'modules/geo-regions/dto/geo-feature-response.dto'; +import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; +import { CommonEUDRFiltersDTO } from '../../utils/base.query-builder'; +import { IsDate, IsOptional, IsUUID } from 'class-validator'; +import { Type } from 'class-transformer'; +import { EUDRDashboard } from './dashboard/types'; + +export class GetDashBoardDTO { + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + startAlertDate: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + endAlertDate: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + producerIds: string[]; +} @ApiTags('EUDR') @Controller('/api/v1/eudr') export class EudrController { constructor( private readonly eudrAlertsService: EudrService, + private readonly dashboard: EudrDashboardService, private readonly suppliersService: SuppliersService, private readonly materialsService: MaterialsService, private readonly geoRegionsService: GeoRegionsService, @@ -199,4 +225,16 @@ export class EudrController { collection: true, }); } + + @ApiOperation({ description: 'Get EUDR Dashboard' }) + @ApiOkResponse({ type: EUDRDashboard }) + @Get('/dashboard') + async getDashboard( + @Query(ValidationPipe) dto: GetDashBoardDTO, + ): Promise<{ data: EUDRDashboard }> { + const dashboard: EUDRDashboard = await this.dashboard.buildDashboard(dto); + return { + data: dashboard, + }; + } } diff --git a/api/src/modules/eudr-alerts/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts index 9c7692615..099b8040a 100644 --- a/api/src/modules/eudr-alerts/eudr.module.ts +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -8,6 +8,7 @@ import { GeoRegionsModule } from 'modules/geo-regions/geo-regions.module'; import { AdminRegionsModule } from 'modules/admin-regions/admin-regions.module'; import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; import { AppConfig } from 'utils/app.config'; +import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; export const IEUDRAlertsRepositoryToken: symbol = Symbol( 'IEUDRAlertsRepository', @@ -31,6 +32,7 @@ const { credentials, dataset } = AppConfig.get<{ ], providers: [ EudrService, + EudrDashboardService, { provide: 'IEUDRAlertsRepository', useClass: AlertsRepository }, { provide: 'EUDRDataset', useValue: dataset }, { provide: 'EUDRCredentials', useValue: credentials }, diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index 9dc9f08e4..07b21bc3d 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -12,8 +12,23 @@ export class EUDRAlertDates { alertDate: DateValue; } +export interface EUDRAlertDatabaseResult { + supplierid: string; + tpl: number; + dfs: number; + sda: number; +} + +export type GetAlertSummary = { + alertStartDate?: Date; + alertEnDate?: Date; + supplierIds?: string[]; +}; + export interface IEUDRAlertsRepository { getAlerts(dto?: GetEUDRAlertsDto): Promise; getDates(dto: GetEUDRAlertsDto): Promise; + + getAlertSummary(dto: GetAlertSummary): Promise; } diff --git a/api/test/utils/service-mocks.ts b/api/test/utils/service-mocks.ts index 45cc6dc31..3df85a9fb 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -4,7 +4,9 @@ import { } from '../../src/modules/notifications/email/email.service.interface'; import { Logger } from '@nestjs/common'; import { + EUDRAlertDatabaseResult, EUDRAlertDates, + GetAlertSummary, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; @@ -32,4 +34,8 @@ export class MockAlertRepository implements IEUDRAlertsRepository { resolve([]); }); } + + getAlertSummary(dto: GetAlertSummary): Promise { + return Promise.resolve([]); + } } From 8821d95f76e44d7f089957206a39ea2fe279628d Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 8 Mar 2024 13:53:18 +0300 Subject: [PATCH 045/153] add filters for dashboard --- .../dashboard/eudr-dashboard.service.ts | 22 +++++++++++++++++-- .../modules/eudr-alerts/eudr.controller.ts | 16 +++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 6df217c85..03d24ad43 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource, SelectQueryBuilder } from 'typeorm'; import { EUDRAlertDatabaseResult, @@ -52,6 +52,9 @@ export class EudrDashboardService { ); const entities: Entities[] = await this.getEntities(dto); + if (!entities) { + throw new NotFoundException('Could not retrive data'); + } const transformedEntities: any = entities.reduce( (acc: any, cur: Entities) => { @@ -100,7 +103,7 @@ export class EudrDashboardService { }; } - async getEntities(dto: any): Promise { + async getEntities(dto: GetDashBoardDTO): Promise { const queryBuilder: SelectQueryBuilder = this.datasource.createQueryBuilder(); queryBuilder @@ -126,6 +129,21 @@ export class EudrDashboardService { producerIds: dto.producerIds, }); } + if (dto.materialIds) { + queryBuilder.andWhere('m.id IN (:...materialIds)', { + materialIds: dto.materialIds, + }); + } + if (dto.originIds) { + queryBuilder.andWhere('ar.id IN (:...originIds)', { + originIds: dto.originIds, + }); + } + if (dto.geoRegionIds) { + queryBuilder.andWhere('sl.geoRegionId IN (:...geoRegionIds)', { + geoRegionIds: dto.geoRegionIds, + }); + } return queryBuilder.getRawMany(); } diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index a1b758a50..a500019a3 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -41,7 +41,6 @@ import { GeoFeatureResponse, } from 'modules/geo-regions/dto/geo-feature-response.dto'; import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; -import { CommonEUDRFiltersDTO } from '../../utils/base.query-builder'; import { IsDate, IsOptional, IsUUID } from 'class-validator'; import { Type } from 'class-transformer'; import { EUDRDashboard } from './dashboard/types'; @@ -63,6 +62,21 @@ export class GetDashBoardDTO { @IsOptional() @IsUUID('4', { each: true }) producerIds: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + materialIds: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + originIds: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsUUID('4', { each: true }) + geoRegionIds: string[]; } @ApiTags('EUDR') From acbec43bb58e5d6a361120cf6d7f6fc93736554f Mon Sep 17 00:00:00 2001 From: alexeh Date: Sat, 9 Mar 2024 08:41:45 +0300 Subject: [PATCH 046/153] First working dashboard breakdown version --- .../dashboard/eudr-dashboard.service.ts | 182 +++++++++++++++++- 1 file changed, 177 insertions(+), 5 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 03d24ad43..9052705f6 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,3 +1,7 @@ +// supress typescript error +// eslint-disable-next-line @typescript-eslint/ban-types +// export type Type = new (...args: any[]) => T; +// @ts-ignore-file import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource, SelectQueryBuilder } from 'typeorm'; import { @@ -10,6 +14,7 @@ import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { SourcingRecord } from 'modules/sourcing-records/sourcing-record.entity'; import { Material } from 'modules/materials/material.entity'; import { GetDashBoardDTO } from '../eudr.controller'; +import { cloneDeep } from 'lodash'; type Entities = { supplierId: string; @@ -20,6 +25,7 @@ type Entities = { materialId: string; materialName: string; totalBaselineVolume: number; + geoRegionCount: number; }; @Injectable() @@ -37,7 +43,25 @@ export class EudrDashboardService { alertEnDate: dto.endAlertDate, supplierIds: dto.producerIds, }); + const entities: Entities[] = await this.getEntities(dto); + if (!entities) { + throw new NotFoundException('Could not retrive data'); + } + + const alertData: EUDRAlertDatabaseResult[] = cloneDeep(alertSummary); + const sourcingData: Entities[] = cloneDeep(entities); + + const materials: any = { + 'Deforestation-free suppliers': { totalPercentage: 0, detail: [] }, + 'Suppliers with deforestation alerts': { totalPercentage: 0, detail: [] }, + 'Suppliers with no location data': { totalPercentage: 0, detail: [] }, + }; + const origins: any = { + 'Deforestation-free suppliers': { totalPercentage: 0, detail: [] }, + 'Suppliers with deforestation alerts': { totalPercentage: 0, detail: [] }, + 'Suppliers with no location data': { totalPercentage: 0, detail: [] }, + }; const transformed: any = alertSummary.reduce( (acc: any, cur: EUDRAlertDatabaseResult) => { acc[cur.supplierid] = { @@ -51,10 +75,157 @@ export class EudrDashboardService { {}, ); - const entities: Entities[] = await this.getEntities(dto); - if (!entities) { - throw new NotFoundException('Could not retrive data'); - } + const materialSuppliersMap: Map = new Map(); + const originSuppliersMap: Map = new Map(); + + sourcingData.forEach( + ({ + supplierId, + materialId, + materialName, + geoRegionCount, + adminRegionId, + adminRegionName, + }) => { + if (!materialSuppliersMap.has(materialId)) { + materialSuppliersMap.set(materialId, { + materialName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + if (!originSuppliersMap.has(adminRegionId)) { + originSuppliersMap.set(adminRegionId, { + originName: adminRegionName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + const material: any = materialSuppliersMap.get(materialId); + const origin: any = originSuppliersMap.get(adminRegionId); + material.suppliers.add(supplierId); + origin.suppliers.add(supplierId); + if (geoRegionCount === 0) { + material.zeroGeoRegionSuppliers += 1; + origin.zeroGeoRegionSuppliers += 1; + } + if (transformed[supplierId].dfs > 0) { + material.dfsSuppliers += 1; + origin.dfsSuppliers += 1; + } + if (transformed[supplierId].sda > 0) { + material.sdaSuppliers += 1; + origin.sdaSuppliers += 1; + } + if (transformed[supplierId].tpl > 0) { + material.tplSuppliers += 1; + origin.tplSuppliers += 1; + } + }, + ); + materialSuppliersMap.forEach( + ( + { + materialName, + suppliers, + zeroGeoRegionSuppliers, + dfsSuppliers, + sdaSuppliers, + tplSuppliers, + }, + materialId: string, + ) => { + const noLocationPercentage: number = + (zeroGeoRegionSuppliers / suppliers.size) * 100; + materials['Suppliers with no location data'].detail.push({ + name: materialName, + value: noLocationPercentage, + }); + const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; + materials['Deforestation-free suppliers'].detail.push({ + name: materialName, + value: dfsPercentage, + }); + const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; + materials['Suppliers with deforestation alerts'].detail.push({ + name: materialName, + value: sdaPercentage, + }); + }, + ); + + originSuppliersMap.forEach( + ( + { + originName, + suppliers, + zeroGeoRegionSuppliers, + dfsSuppliers, + sdaSuppliers, + tplSuppliers, + }, + adminRegionId: string, + ) => { + const noLocationPercentage: number = + (zeroGeoRegionSuppliers / suppliers.size) * 100; + origins['Suppliers with no location data'].detail.push({ + name: originName, + value: noLocationPercentage, + }); + const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; + origins['Deforestation-free suppliers'].detail.push({ + name: originName, + value: dfsPercentage, + }); + const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; + origins['Suppliers with deforestation alerts'].detail.push({ + name: originName, + value: sdaPercentage, + }); + }, + ); + + materials['Suppliers with no location data'].totalPercentage = + materials['Suppliers with no location data'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / materials['Suppliers with no location data'].detail.length; + + materials['Deforestation-free suppliers'].totalPercentage = + materials['Deforestation-free suppliers'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / materials['Deforestation-free suppliers'].detail.length; + + materials['Suppliers with deforestation alerts'].totalPercentage = + materials['Suppliers with deforestation alerts'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / materials['Suppliers with deforestation alerts'].detail.length; + + origins['Suppliers with no location data'].totalPercentage = + origins['Suppliers with no location data'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / origins['Suppliers with no location data'].detail.length; + + origins['Deforestation-free suppliers'].totalPercentage = + origins['Deforestation-free suppliers'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / origins['Deforestation-free suppliers'].detail.length; + + origins['Suppliers with deforestation alerts'].totalPercentage = + origins['Suppliers with deforestation alerts'].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / origins['Suppliers with deforestation alerts'].detail.length; const transformedEntities: any = entities.reduce( (acc: any, cur: Entities) => { @@ -99,7 +270,7 @@ export class EudrDashboardService { }); return { table: result, - breakDown: {}, + breakDown: { materials, origins }, }; } @@ -115,6 +286,7 @@ export class EudrDashboardService { .addSelect('ar.id', 'adminRegionId') .addSelect('ar.name', 'adminRegionName') .addSelect('SUM(sr.tonnage)', 'totalBaselineVolume') + .addSelect('COUNT(sl.geoRegionId)', 'geoRegionsCount') .from(SourcingLocation, 'sl') .leftJoin(Supplier, 's', 's.id = sl.producerId') .leftJoin(AdminRegion, 'ar', 'ar.id = sl.adminRegionId') From 6170749982c4ba95d2ed59c33a1a9ee4c127c54f Mon Sep 17 00:00:00 2001 From: alexeh Date: Sun, 10 Mar 2024 08:58:07 +0300 Subject: [PATCH 047/153] stronger type safety --- .../eudr-dashboard-breakdown.builder.ts | 148 +++++++ .../dashboard/eudr-dashboard.service.ts | 409 +++++++++++------- .../modules/eudr-alerts/dashboard/types.ts | 86 ++-- 3 files changed, 441 insertions(+), 202 deletions(-) create mode 100644 api/src/modules/eudr-alerts/dashboard/eudr-dashboard-breakdown.builder.ts diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard-breakdown.builder.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard-breakdown.builder.ts new file mode 100644 index 000000000..1e4f0e5cb --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard-breakdown.builder.ts @@ -0,0 +1,148 @@ +type Entities = { + supplierId: string; + supplierName: string; + companyId: string; + adminRegionId: string; + adminRegionName: string; + materialId: string; + materialName: string; + totalBaselineVolume: number; + geoRegionCount: number; +}; + +export class EUDRBreakDown { + public materials: { + 'Deforestation-free suppliers': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + 'Suppliers with deforestation alerts': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + 'Suppliers with no location data': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + }; + + public origins: { + 'Deforestation-free suppliers': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + 'Suppliers with deforestation alerts': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + 'Suppliers with no location data': { + totalPercentage: number; + detail: { name: string; value: number }[]; + }; + }; + + soucingData: any[]; + alertData: any; + + materialsBySupplier: Map = new Map(); + originsBySupplier: Map = new Map(); + materialSuppliersMap: Map = new Map(); + originSuppliersMap: Map = new Map(); + + sourcingDataTransformedMap: Map = new Map(); + + constructor(sourcingData: any, alertData: any) { + this.soucingData = sourcingData; + this.alertData = alertData; + this.buildMaps(); + } + + buildMaps(): void { + this.soucingData.forEach((entity: Entities) => { + const { + supplierId, + materialId, + materialName, + geoRegionCount, + adminRegionId, + adminRegionName, + } = entity; + if (!this.materialsBySupplier.has(supplierId)) { + this.materialsBySupplier.set(supplierId, []); + this.originsBySupplier.set(supplierId, []); + } + + this.materialsBySupplier.get(supplierId).push({ + materialName: materialName, + id: materialId, + }); + + this.originsBySupplier.get(supplierId).push({ + originName: adminRegionName, + id: adminRegionId, + }); + if (!this.materialSuppliersMap.has(materialId)) { + this.materialSuppliersMap.set(materialId, { + materialName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + if (!this.originSuppliersMap.has(adminRegionId)) { + this.originSuppliersMap.set(adminRegionId, { + originName: adminRegionName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + const material: { + materialName: string; + suppliers: Set; + zeroGeoRegionSuppliers: number; + dfsSuppliers: number; + sdaSuppliers: number; + tplSuppliers: number; + } = this.materialSuppliersMap.get(materialId); + const origin: { + originName: string; + suppliers: Set; + zeroGeoRegionSuppliers: number; + dfsSuppliers: number; + sdaSuppliers: number; + tplSuppliers: number; + } = this.originSuppliersMap.get(adminRegionId); + material.suppliers.add(supplierId); + origin.suppliers.add(supplierId); + if (geoRegionCount === 0) { + material.zeroGeoRegionSuppliers += 1; + origin.zeroGeoRegionSuppliers += 1; + } + if (this.alertData[supplierId].dfs > 0) { + material.dfsSuppliers += 1; + origin.dfsSuppliers += 1; + } + if (this.alertData[supplierId].sda > 0) { + material.sdaSuppliers += 1; + origin.sdaSuppliers += 1; + } + if (this.alertData[supplierId].tpl > 0) { + material.tplSuppliers += 1; + origin.tplSuppliers += 1; + } + }); + } + + getmaterialSuppliersMap(): Map { + return this.materialSuppliersMap; + } + + getoriginSuppliersMap(): Map { + return this.originSuppliersMap; + } +} diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 9052705f6..4080f653a 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,32 +1,24 @@ // supress typescript error // eslint-disable-next-line @typescript-eslint/ban-types -// export type Type = new (...args: any[]) => T; -// @ts-ignore-file import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource, SelectQueryBuilder } from 'typeorm'; import { EUDRAlertDatabaseResult, IEUDRAlertsRepository, -} from '../eudr.repositoty.interface'; -import { SourcingLocation } from '../../sourcing-locations/sourcing-location.entity'; +} from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; import { Supplier } from 'modules/suppliers/supplier.entity'; import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; import { SourcingRecord } from 'modules/sourcing-records/sourcing-record.entity'; import { Material } from 'modules/materials/material.entity'; -import { GetDashBoardDTO } from '../eudr.controller'; -import { cloneDeep } from 'lodash'; - -type Entities = { - supplierId: string; - supplierName: string; - companyId: string; - adminRegionId: string; - adminRegionName: string; - materialId: string; - materialName: string; - totalBaselineVolume: number; - geoRegionCount: number; -}; +import { GetDashBoardDTO } from 'modules/eudr-alerts/eudr.controller'; +import { + DashBoardTableElements, + EntityMetadata, + EUDRBreakDown, + EUDRDashboard, + EUDRDashBoardFields, +} from 'modules/eudr-alerts/dashboard/types'; @Injectable() export class EudrDashboardService { @@ -36,100 +28,181 @@ export class EudrDashboardService { private readonly datasource: DataSource, ) {} - async buildDashboard(dto: GetDashBoardDTO): Promise { + async buildDashboard(dto: GetDashBoardDTO): Promise { const alertSummary: EUDRAlertDatabaseResult[] = await this.eudrRepository.getAlertSummary({ alertStartDate: dto.startAlertDate, alertEnDate: dto.endAlertDate, supplierIds: dto.producerIds, }); - const entities: Entities[] = await this.getEntities(dto); - if (!entities) { - throw new NotFoundException('Could not retrive data'); + const entityMetadata: EntityMetadata[] = await this.getEntityMetadata(dto); + if (!entityMetadata.length) { + throw new NotFoundException( + 'Could not retrieve EUDR Data. Please contact the administrator', + ); } - - const alertData: EUDRAlertDatabaseResult[] = cloneDeep(alertSummary); - const sourcingData: Entities[] = cloneDeep(entities); - - const materials: any = { - 'Deforestation-free suppliers': { totalPercentage: 0, detail: [] }, - 'Suppliers with deforestation alerts': { totalPercentage: 0, detail: [] }, - 'Suppliers with no location data': { totalPercentage: 0, detail: [] }, + const materials: Record< + EUDRDashBoardFields, + { totalPercentage: number; detail: any[] } + > = { + [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: { + totalPercentage: 0, + detail: [], + }, + [EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS]: { + totalPercentage: 0, + detail: [], + }, + [EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA]: { + totalPercentage: 0, + detail: [], + }, }; - const origins: any = { - 'Deforestation-free suppliers': { totalPercentage: 0, detail: [] }, - 'Suppliers with deforestation alerts': { totalPercentage: 0, detail: [] }, - 'Suppliers with no location data': { totalPercentage: 0, detail: [] }, - }; - const transformed: any = alertSummary.reduce( - (acc: any, cur: EUDRAlertDatabaseResult) => { - acc[cur.supplierid] = { - supplierid: cur.supplierid, - dfs: cur.dfs, - tpl: cur.tpl, - sda: cur.sda, - }; - return acc; + const origins: Record< + EUDRDashBoardFields, + { totalPercentage: number; detail: any[] } + > = { + [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: { + totalPercentage: 0, + detail: [], }, - {}, - ); + [EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS]: { + totalPercentage: 0, + detail: [], + }, + [EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA]: { + totalPercentage: 0, + detail: [], + }, + }; + const alertMap: { + [key: string]: { + supplierId: string; + dfs: number; + tpl: number; + sda: number; + }; + } = alertSummary.reduce((acc: any, cur: EUDRAlertDatabaseResult) => { + acc[cur.supplierid] = { + supplierid: cur.supplierid, + dfs: cur.dfs, + tpl: cur.tpl, + sda: cur.sda, + }; + return acc; + }, {}); - const materialSuppliersMap: Map = new Map(); - const originSuppliersMap: Map = new Map(); + const entityMap: { + [key: string]: { + supplierId: string; + supplierName: string; + companyId: string; + adminRegionId: string; + adminRegionName: string; + materialId: string; + materialName: string; + totalBaselineVolume: number; + geoRegionCount: number; + }; + } = entityMetadata.reduce((acc: any, cur: EntityMetadata) => { + acc[cur.supplierId] = { ...cur }; + return acc; + }, {}); - sourcingData.forEach( - ({ - supplierId, + const supplierToMaterials: Map = + new Map(); + const supplierToOriginis: Map = + new Map(); + const materialMap: Map< + string, + { + materialName: string; + suppliers: Set; + zeroGeoRegionSuppliers: number; + dfsSuppliers: number; + sdaSuppliers: number; + tplSuppliers: number; + } + > = new Map(); + const originMap: Map< + string, + { + originName: string; + suppliers: Set; + zeroGeoRegionSuppliers: number; + dfsSuppliers: number; + sdaSuppliers: number; + tplSuppliers: number; + } + > = new Map(); + + entityMetadata.forEach((entity: EntityMetadata) => { + const { materialId, - materialName, - geoRegionCount, + supplierId, adminRegionId, + materialName, adminRegionName, - }) => { - if (!materialSuppliersMap.has(materialId)) { - materialSuppliersMap.set(materialId, { - materialName, - suppliers: new Set(), - zeroGeoRegionSuppliers: 0, - dfsSuppliers: 0, - sdaSuppliers: 0, - tplSuppliers: 0, - }); - } - if (!originSuppliersMap.has(adminRegionId)) { - originSuppliersMap.set(adminRegionId, { - originName: adminRegionName, - suppliers: new Set(), - zeroGeoRegionSuppliers: 0, - dfsSuppliers: 0, - sdaSuppliers: 0, - tplSuppliers: 0, - }); - } - const material: any = materialSuppliersMap.get(materialId); - const origin: any = originSuppliersMap.get(adminRegionId); - material.suppliers.add(supplierId); - origin.suppliers.add(supplierId); - if (geoRegionCount === 0) { - material.zeroGeoRegionSuppliers += 1; - origin.zeroGeoRegionSuppliers += 1; - } - if (transformed[supplierId].dfs > 0) { - material.dfsSuppliers += 1; - origin.dfsSuppliers += 1; - } - if (transformed[supplierId].sda > 0) { - material.sdaSuppliers += 1; - origin.sdaSuppliers += 1; - } - if (transformed[supplierId].tpl > 0) { - material.tplSuppliers += 1; - origin.tplSuppliers += 1; - } - }, - ); - materialSuppliersMap.forEach( + geoRegionCount, + } = entity; + if (!supplierToMaterials.has(entity.supplierId)) { + supplierToMaterials.set(entity.supplierId, []); + supplierToOriginis.set(entity.supplierId, []); + } + if (!materialMap.has(materialId)) { + materialMap.set(materialId, { + materialName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + if (!originMap.has(adminRegionId)) { + originMap.set(adminRegionId, { + originName: adminRegionName, + suppliers: new Set(), + zeroGeoRegionSuppliers: 0, + dfsSuppliers: 0, + sdaSuppliers: 0, + tplSuppliers: 0, + }); + } + const material: any = materialMap.get(materialId); + const origin: any = originMap.get(adminRegionId); + material.suppliers.add(supplierId); + origin.suppliers.add(supplierId); + if (geoRegionCount === 0) { + material.zeroGeoRegionSuppliers += 1; + origin.zeroGeoRegionSuppliers += 1; + } + if (alertMap[supplierId].dfs > 0) { + material.dfsSuppliers += 1; + origin.dfsSuppliers += 1; + } + if (alertMap[supplierId].sda > 0) { + material.sdaSuppliers += 1; + origin.sdaSuppliers += 1; + } + if (alertMap[supplierId].tpl > 0) { + material.tplSuppliers += 1; + origin.tplSuppliers += 1; + } + + supplierToMaterials.get(entity.supplierId)!.push({ + name: entity.materialName, + id: entity.materialId, + }); + + supplierToOriginis.get(entity.supplierId)!.push({ + name: entity.adminRegionName, + id: entity.adminRegionId, + }); + }); + + materialMap.forEach( ( { materialName, @@ -160,7 +233,7 @@ export class EudrDashboardService { }, ); - originSuppliersMap.forEach( + originMap.forEach( ( { originName, @@ -174,107 +247,107 @@ export class EudrDashboardService { ) => { const noLocationPercentage: number = (zeroGeoRegionSuppliers / suppliers.size) * 100; - origins['Suppliers with no location data'].detail.push({ + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].detail.push({ name: originName, value: noLocationPercentage, }); const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; - origins['Deforestation-free suppliers'].detail.push({ + origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ name: originName, value: dfsPercentage, }); const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; - origins['Suppliers with deforestation alerts'].detail.push({ + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.push({ name: originName, value: sdaPercentage, }); }, ); - materials['Suppliers with no location data'].totalPercentage = - materials['Suppliers with no location data'].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / materials['Suppliers with no location data'].detail.length; + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].totalPercentage = + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / + materials[EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA].detail + .length; - materials['Deforestation-free suppliers'].totalPercentage = - materials['Deforestation-free suppliers'].detail.reduce( + materials[ + EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS + ].totalPercentage = + materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.reduce( (acc: number, cur: any) => acc + cur.value, 0, - ) / materials['Deforestation-free suppliers'].detail.length; + ) / + materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.length; - materials['Suppliers with deforestation alerts'].totalPercentage = - materials['Suppliers with deforestation alerts'].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / materials['Suppliers with deforestation alerts'].detail.length; + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].totalPercentage = + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / + materials[EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS].detail + .length; - origins['Suppliers with no location data'].totalPercentage = - origins['Suppliers with no location data'].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / origins['Suppliers with no location data'].detail.length; + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].totalPercentage = + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / + origins[EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA].detail + .length; - origins['Deforestation-free suppliers'].totalPercentage = - origins['Deforestation-free suppliers'].detail.reduce( + origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].totalPercentage = + origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.reduce( (acc: number, cur: any) => acc + cur.value, 0, - ) / origins['Deforestation-free suppliers'].detail.length; + ) / + origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.length; - origins['Suppliers with deforestation alerts'].totalPercentage = - origins['Suppliers with deforestation alerts'].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / origins['Suppliers with deforestation alerts'].detail.length; + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].totalPercentage = + origins[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / + origins[EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS].detail + .length; - const transformedEntities: any = entities.reduce( - (acc: any, cur: Entities) => { - acc[cur.supplierId] = { ...cur }; - return acc; + const table: DashBoardTableElements[] = Object.keys(alertMap).map( + (key: string) => { + return { + supplierId: key, + supplierName: entityMap[key].supplierName, + companyId: entityMap[key].companyId, + baselineVolume: entityMap[key].totalBaselineVolume, + dfs: alertMap[key].dfs, + sda: alertMap[key].sda, + tpl: alertMap[key].tpl, + materials: supplierToMaterials.get(key) || [], + origins: supplierToOriginis.get(key) || [], + }; }, - {}, ); - const materialsBySupplier = new Map(); - const originsBySupplier = new Map(); - - entities.forEach((entity) => { - if (!materialsBySupplier.has(entity.supplierId)) { - materialsBySupplier.set(entity.supplierId, []); - originsBySupplier.set(entity.supplierId, []); - } - - materialsBySupplier.get(entity.supplierId).push({ - materialName: entity.materialName, - id: entity.materialId, - }); - - originsBySupplier.get(entity.supplierId).push({ - originName: entity.adminRegionName, - id: entity.adminRegionId, - }); - }); - - const result: any = Object.keys(transformed).map((key: string) => { - return { - supplierId: key, - supplierName: transformedEntities[key].supplierName, - companyId: transformedEntities[key].companyId, - baselineVolume: transformedEntities[key].totalBaselineVolume, - dfs: transformed[key].dfs, - sda: transformed[key].sda, - tpl: transformed[key].tpl, - materials: materialsBySupplier.get(key) || [], - origins: originsBySupplier.get(key) || [], - }; - }); return { - table: result, - breakDown: { materials, origins }, + table: table, + breakDown: { materials, origins } as EUDRBreakDown, }; } - async getEntities(dto: GetDashBoardDTO): Promise { + /** + * @description: Retrieves entity related data with some Ids, so we can relate it to the data retrieved from Carto, and add the + * corresponding names to show in the dashboard. + */ + + async getEntityMetadata(dto: GetDashBoardDTO): Promise { const queryBuilder: SelectQueryBuilder = this.datasource.createQueryBuilder(); queryBuilder diff --git a/api/src/modules/eudr-alerts/dashboard/types.ts b/api/src/modules/eudr-alerts/dashboard/types.ts index f9e5cc609..ef6c184c4 100644 --- a/api/src/modules/eudr-alerts/dashboard/types.ts +++ b/api/src/modules/eudr-alerts/dashboard/types.ts @@ -1,14 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -export class EUDRDashboard { - @ApiProperty({ type: () => DashBoardTableElements, isArray: true }) - table: DashBoardTableElements[]; - - @ApiProperty({ description: 'Not available yet' }) - breakDown: any; -} - export class DashBoardTableElements { + supplierId: string; @ApiProperty() supplierName: string; @@ -30,53 +23,78 @@ export class DashBoardTableElements { @ApiProperty() tpl: number; - @ApiProperty({ type: () => EUDRAlertMaterial, isArray: true }) - materials: EUDRAlertMaterial[]; + @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) + materials: EntitiesBySupplier[]; - @ApiProperty({ type: () => EUDRAlertOrigin, isArray: true }) - origins: EUDRAlertOrigin[]; + @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) + origins: EntitiesBySupplier[]; } -export class EUDRAlertMaterial { +class EntitiesBySupplier { @ApiProperty() - materialName: string; + name: string; @ApiProperty() id: string; } -export class EUDRAlertOrigin { - @ApiProperty() - originName: string; +export enum EUDRDashBoardFields { + DEFORASTATION_FREE_SUPPLIERS = 'Deforestation-free suppliers', + SUPPLIERS_WITH_DEFORASTATION_ALERTS = 'Suppliers with deforestation alerts', + SUPPLIERS_WITH_NO_LOCATION_DATA = 'Suppliers with no location data', +} +class CategoryDetail { @ApiProperty() - id: string; + totalPercentage: number; + + @ApiProperty({ type: () => BreakDownByEntity, isArray: true }) + detail: BreakDownByEntity[]; } -export class EUDRBreakDown { - materials: BreakDownMaterialCategory; +export type EntityMetadata = { + supplierId: string; + supplierName: string; + companyId: string; + adminRegionId: string; + adminRegionName: string; + materialId: string; + materialName: string; + totalBaselineVolume: number; + geoRegionCount: number; +}; - origins: any; -} +class BreakDownByEUDRCategory { + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: CategoryDetail; -export class BreakDownMaterialCategory { - category: 'material'; + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS]: CategoryDetail; - detail: BreakDownSupplierDetail[]; + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA]: CategoryDetail; } -export class BreakDownSupplierDetail { - // Deforestation-free, partial-deforestation, etc +class BreakDownByEntity { + @ApiProperty() name: string; - value: string; - data: BreakDownByMaterial[]; + @ApiProperty() + value: number; } -export class BreakDownByMaterial { - // material or origin name - name: string; +export class EUDRBreakDown { + @ApiProperty({ type: () => BreakDownByEUDRCategory }) + materials: BreakDownByEUDRCategory; - // percentaje - value: number; + @ApiProperty({ type: () => BreakDownByEUDRCategory }) + origins: any; +} + +export class EUDRDashboard { + @ApiProperty({ type: () => DashBoardTableElements, isArray: true }) + table: DashBoardTableElements[]; + + @ApiProperty({ type: () => EUDRBreakDown }) + breakDown: EUDRBreakDown; } From c396ca45ab2671e22e82aa98463b4d7e6b86d322 Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 11 Mar 2024 09:19:20 +0300 Subject: [PATCH 048/153] eudr dashboard detail --- .../big-query-alerts-query.builder.ts | 2 - .../modules/eudr-alerts/alerts.repository.ts | 3 +- .../dashboard/dashboard-detail.types.ts | 72 ++++++++ .../{types.ts => dashboard.types.ts} | 0 .../dashboard/eudr-dashboard.service.ts | 161 +++++++++++++++++- .../eudr-alerts/dto/alerts-output.dto.ts | 3 +- .../modules/eudr-alerts/dto/get-alerts.dto.ts | 40 ++--- .../modules/eudr-alerts/eudr.controller.ts | 23 ++- .../modules/geo-regions/geo-region.entity.ts | 2 +- 9 files changed, 278 insertions(+), 28 deletions(-) create mode 100644 api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts rename api/src/modules/eudr-alerts/dashboard/{types.ts => dashboard.types.ts} (100%) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index 398459886..6372c047e 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -95,8 +95,6 @@ export class BigQueryAlertsQueryBuilder { this.queryBuilder.limit(this.dto?.limit); const [query, params] = this.queryBuilder.getQueryAndParameters(); - console.log('query', query); - console.log('params', params); return this.parseToBigQuery(query, params); } diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index ed5a13d28..340d78dc6 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -14,7 +14,6 @@ import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { EUDRAlertDatabaseResult, EUDRAlertDates, - GetAlertSummary, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; @@ -62,6 +61,8 @@ export class AlertsRepository implements IEUDRAlertsRepository { queryBuilder.addSelect('alertconfidence', 'alertConfidence'); queryBuilder.addSelect('year', 'alertYear'); queryBuilder.addSelect('alertcount', 'alertCount'); + queryBuilder.addSelect('georegionid', 'geoRegionId'); + queryBuilder.orderBy('alertdate', 'ASC'); return this.query(queryBuilder, dto); } diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts new file mode 100644 index 000000000..7f17ede5f --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -0,0 +1,72 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class EUDRDashBoardDetail { + @ApiProperty() + name: string; + @ApiProperty() + address: string; + @ApiProperty() + companyId: string; + @ApiProperty({ + type: () => DashBoardDetailSourcingInformation, + isArray: true, + }) + sourcingInformation: DashBoardDetailSourcingInformation[]; + @ApiProperty({ type: () => DashBoardDetailAlerts, isArray: true }) + alerts: DashBoardDetailAlerts[]; +} + +class DashBoardDetailSourcingInformation { + @ApiProperty() + materialName: string; + @ApiProperty() + hsCode: string; + @ApiProperty() + totalArea: number; + @ApiProperty() + totalVolume: number; + @ApiProperty({ type: () => ByVolume, isArray: true }) + byVolume: ByVolume[]; + @ApiProperty({ type: () => ByArea, isArray: true }) + byArea: ByArea[]; +} + +class ByVolume { + @ApiProperty() + year: number; + @ApiProperty() + percentage: number; + @ApiProperty() + volume: number; +} + +class ByArea { + @ApiProperty() + plotName: string; + @ApiProperty() + percentage: number; + @ApiProperty() + area: number; + @ApiProperty() + geoRegionId: string; +} + +class DashBoardDetailAlerts { + @ApiProperty() + startAlertDate: Date; + @ApiProperty() + endAlertDate: number; + @ApiProperty() + totalAlerts: number; + @ApiProperty({ type: () => AlertValues, isArray: true }) + values: AlertValues[]; +} + +class AlertValues { + @ApiProperty() + geoRegionId: string; + @ApiProperty() + alertCount: number; + @ApiProperty() + plotName: string; +} diff --git a/api/src/modules/eudr-alerts/dashboard/types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts similarity index 100% rename from api/src/modules/eudr-alerts/dashboard/types.ts rename to api/src/modules/eudr-alerts/dashboard/dashboard.types.ts diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 4080f653a..d0d077a8a 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,7 +1,7 @@ // supress typescript error // eslint-disable-next-line @typescript-eslint/ban-types import { Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { DataSource, SelectQueryBuilder } from 'typeorm'; +import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; import { EUDRAlertDatabaseResult, IEUDRAlertsRepository, @@ -18,7 +18,13 @@ import { EUDRBreakDown, EUDRDashboard, EUDRDashBoardFields, -} from 'modules/eudr-alerts/dashboard/types'; +} from 'modules/eudr-alerts/dashboard/dashboard.types'; +import { GetEUDRAlertDatesDto } from '../dto/get-alerts.dto'; +import { AdminRegionsService } from '../../admin-regions/admin-regions.service'; +import { AlertsOutput } from '../dto/alerts-output.dto'; + +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { EUDRDashBoardDetail } from './dashboard-detail.types'; @Injectable() export class EudrDashboardService { @@ -392,4 +398,155 @@ export class EudrDashboardService { return queryBuilder.getRawMany(); } + + async buildDashboardDetail( + supplierId: string, + dto?: GetEUDRAlertDatesDto, + ): Promise { + const result: any = {}; + const sourcingInformation: any = {}; + let supplier: Supplier; + const geoRegionMap: Map = new Map(); + + return this.datasource.transaction(async (manager: EntityManager) => { + supplier = await manager + .getRepository(Supplier) + .findOneOrFail({ where: { id: supplierId } }); + result.name = supplier.name; + result.address = supplier.address; + result.companyId = supplier.companyId; + result.sourcingInformation = sourcingInformation; + const sourcingData: { + materialId: string; + materialName: string; + hsCode: string; + countryName: string; + plotName: string; + geoRegionId: string; + plotArea: number; + volume: number; + year: number; + sourcingLocationId: string; + }[] = await manager + .createQueryBuilder(SourcingLocation, 'sl') + .select('m.name', 'materialName') + .addSelect('sl.locationCountryInput', 'countryName') + .addSelect('m.hsCodeId', 'hsCode') + .addSelect('m.id', 'materialId') + .leftJoin(Material, 'm', 'm.id = sl.materialId') + .where('sl.producerId = :producerId', { producerId: supplierId }) + .distinct(true) + .getRawMany(); + + // TODO: we are assuming that each suppliers supplies only one material and for the same country + + const country: AdminRegion = await manager + .getRepository(AdminRegion) + .findOneOrFail({ + where: { name: sourcingData[0].countryName, level: 0 }, + }); + + sourcingInformation.materialName = sourcingData[0].materialName; + sourcingInformation.hsCode = sourcingData[0].hsCode; + sourcingInformation.country = { + name: country.name, + isoA3: country.isoA3, + }; + + for (const material of sourcingData) { + const geoRegions: any[] = await manager + .createQueryBuilder(SourcingLocation, 'sl') + .select('gr.id', 'geoRegionId') + .addSelect('gr.name', 'plotName') + .addSelect('gr.totalArea', 'totalArea') + .distinct(true) + .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId') + .where('sl.materialId = :materialId', { + materialId: material.materialId, + }) + .andWhere('sl.producerId = :supplierId', { supplierId }) + .getRawMany(); + const totalArea: number = geoRegions.reduce( + (acc: number, cur: any) => acc + parseInt(cur.totalArea), + 0, + ); + let sourcingRecords: SourcingRecord[] = []; + for (const geoRegion of geoRegions) { + if (!geoRegionMap.get(geoRegion.geoRegionId)) { + geoRegionMap.set(geoRegion.geoRegionId, { + plotName: geoRegion.plotName, + }); + } + sourcingRecords = await manager + .createQueryBuilder(SourcingRecord, 'sr') + .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') + .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId') + .where('sl.geoRegionId = :geoRegionId', { + geoRegionId: geoRegion.geoRegionId, + }) + .andWhere('sl.producerId = :supplierId', { supplierId: supplierId }) + .andWhere('sl.materialId = :materialId', { + materialId: material.materialId, + }) + .select([ + 'sr.year AS year', + 'sr.tonnage AS volume', + 'gr.name as plotName', + 'gr.id as geoRegionId', + ]) + .getRawMany(); + } + + const totalVolume: number = sourcingRecords.reduce( + (acc: number, cur: any) => acc + parseInt(cur.volume), + 0, + ); + + sourcingInformation.totalArea = totalArea; + sourcingInformation.totalVolume = totalVolume; + sourcingInformation.byArea = geoRegions.map((geoRegion: any) => ({ + plotName: geoRegion.plotName, + geoRegionId: geoRegion.geoRegionId, + percentage: (geoRegion.totalArea / totalArea) * 100, + area: geoRegion.totalArea, + })); + sourcingInformation.byVolume = sourcingRecords.map((record: any) => ({ + plotName: record.plotName, + geoRegionId: record.geoRegionId, + year: record.year, + percentage: (parseInt(record.volume) / totalVolume) * 100, + volume: parseInt(record.volume), + })); + } + + const alertsOutput: AlertsOutput[] = await this.eudrRepository.getAlerts({ + supplierIds: [supplierId], + startAlertDate: dto?.startAlertDate, + endAlertDate: dto?.endAlertDate, + }); + + const totalAlerts: number = alertsOutput.reduce( + (acc: number, cur: AlertsOutput) => acc + cur.alertCount, + 0, + ); + const startAlertDate: string = alertsOutput[0].alertDate.value.toString(); + const endAlertDate: string = + alertsOutput[alertsOutput.length - 1].alertDate.value.toString(); + + const alerts = { + startADateDate: startAlertDate, + endAlertDate: endAlertDate, + totalAlerts, + values: alertsOutput.map((alert: AlertsOutput) => ({ + geoRegionId: alert.geoRegionId, + alertCount: alert.alertCount, + plotName: geoRegionMap.get(alert.geoRegionId)!.plotName, + })), + }; + + result.alerts = alerts; + + return result; + }); + } } diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts index a66b22516..d61ab5583 100644 --- a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -1,10 +1,11 @@ export type AlertsOutput = { - alertCount: boolean; + alertCount: number; alertDate: { value: Date | string; }; year: number; alertConfidence: 'low' | 'medium' | 'high' | 'very high'; + geoRegionId: string; }; export type AlertGeometry = { diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts index 5b6699caa..77a99f425 100644 --- a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -9,47 +9,49 @@ import { IsUUID, } from 'class-validator'; -export class GetEUDRAlertsDto { +export class GetEUDRAlertDatesDto { + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + startAlertDate?: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + endAlertDate?: Date; +} + +export class GetEUDRAlertsDto extends GetEUDRAlertDatesDto { @ApiPropertyOptional() @IsOptional() @IsArray() @IsUUID('4', { each: true }) - supplierIds: string[]; + supplierIds?: string[]; @ApiPropertyOptional() @IsOptional() @IsArray() @IsUUID('4', { each: true }) - geoRegionIds: string[]; + geoRegionIds?: string[]; @ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) - startYear: number; + startYear?: number; @ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) - endYear: number; + endYear?: number; - alertConfidence: 'high' | 'medium' | 'low'; - - @ApiPropertyOptional() - @IsOptional() - @IsDate() - @Type(() => Date) - startAlertDate: Date; - - @ApiPropertyOptional() - @IsOptional() - @IsDate() - @Type(() => Date) - endAlertDate: Date; + alertConfidence?: 'high' | 'medium' | 'low'; @ApiPropertyOptional() @IsOptional() @IsInt() - limit: number = 1000; + limit?: number = 1000; } diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index a500019a3..41161456b 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, + Param, Query, UseInterceptors, ValidationPipe, @@ -33,7 +34,10 @@ import { import { JSONAPIQueryParams } from 'decorators/json-api-parameters.decorator'; import { GetEUDRGeoRegions } from 'modules/geo-regions/dto/get-geo-region.dto'; import { EudrService } from 'modules/eudr-alerts/eudr.service'; -import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { + GetEUDRAlertDatesDto, + GetEUDRAlertsDto, +} from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertDates } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRFeaturesGeoJSONDto } from 'modules/geo-regions/dto/get-features-geojson.dto'; import { @@ -43,7 +47,8 @@ import { import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; import { IsDate, IsOptional, IsUUID } from 'class-validator'; import { Type } from 'class-transformer'; -import { EUDRDashboard } from './dashboard/types'; +import { EUDRDashboard } from './dashboard/dashboard.types'; +import { EUDRDashBoardDetail } from './dashboard/dashboard-detail.types'; export class GetDashBoardDTO { @ApiPropertyOptional() @@ -251,4 +256,18 @@ export class EudrController { data: dashboard, }; } + + @ApiOperation({ description: 'Get EUDR Dashboard Detail' }) + @ApiOkResponse({ type: EUDRDashBoardDetail }) + @Get('/dashboard/detail/:supplierId') + async getDashboardDetail( + @Param('supplierId') supplierId: string, + @Query(ValidationPipe) dto: GetEUDRAlertDatesDto, + ): Promise<{ data: EUDRDashBoardDetail }> { + const dashboard: EUDRDashBoardDetail = + await this.dashboard.buildDashboardDetail(supplierId, dto); + return { + data: dashboard, + }; + } } diff --git a/api/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index 6c13c6df2..40bd08bda 100644 --- a/api/src/modules/geo-regions/geo-region.entity.ts +++ b/api/src/modules/geo-regions/geo-region.entity.ts @@ -53,7 +53,7 @@ export class GeoRegion extends BaseEntity { // TODO: It might be interesting to add a trigger to calculate the value in case it's not provided. We are considering that EUDR will alwaus provide the value // but not the regular ingestion @Column({ type: 'decimal', nullable: true }) - totalArea?: number; + totalArea: number; @Column({ type: 'boolean', default: true }) isCreatedByUser: boolean; From 37647f87dbf40d2392b9a0c5697b91ee064273a4 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 08:10:02 +0300 Subject: [PATCH 049/153] eudr dashboard cleanup --- .../big-query-alerts-query.builder.ts | 5 +++ .../modules/eudr-alerts/alerts.repository.ts | 32 +++++-------------- .../dashboard/dashboard-detail.types.ts | 11 +++++++ .../dashboard/eudr-dashboard.service.ts | 3 -- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index 6372c047e..ff7f12ed0 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -34,6 +34,11 @@ export class BigQueryAlertsQueryBuilder { return this; } + orderBy(field: string, order: 'ASC' | 'DESC'): this { + this.queryBuilder.orderBy(field, order); + return this; + } + getQueryBuilder(): SelectQueryBuilder { return this.queryBuilder; } diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index 340d78dc6..8dab19443 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -53,8 +53,8 @@ export class AlertsRepository implements IEUDRAlertsRepository { } async getAlerts(dto?: GetEUDRAlertsDto): Promise { - const queryBuilder: SelectQueryBuilder = - this.dataSource.createQueryBuilder(); + const queryBuilder: BigQueryAlertsQueryBuilder = + new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); // TODO: Make field selection dynamic queryBuilder.from(this.baseDataset, 'alerts'); queryBuilder.select('alertdate', 'alertDate'); @@ -63,17 +63,17 @@ export class AlertsRepository implements IEUDRAlertsRepository { queryBuilder.addSelect('alertcount', 'alertCount'); queryBuilder.addSelect('georegionid', 'geoRegionId'); queryBuilder.orderBy('alertdate', 'ASC'); - return this.query(queryBuilder, dto); + return this.query(queryBuilder); } async getDates(dto: GetEUDRAlertsDto): Promise { - const queryBuilder: SelectQueryBuilder = - this.dataSource.createQueryBuilder(); + const queryBuilder: BigQueryAlertsQueryBuilder = + new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); queryBuilder.from(this.baseDataset, 'alerts'); queryBuilder.select('alertdate', 'alertDate'); queryBuilder.orderBy('alertdate', 'ASC'); queryBuilder.groupBy('alertdate'); - return this.query(queryBuilder, dto); + return this.query(queryBuilder); } async getAlertSummary(dto: any): Promise { @@ -120,13 +120,10 @@ export class AlertsRepository implements IEUDRAlertsRepository { return this.query(mainQueryBuilder); } - private async query( - queryBuilder: SelectQueryBuilder | BigQueryAlertsQueryBuilder, - dto?: GetEUDRAlertsDto, - ): Promise { + private async query(queryBuilder: BigQueryAlertsQueryBuilder): Promise { try { const response: SimpleQueryRowsResponse = await this.bigQueryClient.query( - this.buildQuery(queryBuilder, dto), + queryBuilder.buildQuery(), ); if (!response.length || 'error' in response) { this.logger.error('Error in query', response); @@ -140,17 +137,4 @@ export class AlertsRepository implements IEUDRAlertsRepository { ); } } - - private buildQuery( - queryBuilder: SelectQueryBuilder | BigQueryAlertsQueryBuilder, - dto?: GetEUDRAlertsDto, - ): Query { - if (queryBuilder instanceof BigQueryAlertsQueryBuilder) { - return queryBuilder.buildQuery(); - } - const alertsQueryBuilder: BigQueryAlertsQueryBuilder = - new BigQueryAlertsQueryBuilder(queryBuilder, dto); - - return alertsQueryBuilder.buildQuery(); - } } diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts index 7f17ede5f..55d09e9b3 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -16,11 +16,22 @@ export class EUDRDashBoardDetail { alerts: DashBoardDetailAlerts[]; } +class DashBoardDetailCountry { + @ApiProperty() + name: string; + + @ApiProperty() + isoA3: string; +} + class DashBoardDetailSourcingInformation { @ApiProperty() materialName: string; @ApiProperty() hsCode: string; + + @ApiProperty({ type: () => DashBoardDetailCountry }) + country: DashBoardDetailCountry; @ApiProperty() totalArea: number; @ApiProperty() diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index d0d077a8a..60dfdec58 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,5 +1,3 @@ -// supress typescript error -// eslint-disable-next-line @typescript-eslint/ban-types import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; import { @@ -20,7 +18,6 @@ import { EUDRDashBoardFields, } from 'modules/eudr-alerts/dashboard/dashboard.types'; import { GetEUDRAlertDatesDto } from '../dto/get-alerts.dto'; -import { AdminRegionsService } from '../../admin-regions/admin-regions.service'; import { AlertsOutput } from '../dto/alerts-output.dto'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; From 5227314a9067617c0d1fa19f1c7dbf3b9ebc32b0 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 12:32:34 +0300 Subject: [PATCH 050/153] remove data root prop from dashboard responses --- api/src/modules/eudr-alerts/eudr.controller.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 41161456b..a0479b7c4 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -250,11 +250,8 @@ export class EudrController { @Get('/dashboard') async getDashboard( @Query(ValidationPipe) dto: GetDashBoardDTO, - ): Promise<{ data: EUDRDashboard }> { - const dashboard: EUDRDashboard = await this.dashboard.buildDashboard(dto); - return { - data: dashboard, - }; + ): Promise { + return this.dashboard.buildDashboard(dto); } @ApiOperation({ description: 'Get EUDR Dashboard Detail' }) @@ -263,11 +260,7 @@ export class EudrController { async getDashboardDetail( @Param('supplierId') supplierId: string, @Query(ValidationPipe) dto: GetEUDRAlertDatesDto, - ): Promise<{ data: EUDRDashBoardDetail }> { - const dashboard: EUDRDashBoardDetail = - await this.dashboard.buildDashboardDetail(supplierId, dto); - return { - data: dashboard, - }; + ): Promise { + return this.dashboard.buildDashboardDetail(supplierId, dto); } } From 7fd4ec673bed4d6e412cddc15b8ba737be5305e6 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 15:58:29 +0300 Subject: [PATCH 051/153] Save georegions with no name and no geom --- .../eudr/eudr.dto-processor.service.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index f587ebcdc..9a0f621c4 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -86,25 +86,19 @@ export class EUDRDTOProcessor { savedSupplier = foundSupplier; } const geoRegion: GeoRegion = new GeoRegion(); - let savedGeoRegion: GeoRegion; geoRegion.totalArea = row.total_area_ha; - geoRegion.theGeom = wellknown.parse(row.geometry) as Geometry; + geoRegion.theGeom = row.geometry + ? (wellknown.parse(row.geometry) as Geometry) + : (null as unknown as Geometry); geoRegion.isCreatedByUser = true; geoRegion.name = row.plot_name; - const foundGeoRegion: GeoRegion | null = - await geoRegionRepository.findOne({ - where: { name: geoRegion.name }, - }); - if (!foundGeoRegion) { - savedGeoRegion = await geoRegionRepository.save(geoRegion); - } else { - savedGeoRegion = foundGeoRegion; - } + const savedGeoRegion: GeoRegion = await geoRegionRepository.save( + geoRegion, + ); const sourcingLocation: SourcingLocation = new SourcingLocation(); sourcingLocation.locationType = LOCATION_TYPES.EUDR; sourcingLocation.locationCountryInput = row.sourcing_country; sourcingLocation.locationAddressInput = row.sourcing_district; - // TODO: materialId is coming like mpath, this is an error in the input file sourcingLocation.materialId = row.material_id .split('.') .filter(Boolean) From fac3597568bcc6cb4b3aaaae670e478247958acc Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 16:08:55 +0300 Subject: [PATCH 052/153] filter out geoRegions with no geom --- .../modules/geo-regions/geo-region.repository.ts | 1 + api/test/e2e/eudr/eudr-geo-region-filters.spec.ts | 6 ++++++ api/test/e2e/eudr/fixtures.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index 5bd49584c..8997b65de 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -142,6 +142,7 @@ export class GeoRegionRepository extends Repository { .groupBy('gr.id'); const queryBuilder: SelectQueryBuilder = BaseQueryBuilder.addFilters(initialQueryBuilder, getGeoRegionsDto); + queryBuilder.andWhere('gr.theGeom IS NOT NULL'); return queryBuilder.getMany(); } diff --git a/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts index ed209a296..5cd0bf731 100644 --- a/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts +++ b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts @@ -25,5 +25,11 @@ describe('GeoRegions Filters (e2e)', () => { const response = await testManager.WhenIRequestEUDRGeoRegions(); testManager.ThenIShouldOnlyReceiveCorrespondingGeoRegions(eudrGeoRegions); }); + it('should not show georegions that have no geometries', async () => { + const { eudrGeoRegions } = await testManager.GivenEUDRGeoRegions(); + await testManager.GivenEUDRGeoRegionsWithNoGeometry(); + const response = await testManager.WhenIRequestEUDRGeoRegions(); + testManager.ThenIShouldOnlyReceiveCorrespondingGeoRegions(eudrGeoRegions); + }); }); }); diff --git a/api/test/e2e/eudr/fixtures.ts b/api/test/e2e/eudr/fixtures.ts index 07ea1c727..747b375bc 100644 --- a/api/test/e2e/eudr/fixtures.ts +++ b/api/test/e2e/eudr/fixtures.ts @@ -144,6 +144,20 @@ export class EUDRTestManager extends TestManager { }; }; + GivenEUDRGeoRegionsWithNoGeometry = async () => { + const geoRegion = await createGeoRegion({ + name: 'EUDR GeoRegion with No Geom', + theGeom: null as any, + }); + await createSourcingLocation({ + geoRegionId: geoRegion.id, + locationType: LOCATION_TYPES.EUDR, + }); + return { + noGeomEUDRGeoRegions: [geoRegion], + }; + }; + WhenIRequestEUDRGeoRegions = async (filters?: { 'producerIds[]'?: string[]; 'materialIds[]'?: string[]; From 31b648d161be98e9b7d17ee47297b39b48e95c24 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 17:00:43 +0300 Subject: [PATCH 053/153] get material and origin descendants for dashboard filters --- .../dashboard/eudr-dashboard.service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 60dfdec58..6befaf73d 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -22,6 +22,8 @@ import { AlertsOutput } from '../dto/alerts-output.dto'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { EUDRDashBoardDetail } from './dashboard-detail.types'; +import { MaterialsService } from 'modules/materials/materials.service'; +import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; @Injectable() export class EudrDashboardService { @@ -29,9 +31,21 @@ export class EudrDashboardService { @Inject('IEUDRAlertsRepository') private readonly eudrRepository: IEUDRAlertsRepository, private readonly datasource: DataSource, + private readonly materialsService: MaterialsService, + private readonly adminRegionService: AdminRegionsService, ) {} async buildDashboard(dto: GetDashBoardDTO): Promise { + if (dto.originIds) { + dto.originIds = await this.adminRegionService.getAdminRegionDescendants( + dto.originIds, + ); + } + if (dto.materialIds) { + dto.materialIds = await this.materialsService.getMaterialsDescendants( + dto.materialIds, + ); + } const alertSummary: EUDRAlertDatabaseResult[] = await this.eudrRepository.getAlertSummary({ alertStartDate: dto.startAlertDate, From d38a2cc4b503334ee016ed4647b5e667b17d2618 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 17:16:51 +0300 Subject: [PATCH 054/153] Add alert filter by georegion --- api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts | 1 + api/src/modules/eudr-alerts/eudr.repositoty.interface.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 6befaf73d..bab92620f 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -51,6 +51,7 @@ export class EudrDashboardService { alertStartDate: dto.startAlertDate, alertEnDate: dto.endAlertDate, supplierIds: dto.producerIds, + geoRegionIds: dto.originIds, }); const entityMetadata: EntityMetadata[] = await this.getEntityMetadata(dto); if (!entityMetadata.length) { diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index 07b21bc3d..a2e9e7692 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -23,6 +23,7 @@ export type GetAlertSummary = { alertStartDate?: Date; alertEnDate?: Date; supplierIds?: string[]; + geoRegionIds?: string[]; }; export interface IEUDRAlertsRepository { From 06e6ac405cbdee140e4bbb756687d977e25e3c5d Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 12 Mar 2024 17:40:26 +0300 Subject: [PATCH 055/153] add isoA3 to table origins and breakdown origins --- .../eudr-alerts/dashboard/dashboard.types.ts | 1 + .../dashboard/eudr-dashboard.service.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index ef6c184c4..4de174ed6 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -62,6 +62,7 @@ export type EntityMetadata = { materialName: string; totalBaselineVolume: number; geoRegionCount: number; + isoA3: string; }; class BreakDownByEUDRCategory { diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index bab92620f..b554c5795 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -122,6 +122,7 @@ export class EudrDashboardService { materialName: string; totalBaselineVolume: number; geoRegionCount: number; + isoA3: string; }; } = entityMetadata.reduce((acc: any, cur: EntityMetadata) => { acc[cur.supplierId] = { ...cur }; @@ -130,8 +131,10 @@ export class EudrDashboardService { const supplierToMaterials: Map = new Map(); - const supplierToOriginis: Map = - new Map(); + const supplierToOriginis: Map< + string, + { name: string; id: string; isoA3: string }[] + > = new Map(); const materialMap: Map< string, { @@ -152,6 +155,7 @@ export class EudrDashboardService { dfsSuppliers: number; sdaSuppliers: number; tplSuppliers: number; + isoA3: string; } > = new Map(); @@ -163,6 +167,7 @@ export class EudrDashboardService { materialName, adminRegionName, geoRegionCount, + isoA3, } = entity; if (!supplierToMaterials.has(entity.supplierId)) { supplierToMaterials.set(entity.supplierId, []); @@ -181,6 +186,7 @@ export class EudrDashboardService { if (!originMap.has(adminRegionId)) { originMap.set(adminRegionId, { originName: adminRegionName, + isoA3: isoA3, suppliers: new Set(), zeroGeoRegionSuppliers: 0, dfsSuppliers: 0, @@ -217,6 +223,7 @@ export class EudrDashboardService { supplierToOriginis.get(entity.supplierId)!.push({ name: entity.adminRegionName, id: entity.adminRegionId, + isoA3: entity.isoA3, }); }); @@ -260,6 +267,7 @@ export class EudrDashboardService { dfsSuppliers, sdaSuppliers, tplSuppliers, + isoA3, }, adminRegionId: string, ) => { @@ -270,11 +278,13 @@ export class EudrDashboardService { ].detail.push({ name: originName, value: noLocationPercentage, + isoA3, }); const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ name: originName, value: dfsPercentage, + isoA3, }); const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; origins[ @@ -282,6 +292,7 @@ export class EudrDashboardService { ].detail.push({ name: originName, value: sdaPercentage, + isoA3, }); }, ); @@ -378,9 +389,10 @@ export class EudrDashboardService { .addSelect('ar.name', 'adminRegionName') .addSelect('SUM(sr.tonnage)', 'totalBaselineVolume') .addSelect('COUNT(sl.geoRegionId)', 'geoRegionsCount') + .addSelect('ar.isoA3', 'isoA3') .from(SourcingLocation, 'sl') .leftJoin(Supplier, 's', 's.id = sl.producerId') - .leftJoin(AdminRegion, 'ar', 'ar.id = sl.adminRegionId') + .leftJoin(AdminRegion, 'ar', 'ar.name = sl.locationCountryInput') .leftJoin(Material, 'm', 'm.id = sl.materialId') .leftJoin(SourcingRecord, 'sr', 'sr.sourcingLocationId = sl.id') .where('sr.year = :year', { year: 2020 }) From 4b453685aa5fe7f7a022efc584beb0441df6f767 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 13 Mar 2024 13:36:56 +0300 Subject: [PATCH 056/153] Do not save missing geometries as georegions --- .../eudr/eudr.dto-processor.service.ts | 10 ++++++---- .../import-data/eudr/eudr.import.service.ts | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index 9a0f621c4..bae0a3488 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -92,9 +92,10 @@ export class EUDRDTOProcessor { : (null as unknown as Geometry); geoRegion.isCreatedByUser = true; geoRegion.name = row.plot_name; - const savedGeoRegion: GeoRegion = await geoRegionRepository.save( - geoRegion, - ); + let savedGeoRegion: GeoRegion; + if (geoRegion.theGeom && geoRegion.name) { + savedGeoRegion = await geoRegionRepository.save(geoRegion); + } const sourcingLocation: SourcingLocation = new SourcingLocation(); sourcingLocation.locationType = LOCATION_TYPES.EUDR; sourcingLocation.locationCountryInput = row.sourcing_country; @@ -104,7 +105,8 @@ export class EUDRDTOProcessor { .filter(Boolean) .pop() as string; sourcingLocation.producer = savedSupplier; - sourcingLocation.geoRegion = savedGeoRegion; + // @ts-ignore + sourcingLocation.geoRegion = savedGeoRegion ?? null; sourcingLocation.sourcingRecords = []; sourcingLocation.adminRegionId = row.sourcing_district ? await this.getAdminRegionByAddress( diff --git a/api/src/modules/import-data/eudr/eudr.import.service.ts b/api/src/modules/import-data/eudr/eudr.import.service.ts index 4183551e3..91aff4f1d 100644 --- a/api/src/modules/import-data/eudr/eudr.import.service.ts +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -97,11 +97,16 @@ export class EudrImportService { .leftJoin(GeoRegion, 'g', 'sl.geoRegionId = g.id') .leftJoin(Supplier, 's', 'sl.producerId = s.id') - .execute(); + .getRawMany(); - const fakedCartoOutput: any[] = data.map((row: any) => - this.generateFakeAlerts(row), - ); + const fakedCartoOutput: any[] = data.reduce((acc: any[], row: any) => { + if (!row.geoRegionId && !row.geometry) { + return acc; + } + const fakeAlert = this.generateFakeAlerts(row); + acc.push(fakeAlert); + return acc; + }, []); const parsed: any = await new AsyncParser({ fields: [ @@ -116,6 +121,7 @@ export class EudrImportService { }) .parse(fakedCartoOutput) .promise(); + try { await fs.promises.writeFile('fakedCartoOutput.csv', parsed); } catch (e: any) { @@ -134,7 +140,7 @@ export class EudrImportService { geometry: string; year: number; }): any { - const { geoRegionId, supplierId, geometry } = row; + const { geoRegionId, supplierId, geometry, year } = row; const alertConfidence: string = Math.random() > 0.5 ? 'high' : 'low'; const startDate: Date = new Date(row.year, 0, 1); const endDate: Date = new Date(row.year, 11, 31); @@ -150,6 +156,7 @@ export class EudrImportService { geometry, alertDate, alertConfidence, + year, alertCount, }; } From 708a2db14bdb5063d0f9678fd2b20ee1377e7699 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 13 Mar 2024 13:38:04 +0300 Subject: [PATCH 057/153] Update approach to calculate percentage of missing georegions per supplier, material and origins --- .../modules/eudr-alerts/alerts.repository.ts | 27 ++-- .../dashboard/eudr-dashboard.service.ts | 145 +++++++++++------- 2 files changed, 100 insertions(+), 72 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index 8dab19443..561c17286 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -54,7 +54,7 @@ export class AlertsRepository implements IEUDRAlertsRepository { async getAlerts(dto?: GetEUDRAlertsDto): Promise { const queryBuilder: BigQueryAlertsQueryBuilder = - new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); + this.createQueryBuilder(dto); // TODO: Make field selection dynamic queryBuilder.from(this.baseDataset, 'alerts'); queryBuilder.select('alertdate', 'alertDate'); @@ -68,7 +68,7 @@ export class AlertsRepository implements IEUDRAlertsRepository { async getDates(dto: GetEUDRAlertsDto): Promise { const queryBuilder: BigQueryAlertsQueryBuilder = - new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); + this.createQueryBuilder(dto); queryBuilder.from(this.baseDataset, 'alerts'); queryBuilder.select('alertdate', 'alertDate'); queryBuilder.orderBy('alertdate', 'ASC'); @@ -78,14 +78,10 @@ export class AlertsRepository implements IEUDRAlertsRepository { async getAlertSummary(dto: any): Promise { const bigQueryBuilder: BigQueryAlertsQueryBuilder = - new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder(), dto); + this.createQueryBuilder(dto); bigQueryBuilder .from(this.baseDataset, 'alerts') .select('supplierid', 'supplierId') - .addSelect( - 'SUM(CASE WHEN georegionid IS NULL THEN 1 ELSE 0 END)', - 'null_geo_regions_count', - ) .addSelect( 'SUM(CASE WHEN alertcount = 0 THEN 1 ELSE 0 END)', 'zero_alerts', @@ -95,10 +91,10 @@ export class AlertsRepository implements IEUDRAlertsRepository { 'nonzero_alerts', ) .addSelect('COUNT(*)', 'total_geo_regions') - .groupBy('supplierid'); + const mainQueryBuilder: BigQueryAlertsQueryBuilder = - new BigQueryAlertsQueryBuilder(this.dataSource.createQueryBuilder()); + this.createQueryBuilder(); mainQueryBuilder .select('supplierid') @@ -110,10 +106,6 @@ export class AlertsRepository implements IEUDRAlertsRepository { '(CAST(nonzero_alerts AS FLOAT64) / NULLIF(total_geo_regions, 0)) * 100', 'sda', ) - .addSelect( - '(CAST(null_geo_regions_count AS FLOAT64) / NULLIF(total_geo_regions, 0)) * 100', - 'tpl', - ) .from('(' + bigQueryBuilder.getQuery() + ')', 'alerts_summary') .setParameters(bigQueryBuilder.getParameters()); @@ -137,4 +129,13 @@ export class AlertsRepository implements IEUDRAlertsRepository { ); } } + + private createQueryBuilder( + dto?: GetEUDRAlertsDto, + ): BigQueryAlertsQueryBuilder { + return new BigQueryAlertsQueryBuilder( + this.dataSource.createQueryBuilder(), + dto, + ); + } } diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index b554c5795..df3b3ddb9 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -46,19 +46,23 @@ export class EudrDashboardService { dto.materialIds, ); } - const alertSummary: EUDRAlertDatabaseResult[] = - await this.eudrRepository.getAlertSummary({ - alertStartDate: dto.startAlertDate, - alertEnDate: dto.endAlertDate, - supplierIds: dto.producerIds, - geoRegionIds: dto.originIds, - }); + const entityMetadata: EntityMetadata[] = await this.getEntityMetadata(dto); if (!entityMetadata.length) { throw new NotFoundException( 'Could not retrieve EUDR Data. Please contact the administrator', ); } + + const alertSummary: EUDRAlertDatabaseResult[] = + await this.eudrRepository.getAlertSummary({ + alertStartDate: dto.startAlertDate, + alertEnDate: dto.endAlertDate, + supplierIds: entityMetadata.map( + (entity: EntityMetadata) => entity.supplierId, + ), + }); + const materials: Record< EUDRDashBoardFields, { totalPercentage: number; detail: any[] } @@ -105,7 +109,6 @@ export class EudrDashboardService { acc[cur.supplierid] = { supplierid: cur.supplierid, dfs: cur.dfs, - tpl: cur.tpl, sda: cur.sda, }; return acc; @@ -116,12 +119,13 @@ export class EudrDashboardService { supplierId: string; supplierName: string; companyId: string; - adminRegionId: string; - adminRegionName: string; materialId: string; materialName: string; + adminRegionId: string; + adminRegionName: string; totalBaselineVolume: number; - geoRegionCount: number; + knownGeoRegions: number; + totalSourcingLocations: number; isoA3: string; }; } = entityMetadata.reduce((acc: any, cur: EntityMetadata) => { @@ -140,10 +144,10 @@ export class EudrDashboardService { { materialName: string; suppliers: Set; - zeroGeoRegionSuppliers: number; dfsSuppliers: number; sdaSuppliers: number; - tplSuppliers: number; + totalSourcingLocations: number; + knownGeoRegions: number; } > = new Map(); const originMap: Map< @@ -151,10 +155,10 @@ export class EudrDashboardService { { originName: string; suppliers: Set; - zeroGeoRegionSuppliers: number; dfsSuppliers: number; sdaSuppliers: number; - tplSuppliers: number; + totalSourcingLocations: number; + knownGeoRegions: number; isoA3: string; } > = new Map(); @@ -166,21 +170,42 @@ export class EudrDashboardService { adminRegionId, materialName, adminRegionName, - geoRegionCount, + knownGeoRegions, + totalSourcingLocations, isoA3, } = entity; - if (!supplierToMaterials.has(entity.supplierId)) { - supplierToMaterials.set(entity.supplierId, []); - supplierToOriginis.set(entity.supplierId, []); + + const unknownGeoRegions: number = + totalSourcingLocations - knownGeoRegions; + const tplPercentage: number = + (unknownGeoRegions / totalSourcingLocations) * 100; + + if (alertMap[supplierId]) { + alertMap[supplierId].tpl = tplPercentage; + } else { + alertMap[supplierId] = { + supplierId, + dfs: 0, + sda: 0, + tpl: tplPercentage, + }; } + + if (!supplierToMaterials.has(supplierId)) { + supplierToMaterials.set(supplierId, []); + } + if (!supplierToOriginis.has(supplierId)) { + supplierToOriginis.set(supplierId, []); + } + if (!materialMap.has(materialId)) { materialMap.set(materialId, { materialName, suppliers: new Set(), - zeroGeoRegionSuppliers: 0, dfsSuppliers: 0, sdaSuppliers: 0, - tplSuppliers: 0, + totalSourcingLocations: 0, + knownGeoRegions: 0, }); } if (!originMap.has(adminRegionId)) { @@ -188,42 +213,43 @@ export class EudrDashboardService { originName: adminRegionName, isoA3: isoA3, suppliers: new Set(), - zeroGeoRegionSuppliers: 0, dfsSuppliers: 0, sdaSuppliers: 0, - tplSuppliers: 0, + totalSourcingLocations: 0, + knownGeoRegions: 0, }); } + const material: any = materialMap.get(materialId); const origin: any = originMap.get(adminRegionId); material.suppliers.add(supplierId); + material.totalSourcingLocations += totalSourcingLocations; + material.knownGeoRegions += knownGeoRegions; origin.suppliers.add(supplierId); - if (geoRegionCount === 0) { - material.zeroGeoRegionSuppliers += 1; - origin.zeroGeoRegionSuppliers += 1; - } - if (alertMap[supplierId].dfs > 0) { - material.dfsSuppliers += 1; - origin.dfsSuppliers += 1; - } - if (alertMap[supplierId].sda > 0) { - material.sdaSuppliers += 1; - origin.sdaSuppliers += 1; - } - if (alertMap[supplierId].tpl > 0) { - material.tplSuppliers += 1; - origin.tplSuppliers += 1; + origin.totalSourcingLocations += totalSourcingLocations; + origin.knownGeoRegions += knownGeoRegions; + + const alertData: any = alertMap[supplierId]; + if (alertData) { + if (alertData.dfs > 0) { + material.dfsSuppliers += 1; + origin.dfsSuppliers += 1; + } + if (alertData.sda > 0) { + material.sdaSuppliers += 1; + origin.sdaSuppliers += 1; + } } - supplierToMaterials.get(entity.supplierId)!.push({ - name: entity.materialName, - id: entity.materialId, + // Añadir detalles de material y región al supplier + supplierToMaterials.get(supplierId)!.push({ + name: materialName, + id: materialId, }); - - supplierToOriginis.get(entity.supplierId)!.push({ - name: entity.adminRegionName, - id: entity.adminRegionId, - isoA3: entity.isoA3, + supplierToOriginis.get(supplierId)!.push({ + name: adminRegionName, + id: adminRegionId, + isoA3: isoA3, }); }); @@ -232,18 +258,18 @@ export class EudrDashboardService { { materialName, suppliers, - zeroGeoRegionSuppliers, + totalSourcingLocations, + knownGeoRegions, dfsSuppliers, sdaSuppliers, - tplSuppliers, }, materialId: string, ) => { - const noLocationPercentage: number = - (zeroGeoRegionSuppliers / suppliers.size) * 100; + const tplPercentage: number = + ((totalSourcingLocations - knownGeoRegions) / suppliers.size) * 100; materials['Suppliers with no location data'].detail.push({ name: materialName, - value: noLocationPercentage, + value: tplPercentage, }); const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; materials['Deforestation-free suppliers'].detail.push({ @@ -263,21 +289,21 @@ export class EudrDashboardService { { originName, suppliers, - zeroGeoRegionSuppliers, + totalSourcingLocations, + knownGeoRegions, dfsSuppliers, sdaSuppliers, - tplSuppliers, isoA3, }, adminRegionId: string, ) => { - const noLocationPercentage: number = - (zeroGeoRegionSuppliers / suppliers.size) * 100; + const tplPercentage: number = + ((totalSourcingLocations - knownGeoRegions) / suppliers.size) * 100; origins[ EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA ].detail.push({ name: originName, - value: noLocationPercentage, + value: tplPercentage, isoA3, }); const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; @@ -376,8 +402,8 @@ export class EudrDashboardService { * corresponding names to show in the dashboard. */ - async getEntityMetadata(dto: GetDashBoardDTO): Promise { - const queryBuilder: SelectQueryBuilder = + async getEntityMetadata(dto: GetDashBoardDTO): Promise { + const queryBuilder: SelectQueryBuilder = this.datasource.createQueryBuilder(); queryBuilder .select('s.id', 'supplierId') @@ -388,7 +414,8 @@ export class EudrDashboardService { .addSelect('ar.id', 'adminRegionId') .addSelect('ar.name', 'adminRegionName') .addSelect('SUM(sr.tonnage)', 'totalBaselineVolume') - .addSelect('COUNT(sl.geoRegionId)', 'geoRegionsCount') + .addSelect('COUNT(sl.geoRegionId)', 'knownGeoRegions') + .addSelect('COUNT(sl.id)', 'totalSourcingLocations') .addSelect('ar.isoA3', 'isoA3') .from(SourcingLocation, 'sl') .leftJoin(Supplier, 's', 's.id = sl.producerId') From 1dd906347740373b1269d71ac4de18189a2c94cd Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 13 Mar 2024 16:10:35 +0300 Subject: [PATCH 058/153] Add plotName and id to byVolume dashboard detail --- .../dashboard/eudr-dashboard.service.ts | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index df3b3ddb9..c77082966 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -19,7 +19,6 @@ import { } from 'modules/eudr-alerts/dashboard/dashboard.types'; import { GetEUDRAlertDatesDto } from '../dto/get-alerts.dto'; import { AlertsOutput } from '../dto/alerts-output.dto'; - import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { EUDRDashBoardDetail } from './dashboard-detail.types'; import { MaterialsService } from 'modules/materials/materials.service'; @@ -521,19 +520,24 @@ export class EudrDashboardService { (acc: number, cur: any) => acc + parseInt(cur.totalArea), 0, ); - let sourcingRecords: SourcingRecord[] = []; + const sourcingRecords: SourcingRecord[] = []; for (const geoRegion of geoRegions) { + geoRegion.geoRegionId = geoRegion.geoRegionId ?? 'Unknown'; + geoRegion.plotName = geoRegion.plotName ?? 'Unknown'; if (!geoRegionMap.get(geoRegion.geoRegionId)) { geoRegionMap.set(geoRegion.geoRegionId, { plotName: geoRegion.plotName, }); } - sourcingRecords = await manager + const newSourcingRecords: any[] = await manager .createQueryBuilder(SourcingRecord, 'sr') .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId') .where('sl.geoRegionId = :geoRegionId', { - geoRegionId: geoRegion.geoRegionId, + geoRegionId: + geoRegion.geoRegionId === 'Unknown' + ? null + : geoRegion.geoRegionId, }) .andWhere('sl.producerId = :supplierId', { supplierId: supplierId }) .andWhere('sl.materialId = :materialId', { @@ -542,10 +546,12 @@ export class EudrDashboardService { .select([ 'sr.year AS year', 'sr.tonnage AS volume', - 'gr.name as plotName', - 'gr.id as geoRegionId', + 'gr.name as "plotName"', + 'gr.id as "geoRegionId"', ]) .getRawMany(); + + sourcingRecords.push(...newSourcingRecords); } const totalVolume: number = sourcingRecords.reduce( @@ -553,6 +559,14 @@ export class EudrDashboardService { 0, ); + const test = sourcingRecords.map((record: any) => ({ + plotName: record.plotName, + geoRegionId: record.geoRegionId, + year: record.year, + percentage: (parseInt(record.volume) / totalVolume) * 100, + volume: parseInt(record.volume), + })); + sourcingInformation.totalArea = totalArea; sourcingInformation.totalVolume = totalVolume; sourcingInformation.byArea = geoRegions.map((geoRegion: any) => ({ @@ -580,24 +594,26 @@ export class EudrDashboardService { (acc: number, cur: AlertsOutput) => acc + cur.alertCount, 0, ); - const startAlertDate: string = alertsOutput[0].alertDate.value.toString(); - const endAlertDate: string = - alertsOutput[alertsOutput.length - 1].alertDate.value.toString(); + const startAlertDate: string | null = + alertsOutput[0]?.alertDate?.value.toString() || null; + const endAlertDate: string | null = + alertsOutput[alertsOutput.length - 1]?.alertDate?.value.toString() || + null; const alerts = { - startADateDate: startAlertDate, + startAlertDate: startAlertDate, endAlertDate: endAlertDate, totalAlerts, values: alertsOutput.map((alert: AlertsOutput) => ({ geoRegionId: alert.geoRegionId, - alertCount: alert.alertCount, + alertCount: alert.alertCount || null, plotName: geoRegionMap.get(alert.geoRegionId)!.plotName, })), }; result.alerts = alerts; - return result; + return result.sourcingInformation; }); } } From d8010b54aaaeab88ff12ff6ebb7e60b1b56a63a9 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 14 Mar 2024 07:23:31 +0300 Subject: [PATCH 059/153] handle unknown georegions, aggregate volumes to unknown --- .../dashboard/eudr-dashboard.service.ts | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index c77082966..11327ce93 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -517,7 +517,7 @@ export class EudrDashboardService { .andWhere('sl.producerId = :supplierId', { supplierId }) .getRawMany(); const totalArea: number = geoRegions.reduce( - (acc: number, cur: any) => acc + parseInt(cur.totalArea), + (acc: number, cur: any) => acc + parseInt(cur.totalArea ?? 0), 0, ); const sourcingRecords: SourcingRecord[] = []; @@ -529,16 +529,18 @@ export class EudrDashboardService { plotName: geoRegion.plotName, }); } - const newSourcingRecords: any[] = await manager + const queryBuilder = manager .createQueryBuilder(SourcingRecord, 'sr') .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') - .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId') - .where('sl.geoRegionId = :geoRegionId', { - geoRegionId: - geoRegion.geoRegionId === 'Unknown' - ? null - : geoRegion.geoRegionId, - }) + .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId'); + if (geoRegion.geoRegionId === 'Unknown') { + queryBuilder.andWhere('sl.geoRegionId IS NULL'); + } else { + queryBuilder.andWhere('sl.geoRegionId = :geoRegionId', { + geoRegionId: geoRegion.geoRegionId, + }); + } + queryBuilder .andWhere('sl.producerId = :supplierId', { supplierId: supplierId }) .andWhere('sl.materialId = :materialId', { materialId: material.materialId, @@ -548,8 +550,9 @@ export class EudrDashboardService { 'sr.tonnage AS volume', 'gr.name as "plotName"', 'gr.id as "geoRegionId"', - ]) - .getRawMany(); + ]); + + const newSourcingRecords: any[] = await queryBuilder.getRawMany(); sourcingRecords.push(...newSourcingRecords); } @@ -559,14 +562,6 @@ export class EudrDashboardService { 0, ); - const test = sourcingRecords.map((record: any) => ({ - plotName: record.plotName, - geoRegionId: record.geoRegionId, - year: record.year, - percentage: (parseInt(record.volume) / totalVolume) * 100, - volume: parseInt(record.volume), - })); - sourcingInformation.totalArea = totalArea; sourcingInformation.totalVolume = totalVolume; sourcingInformation.byArea = geoRegions.map((geoRegion: any) => ({ @@ -575,7 +570,9 @@ export class EudrDashboardService { percentage: (geoRegion.totalArea / totalArea) * 100, area: geoRegion.totalArea, })); - sourcingInformation.byVolume = sourcingRecords.map((record: any) => ({ + sourcingInformation.byVolume = aggregateUnknownGeoRegionVolumeValues( + sourcingRecords, + ).map((record: any) => ({ plotName: record.plotName, geoRegionId: record.geoRegionId, year: record.year, @@ -607,13 +604,34 @@ export class EudrDashboardService { values: alertsOutput.map((alert: AlertsOutput) => ({ geoRegionId: alert.geoRegionId, alertCount: alert.alertCount || null, - plotName: geoRegionMap.get(alert.geoRegionId)!.plotName, + plotName: geoRegionMap.get(alert.geoRegionId)?.plotName, })), }; result.alerts = alerts; - return result.sourcingInformation; + return result; }); } } + +const aggregateUnknownGeoRegionVolumeValues = (arr: any[]): any[] => { + const finalRecords = arr.filter((record) => record.geoRegionId !== null); + + arr.forEach((record) => { + if (record.geoRegionId === null) { + const existingRecord = finalRecords.find( + (r) => r.year === record.year && r.geoRegionId === null, + ); + if (existingRecord) { + existingRecord.plotName = 'Unknown'; + existingRecord.volume = ( + parseFloat(existingRecord.volume) + parseFloat(record.volume) + ).toString(); + } else { + finalRecords.push({ ...record }); + } + } + }); + return finalRecords; +}; From f9617d5657d966e2e0faddd42d055d0a6fec1bf6 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 14 Mar 2024 07:47:55 +0300 Subject: [PATCH 060/153] group plots by alert date in eudr dashboard detail alerts --- .../dashboard/dashboard-detail.types.ts | 15 +++++++- .../eudr-alerts/dashboard/dashboard.types.ts | 7 ++-- .../dashboard/eudr-dashboard.service.ts | 38 +++++++++++++++---- .../eudr-alerts/eudr.repositoty.interface.ts | 1 - 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts index 55d09e9b3..6bf7253e6 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -49,6 +49,10 @@ class ByVolume { percentage: number; @ApiProperty() volume: number; + @ApiProperty() + geoRegionId: string; + @ApiProperty() + plotName: string; } class ByArea { @@ -75,9 +79,16 @@ class DashBoardDetailAlerts { class AlertValues { @ApiProperty() - geoRegionId: string; + alertDate: string; + @ApiProperty({ type: () => AlertPlots, isArray: true }) + plots: AlertPlots[]; +} + +class AlertPlots { @ApiProperty() - alertCount: number; + geoRegionId: string; @ApiProperty() plotName: string; + @ApiProperty() + alertCount: number; } diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index 4de174ed6..1adac52d1 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -56,12 +56,13 @@ export type EntityMetadata = { supplierId: string; supplierName: string; companyId: string; - adminRegionId: string; - adminRegionName: string; materialId: string; materialName: string; + adminRegionId: string; + adminRegionName: string; totalBaselineVolume: number; - geoRegionCount: number; + knownGeoRegions: number; + totalSourcingLocations: number; isoA3: string; }; diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 11327ce93..ee85b9509 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -522,7 +522,7 @@ export class EudrDashboardService { ); const sourcingRecords: SourcingRecord[] = []; for (const geoRegion of geoRegions) { - geoRegion.geoRegionId = geoRegion.geoRegionId ?? 'Unknown'; + geoRegion.geoRegionId = geoRegion.geoRegionId ?? null; geoRegion.plotName = geoRegion.plotName ?? 'Unknown'; if (!geoRegionMap.get(geoRegion.geoRegionId)) { geoRegionMap.set(geoRegion.geoRegionId, { @@ -533,7 +533,7 @@ export class EudrDashboardService { .createQueryBuilder(SourcingRecord, 'sr') .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId'); - if (geoRegion.geoRegionId === 'Unknown') { + if (!geoRegion.geoRegionId) { queryBuilder.andWhere('sl.geoRegionId IS NULL'); } else { queryBuilder.andWhere('sl.geoRegionId = :geoRegionId', { @@ -601,11 +601,13 @@ export class EudrDashboardService { startAlertDate: startAlertDate, endAlertDate: endAlertDate, totalAlerts, - values: alertsOutput.map((alert: AlertsOutput) => ({ - geoRegionId: alert.geoRegionId, - alertCount: alert.alertCount || null, - plotName: geoRegionMap.get(alert.geoRegionId)?.plotName, - })), + // values: alertsOutput.map((alert: AlertsOutput) => ({ + // alertDate: alert.alertDate.value, + // geoRegionId: alert.geoRegionId, + // alertCount: alert.alertCount || null, + // plotName: geoRegionMap.get(alert.geoRegionId)?.plotName, + // })), + values: groupAlertsByDate(alertsOutput, geoRegionMap), }; result.alerts = alerts; @@ -635,3 +637,25 @@ const aggregateUnknownGeoRegionVolumeValues = (arr: any[]): any[] => { }); return finalRecords; }; + +const groupAlertsByDate = ( + alerts: AlertsOutput[], + geoRegionMap: Map, +): any[] => { + const alertsByDate: any = alerts.reduce((acc: any, cur: AlertsOutput) => { + const date: string = cur.alertDate.value.toString(); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push({ + plotName: geoRegionMap.get(cur.geoRegionId)?.plotName, + geoRegionId: cur.geoRegionId, + alertCount: cur.alertCount, + }); + return acc; + }, {}); + return Object.keys(alertsByDate).map((key) => ({ + alertDate: key, + plots: alertsByDate[key], + })); +}; diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index a2e9e7692..06da6c8bd 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -14,7 +14,6 @@ export class EUDRAlertDates { export interface EUDRAlertDatabaseResult { supplierid: string; - tpl: number; dfs: number; sda: number; } From adcfe671cd5ad57a7bd88ae7552faed9653dd1c2 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 14 Mar 2024 10:07:35 +0300 Subject: [PATCH 061/153] bigquery builder unit tests --- .github/workflows/testing-api.yml | 22 +++++++++ api/package.json | 1 + .../dashboard/eudr-dashboard.service.ts | 6 +-- .../modules/eudr-alerts/dto/get-alerts.dto.ts | 2 +- .../bigquery-query-builder.spec.ts | 47 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts diff --git a/.github/workflows/testing-api.yml b/.github/workflows/testing-api.yml index 935f8cb69..5323b26bd 100644 --- a/.github/workflows/testing-api.yml +++ b/.github/workflows/testing-api.yml @@ -105,6 +105,28 @@ jobs: working-directory: api run: yarn test:integration + testing-api-unit: + name: Unit Tests + runs-on: ubuntu-22.04 + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use Node.js 18.16 + uses: actions/setup-node@v3 + with: + node-version: '18.16' + + - name: Install API dependencies + working-directory: api + run: yarn install + + - name: Run API tests + coverage + working-directory: api + run: yarn test:unit + # - name: Generate API coverage artifact # uses: actions/upload-artifact@v2 # with: diff --git a/api/package.json b/api/package.json index 9ad724b87..9028353d7 100644 --- a/api/package.json +++ b/api/package.json @@ -20,6 +20,7 @@ "test:cov": "node --expose-gc ./node_modules/.bin/jest --config test/jest-config.json --coverage --forceExit", "test:debug": "node --inspect-brk --expose-gc -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --config test/jest-config.json --detectOpenHandles --forceExit", "test:integration": "node --expose-gc ./node_modules/.bin/jest --config test/jest-config.json --forceExit -i test/integration/", + "test:unit": "node --expose-gc ./node_modules/.bin/jest --config test/jest-config.json --forceExit -i test/unit/", "test:e2e": "NODE_OPTIONS=\"--max-old-space-size=6144\" node --expose-gc ./node_modules/.bin/jest --config test/jest-config.json --logHeapUsage --forceExit -i test/e2e/" }, "engines": { diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index ee85b9509..9fc5d782c 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -17,10 +17,10 @@ import { EUDRDashboard, EUDRDashBoardFields, } from 'modules/eudr-alerts/dashboard/dashboard.types'; -import { GetEUDRAlertDatesDto } from '../dto/get-alerts.dto'; -import { AlertsOutput } from '../dto/alerts-output.dto'; +import { GetEUDRAlertDatesDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; -import { EUDRDashBoardDetail } from './dashboard-detail.types'; +import { EUDRDashBoardDetail } from 'modules/eudr-alerts/dashboard/dashboard-detail.types'; import { MaterialsService } from 'modules/materials/materials.service'; import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts index 77a99f425..0f55caac4 100644 --- a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -53,5 +53,5 @@ export class GetEUDRAlertsDto extends GetEUDRAlertDatesDto { @ApiPropertyOptional() @IsOptional() @IsInt() - limit?: number = 1000; + limit?: number; } diff --git a/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts b/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts new file mode 100644 index 000000000..6c6d32214 --- /dev/null +++ b/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts @@ -0,0 +1,47 @@ +import { DataSource, SelectQueryBuilder } from 'typeorm'; +import { BigQueryAlertsQueryBuilder } from '../../../src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; +import { typeOrmConfig } from '../../../src/typeorm.config'; + +describe('BigQueryAlertsQueryBuilder', () => { + let queryBuilder: SelectQueryBuilder; + const dataSource = new DataSource(typeOrmConfig); + + beforeEach(() => { + queryBuilder = dataSource.createQueryBuilder().from('falsetable', 'alerts'); + }); + test('without DTO parameters should return a select with table name and alias', () => { + const bigQueryBuilder = new BigQueryAlertsQueryBuilder(queryBuilder); + const result = bigQueryBuilder.buildQuery(); + expect(result.query).toBe('SELECT * FROM falsetable alerts'); + expect(result.params).toEqual([]); + }); + + test('with date range parameters should add a where statement with parsed DATE formats', () => { + const bigQueryBuilder = new BigQueryAlertsQueryBuilder(queryBuilder, { + startAlertDate: new Date('2020-01-01'), + endAlertDate: new Date('2020-01-31'), + }); + const result = bigQueryBuilder.buildQuery(); + expect(result.query).toContain( + 'WHERE DATE(alertdate) BETWEEN DATE(?) AND DATE(?)', + ); + }); + + test('with a single supplier id should add a WHERE IN statement with a single parameter', () => { + const bigQueryBuilder = new BigQueryAlertsQueryBuilder(queryBuilder, { + supplierIds: ['supplier1'], + }); + const result = bigQueryBuilder.buildQuery(); + expect(result.query).toContain('WHERE supplierid IN (?)'); + expect(result.params).toEqual(['supplier1']); + }); + + test('with 2 supplier id should add a WHERE IN statement with 2 parameters', () => { + const bigQueryBuilder = new BigQueryAlertsQueryBuilder(queryBuilder, { + supplierIds: ['supplier1', 'supplier2'], + }); + const result = bigQueryBuilder.buildQuery(); + expect(result.query).toContain('WHERE supplierid IN (?, ?)'); + expect(result.params).toEqual(['supplier1', 'supplier2']); + }); +}); From 2cb79dea32566a9f4de9861ac7184c8a117c8910 Mon Sep 17 00:00:00 2001 From: alexeh Date: Fri, 15 Mar 2024 09:26:38 +0300 Subject: [PATCH 062/153] calculate volume percentage by year, fix total plot area sum --- .../modules/eudr-alerts/alerts.repository.ts | 2 +- .../eudr-alerts/dashboard/dashboard-utils.ts | 81 ++++++++++++++++ .../dashboard/eudr-dashboard.service.ts | 92 ++++++------------- 3 files changed, 108 insertions(+), 67 deletions(-) create mode 100644 api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index 561c17286..abace2a2e 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -9,7 +9,7 @@ import { Logger, ServiceUnavailableException, } from '@nestjs/common'; -import { DataSource, SelectQueryBuilder } from 'typeorm'; +import { DataSource } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { EUDRAlertDatabaseResult, diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts new file mode 100644 index 000000000..2052bc03e --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts @@ -0,0 +1,81 @@ +import { AlertsOutput } from '../dto/alerts-output.dto'; + +interface VolumeAndPlotByYear { + year: number; + volume: string; + plotName?: string; + geoRegionId?: string | null; +} + +export interface AggregatedVoumeAndPlotByYear extends VolumeAndPlotByYear { + percentage?: number; +} + +export const aggregateAndCalculatePercentage = ( + records: any[], +): AggregatedVoumeAndPlotByYear[] => { + const withGeoRegion: VolumeAndPlotByYear[] = records.filter( + (record: VolumeAndPlotByYear) => record.geoRegionId !== null, + ); + + // Group and aggregate records for unknown GeoRegions + const withoutGeoRegion: VolumeAndPlotByYear[] = records + .filter((record: VolumeAndPlotByYear) => record.geoRegionId === null) + .reduce( + (acc: VolumeAndPlotByYear[], { year, volume }) => { + const existingYearRecord: VolumeAndPlotByYear | undefined = acc.find( + (record: VolumeAndPlotByYear) => record.year === year, + ); + if (existingYearRecord) { + existingYearRecord.volume = ( + parseFloat(existingYearRecord.volume) + parseFloat(volume) + ).toString(); + } else { + acc.push({ year, volume, plotName: 'Unknown', geoRegionId: null }); + } + return acc; + }, + [], + ); + + // Merge records with known and unknown GeoRegions + const combinedRecords: VolumeAndPlotByYear[] = [ + ...withGeoRegion, + ...withoutGeoRegion, + ]; + + // Calculate total volume per year + const yearTotals: { [key: number]: number } = combinedRecords.reduce<{ + [key: number]: number; + }>((acc: { [p: number]: number }, { year, volume }) => { + acc[year] = (acc[year] || 0) + parseFloat(volume); + return acc; + }, {}); + + return combinedRecords.map((record: VolumeAndPlotByYear) => ({ + ...record, + percentage: (parseFloat(record.volume) / yearTotals[record.year]) * 100, + })); +}; + +export const groupAlertsByDate = ( + alerts: AlertsOutput[], + geoRegionMap: Map, +): any[] => { + const alertsByDate: any = alerts.reduce((acc: any, cur: AlertsOutput) => { + const date: string = cur.alertDate.value.toString(); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push({ + plotName: geoRegionMap.get(cur.geoRegionId)?.plotName, + geoRegionId: cur.geoRegionId, + alertCount: cur.alertCount, + }); + return acc; + }, {}); + return Object.keys(alertsByDate).map((key) => ({ + alertDate: key, + plots: alertsByDate[key], + })); +}; diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 9fc5d782c..39726e562 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -23,6 +23,10 @@ import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; import { EUDRDashBoardDetail } from 'modules/eudr-alerts/dashboard/dashboard-detail.types'; import { MaterialsService } from 'modules/materials/materials.service'; import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { + groupAlertsByDate, + aggregateAndCalculatePercentage, +} from './dashboard-utils'; @Injectable() export class EudrDashboardService { @@ -516,11 +520,13 @@ export class EudrDashboardService { }) .andWhere('sl.producerId = :supplierId', { supplierId }) .getRawMany(); - const totalArea: number = geoRegions.reduce( - (acc: number, cur: any) => acc + parseInt(cur.totalArea ?? 0), - 0, - ); - const sourcingRecords: SourcingRecord[] = []; + + const sourcingRecords: { + year: number; + volume: number; + plotName: string; + geoRegionId: string; + }[] = []; for (const geoRegion of geoRegions) { geoRegion.geoRegionId = geoRegion.geoRegionId ?? null; geoRegion.plotName = geoRegion.plotName ?? 'Unknown'; @@ -529,7 +535,7 @@ export class EudrDashboardService { plotName: geoRegion.plotName, }); } - const queryBuilder = manager + const queryBuilder: SelectQueryBuilder = manager .createQueryBuilder(SourcingRecord, 'sr') .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId'); @@ -552,13 +558,22 @@ export class EudrDashboardService { 'gr.id as "geoRegionId"', ]); - const newSourcingRecords: any[] = await queryBuilder.getRawMany(); + const newSourcingRecords: { + year: number; + volume: number; + plotName: string; + geoRegionId: string; + }[] = await queryBuilder.getRawMany(); sourcingRecords.push(...newSourcingRecords); } const totalVolume: number = sourcingRecords.reduce( - (acc: number, cur: any) => acc + parseInt(cur.volume), + (acc: number, cur: any) => acc + parseFloat(cur.volume), + 0, + ); + const totalArea: number = geoRegions.reduce( + (acc: number, cur: any) => acc + parseFloat(cur.totalArea ?? 0), 0, ); @@ -570,15 +585,9 @@ export class EudrDashboardService { percentage: (geoRegion.totalArea / totalArea) * 100, area: geoRegion.totalArea, })); - sourcingInformation.byVolume = aggregateUnknownGeoRegionVolumeValues( - sourcingRecords, - ).map((record: any) => ({ - plotName: record.plotName, - geoRegionId: record.geoRegionId, - year: record.year, - percentage: (parseInt(record.volume) / totalVolume) * 100, - volume: parseInt(record.volume), - })); + + sourcingInformation.byVolume = + aggregateAndCalculatePercentage(sourcingRecords); } const alertsOutput: AlertsOutput[] = await this.eudrRepository.getAlerts({ @@ -601,12 +610,6 @@ export class EudrDashboardService { startAlertDate: startAlertDate, endAlertDate: endAlertDate, totalAlerts, - // values: alertsOutput.map((alert: AlertsOutput) => ({ - // alertDate: alert.alertDate.value, - // geoRegionId: alert.geoRegionId, - // alertCount: alert.alertCount || null, - // plotName: geoRegionMap.get(alert.geoRegionId)?.plotName, - // })), values: groupAlertsByDate(alertsOutput, geoRegionMap), }; @@ -616,46 +619,3 @@ export class EudrDashboardService { }); } } - -const aggregateUnknownGeoRegionVolumeValues = (arr: any[]): any[] => { - const finalRecords = arr.filter((record) => record.geoRegionId !== null); - - arr.forEach((record) => { - if (record.geoRegionId === null) { - const existingRecord = finalRecords.find( - (r) => r.year === record.year && r.geoRegionId === null, - ); - if (existingRecord) { - existingRecord.plotName = 'Unknown'; - existingRecord.volume = ( - parseFloat(existingRecord.volume) + parseFloat(record.volume) - ).toString(); - } else { - finalRecords.push({ ...record }); - } - } - }); - return finalRecords; -}; - -const groupAlertsByDate = ( - alerts: AlertsOutput[], - geoRegionMap: Map, -): any[] => { - const alertsByDate: any = alerts.reduce((acc: any, cur: AlertsOutput) => { - const date: string = cur.alertDate.value.toString(); - if (!acc[date]) { - acc[date] = []; - } - acc[date].push({ - plotName: geoRegionMap.get(cur.geoRegionId)?.plotName, - geoRegionId: cur.geoRegionId, - alertCount: cur.alertCount, - }); - return acc; - }, {}); - return Object.keys(alertsByDate).map((key) => ({ - alertDate: key, - plots: alertsByDate[key], - })); -}; From ab4f5dee8f0522de81f374eb4738bf79499e0c32 Mon Sep 17 00:00:00 2001 From: alexeh Date: Sun, 17 Mar 2024 10:57:12 +0300 Subject: [PATCH 063/153] Refactor approach to calculate alert values --- .../modules/eudr-alerts/alerts.repository.ts | 23 +- .../dashboard/eudr-dashboard.service.ts | 484 ++++++++---------- .../eudr-alerts/dto/alerts-output.dto.ts | 2 +- .../modules/eudr-alerts/dto/get-alerts.dto.ts | 12 + .../eudr-alerts/eudr.repositoty.interface.ts | 11 + api/test/utils/service-mocks.ts | 9 + 6 files changed, 256 insertions(+), 285 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index abace2a2e..c7e3f92ad 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -12,6 +12,7 @@ import { import { DataSource } from 'typeorm'; import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; import { + AlertedGeoregionsBySupplier, EUDRAlertDatabaseResult, EUDRAlertDates, IEUDRAlertsRepository, @@ -57,12 +58,24 @@ export class AlertsRepository implements IEUDRAlertsRepository { this.createQueryBuilder(dto); // TODO: Make field selection dynamic queryBuilder.from(this.baseDataset, 'alerts'); - queryBuilder.select('alertdate', 'alertDate'); - queryBuilder.addSelect('alertconfidence', 'alertConfidence'); - queryBuilder.addSelect('year', 'alertYear'); - queryBuilder.addSelect('alertcount', 'alertCount'); + queryBuilder.select('alert_date', 'alertDate'); + queryBuilder.addSelect('supplierid', 'supplierId'); + queryBuilder.addSelect('alert_count', 'alertCount'); queryBuilder.addSelect('georegionid', 'geoRegionId'); - queryBuilder.orderBy('alertdate', 'ASC'); + queryBuilder.orderBy('alert_date', 'ASC'); + return this.query(queryBuilder); + } + + async getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise { + const queryBuilder: BigQueryAlertsQueryBuilder = + this.createQueryBuilder(dto); + queryBuilder.from(this.baseDataset, 'alerts'); + queryBuilder.select('georegionid', 'geoRegionId'); + queryBuilder.addSelect('supplierid', 'supplierId'); return this.query(queryBuilder); } diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 39726e562..2b99a56d5 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, NotFoundException } from '@nestjs/common'; import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; import { - EUDRAlertDatabaseResult, + AlertedGeoregionsBySupplier, IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { SourcingLocation } from 'modules/sourcing-locations/sourcing-location.entity'; @@ -50,26 +50,7 @@ export class EudrDashboardService { ); } - const entityMetadata: EntityMetadata[] = await this.getEntityMetadata(dto); - if (!entityMetadata.length) { - throw new NotFoundException( - 'Could not retrieve EUDR Data. Please contact the administrator', - ); - } - - const alertSummary: EUDRAlertDatabaseResult[] = - await this.eudrRepository.getAlertSummary({ - alertStartDate: dto.startAlertDate, - alertEnDate: dto.endAlertDate, - supplierIds: entityMetadata.map( - (entity: EntityMetadata) => entity.supplierId, - ), - }); - - const materials: Record< - EUDRDashBoardFields, - { totalPercentage: number; detail: any[] } - > = { + const materials: Record = { [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: { totalPercentage: 0, detail: [], @@ -84,10 +65,7 @@ export class EudrDashboardService { }, }; - const origins: Record< - EUDRDashBoardFields, - { totalPercentage: number; detail: any[] } - > = { + const origins: Record = { [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: { totalPercentage: 0, detail: [], @@ -101,302 +79,250 @@ export class EudrDashboardService { detail: [], }, }; - const alertMap: { - [key: string]: { - supplierId: string; - dfs: number; - tpl: number; - sda: number; - }; - } = alertSummary.reduce((acc: any, cur: EUDRAlertDatabaseResult) => { - acc[cur.supplierid] = { - supplierid: cur.supplierid, - dfs: cur.dfs, - sda: cur.sda, - }; - return acc; - }, {}); - - const entityMap: { - [key: string]: { - supplierId: string; - supplierName: string; - companyId: string; - materialId: string; - materialName: string; - adminRegionId: string; - adminRegionName: string; - totalBaselineVolume: number; - knownGeoRegions: number; - totalSourcingLocations: number; - isoA3: string; - }; - } = entityMetadata.reduce((acc: any, cur: EntityMetadata) => { - acc[cur.supplierId] = { ...cur }; - return acc; - }, {}); - - const supplierToMaterials: Map = - new Map(); - const supplierToOriginis: Map< - string, - { name: string; id: string; isoA3: string }[] - > = new Map(); - const materialMap: Map< - string, - { - materialName: string; - suppliers: Set; - dfsSuppliers: number; - sdaSuppliers: number; - totalSourcingLocations: number; - knownGeoRegions: number; - } - > = new Map(); - const originMap: Map< - string, - { - originName: string; - suppliers: Set; - dfsSuppliers: number; - sdaSuppliers: number; - totalSourcingLocations: number; - knownGeoRegions: number; - isoA3: string; + + const entityMetadata: EntityMetadata[] = await this.getEntityMetadata(dto); + if (!entityMetadata.length) { + throw new NotFoundException( + 'Could not retrieve EUDR Data. Please contact the administrator', + ); + } + + const alerts: AlertedGeoregionsBySupplier[] = + await this.eudrRepository.getAlertedGeoRegionsBySupplier({ + startAlertDate: dto.startAlertDate, + endAlertDate: dto.endAlertDate, + supplierIds: entityMetadata.map( + (entity: EntityMetadata) => entity.supplierId, + ), + }); + + const alertMap = new Map>(); + + alerts.forEach((alert: AlertedGeoregionsBySupplier) => { + const { supplierId, geoRegionId } = alert; + + if (!alertMap.has(supplierId)) { + alertMap.set(supplierId, new Set()); } - > = new Map(); - entityMetadata.forEach((entity: EntityMetadata) => { + // @ts-ignore + alertMap.get(supplierId).add(geoRegionId); + }); + + const suppliersMap = new Map(); + const materialsMap = new Map(); + const originsMap = new Map(); + + entityMetadata.forEach((entity) => { const { - materialId, supplierId, + materialId, adminRegionId, + totalSourcingLocations, + knownGeoRegions, + supplierName, + companyId, materialName, adminRegionName, - knownGeoRegions, - totalSourcingLocations, + totalBaselineVolume, isoA3, } = entity; - const unknownGeoRegions: number = - totalSourcingLocations - knownGeoRegions; - const tplPercentage: number = - (unknownGeoRegions / totalSourcingLocations) * 100; - - if (alertMap[supplierId]) { - alertMap[supplierId].tpl = tplPercentage; - } else { - alertMap[supplierId] = { + const alertedGeoRegionsCount = alertMap.get(supplierId)?.size || 0; + const nonAlertedGeoRegions = + parseInt(String(totalSourcingLocations)) - + parseInt(String(alertedGeoRegionsCount)); + const unknownGeoRegions = + parseInt(String(totalSourcingLocations)) - + parseInt(String(knownGeoRegions)); + + const sdaPercentage = + (alertedGeoRegionsCount / totalSourcingLocations) * 100; + const tplPercentage = (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage = + 100 - (sdaPercentage + tplPercentage) > 0 + ? 100 - (sdaPercentage + tplPercentage) + : 0; + + if (!suppliersMap.has(supplierId)) { + suppliersMap.set(supplierId, { supplierId, + supplierName, + companyId, + materials: [], + origins: [], + totalBaselineVolume: 0, dfs: 0, sda: 0, - tpl: tplPercentage, - }; - } - - if (!supplierToMaterials.has(supplierId)) { - supplierToMaterials.set(supplierId, []); - } - if (!supplierToOriginis.has(supplierId)) { - supplierToOriginis.set(supplierId, []); + tpl: 0, + }); } + const supplier = suppliersMap.get(supplierId); + supplier.totalBaselineVolume = totalBaselineVolume; + supplier.dfs = dfsPercentage; + supplier.sda = sdaPercentage; + supplier.tpl = tplPercentage; + supplier.materials.push({ id: materialId, name: materialName }); + supplier.origins.push({ + id: adminRegionId, + name: adminRegionName, + isoA3: isoA3, + }); - if (!materialMap.has(materialId)) { - materialMap.set(materialId, { + if (!materialsMap.has(materialId)) { + materialsMap.set(materialId, { + materialId, materialName, suppliers: new Set(), - dfsSuppliers: 0, - sdaSuppliers: 0, totalSourcingLocations: 0, knownGeoRegions: 0, + alertedGeoRegions: 0, }); } - if (!originMap.has(adminRegionId)) { - originMap.set(adminRegionId, { - originName: adminRegionName, - isoA3: isoA3, + const material = materialsMap.get(materialId); + material.suppliers.add(supplierId); + material.totalSourcingLocations += parseFloat( + String(totalSourcingLocations), + ); + material.knownGeoRegions += parseInt(String(knownGeoRegions)); + material.alertedGeoRegions += alertMap.get(supplierId)?.size || 0; + + if (!originsMap.has(adminRegionId)) { + originsMap.set(adminRegionId, { + adminRegionId, + adminRegionName, + isoA3, suppliers: new Set(), - dfsSuppliers: 0, - sdaSuppliers: 0, totalSourcingLocations: 0, knownGeoRegions: 0, + alertedGeoRegions: 0, }); } - - const material: any = materialMap.get(materialId); - const origin: any = originMap.get(adminRegionId); - material.suppliers.add(supplierId); - material.totalSourcingLocations += totalSourcingLocations; - material.knownGeoRegions += knownGeoRegions; + const origin = originsMap.get(adminRegionId); origin.suppliers.add(supplierId); - origin.totalSourcingLocations += totalSourcingLocations; - origin.knownGeoRegions += knownGeoRegions; - - const alertData: any = alertMap[supplierId]; - if (alertData) { - if (alertData.dfs > 0) { - material.dfsSuppliers += 1; - origin.dfsSuppliers += 1; - } - if (alertData.sda > 0) { - material.sdaSuppliers += 1; - origin.sdaSuppliers += 1; - } - } + origin.totalSourcingLocations += parseInt(String(totalSourcingLocations)); + origin.knownGeoRegions += parseInt(String(knownGeoRegions)); + origin.alertedGeoRegions += alertMap.get(supplierId)?.size || 0; + }); - // Añadir detalles de material y región al supplier - supplierToMaterials.get(supplierId)!.push({ + materialsMap.forEach((material, materialId) => { + const { + materialName, + totalSourcingLocations, + knownGeoRegions, + alertedGeoRegions, + } = material; + const nonAlertedGeoRegions = knownGeoRegions - alertedGeoRegions; + const unknownGeoRegions = totalSourcingLocations - knownGeoRegions; + + const sdaPercentage = (alertedGeoRegions / totalSourcingLocations) * 100; + const tplPercentage = (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage = + 100 - (sdaPercentage + tplPercentage) > 0 + ? 100 - (sdaPercentage + tplPercentage) + : 0; + + materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ name: materialName, id: materialId, + value: dfsPercentage, }); - supplierToOriginis.get(supplierId)!.push({ - name: adminRegionName, - id: adminRegionId, - isoA3: isoA3, + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.push({ + name: materialName, + id: materialId, + value: sdaPercentage, }); - }); - - materialMap.forEach( - ( - { - materialName, - suppliers, - totalSourcingLocations, - knownGeoRegions, - dfsSuppliers, - sdaSuppliers, - }, - materialId: string, - ) => { - const tplPercentage: number = - ((totalSourcingLocations - knownGeoRegions) / suppliers.size) * 100; - materials['Suppliers with no location data'].detail.push({ - name: materialName, - value: tplPercentage, - }); - const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; - materials['Deforestation-free suppliers'].detail.push({ - name: materialName, - value: dfsPercentage, - }); - const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; - materials['Suppliers with deforestation alerts'].detail.push({ - name: materialName, - value: sdaPercentage, - }); - }, - ); - - originMap.forEach( - ( - { - originName, - suppliers, - totalSourcingLocations, - knownGeoRegions, - dfsSuppliers, - sdaSuppliers, - isoA3, - }, - adminRegionId: string, - ) => { - const tplPercentage: number = - ((totalSourcingLocations - knownGeoRegions) / suppliers.size) * 100; - origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA - ].detail.push({ - name: originName, - value: tplPercentage, - isoA3, - }); - const dfsPercentage: number = (dfsSuppliers / suppliers.size) * 100; - origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ - name: originName, - value: dfsPercentage, - isoA3, - }); - const sdaPercentage: number = (sdaSuppliers / suppliers.size) * 100; - origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS - ].detail.push({ - name: originName, - value: sdaPercentage, - isoA3, - }); - }, - ); - - materials[ - EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA - ].totalPercentage = materials[ EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA - ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / - materials[EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA].detail - .length; - - materials[ - EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS - ].totalPercentage = - materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / - materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.length; + ].detail.push({ + name: materialName, + id: materialId, + value: tplPercentage, + }); + }); - materials[ - EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS - ].totalPercentage = - materials[ - EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS - ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / - materials[EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS].detail - .length; + // @ts-ignore + Object.keys(materials).forEach((key: EUDRDashBoardFields) => { + const totalPercentage: number = + materials[key].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / materials[key].detail.length; + materials[key].totalPercentage = totalPercentage; + }); - origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA - ].totalPercentage = + originsMap.forEach((origin, adminRegionId) => { + const { + adminRegionName, + isoA3, + totalSourcingLocations, + knownGeoRegions, + alertedGeoRegions, + } = origin; + const nonAlertedGeoRegions = knownGeoRegions - alertedGeoRegions; + const unknownGeoRegions = totalSourcingLocations - knownGeoRegions; + + const sdaPercentage = (alertedGeoRegions / totalSourcingLocations) * 100; + const tplPercentage = (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage = + 100 - (sdaPercentage + tplPercentage) > 0 + ? 100 - (sdaPercentage + tplPercentage) + : 0; + + origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ + name: adminRegionName, + id: adminRegionId, + isoA3: isoA3, + value: dfsPercentage, + }); origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA - ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / - origins[EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA].detail - .length; + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.push({ + name: adminRegionName, + id: adminRegionId, + isoA3: isoA3, + value: sdaPercentage, + }); + origins[EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA].detail.push({ + name: adminRegionName, + id: adminRegionId, + isoA3: isoA3, + value: tplPercentage, + }); + }); - origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].totalPercentage = - origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.reduce( - (acc: number, cur: any) => acc + cur.value, - 0, - ) / - origins[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.length; + // @ts-ignore + Object.keys(origins).forEach((key: EUDRDashBoardFields) => { + const totalPercentage: number = + origins[key].detail.reduce( + (acc: number, cur: any) => acc + cur.value, + 0, + ) / origins[key].detail.length; + origins[key].totalPercentage = isNaN(totalPercentage) + ? 0 + : totalPercentage; + }); - origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS - ].totalPercentage = - origins[ - EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS - ].detail.reduce((acc: number, cur: any) => acc + cur.value, 0) / - origins[EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS].detail - .length; - - const table: DashBoardTableElements[] = Object.keys(alertMap).map( - (key: string) => { - return { - supplierId: key, - supplierName: entityMap[key].supplierName, - companyId: entityMap[key].companyId, - baselineVolume: entityMap[key].totalBaselineVolume, - dfs: alertMap[key].dfs, - sda: alertMap[key].sda, - tpl: alertMap[key].tpl, - materials: supplierToMaterials.get(key) || [], - origins: supplierToOriginis.get(key) || [], - }; - }, - ); + const table: DashBoardTableElements[] = []; + suppliersMap.forEach((supplier: any) => { + table.push({ + supplierId: supplier.supplierId, + supplierName: supplier.supplierName, + companyId: supplier.companyId, + materials: supplier.materials, + origins: supplier.origins, + baselineVolume: supplier.totalBaselineVolume, + dfs: supplier.dfs, + sda: supplier.sda, + tpl: supplier.tpl, + }); + }); return { table: table, - breakDown: { materials, origins } as EUDRBreakDown, + breakDown: { materials, origins } as any, }; } diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts index d61ab5583..3167d2468 100644 --- a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -4,7 +4,7 @@ export type AlertsOutput = { value: Date | string; }; year: number; - alertConfidence: 'low' | 'medium' | 'high' | 'very high'; + supplierId: string; geoRegionId: string; }; diff --git a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts index 0f55caac4..df21cbafd 100644 --- a/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -50,6 +50,18 @@ export class GetEUDRAlertsDto extends GetEUDRAlertDatesDto { alertConfidence?: 'high' | 'medium' | 'low'; + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + startAlertDate?: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + endAlertDate?: Date; + @ApiPropertyOptional() @IsOptional() @IsInt() diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index 06da6c8bd..a35792fa3 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -18,6 +18,11 @@ export interface EUDRAlertDatabaseResult { sda: number; } +export interface AlertedGeoregionsBySupplier { + supplierId: string; + geoRegionId: string; +} + export type GetAlertSummary = { alertStartDate?: Date; alertEnDate?: Date; @@ -31,4 +36,10 @@ export interface IEUDRAlertsRepository { getDates(dto: GetEUDRAlertsDto): Promise; getAlertSummary(dto: GetAlertSummary): Promise; + + getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise; } diff --git a/api/test/utils/service-mocks.ts b/api/test/utils/service-mocks.ts index 3df85a9fb..1dfaeae4c 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -4,6 +4,7 @@ import { } from '../../src/modules/notifications/email/email.service.interface'; import { Logger } from '@nestjs/common'; import { + AlertedGeoregionsBySupplier, EUDRAlertDatabaseResult, EUDRAlertDates, GetAlertSummary, @@ -38,4 +39,12 @@ export class MockAlertRepository implements IEUDRAlertsRepository { getAlertSummary(dto: GetAlertSummary): Promise { return Promise.resolve([]); } + + getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise { + return Promise.resolve([]); + } } From 7d5524548cc9b424b166f6d790aab1c510735e41 Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 18 Mar 2024 06:44:36 +0300 Subject: [PATCH 064/153] Add company address to import --- api/src/modules/import-data/eudr/eudr.dto-processor.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts index bae0a3488..529d2aada 100644 --- a/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -37,6 +37,7 @@ export interface EudrInputShape { plot_name: string; company_id: string; company_name: string; + company_address: string; total_area_ha: number; sourcing_country: string; sourcing_district: string; @@ -74,6 +75,7 @@ export class EUDRDTOProcessor { let savedSupplier: Supplier; supplier.name = row.company_name; supplier.description = row.company_name; + supplier.address = row.company_address; supplier.companyId = row.company_id; const foundSupplier: Supplier | null = await supplierRepository.findOne( { From 2ce91eea55c0d2628df51dc4fa1a23b0afd3f311 Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 18 Mar 2024 07:03:44 +0300 Subject: [PATCH 065/153] update field names in bigquerybuilder --- .../big-query-alerts-query.builder.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index ff7f12ed0..0a163966a 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -76,7 +76,7 @@ export class BigQueryAlertsQueryBuilder { ); } if (this.dto?.alertConfidence) { - this.queryBuilder.andWhere('alertConfidence = :alertConfidence', { + this.queryBuilder.andWhere('alert_confidence = :alertConfidence', { alertConfidence: this.dto.alertConfidence, }); } @@ -125,7 +125,7 @@ export class BigQueryAlertsQueryBuilder { addAlertDateRange(): void { this.queryBuilder.andWhere( - 'DATE(alertdate) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)', + 'DATE(alert_date) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)', { startAlertDate: this.dto?.startAlertDate, endAlertDate: this.dto?.endAlertDate, @@ -134,13 +134,13 @@ export class BigQueryAlertsQueryBuilder { } addAlertDateGreaterThanOrEqual(): void { - this.queryBuilder.andWhere('DATE(alertdate) >= DATE(:startAlertDate)', { + this.queryBuilder.andWhere('DATE(alert_date) >= DATE(:startAlertDate)', { startAlertDate: this.dto?.startAlertDate, }); } addAlertDateLessThanOrEqual(): void { - this.queryBuilder.andWhere('DATE(alertDate) <= :DATE(endAlertDate)', { + this.queryBuilder.andWhere('DATE(alert_date) <= :DATE(endAlertDate)', { endAlertDate: this.dto?.endAlertDate, }); } From b8724f56bbdec389cd1c984b8e68aff673e04bfe Mon Sep 17 00:00:00 2001 From: alexeh Date: Mon, 18 Mar 2024 07:52:23 +0300 Subject: [PATCH 066/153] Add total carbon removals to dashboard and detail --- .../big-query-alerts-query.builder.ts | 39 ++++++--- .../modules/eudr-alerts/alerts.repository.ts | 45 ++++++++--- .../eudr-alerts/dashboard/dashboard.types.ts | 3 + .../dashboard/eudr-dashboard.service.ts | 81 ++++++++++++++----- .../eudr-alerts/dto/alerts-output.dto.ts | 1 + .../eudr-alerts/eudr.repositoty.interface.ts | 1 + .../bigquery-query-builder.spec.ts | 7 +- 7 files changed, 132 insertions(+), 45 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index 0a163966a..7978a630b 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -4,6 +4,16 @@ import { Query } from '@google-cloud/bigquery'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertsFields } from 'modules/eudr-alerts/alerts.repository'; +export enum EUDR_ALERTS_DATABASE_FIELDS { + alertDate = 'alert_date', + alertConfidence = 'alert_confidence', + alertCount = 'alert_count', + geoRegionId = 'georegionid', + supplierId = 'supplierid', + carbonRemovals = 'carbon_removals', + dataset = 'dataset', +} + export class BigQueryAlertsQueryBuilder { queryBuilder: SelectQueryBuilder; dto?: GetEUDRAlertsDto; @@ -76,9 +86,12 @@ export class BigQueryAlertsQueryBuilder { ); } if (this.dto?.alertConfidence) { - this.queryBuilder.andWhere('alert_confidence = :alertConfidence', { - alertConfidence: this.dto.alertConfidence, - }); + this.queryBuilder.andWhere( + `${EUDR_ALERTS_DATABASE_FIELDS.alertConfidence} = :alertConfidence`, + { + alertConfidence: this.dto.alertConfidence, + }, + ); } if (this.dto?.startYear && this.dto?.endYear) { @@ -125,7 +138,7 @@ export class BigQueryAlertsQueryBuilder { addAlertDateRange(): void { this.queryBuilder.andWhere( - 'DATE(alert_date) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)', + `DATE(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)`, { startAlertDate: this.dto?.startAlertDate, endAlertDate: this.dto?.endAlertDate, @@ -134,15 +147,21 @@ export class BigQueryAlertsQueryBuilder { } addAlertDateGreaterThanOrEqual(): void { - this.queryBuilder.andWhere('DATE(alert_date) >= DATE(:startAlertDate)', { - startAlertDate: this.dto?.startAlertDate, - }); + this.queryBuilder.andWhere( + `DATE(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) >= DATE(:startAlertDate)`, + { + startAlertDate: this.dto?.startAlertDate, + }, + ); } addAlertDateLessThanOrEqual(): void { - this.queryBuilder.andWhere('DATE(alert_date) <= :DATE(endAlertDate)', { - endAlertDate: this.dto?.endAlertDate, - }); + this.queryBuilder.andWhere( + `DATE(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) <= :DATE(endAlertDate)`, + { + endAlertDate: this.dto?.endAlertDate, + }, + ); } parseToBigQuery(query: string, params: any[]): Query { diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index c7e3f92ad..ac9019593 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -18,7 +18,10 @@ import { IEUDRAlertsRepository, } from 'modules/eudr-alerts/eudr.repositoty.interface'; import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; -import { BigQueryAlertsQueryBuilder } from 'modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; +import { + BigQueryAlertsQueryBuilder, + EUDR_ALERTS_DATABASE_FIELDS, +} from 'modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; const projectId: string = 'carto-dw-ac-zk2uhih6'; @@ -58,11 +61,24 @@ export class AlertsRepository implements IEUDRAlertsRepository { this.createQueryBuilder(dto); // TODO: Make field selection dynamic queryBuilder.from(this.baseDataset, 'alerts'); - queryBuilder.select('alert_date', 'alertDate'); - queryBuilder.addSelect('supplierid', 'supplierId'); - queryBuilder.addSelect('alert_count', 'alertCount'); - queryBuilder.addSelect('georegionid', 'geoRegionId'); - queryBuilder.orderBy('alert_date', 'ASC'); + queryBuilder.select(EUDR_ALERTS_DATABASE_FIELDS.alertDate, 'alertDate'); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.supplierId, + 'supplierId', + ); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.alertCount, + 'alertCount', + ); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.geoRegionId, + 'geoRegionId', + ); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, + 'carbonRemovals', + ); + queryBuilder.orderBy(EUDR_ALERTS_DATABASE_FIELDS.alertDate, 'ASC'); return this.query(queryBuilder); } @@ -74,8 +90,15 @@ export class AlertsRepository implements IEUDRAlertsRepository { const queryBuilder: BigQueryAlertsQueryBuilder = this.createQueryBuilder(dto); queryBuilder.from(this.baseDataset, 'alerts'); - queryBuilder.select('georegionid', 'geoRegionId'); - queryBuilder.addSelect('supplierid', 'supplierId'); + queryBuilder.select(EUDR_ALERTS_DATABASE_FIELDS.geoRegionId, 'geoRegionId'); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.supplierId, + 'supplierId', + ); + queryBuilder.addSelect( + EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, + 'carbonRemovals', + ); return this.query(queryBuilder); } @@ -83,9 +106,9 @@ export class AlertsRepository implements IEUDRAlertsRepository { const queryBuilder: BigQueryAlertsQueryBuilder = this.createQueryBuilder(dto); queryBuilder.from(this.baseDataset, 'alerts'); - queryBuilder.select('alertdate', 'alertDate'); - queryBuilder.orderBy('alertdate', 'ASC'); - queryBuilder.groupBy('alertdate'); + queryBuilder.select(EUDR_ALERTS_DATABASE_FIELDS.alertDate, 'alertDate'); + queryBuilder.orderBy(EUDR_ALERTS_DATABASE_FIELDS.alertDate, 'ASC'); + queryBuilder.groupBy(EUDR_ALERTS_DATABASE_FIELDS.alertDate); return this.query(queryBuilder); } diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index 1adac52d1..415aa3d07 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -23,6 +23,9 @@ export class DashBoardTableElements { @ApiProperty() tpl: number; + @ApiProperty() + crm: number; + @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) materials: EntitiesBySupplier[]; diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 2b99a56d5..e4bb1c747 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -13,7 +13,6 @@ import { GetDashBoardDTO } from 'modules/eudr-alerts/eudr.controller'; import { DashBoardTableElements, EntityMetadata, - EUDRBreakDown, EUDRDashboard, EUDRDashBoardFields, } from 'modules/eudr-alerts/dashboard/dashboard.types'; @@ -96,24 +95,38 @@ export class EudrDashboardService { ), }); - const alertMap = new Map>(); + console.log(alerts); + + const alertMap: Map< + string, + { geoRegionIdSet: Set; carbonRemovalValuesForSupplier: number[] } + > = new Map< + string, + { geoRegionIdSet: Set; carbonRemovalValuesForSupplier: number[] } + >(); alerts.forEach((alert: AlertedGeoregionsBySupplier) => { const { supplierId, geoRegionId } = alert; if (!alertMap.has(supplierId)) { - alertMap.set(supplierId, new Set()); + const geoRegionIdSet: Set = new Set(); + const carbonRemovalValuesForSupplier: number[] = []; + alertMap.set(supplierId, { + geoRegionIdSet, + carbonRemovalValuesForSupplier, + }); } - - // @ts-ignore - alertMap.get(supplierId).add(geoRegionId); + alertMap.get(supplierId)!.geoRegionIdSet.add(geoRegionId); + alertMap + .get(supplierId)! + .carbonRemovalValuesForSupplier.push(alert.carbonRemovals); }); const suppliersMap = new Map(); const materialsMap = new Map(); const originsMap = new Map(); - entityMetadata.forEach((entity) => { + entityMetadata.forEach((entity: EntityMetadata) => { const { supplierId, materialId, @@ -128,21 +141,30 @@ export class EudrDashboardService { isoA3, } = entity; - const alertedGeoRegionsCount = alertMap.get(supplierId)?.size || 0; - const nonAlertedGeoRegions = + const alertedGeoRegionsCount: number = + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; + const nonAlertedGeoRegions: number = parseInt(String(totalSourcingLocations)) - parseInt(String(alertedGeoRegionsCount)); - const unknownGeoRegions = + const unknownGeoRegions: number = parseInt(String(totalSourcingLocations)) - parseInt(String(knownGeoRegions)); - const sdaPercentage = + const sdaPercentage: number = (alertedGeoRegionsCount / totalSourcingLocations) * 100; - const tplPercentage = (unknownGeoRegions / totalSourcingLocations) * 100; - const dfsPercentage = + const tplPercentage: number = + (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage: number = 100 - (sdaPercentage + tplPercentage) > 0 ? 100 - (sdaPercentage + tplPercentage) : 0; + const carbonRemovalSumForSupplier: number = + alertMap + .get(supplierId) + ?.carbonRemovalValuesForSupplier.reduce( + (acc: number, cur: number) => acc + cur, + 0, + ) || 0; if (!suppliersMap.has(supplierId)) { suppliersMap.set(supplierId, { @@ -155,6 +177,7 @@ export class EudrDashboardService { dfs: 0, sda: 0, tpl: 0, + crm: 0, }); } const supplier = suppliersMap.get(supplierId); @@ -162,6 +185,7 @@ export class EudrDashboardService { supplier.dfs = dfsPercentage; supplier.sda = sdaPercentage; supplier.tpl = tplPercentage; + supplier.crm = carbonRemovalSumForSupplier; supplier.materials.push({ id: materialId, name: materialName }); supplier.origins.push({ id: adminRegionId, @@ -185,7 +209,8 @@ export class EudrDashboardService { String(totalSourcingLocations), ); material.knownGeoRegions += parseInt(String(knownGeoRegions)); - material.alertedGeoRegions += alertMap.get(supplierId)?.size || 0; + material.alertedGeoRegions += + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; if (!originsMap.has(adminRegionId)) { originsMap.set(adminRegionId, { @@ -202,7 +227,8 @@ export class EudrDashboardService { origin.suppliers.add(supplierId); origin.totalSourcingLocations += parseInt(String(totalSourcingLocations)); origin.knownGeoRegions += parseInt(String(knownGeoRegions)); - origin.alertedGeoRegions += alertMap.get(supplierId)?.size || 0; + origin.alertedGeoRegions += + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; }); materialsMap.forEach((material, materialId) => { @@ -212,12 +238,14 @@ export class EudrDashboardService { knownGeoRegions, alertedGeoRegions, } = material; - const nonAlertedGeoRegions = knownGeoRegions - alertedGeoRegions; + const nonAlertedGeoRegions: number = knownGeoRegions - alertedGeoRegions; const unknownGeoRegions = totalSourcingLocations - knownGeoRegions; - const sdaPercentage = (alertedGeoRegions / totalSourcingLocations) * 100; - const tplPercentage = (unknownGeoRegions / totalSourcingLocations) * 100; - const dfsPercentage = + const sdaPercentage: number = + (alertedGeoRegions / totalSourcingLocations) * 100; + const tplPercentage: number = + (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage: number = 100 - (sdaPercentage + tplPercentage) > 0 ? 100 - (sdaPercentage + tplPercentage) : 0; @@ -317,6 +345,7 @@ export class EudrDashboardService { dfs: supplier.dfs, sda: supplier.sda, tpl: supplier.tpl, + crm: supplier.crm, }); }); @@ -522,9 +551,16 @@ export class EudrDashboardService { endAlertDate: dto?.endAlertDate, }); - const totalAlerts: number = alertsOutput.reduce( - (acc: number, cur: AlertsOutput) => acc + cur.alertCount, - 0, + const { totalAlerts, totalCarbonRemovals } = alertsOutput.reduce( + ( + acc: { totalAlerts: number; totalCarbonRemovals: number }, + cur: AlertsOutput, + ) => { + acc.totalAlerts += cur.alertCount; + acc.totalCarbonRemovals += cur.carbonRemovals; + return acc; + }, + { totalAlerts: 0, totalCarbonRemovals: 0 }, ); const startAlertDate: string | null = alertsOutput[0]?.alertDate?.value.toString() || null; @@ -536,6 +572,7 @@ export class EudrDashboardService { startAlertDate: startAlertDate, endAlertDate: endAlertDate, totalAlerts, + totalCarbonRemovals, values: groupAlertsByDate(alertsOutput, geoRegionMap), }; diff --git a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts index 3167d2468..8c91c9216 100644 --- a/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -6,6 +6,7 @@ export type AlertsOutput = { year: number; supplierId: string; geoRegionId: string; + carbonRemovals: number; }; export type AlertGeometry = { diff --git a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts index a35792fa3..b1aee2987 100644 --- a/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -21,6 +21,7 @@ export interface EUDRAlertDatabaseResult { export interface AlertedGeoregionsBySupplier { supplierId: string; geoRegionId: string; + carbonRemovals: number; } export type GetAlertSummary = { diff --git a/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts b/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts index 6c6d32214..ad6d725c2 100644 --- a/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts +++ b/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts @@ -1,5 +1,8 @@ import { DataSource, SelectQueryBuilder } from 'typeorm'; -import { BigQueryAlertsQueryBuilder } from '../../../src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; +import { + BigQueryAlertsQueryBuilder, + EUDR_ALERTS_DATABASE_FIELDS, +} from '../../../src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder'; import { typeOrmConfig } from '../../../src/typeorm.config'; describe('BigQueryAlertsQueryBuilder', () => { @@ -23,7 +26,7 @@ describe('BigQueryAlertsQueryBuilder', () => { }); const result = bigQueryBuilder.buildQuery(); expect(result.query).toContain( - 'WHERE DATE(alertdate) BETWEEN DATE(?) AND DATE(?)', + `WHERE DATE(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) BETWEEN DATE(?) AND DATE(?)`, ); }); From 98fec7e7e7656bbbb287e2f78c6619ac0159377d Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 22 Feb 2024 11:55:13 +0100 Subject: [PATCH 067/153] migration from Mapbox GL to MapLibre GL --- client/package.json | 4 +- client/src/components/map/component.tsx | 94 +- client/src/components/map/constants.ts | 6 +- .../map/controls/zoom/component.tsx | 2 +- client/src/components/map/index.ts | 1 + .../components/map/layer-manager/provider.tsx | 2 +- .../src/components/map/layers/deck/index.tsx | 2 +- .../{mapbox => maplibre}/raster/hooks.tsx | 18 +- .../{mapbox => maplibre}/raster/index.tsx | 2 +- .../{mapbox => maplibre}/raster/utils.ts | 0 .../map/styles/map-style-maplibre.json | 5903 +++++++++++++++++ .../styles/map-style-satellite-maplibre.json | 44 + client/src/components/map/types.d.ts | 4 +- .../analysis-map/component.tsx | 3 +- .../analysis-map/layers/contextual/index.tsx | 2 +- client/src/pages/_document.tsx | 2 +- client/src/pages/analysis/map.tsx | 2 +- client/yarn.lock | 363 +- 18 files changed, 6297 insertions(+), 157 deletions(-) rename client/src/components/map/layers/{mapbox => maplibre}/raster/hooks.tsx (61%) rename client/src/components/map/layers/{mapbox => maplibre}/raster/index.tsx (89%) rename client/src/components/map/layers/{mapbox => maplibre}/raster/utils.ts (100%) create mode 100644 client/src/components/map/styles/map-style-maplibre.json create mode 100644 client/src/components/map/styles/map-style-satellite-maplibre.json diff --git a/client/package.json b/client/package.json index eaba3fda1..216213c40 100644 --- a/client/package.json +++ b/client/package.json @@ -52,7 +52,7 @@ "jsona": "1.9.2", "lodash-es": "4.17.21", "lottie-react": "2.4.0", - "mapbox-gl": "2.13.0", + "maplibre-gl": "3.6.2", "next": "13.5.5", "next-auth": "4.19.2", "pino": "8.1.0", @@ -64,7 +64,7 @@ "react-dropzone": "14.2.2", "react-hook-form": "7.43.1", "react-hot-toast": "2.2.0", - "react-map-gl": "7.0.23", + "react-map-gl": "7.1.7", "react-range": "1.8.14", "react-redux": "8.0.2", "recharts": "2.9.0", diff --git a/client/src/components/map/component.tsx b/client/src/components/map/component.tsx index 1fcfba454..f865b23fd 100644 --- a/client/src/components/map/component.tsx +++ b/client/src/components/map/component.tsx @@ -1,24 +1,13 @@ import React, { useEffect, useState, useCallback } from 'react'; -import ReactMapGL, { useMap } from 'react-map-gl'; +import ReactMapGL, { useMap } from 'react-map-gl/maplibre'; import { useDebounce } from 'rooks'; -import { DEFAULT_VIEW_STATE, MAP_STYLES } from './constants'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from './constants'; -import type { ViewState, ViewStateChangeEvent, MapboxEvent } from 'react-map-gl'; +import type { ViewState, ViewStateChangeEvent } from 'react-map-gl/maplibre'; import type { FC } from 'react'; import type { CustomMapProps } from './types'; -const MAPBOX_API_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_API_TOKEN; - -export const INITIAL_VIEW_STATE: ViewState = { - longitude: 0, - latitude: 0, - zoom: 2, - pitch: 0, - bearing: 0, - padding: null, -}; - export const Map: FC = ({ id = 'default', mapStyle = 'terrain', @@ -33,7 +22,9 @@ export const Map: FC = ({ doubleClickZoom, onLoad, sidebarCollapsed = false, - ...mapboxProps + touchZoomRotate, // not supported in MapLibre + touchPitch, // not supported in MapLibre + ...otherMapProps }: CustomMapProps) => { /** * REFS @@ -45,30 +36,11 @@ export const Map: FC = ({ */ const [localViewState, setLocalViewState] = useState>( !initialViewState && { - ...DEFAULT_VIEW_STATE, + ...INITIAL_VIEW_STATE, ...viewState, }, ); const onMapViewStateChangeDebounced = useDebounce(onMapViewStateChange, 150); - const [isFlying, setFlying] = useState(false); - - /** - * CALLBACKS - */ - const handleFitBounds = useCallback(() => { - const { bbox, options } = bounds; - - // enabling fly mode avoids the map to be interrupted during the bounds transition - setFlying(true); - - mapRef.fitBounds( - [ - [bbox[0], bbox[1]], - [bbox[2], bbox[3]], - ], - options, - ); - }, [bounds, mapRef]); const handleMapMove = useCallback( ({ viewState: _viewState }: ViewStateChangeEvent) => { @@ -84,7 +56,7 @@ export const Map: FC = ({ // Cancel last timeout if a new one it triggered clearTimeout(resizeWhenCollapse); - // Trigger the map resize if the sibe bar has been collapsed. There is no need to resize if the sidebar has been expanded because the container will hide the excess width + // Trigger the map resize if the sidebar has been collapsed. There is no need to resize if the sidebar has been expanded because the container will hide the excess width if (sidebarCollapsed) { resizeWhenCollapse = setTimeout(() => { mapRef?.resize(); @@ -92,61 +64,15 @@ export const Map: FC = ({ } }, [sidebarCollapsed, mapRef]); - useEffect(() => { - if (mapRef && bounds) { - handleFitBounds(); - } - }, [mapRef, bounds, handleFitBounds]); - - useEffect(() => { - setLocalViewState((prevViewState) => ({ - ...prevViewState, - ...viewState, - })); - }, [viewState]); - - useEffect(() => { - if (!bounds) return undefined; - - const { options } = bounds; - const animationDuration = options?.duration || 0; - let timeoutId: number = null; - - if (isFlying) { - timeoutId = window.setTimeout(() => { - setFlying(false); - }, animationDuration); - } - - return () => { - if (timeoutId) { - window.clearInterval(timeoutId); - } - }; - }, [bounds, isFlying]); - - const handleMapLoad = useCallback( - (evt: MapboxEvent) => { - if (onLoad) onLoad(evt); - }, - [onLoad], - ); - return ( {!!mapRef && children(mapRef.getMap())} diff --git a/client/src/components/map/constants.ts b/client/src/components/map/constants.ts index 288810081..ca0242584 100644 --- a/client/src/components/map/constants.ts +++ b/client/src/components/map/constants.ts @@ -1,7 +1,7 @@ -import DefaultMapStyle from './styles/map-style.json'; -import SatelliteMapStyle from './styles/map-style-satellite.json'; +import DefaultMapStyle from './styles/map-style-maplibre.json'; +import SatelliteMapStyle from './styles/map-style-satellite-maplibre.json'; -import type { ViewState, MapProps } from 'react-map-gl'; +import type { ViewState, MapProps } from 'react-map-gl/maplibre'; export const DEFAULT_VIEW_STATE: Partial = { zoom: 2, diff --git a/client/src/components/map/controls/zoom/component.tsx b/client/src/components/map/controls/zoom/component.tsx index 0a7c15a27..84a8d8406 100644 --- a/client/src/components/map/controls/zoom/component.tsx +++ b/client/src/components/map/controls/zoom/component.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import cx from 'classnames'; -import { useMap } from 'react-map-gl'; +import { useMap } from 'react-map-gl/maplibre'; import { MinusIcon, PlusIcon } from '@heroicons/react/solid'; import type { MouseEventHandler } from 'react'; diff --git a/client/src/components/map/index.ts b/client/src/components/map/index.ts index c6cf17eac..ff24f47ed 100644 --- a/client/src/components/map/index.ts +++ b/client/src/components/map/index.ts @@ -1,2 +1,3 @@ export { default } from './component'; export * from './component'; +export * from './constants'; diff --git a/client/src/components/map/layer-manager/provider.tsx b/client/src/components/map/layer-manager/provider.tsx index dda302902..4c011f2c8 100644 --- a/client/src/components/map/layer-manager/provider.tsx +++ b/client/src/components/map/layer-manager/provider.tsx @@ -1,5 +1,5 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; -import { useControl } from 'react-map-gl'; +import { useControl } from 'react-map-gl/maplibre'; import { MapboxOverlay } from '@deck.gl/mapbox/typed'; import type { MapboxOverlayProps } from '@deck.gl/mapbox/typed'; diff --git a/client/src/components/map/layers/deck/index.tsx b/client/src/components/map/layers/deck/index.tsx index b9432dfbd..f05dba3a7 100644 --- a/client/src/components/map/layers/deck/index.tsx +++ b/client/src/components/map/layers/deck/index.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { Layer } from 'react-map-gl'; +import { Layer } from 'react-map-gl/maplibre'; import { useMapboxOverlayContext } from 'components/map/layer-manager/provider'; diff --git a/client/src/components/map/layers/mapbox/raster/hooks.tsx b/client/src/components/map/layers/maplibre/raster/hooks.tsx similarity index 61% rename from client/src/components/map/layers/mapbox/raster/hooks.tsx rename to client/src/components/map/layers/maplibre/raster/hooks.tsx index 0b4f97531..4a594a917 100644 --- a/client/src/components/map/layers/mapbox/raster/hooks.tsx +++ b/client/src/components/map/layers/maplibre/raster/hooks.tsx @@ -2,39 +2,43 @@ import { useMemo } from 'react'; import { getTiler } from './utils'; -import type { AnyLayer, AnySourceData } from 'mapbox-gl'; +import type { RasterLayerSpecification, RasterSourceSpecification } from 'maplibre-gl'; import type { LayerSettings, LayerProps } from 'components/map/layers/types'; export function useSource({ - id, + // id, tilerUrl, defaultTilerParams, -}: LayerProps): AnySourceData { +}: LayerProps): RasterSourceSpecification { const tiler = useMemo( () => getTiler(tilerUrl, defaultTilerParams), [tilerUrl, defaultTilerParams], ); return { - id: `${id}-source`, + // id: `${id}-source`, type: 'raster', tiles: [tiler], }; } -export function useLayer({ id, settings = {} }: LayerProps): AnyLayer { +export function useLayer({ + id, + settings = {}, +}: LayerProps): RasterLayerSpecification { const visibility = settings.visibility ?? true; - const layer = useMemo(() => { + const layer = useMemo((): RasterLayerSpecification => { return { id, type: 'raster', + source: `${id}-source`, paint: { 'raster-opacity': settings.opacity ?? 1, }, layout: { visibility: visibility ? 'visible' : 'none', }, - } satisfies AnyLayer; + } satisfies RasterLayerSpecification; }, [id, settings, visibility]); return layer; diff --git a/client/src/components/map/layers/mapbox/raster/index.tsx b/client/src/components/map/layers/maplibre/raster/index.tsx similarity index 89% rename from client/src/components/map/layers/mapbox/raster/index.tsx rename to client/src/components/map/layers/maplibre/raster/index.tsx index 1a0168732..7ac25bfb4 100644 --- a/client/src/components/map/layers/mapbox/raster/index.tsx +++ b/client/src/components/map/layers/maplibre/raster/index.tsx @@ -1,4 +1,4 @@ -import { Source, Layer } from 'react-map-gl'; +import { Source, Layer } from 'react-map-gl/maplibre'; import { useLayer, useSource } from './hooks'; diff --git a/client/src/components/map/layers/mapbox/raster/utils.ts b/client/src/components/map/layers/maplibre/raster/utils.ts similarity index 100% rename from client/src/components/map/layers/mapbox/raster/utils.ts rename to client/src/components/map/layers/maplibre/raster/utils.ts diff --git a/client/src/components/map/styles/map-style-maplibre.json b/client/src/components/map/styles/map-style-maplibre.json new file mode 100644 index 000000000..22eab4ec2 --- /dev/null +++ b/client/src/components/map/styles/map-style-maplibre.json @@ -0,0 +1,5903 @@ +{ + "version": 8, + "name": "Positron", + "metadata": { + + }, + "sources": { + "carto": { + "type": "vector", + "url": "https://tiles.basemaps.cartocdn.com/vector/carto.streets/v1/tiles.json" + } + }, + "sprite": "https://tiles.basemaps.cartocdn.com/gl/positron-gl-style/sprite", + "glyphs": "https://tiles.basemaps.cartocdn.com/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "layout": { + "visibility": "visible" + }, + "paint": { + "background-color": "#f9f5f3", + "background-opacity": 1 + } + }, + { + "id": "custom-layers", + "type": "background", + "paint": { + "background-color": "#000", + "background-opacity": 0 + } + }, + { + "id": "landcover", + "type": "fill", + "source": "carto", + "source-layer": "landcover", + "filter": [ + "any", + [ + "==", + "class", + "wood" + ], + [ + "==", + "class", + "grass" + ], + [ + "==", + "subclass", + "recreation_ground" + ] + ], + "paint": { + "fill-color": { + "stops": [ + [ + 8, + "rgba(234, 241, 233, 0.5)" + ], + [ + 9, + "rgba(234, 241, 233, 0.5)" + ], + [ + 11, + "rgba(234, 241, 233, 0.5)" + ], + [ + 13, + "rgba(234, 241, 233, 0.5)" + ], + [ + 15, + "rgba(234, 241, 233, 0.5)" + ] + ] + }, + "fill-opacity": 1 + } + }, + { + "id": "park_national_park", + "type": "fill", + "source": "carto", + "source-layer": "park", + "minzoom": 9, + "filter": [ + "all", + [ + "==", + "class", + "national_park" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "stops": [ + [ + 8, + "rgba(234, 241, 233, 0.5)" + ], + [ + 9, + "rgba(234, 241, 233, 0.5)" + ], + [ + 11, + "rgba(234, 241, 233, 0.5)" + ], + [ + 13, + "rgba(234, 241, 233, 0.5)" + ], + [ + 15, + "rgba(234, 241, 233, 0.5)" + ] + ] + }, + "fill-opacity": 1, + "fill-translate-anchor": "map" + } + }, + { + "id": "park_nature_reserve", + "type": "fill", + "source": "carto", + "source-layer": "park", + "minzoom": 0, + "filter": [ + "all", + [ + "==", + "class", + "nature_reserve" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "stops": [ + [ + 8, + "rgba(234, 241, 233, 0.5)" + ], + [ + 9, + "rgba(234, 241, 233, 0.5)" + ], + [ + 11, + "rgba(234, 241, 233, 0.5)" + ], + [ + 13, + "rgba(234, 241, 233, 0.5)" + ], + [ + 15, + "rgba(234, 241, 233, 0.5)" + ] + ] + }, + "fill-antialias": true, + "fill-opacity": { + "stops": [ + [ + 6, + 0.7 + ], + [ + 9, + 0.9 + ] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "carto", + "source-layer": "landuse", + "minzoom": 6, + "filter": [ + "any", + [ + "==", + "class", + "residential" + ] + ], + "paint": { + "fill-color": { + "stops": [ + [ + 5, + "rgba(237, 237, 237, 0.5)" + ], + [ + 8, + "rgba(237, 237, 237, 0.45)" + ], + [ + 9, + "rgba(237, 237, 237, 0.4)" + ], + [ + 11, + "rgba(237, 237, 237, 0.35)" + ], + [ + 13, + "rgba(237, 237, 237, 0.3)" + ], + [ + 15, + "rgba(237, 237, 237, 0.25)" + ], + [ + 16, + "rgba(237, 237, 237, 0.25)" + ] + ] + }, + "fill-opacity": { + "stops": [ + [ + 6, + 0.6 + ], + [ + 9, + 1 + ] + ] + } + } + }, + { + "id": "landuse", + "type": "fill", + "source": "carto", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "class", + "cemetery" + ], + [ + "==", + "class", + "stadium" + ] + ], + "paint": { + "fill-color": { + "stops": [ + [ + 8, + "rgba(234, 241, 233, 0.5)" + ], + [ + 9, + "rgba(234, 241, 233, 0.5)" + ], + [ + 11, + "rgba(234, 241, 233, 0.5)" + ], + [ + 13, + "rgba(234, 241, 233, 0.5)" + ], + [ + 15, + "rgba(234, 241, 233, 0.5)" + ] + ] + } + } + }, + { + "id": "waterway", + "type": "line", + "source": "carto", + "source-layer": "waterway", + "paint": { + "line-color": "#d1dbdf", + "line-width": { + "stops": [ + [ + 8, + 0.5 + ], + [ + 9, + 1 + ], + [ + 15, + 2 + ], + [ + 16, + 3 + ] + ] + } + } + }, + { + "id": "boundary_county", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 9, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "admin_level", + 6 + ], + [ + "==", + "maritime", + 0 + ] + ], + "paint": { + "line-color": { + "stops": [ + [ + 4, + "#ead5d7" + ], + [ + 5, + "#ead5d7" + ], + [ + 6, + "#e1c5c7" + ] + ] + }, + "line-width": { + "stops": [ + [ + 4, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-dasharray": { + "stops": [ + [ + 6, + [ + 1 + ] + ], + [ + 7, + [ + 2, + 2 + ] + ] + ] + } + } + }, + { + "id": "boundary_state", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 4, + "filter": [ + "all", + [ + "==", + "admin_level", + 4 + ], + [ + "==", + "maritime", + 0 + ] + ], + "paint": { + "line-color": { + "stops": [ + [ + 4, + "#ead5d7" + ], + [ + 5, + "#ead5d7" + ], + [ + 6, + "#e1c5c7" + ] + ] + }, + "line-width": { + "stops": [ + [ + 4, + 0.5 + ], + [ + 7, + 1 + ], + [ + 8, + 1 + ], + [ + 9, + 1.2 + ] + ] + }, + "line-dasharray": { + "stops": [ + [ + 6, + [ + 1 + ] + ], + [ + 7, + [ + 2, + 2 + ] + ] + ] + } + } + }, + { + "id": "water", + "type": "fill", + "source": "carto", + "source-layer": "water", + "minzoom": 0, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#d4dadc", + "fill-antialias": true, + "fill-translate-anchor": "map", + "fill-opacity": 1 + } + }, + { + "id": "water_shadow", + "type": "fill", + "source": "carto", + "source-layer": "water", + "minzoom": 0, + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "transparent", + "fill-antialias": true, + "fill-translate-anchor": "map", + "fill-opacity": 1, + "fill-translate": { + "stops": [ + [ + 0, + [ + 0, + 2 + ] + ], + [ + 6, + [ + 0, + 1 + ] + ], + [ + 14, + [ + 0, + 1 + ] + ], + [ + 17, + [ + 0, + 2 + ] + ] + ] + } + } + }, + { + "id": "aeroway-runway", + "type": "line", + "source": "carto", + "source-layer": "aeroway", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "class", + "runway" + ] + ], + "layout": { + "line-cap": "square" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ] + ] + }, + "line-color": "#e8e8e8" + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "source": "carto", + "source-layer": "aeroway", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "class", + "taxiway" + ] + ], + "paint": { + "line-color": "#e8e8e8", + "line-width": { + "stops": [ + [ + 13, + 0.5 + ], + [ + 14, + 1 + ], + [ + 15, + 2 + ], + [ + 16, + 4 + ] + ] + } + } + }, + { + "id": "waterway_label", + "type": "symbol", + "source": "carto", + "source-layer": "waterway", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "==", + "class", + "river" + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "symbol-placement": "line", + "symbol-spacing": 300, + "symbol-avoid-edges": false, + "text-size": { + "stops": [ + [ + 9, + 8 + ], + [ + 10, + 9 + ] + ] + }, + "text-padding": 2, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-offset": { + "stops": [ + [ + 6, + [ + 0, + -0.2 + ] + ], + [ + 11, + [ + 0, + -0.4 + ] + ], + [ + 12, + [ + 0, + -0.6 + ] + ] + ] + }, + "text-letter-spacing": 0, + "text-keep-upright": true + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "tunnel_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 1 + ], + [ + 16, + 3 + ], + [ + 17, + 6 + ], + [ + 18, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 0.5 + ], + [ + 14, + 2 + ], + [ + 15, + 4 + ], + [ + 16, + 6 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_sec_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 5 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_pri_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 8, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_trunk_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_mot_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 12, + 4 + ], + [ + 13, + 5 + ], + [ + 14, + 7 + ], + [ + 15, + 9 + ], + [ + 16, + 11 + ], + [ + 17, + 13 + ], + [ + 18, + 22 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "path" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 16, + 1 + ], + [ + 18, + 3 + ] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [ + 15, + [ + 2, + 2 + ] + ], + [ + 18, + [ + 3, + 3 + ] + ] + ] + } + } + }, + { + "id": "tunnel_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 2 + ], + [ + 16, + 2 + ], + [ + 17, + 4 + ], + [ + 18, + 6 + ] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 3 + ], + [ + 16, + 4 + ], + [ + 17, + 8 + ], + [ + 18, + 12 + ] + ] + }, + "line-opacity": 1, + "line-color": "rgba(238, 238, 238, 1)" + } + }, + { + "id": "tunnel_sec_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 2 + ], + [ + 13, + 2 + ], + [ + 14, + 3 + ], + [ + 15, + 4 + ], + [ + 16, + 6 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_pri_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_trunk_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_mot_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 10, + 1 + ], + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 5 + ], + [ + 15, + 7 + ], + [ + 16, + 9 + ], + [ + 17, + 11 + ], + [ + 18, + 20 + ] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_rail", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#dddddd", + "line-width": { + "base": 1.3, + "stops": [ + [ + 13, + 0.5 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 16, + 3 + ], + [ + 21, + 7 + ] + ] + }, + "line-opacity": 0.5 + } + }, + { + "id": "tunnel_rail_dash", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "==", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.3, + "stops": [ + [ + 15, + 0.5 + ], + [ + 16, + 1 + ], + [ + 20, + 5 + ] + ] + }, + "line-dasharray": { + "stops": [ + [ + 15, + [ + 5, + 5 + ] + ], + [ + 16, + [ + 6, + 6 + ] + ] + ] + }, + "line-opacity": 0.5 + } + }, + { + "id": "road_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 1 + ], + [ + 16, + 3 + ], + [ + 17, + 6 + ], + [ + 18, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "road_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 0.5 + ], + [ + 14, + 2 + ], + [ + 15, + 3 + ], + [ + 16, + 4.3 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 13, + "#e6e6e6" + ], + [ + 15.7, + "#e6e6e6" + ], + [ + 16, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_pri_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 4 + ], + [ + 15, + 5 + ], + [ + 16, + 8 + ], + [ + 17, + 10 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "road_trunk_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 4 + ], + [ + 15, + 5 + ], + [ + 16, + 8 + ], + [ + 17, + 10 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 12, + "#e6e6e6" + ], + [ + 14, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_mot_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 4 + ], + [ + 15, + 5 + ], + [ + 16, + 8 + ], + [ + 17, + 10 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 12, + "#e6e6e6" + ], + [ + 14, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_sec_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 1.5 + ], + [ + 13, + 3 + ], + [ + 14, + 5 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 11, + "#e6e6e6" + ], + [ + 12.99, + "#e6e6e6" + ], + [ + 13, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_pri_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 7, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 7, + "#e6e6e6" + ], + [ + 12, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_trunk_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 5, + "#e6e6e6" + ], + [ + 12, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_mot_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.7 + ], + [ + 8, + 0.8 + ], + [ + 11, + 3 + ], + [ + 12, + 4 + ], + [ + 13, + 5 + ], + [ + 14, + 7 + ], + [ + 15, + 9 + ], + [ + 16, + 11 + ], + [ + 17, + 13 + ], + [ + 18, + 22 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 5, + "#e6e6e6" + ], + [ + 12, + "#ddd" + ] + ] + } + } + }, + { + "id": "road_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "path", + "track" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 16, + 1 + ], + [ + 18, + 3 + ] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [ + 15, + [ + 2, + 2 + ] + ], + [ + 18, + [ + 3, + 3 + ] + ] + ] + } + } + }, + { + "id": "road_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 2 + ], + [ + 16, + 2 + ], + [ + 17, + 4 + ], + [ + 18, + 6 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "road_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 3 + ], + [ + 16, + 4 + ], + [ + 17, + 8 + ], + [ + 18, + 12 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "road_pri_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 1 + ], + [ + 13, + 1.5 + ], + [ + 14, + 2 + ], + [ + 15, + 3 + ], + [ + 16, + 6 + ], + [ + 17, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_trunk_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "square", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 1 + ], + [ + 13, + 1.5 + ], + [ + 14, + 2 + ], + [ + 15, + 3 + ], + [ + 16, + 6 + ], + [ + 17, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_mot_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "==", + "ramp", + 1 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 12, + 1 + ], + [ + 13, + 1.5 + ], + [ + 14, + 2 + ], + [ + 15, + 3 + ], + [ + 16, + 6 + ], + [ + 17, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_sec_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 2 + ], + [ + 13, + 2 + ], + [ + 14, + 3 + ], + [ + 15, + 4 + ], + [ + 16, + 6 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_pri_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 10, + 0.3 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_trunk_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_mot_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "!has", + "brunnel" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 10, + 1 + ], + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 5 + ], + [ + 15, + 7 + ], + [ + 16, + 9 + ], + [ + 17, + 11 + ], + [ + 18, + 20 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "rail", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#dddddd", + "line-width": { + "base": 1.3, + "stops": [ + [ + 13, + 0.5 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ], + [ + 16, + 3 + ], + [ + 21, + 7 + ] + ] + } + } + }, + { + "id": "rail_dash", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "class", + "rail" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.3, + "stops": [ + [ + 15, + 0.5 + ], + [ + 16, + 1 + ], + [ + 20, + 5 + ] + ] + }, + "line-dasharray": { + "stops": [ + [ + 15, + [ + 5, + 5 + ] + ], + [ + 16, + [ + 6, + 6 + ] + ] + ] + } + } + }, + { + "id": "bridge_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 1 + ], + [ + 16, + 3 + ], + [ + 17, + 6 + ], + [ + 18, + 8 + ] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "bridge_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 0.5 + ], + [ + 14, + 2 + ], + [ + 15, + 3 + ], + [ + 16, + 4.3 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 13, + "#e6e6e6" + ], + [ + 15.7, + "#e6e6e6" + ], + [ + 16, + "#ddd" + ] + ] + } + } + }, + { + "id": "bridge_sec_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 0.5 + ], + [ + 12, + 1.5 + ], + [ + 13, + 3 + ], + [ + 14, + 5 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [ + 11, + "#e6e6e6" + ], + [ + 12.99, + "#e6e6e6" + ], + [ + 13, + "#ddd" + ] + ] + } + } + }, + { + "id": "bridge_pri_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 8, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 8, + "#e6e6e6" + ], + [ + 12, + "#ddd" + ] + ] + } + } + }, + { + "id": "bridge_trunk_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 13, + 4 + ], + [ + 14, + 6 + ], + [ + 15, + 8 + ], + [ + 16, + 10 + ], + [ + 17, + 14 + ], + [ + 18, + 18 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 5, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 5, + "#e6e6e6" + ], + [ + 12, + "#ddd" + ] + ] + } + } + }, + { + "id": "bridge_mot_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 0.8 + ], + [ + 8, + 1 + ], + [ + 11, + 3 + ], + [ + 12, + 4 + ], + [ + 13, + 5 + ], + [ + 14, + 7 + ], + [ + 15, + 9 + ], + [ + 16, + 11 + ], + [ + 17, + 13 + ], + [ + 18, + 22 + ] + ] + }, + "line-opacity": { + "stops": [ + [ + 6, + 0.5 + ], + [ + 7, + 1 + ] + ] + }, + "line-color": { + "stops": [ + [ + 5, + "#e6e6e6" + ], + [ + 10, + "#ddd" + ] + ] + } + } + }, + { + "id": "bridge_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "path" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 0.5 + ], + [ + 16, + 1 + ], + [ + 18, + 3 + ] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [ + 15, + [ + 2, + 2 + ] + ], + [ + 18, + [ + 3, + 3 + ] + ] + ] + } + } + }, + { + "id": "bridge_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "service" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 2 + ], + [ + 16, + 2 + ], + [ + 17, + 4 + ], + [ + 18, + 6 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "bridge_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "minor" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 15, + 3 + ], + [ + 16, + 4 + ], + [ + 17, + 8 + ], + [ + 18, + 12 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "bridge_sec_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 2 + ], + [ + 13, + 2 + ], + [ + 14, + 3 + ], + [ + 15, + 4 + ], + [ + 16, + 6 + ], + [ + 17, + 10 + ], + [ + 18, + 14 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_pri_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "primary" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_trunk_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "trunk" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [ + 11, + 1 + ], + [ + 13, + 2 + ], + [ + 14, + 4 + ], + [ + 15, + 6 + ], + [ + 16, + 8 + ], + [ + 17, + 12 + ], + [ + 18, + 16 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_mot_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "class", + "motorway" + ], + [ + "!=", + "ramp", + 1 + ], + [ + "==", + "brunnel", + "bridge" + ] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [ + 10, + 1 + ], + [ + 12, + 2 + ], + [ + 13, + 3 + ], + [ + 14, + 5 + ], + [ + 15, + 7 + ], + [ + 16, + 9 + ], + [ + 17, + 11 + ], + [ + 18, + 20 + ] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "building", + "type": "fill", + "source": "carto", + "source-layer": "building", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "base": 1, + "stops": [ + [ + 15.5, + "#dfdfdf" + ], + [ + 16, + "#dfdfdf" + ] + ] + }, + "fill-antialias": true + } + }, + { + "id": "building-top", + "type": "fill", + "source": "carto", + "source-layer": "building", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-translate": { + "base": 1, + "stops": [ + [ + 14, + [ + 0, + 0 + ] + ], + [ + 16, + [ + -2, + -2 + ] + ] + ] + }, + "fill-outline-color": "#dfdfdf", + "fill-color": "#ededed", + "fill-opacity": { + "base": 1, + "stops": [ + [ + 13, + 0 + ], + [ + 16, + 1 + ] + ] + } + } + }, + { + "id": "boundary_country_outline", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 6, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "==", + "maritime", + 0 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "#f3efed", + "line-opacity": 0.5, + "line-width": 8, + "line-offset": 0 + } + }, + { + "id": "boundary_country_inner", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 0, + "filter": [ + "all", + [ + "==", + "admin_level", + 2 + ], + [ + "==", + "maritime", + 0 + ] + ], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": { + "stops": [ + [ + 4, + "#f2e6e7" + ], + [ + 5, + "#ebd6d8" + ], + [ + 6, + "#ebd6d8" + ] + ] + }, + "line-opacity": 1, + "line-width": { + "stops": [ + [ + 3, + 1 + ], + [ + 6, + 1.5 + ] + ] + }, + "line-offset": 0 + } + }, + { + "id": "watername_ocean", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 0, + "maxzoom": 5, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "ocean" + ] + ], + "layout": { + "text-field": "{name}", + "symbol-placement": "point", + "text-size": { + "stops": [ + [ + 0, + 13 + ], + [ + 2, + 14 + ], + [ + 4, + 18 + ] + ] + }, + "text-font": [ + "Montserrat Medium Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-max-width": 6, + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#abb6be", + "text-halo-color": "#d4dadc", + "text-halo-width": 1, + "text-halo-blur": 0 + } + }, + { + "id": "watername_sea", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 5, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "sea" + ] + ], + "layout": { + "text-field": "{name}", + "symbol-placement": "point", + "text-size": 12, + "text-font": [ + "Montserrat Medium Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-max-width": 6, + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#abb6be", + "text-halo-color": "#d4dadc", + "text-halo-width": 1, + "text-halo-blur": 0 + } + }, + { + "id": "watername_lake", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 4, + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "lake" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "symbol-placement": "point", + "text-size": { + "stops": [ + [ + 13, + 9 + ], + [ + 14, + 10 + ], + [ + 15, + 11 + ], + [ + 16, + 12 + ], + [ + 17, + 13 + ] + ] + }, + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto" + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "watername_lake_line", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "filter": [ + "all", + [ + "has", + "name" + ], + [ + "==", + "$type", + "LineString" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "symbol-placement": "line", + "text-size": { + "stops": [ + [ + 13, + 9 + ], + [ + 14, + 10 + ], + [ + 15, + 11 + ], + [ + 16, + 12 + ], + [ + 17, + 13 + ] + ] + }, + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "symbol-spacing": 350, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-line-height": 1.2 + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "place_hamlet", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 12, + "maxzoom": 16, + "filter": [ + "any", + [ + "==", + "class", + "neighbourhood" + ], + [ + "==", + "class", + "hamlet" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 14, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 13, + 8 + ], + [ + 14, + 10 + ], + [ + 16, + 11 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": { + "stops": [ + [ + 12, + "none" + ], + [ + 14, + "uppercase" + ] + ] + } + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_suburbs", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 12, + "maxzoom": 16, + "filter": [ + "all", + [ + "==", + "class", + "suburb" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 12, + 9 + ], + [ + 13, + 10 + ], + [ + 14, + 11 + ], + [ + 15, + 12 + ], + [ + 16, + 13 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": { + "stops": [ + [ + 8, + "none" + ], + [ + 12, + "uppercase" + ] + ] + } + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_villages", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 10, + "maxzoom": 16, + "filter": [ + "all", + [ + "==", + "class", + "village" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 10, + 9 + ], + [ + 12, + 10 + ], + [ + 13, + 11 + ], + [ + 14, + 12 + ], + [ + 16, + 13 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": "none" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "class", + "town" + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 8, + 10 + ], + [ + 9, + 10 + ], + [ + 10, + 11 + ], + [ + 13, + 14 + ], + [ + 14, + 15 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": "none" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_country_2", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 3, + "maxzoom": 10, + "filter": [ + "all", + [ + "==", + "class", + "country" + ], + [ + ">=", + "rank", + 3 + ], + [ + "has", + "iso_a2" + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 3, + 10 + ], + [ + 5, + 11 + ], + [ + 6, + 12 + ], + [ + 7, + 13 + ], + [ + 8, + 14 + ] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": { + "stops": [ + [ + 3, + "#8a99a4" + ], + [ + 5, + "#a1adb6" + ], + [ + 6, + "#b9c2c9" + ] + ] + }, + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_country_1", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 2, + "maxzoom": 7, + "filter": [ + "all", + [ + "==", + "class", + "country" + ], + [ + "<=", + "rank", + 2 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 3, + 11 + ], + [ + 4, + 12 + ], + [ + 5, + 13 + ], + [ + 6, + 14 + ] + ] + }, + "text-transform": "uppercase", + "text-max-width": { + "stops": [ + [ + 2, + 6 + ], + [ + 3, + 6 + ], + [ + 4, + 9 + ], + [ + 5, + 12 + ] + ] + } + }, + "paint": { + "text-color": { + "stops": [ + [ + 3, + "#8a99a4" + ], + [ + 5, + "#a1adb6" + ], + [ + 6, + "#b9c2c9" + ] + ] + }, + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_state", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 10, + "filter": [ + "all", + [ + "==", + "class", + "state" + ], + [ + "<=", + "rank", + 4 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 5, + 12 + ], + [ + 7, + 14 + ] + ] + }, + "text-transform": "uppercase", + "text-max-width": 9 + }, + "paint": { + "text-color": "#97a4ae", + "text-halo-color": "#fafaf8", + "text-halo-width": 0 + } + }, + { + "id": "place_continent", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 0, + "maxzoom": 2, + "filter": [ + "all", + [ + "==", + "class", + "continent" + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-transform": "uppercase", + "text-size": 14, + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-justify": "center", + "text-keep-upright": false + }, + "paint": { + "text-color": "#697b89", + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_city_r6", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "class", + "city" + ], + [ + ">=", + "rank", + 6 + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 8, + 12 + ], + [ + 9, + 13 + ], + [ + 10, + 14 + ], + [ + 13, + 17 + ], + [ + 14, + 20 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_r5", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "class", + "city" + ], + [ + ">=", + "rank", + 0 + ], + [ + "<=", + "rank", + 5 + ] + ], + "layout": { + "text-field": { + "stops": [ + [ + 8, + "{name_en}" + ], + [ + 13, + "{name}" + ] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 8, + 14 + ], + [ + 10, + 16 + ], + [ + 13, + 19 + ], + [ + 14, + 22 + ] + ] + }, + "icon-image": "", + "icon-offset": [ + 16, + 0 + ], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 6, + "maxzoom": 7, + "filter": [ + "all", + [ + "==", + "class", + "city" + ], + [ + "<=", + "rank", + 7 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [ + 16, + 5 + ], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r4", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 7, + "filter": [ + "all", + [ + "==", + "class", + "city" + ], + [ + "<=", + "rank", + 4 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [ + 16, + 5 + ], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r2", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 4, + "maxzoom": 7, + "filter": [ + "all", + [ + "==", + "class", + "city" + ], + [ + "<=", + "rank", + 2 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [ + 16, + 5 + ], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_z7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 7, + "maxzoom": 8, + "filter": [ + "all", + [ + "!has", + "capital" + ], + [ + "!in", + "class", + "country", + "state" + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [ + 16, + 5 + ], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_capital_dot_z7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 7, + "maxzoom": 8, + "filter": [ + "all", + [ + ">", + "capital", + 0 + ] + ], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [ + 16, + 5 + ], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [ + 0.2, + 0.2 + ], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "poi_stadium", + "type": "symbol", + "source": "carto", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + [ + "in", + "class", + "stadium", + "cemetery", + "attraction" + ], + [ + "<=", + "rank", + 3 + ] + ], + "layout": { + "text-field": "{name}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 15, + 8 + ], + [ + 17, + 9 + ], + [ + 18, + 10 + ] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#7d9c83", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "poi_park", + "type": "symbol", + "source": "carto", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + [ + "==", + "class", + "park" + ] + ], + "layout": { + "text-field": "{name}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 15, + 8 + ], + [ + 17, + 9 + ], + [ + 18, + 10 + ] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#7d9c83", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "roadname_minor", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 16, + "filter": [ + "all", + [ + "in", + "class", + "minor", + "service" + ] + ], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 9, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": 200, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center" + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_sec", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 15, + "filter": [ + "all", + [ + "in", + "class", + "secondary", + "tertiary" + ] + ], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 15, + 9 + ], + [ + 16, + 11 + ], + [ + 18, + 12 + ] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": 200, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center" + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_pri", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 14, + "filter": [ + "all", + [ + "in", + "class", + "primary" + ] + ], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 14, + 10 + ], + [ + 15, + 10 + ], + [ + 16, + 11 + ], + [ + 18, + 12 + ] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": { + "stops": [ + [ + 6, + 200 + ], + [ + 16, + 250 + ] + ] + }, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center", + "text-letter-spacing": { + "stops": [ + [ + 14, + 0 + ], + [ + 16, + 0.2 + ] + ] + } + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_major", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 13, + "filter": [ + "all", + [ + "in", + "class", + "trunk", + "motorway" + ] + ], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [ + 14, + 10 + ], + [ + 15, + 10 + ], + [ + 16, + 11 + ], + [ + 18, + 12 + ] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": { + "stops": [ + [ + 6, + 200 + ], + [ + 16, + 250 + ] + ] + }, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center", + "text-letter-spacing": { + "stops": [ + [ + 13, + 0 + ], + [ + 16, + 0.2 + ] + ] + } + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "housenumber", + "type": "symbol", + "source": "carto", + "source-layer": "housenumber", + "minzoom": 17, + "maxzoom": 24, + "layout": { + "text-field": "{housenumber}", + "text-size": { + "stops": [ + [ + 17, + 9 + ], + [ + 18, + 11 + ] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ] + }, + "paint": { + "text-halo-color": "transparent", + "text-color": "transparent", + "text-halo-width": 0.75 + } + } + ], + "id": "voyager", + "owner": "Carto" +} diff --git a/client/src/components/map/styles/map-style-satellite-maplibre.json b/client/src/components/map/styles/map-style-satellite-maplibre.json new file mode 100644 index 000000000..c178f288a --- /dev/null +++ b/client/src/components/map/styles/map-style-satellite-maplibre.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "name": "ESRI - World Imagery", + "metadata": { + + }, + "sources": { + "esri": { + "type": "raster", + "tiles": ["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"], + "attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + } + }, + "sprite": "https://tiles.basemaps.cartocdn.com/gl/positron-gl-style/sprite", + "glyphs": "https://tiles.basemaps.cartocdn.com/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "layout": { + "visibility": "visible" + }, + "paint": { + "background-color": "#f9f5f3", + "background-opacity": 1 + } + }, + { + "id": "esri-world-imagery", + "type": "raster", + "source": "esri", + "minzoom": 2 + }, + { + "id": "custom-layers", + "type": "background", + "paint": { + "background-color": "#000", + "background-opacity": 0 + } + } + ], + "id": "esri-world-imagery" +} diff --git a/client/src/components/map/types.d.ts b/client/src/components/map/types.d.ts index b3070cf01..2e19a87a3 100644 --- a/client/src/components/map/types.d.ts +++ b/client/src/components/map/types.d.ts @@ -1,11 +1,11 @@ import type { StyleIds } from './constants'; -import type { ViewState, MapProps, FitBoundsOptions, MapboxMap } from 'react-map-gl'; +import type { ViewState, MapProps, FitBoundsOptions, MapRef } from 'react-map-gl/maplibre'; export type MapStyle = keyof typeof StyleIds; export interface CustomMapProps extends MapProps { /** A function that returns the map instance */ - children?: (map: MapboxMap) => React.ReactNode; + children?: (map: typeof MapRef.getMap) => React.ReactNode; /** Custom css class for styling */ className?: string; diff --git a/client/src/containers/analysis-visualization/analysis-map/component.tsx b/client/src/containers/analysis-visualization/analysis-map/component.tsx index ac76f83bc..333b48148 100644 --- a/client/src/containers/analysis-visualization/analysis-map/component.tsx +++ b/client/src/containers/analysis-visualization/analysis-map/component.tsx @@ -18,7 +18,7 @@ import { getLayerConfig } from 'components/map/layers/utils'; import type { LayerConstructor } from 'components/map/layers/utils'; import type { H3HexagonLayerProps } from '@deck.gl/geo-layers/typed'; -import type { ViewState } from 'react-map-gl'; +import type { ViewState } from 'react-map-gl/maplibre'; import type { MapStyle } from 'components/map/types'; import type { BasemapValue } from 'components/map/controls/basemap/types'; import type { Layer, Legend as LegendType } from 'types'; @@ -105,7 +105,6 @@ const AnalysisMap = () => { mapStyle={mapStyle} viewState={viewState} onMapViewStateChange={handleViewState} - // style={{ width}} sidebarCollapsed={isSidebarCollapsed} > {() => ( diff --git a/client/src/containers/analysis-visualization/analysis-map/layers/contextual/index.tsx b/client/src/containers/analysis-visualization/analysis-map/layers/contextual/index.tsx index a6c0acfc8..6db68695d 100644 --- a/client/src/containers/analysis-visualization/analysis-map/layers/contextual/index.tsx +++ b/client/src/containers/analysis-visualization/analysis-map/layers/contextual/index.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { ContextualDeckLayer } from './hooks'; -import MapboxRasterLayer from 'components/map/layers/mapbox/raster'; +import MapboxRasterLayer from 'components/map/layers/maplibre/raster'; import useContextualLayers from 'hooks/layers/getContextualLayers'; import type { LayerSettings, LayerProps } from 'components/map/layers/types'; diff --git a/client/src/pages/_document.tsx b/client/src/pages/_document.tsx index 124bfb358..4a8324adc 100644 --- a/client/src/pages/_document.tsx +++ b/client/src/pages/_document.tsx @@ -17,7 +17,7 @@ class MyDocument extends Document { content="Manage food and agriculture supply chain deforestation, climate, water risk and sustainability" /> - + {/* Fonts */} diff --git a/client/src/pages/analysis/map.tsx b/client/src/pages/analysis/map.tsx index 91082e11e..c9894974c 100644 --- a/client/src/pages/analysis/map.tsx +++ b/client/src/pages/analysis/map.tsx @@ -1,4 +1,4 @@ -import { MapProvider } from 'react-map-gl'; +import { MapProvider } from 'react-map-gl/maplibre'; import useEffectOnce from 'hooks/once'; import { setVisualizationMode } from 'store/features/analysis'; diff --git a/client/yarn.lock b/client/yarn.lock index 2ff8fa2fc..4650448ed 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1239,20 +1239,13 @@ __metadata: languageName: node linkType: hard -"@mapbox/jsonlint-lines-primitives@npm:^2.0.2": +"@mapbox/jsonlint-lines-primitives@npm:^2.0.2, @mapbox/jsonlint-lines-primitives@npm:~2.0.2": version: 2.0.2 resolution: "@mapbox/jsonlint-lines-primitives@npm:2.0.2" checksum: 4eb31edd3ccff530f7b687ddc6d813d6e24fc66e9a563460882e7861b49f9331c5ded6fd7e927b37affbbd98f83bff1f7b916119044f1931df03c6ffedba2cfb languageName: node linkType: hard -"@mapbox/mapbox-gl-supported@npm:^2.0.1": - version: 2.0.1 - resolution: "@mapbox/mapbox-gl-supported@npm:2.0.1" - checksum: 1dffc96baacc56e34b09f2ae6ac4b2b8f10ad51a9d7c4946dc2057f456deeacd5d0729e340120857b3204a14d9961266f3218d26e101d46e81717954f1c129af - languageName: node - linkType: hard - "@mapbox/martini@npm:^0.2.0": version: 0.2.0 resolution: "@mapbox/martini@npm:0.2.0" @@ -1304,6 +1297,24 @@ __metadata: languageName: node linkType: hard +"@maplibre/maplibre-gl-style-spec@npm:^19.2.1, @maplibre/maplibre-gl-style-spec@npm:^19.3.3": + version: 19.3.3 + resolution: "@maplibre/maplibre-gl-style-spec@npm:19.3.3" + dependencies: + "@mapbox/jsonlint-lines-primitives": ~2.0.2 + "@mapbox/unitbezier": ^0.0.1 + json-stringify-pretty-compact: ^3.0.0 + minimist: ^1.2.8 + rw: ^1.3.3 + sort-object: ^3.0.3 + bin: + gl-style-format: dist/gl-style-format.mjs + gl-style-migrate: dist/gl-style-migrate.mjs + gl-style-validate: dist/gl-style-validate.mjs + checksum: b83f539f0df6cb6e4b06d78b05a6bc67e17a31d0999a3faf6fe0585e0b934b31ea5932ed1cf9773249a3a557cf5caf35221b7e7ca65e5c74737eff799f80d386 + languageName: node + linkType: hard + "@math.gl/core@npm:3.6.3, @math.gl/core@npm:^3.5.0, @math.gl/core@npm:^3.5.1, @math.gl/core@npm:^3.6.2": version: 3.6.3 resolution: "@math.gl/core@npm:3.6.3" @@ -1857,6 +1868,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:^7946.0.13": + version: 7946.0.14 + resolution: "@types/geojson@npm:7946.0.14" + checksum: ae511bee6488ae3bd5a3a3347aedb0371e997b14225b8983679284e22fa4ebd88627c6e3ff8b08bf4cc35068cb29310c89427311ffc9322c255615821a922e71 + languageName: node + linkType: hard + "@types/hammerjs@npm:^2.0.41": version: 2.0.41 resolution: "@types/hammerjs@npm:2.0.41" @@ -1918,7 +1936,16 @@ __metadata: languageName: node linkType: hard -"@types/mapbox-gl@npm:^2.6.0, @types/mapbox-gl@npm:^2.6.3": +"@types/mapbox-gl@npm:>=1.0.0": + version: 2.7.21 + resolution: "@types/mapbox-gl@npm:2.7.21" + dependencies: + "@types/geojson": "*" + checksum: 32ab29723a4fa4a9ea966a985160226c9f07bbbd9dc30c80b3558032dd68c4ea25f06f199a1a7c1b27a7da74600b4823bd6b14199f53372776d5aea6b1a47ea9 + languageName: node + linkType: hard + +"@types/mapbox-gl@npm:^2.6.3": version: 2.7.10 resolution: "@types/mapbox-gl@npm:2.7.10" dependencies: @@ -1927,6 +1954,24 @@ __metadata: languageName: node linkType: hard +"@types/mapbox__point-geometry@npm:*, @types/mapbox__point-geometry@npm:^0.1.4": + version: 0.1.4 + resolution: "@types/mapbox__point-geometry@npm:0.1.4" + checksum: d315f3e396bebd40f1cab682595f3d1c5ac46c5ddb080cf65dfcd0401dc6a3f235a7ac9ada2d28e6c49485fa5f231458f29fee87069e42a137e20e5865801dd1 + languageName: node + linkType: hard + +"@types/mapbox__vector-tile@npm:^1.3.4": + version: 1.3.4 + resolution: "@types/mapbox__vector-tile@npm:1.3.4" + dependencies: + "@types/geojson": "*" + "@types/mapbox__point-geometry": "*" + "@types/pbf": "*" + checksum: 5715d9da88a5ecadb63e3ca4d52272ead2c1d63fcf616841932719788e458fc10dd9919ad01aa9c95b15c83e9074dae9ffc7193a7ae4ae7b8436d26630f0e269 + languageName: node + linkType: hard + "@types/node@npm:*": version: 18.7.6 resolution: "@types/node@npm:18.7.6" @@ -1955,6 +2000,13 @@ __metadata: languageName: node linkType: hard +"@types/pbf@npm:*, @types/pbf@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/pbf@npm:3.0.5" + checksum: 9115eb3cc61e535748dd6de98c7a8bd64e02a4052646796013b075fed66fd52a3a2aaae6b75648e9c0361e8ed462a50549ca0af1015e2e48296cd8c31bb54577 + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.5 resolution: "@types/prop-types@npm:15.7.5" @@ -2021,6 +2073,15 @@ __metadata: languageName: node linkType: hard +"@types/supercluster@npm:^7.1.3": + version: 7.1.3 + resolution: "@types/supercluster@npm:7.1.3" + dependencies: + "@types/geojson": "*" + checksum: 724188fb6ebdf0835821559da5480e5951c3e51afa86fcf83f5bf6984b89652f947081a3f6835cb082a6865fe5f1f8f667e92346f237d3518c2159121bb7c5cc + languageName: node + linkType: hard + "@types/use-sync-external-store@npm:^0.0.3": version: 0.0.3 resolution: "@types/use-sync-external-store@npm:0.0.3" @@ -2555,6 +2616,13 @@ __metadata: languageName: node linkType: hard +"arr-union@npm:^3.1.0": + version: 3.1.0 + resolution: "arr-union@npm:3.1.0" + checksum: b5b0408c6eb7591143c394f3be082fee690ddd21f0fdde0a0a01106799e847f67fcae1b7e56b0a0c173290e29c6aca9562e82b300708a268bc8f88f3d6613cb9 + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.0": version: 1.0.0 resolution: "array-buffer-byte-length@npm:1.0.0" @@ -2691,6 +2759,13 @@ __metadata: languageName: node linkType: hard +"assign-symbols@npm:^1.0.0": + version: 1.0.0 + resolution: "assign-symbols@npm:1.0.0" + checksum: c0eb895911d05b6b2d245154f70461c5e42c107457972e5ebba38d48967870dee53bcdf6c7047990586daa80fab8dab3cc6300800fbd47b454247fdedd859a2c + languageName: node + linkType: hard + "ast-types-flow@npm:^0.0.7": version: 0.0.7 resolution: "ast-types-flow@npm:0.0.7" @@ -2961,6 +3036,25 @@ __metadata: languageName: node linkType: hard +"bytewise-core@npm:^1.2.2": + version: 1.2.3 + resolution: "bytewise-core@npm:1.2.3" + dependencies: + typewise-core: ^1.2 + checksum: e0d28fb7ff5bb6fd9320eef31c6b37e98da3b9a24d9893e2c17e0ee544457e0c76c2d3fc642c99d82daa0f18dcd49e7dce8dcc338711200e9ced79107cb78e8e + languageName: node + linkType: hard + +"bytewise@npm:^1.1.0": + version: 1.1.0 + resolution: "bytewise@npm:1.1.0" + dependencies: + bytewise-core: ^1.2.2 + typewise: ^1.0.3 + checksum: 20d7387ecf8c29adc4740e626fb02eaa27f34ae4c5ca881657d403e792730c0625ba4fed824462b3ddb7d3ebe41b7abbfe24f1cd3bf07cecc5a631f154d2d8d2 + languageName: node + linkType: hard + "cacache@npm:^16.1.0": version: 16.1.2 resolution: "cacache@npm:16.1.2" @@ -3385,13 +3479,6 @@ __metadata: languageName: node linkType: hard -"csscolorparser@npm:~1.0.3": - version: 1.0.3 - resolution: "csscolorparser@npm:1.0.3" - checksum: e40f3045ea15c7e7eaa78e110412fe8b820d47b698c1eb1d1e7ecb42703bf447406a24304b891ae9df61e85d947f33fc67bd0120c7f9e3a5183e6e0b9afff92c - languageName: node - linkType: hard - "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -4664,6 +4751,25 @@ __metadata: languageName: node linkType: hard +"extend-shallow@npm:^2.0.1": + version: 2.0.1 + resolution: "extend-shallow@npm:2.0.1" + dependencies: + is-extendable: ^0.1.0 + checksum: 8fb58d9d7a511f4baf78d383e637bd7d2e80843bd9cd0853649108ea835208fb614da502a553acc30208e1325240bb7cc4a68473021612496bb89725483656d8 + languageName: node + linkType: hard + +"extend-shallow@npm:^3.0.0": + version: 3.0.2 + resolution: "extend-shallow@npm:3.0.2" + dependencies: + assign-symbols: ^1.0.0 + is-extendable: ^1.0.1 + checksum: a920b0cd5838a9995ace31dfd11ab5e79bf6e295aa566910ce53dff19f4b1c0fda2ef21f26b28586c7a2450ca2b42d97bd8c0f5cec9351a819222bf861e02461 + languageName: node + linkType: hard + "extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -5207,6 +5313,13 @@ __metadata: languageName: node linkType: hard +"get-value@npm:^2.0.2, get-value@npm:^2.0.6": + version: 2.0.6 + resolution: "get-value@npm:2.0.6" + checksum: 5c3b99cb5398ea8016bf46ff17afc5d1d286874d2ad38ca5edb6e87d75c0965b0094cb9a9dddef2c59c23d250702323539a7fbdd870620db38c7e7d7ec87c1eb + languageName: node + linkType: hard + "getos@npm:^3.2.1": version: 3.2.1 resolution: "getos@npm:3.2.1" @@ -5328,6 +5441,17 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^3.0.0": + version: 3.0.0 + resolution: "global-prefix@npm:3.0.0" + dependencies: + ini: ^1.3.5 + kind-of: ^6.0.2 + which: ^1.3.1 + checksum: 8a82fc1d6f22c45484a4e34656cc91bf021a03e03213b0035098d605bfc612d7141f1e14a21097e8a0413b4884afd5b260df0b6a25605ce9d722e11f1df2881d + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -5433,13 +5557,6 @@ __metadata: languageName: node linkType: hard -"grid-index@npm:^1.1.0": - version: 1.1.0 - resolution: "grid-index@npm:1.1.0" - checksum: 0e9d427b606ac644a723719116bb067639c01dccc881f161525e8eddb13b2de3b8a274641ef6d926d7629877ad8ed06b45290d52dd2d8af45532c50ccbbefe43 - languageName: node - linkType: hard - "h3-js@npm:^3.7.0": version: 3.7.2 resolution: "h3-js@npm:3.7.2" @@ -5712,7 +5829,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:~1.3.0": +"ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 @@ -5882,6 +5999,22 @@ __metadata: languageName: node linkType: hard +"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1": + version: 0.1.1 + resolution: "is-extendable@npm:0.1.1" + checksum: 3875571d20a7563772ecc7a5f36cb03167e9be31ad259041b4a8f73f33f885441f778cee1f1fe0085eb4bc71679b9d8c923690003a36a6a5fdf8023e6e3f0672 + languageName: node + linkType: hard + +"is-extendable@npm:^1.0.1": + version: 1.0.1 + resolution: "is-extendable@npm:1.0.1" + dependencies: + is-plain-object: ^2.0.4 + checksum: db07bc1e9de6170de70eff7001943691f05b9d1547730b11be01c0ebfe67362912ba743cf4be6fd20a5e03b4180c685dad80b7c509fe717037e3eee30ad8e84f + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -5977,6 +6110,15 @@ __metadata: languageName: node linkType: hard +"is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": + version: 2.0.4 + resolution: "is-plain-object@npm:2.0.4" + dependencies: + isobject: ^3.0.1 + checksum: 2a401140cfd86cabe25214956ae2cfee6fbd8186809555cd0e84574f88de7b17abacb2e477a6a658fa54c6083ecbda1e6ae404c7720244cd198903848fca70ca + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -6120,6 +6262,13 @@ __metadata: languageName: node linkType: hard +"isobject@npm:^3.0.1": + version: 3.0.1 + resolution: "isobject@npm:3.0.1" + checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 + languageName: node + linkType: hard + "isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" @@ -6352,6 +6501,13 @@ __metadata: languageName: node linkType: hard +"json-stringify-pretty-compact@npm:^3.0.0": + version: 3.0.0 + resolution: "json-stringify-pretty-compact@npm:3.0.0" + checksum: 01ab5c5c8260299414868d96db97f53aef93c290fe469edd9a1363818e795006e01c952fa2fd7b47cbbab506d5768998eccc25e1da4fa2ccfebd1788c6098791 + languageName: node + linkType: hard + "json-stringify-safe@npm:~5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" @@ -6444,10 +6600,17 @@ __metadata: languageName: node linkType: hard -"kdbush@npm:^3.0.0": - version: 3.0.0 - resolution: "kdbush@npm:3.0.0" - checksum: bc5fa433958e42664a8a92457e4f0d1db55b3b8e36956aac0102964adb2eab043bdbff156570dc8d867144ceff588fb7a1c6e099ba9be068cd1767a73e1ace92 +"kdbush@npm:^4.0.2": + version: 4.0.2 + resolution: "kdbush@npm:4.0.2" + checksum: 6782ef2cdaec9322376b9955a16b0163beda0cefa2f87da76e8970ade2572d8b63bec915347aaeac609484b0c6e84d7b591f229ef353b68b460238095bacde2d + languageName: node + linkType: hard + +"kind-of@npm:^6.0.2": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b languageName: node linkType: hard @@ -6517,7 +6680,7 @@ __metadata: jsona: 1.9.2 lodash-es: 4.17.21 lottie-react: 2.4.0 - mapbox-gl: 2.13.0 + maplibre-gl: 3.6.2 next: 13.5.5 next-auth: 4.19.2 nyc: 15.1.0 @@ -6533,7 +6696,7 @@ __metadata: react-dropzone: 14.2.2 react-hook-form: 7.43.1 react-hot-toast: 2.2.0 - react-map-gl: 7.0.23 + react-map-gl: 7.1.7 react-range: 1.8.14 react-redux: 8.0.2 recharts: 2.9.0 @@ -6827,32 +6990,36 @@ __metadata: languageName: node linkType: hard -"mapbox-gl@npm:2.13.0": - version: 2.13.0 - resolution: "mapbox-gl@npm:2.13.0" +"maplibre-gl@npm:3.6.2": + version: 3.6.2 + resolution: "maplibre-gl@npm:3.6.2" dependencies: "@mapbox/geojson-rewind": ^0.5.2 "@mapbox/jsonlint-lines-primitives": ^2.0.2 - "@mapbox/mapbox-gl-supported": ^2.0.1 "@mapbox/point-geometry": ^0.1.0 "@mapbox/tiny-sdf": ^2.0.6 "@mapbox/unitbezier": ^0.0.1 "@mapbox/vector-tile": ^1.3.1 "@mapbox/whoots-js": ^3.1.0 - csscolorparser: ~1.0.3 + "@maplibre/maplibre-gl-style-spec": ^19.3.3 + "@types/geojson": ^7946.0.13 + "@types/mapbox__point-geometry": ^0.1.4 + "@types/mapbox__vector-tile": ^1.3.4 + "@types/pbf": ^3.0.5 + "@types/supercluster": ^7.1.3 earcut: ^2.2.4 geojson-vt: ^3.2.1 gl-matrix: ^3.4.3 - grid-index: ^1.1.0 + global-prefix: ^3.0.0 + kdbush: ^4.0.2 murmurhash-js: ^1.0.0 pbf: ^3.2.1 potpack: ^2.0.0 quickselect: ^2.0.0 - rw: ^1.3.3 - supercluster: ^7.1.5 + supercluster: ^8.0.1 tinyqueue: ^2.0.3 vt-pbf: ^3.1.3 - checksum: de0de328f31ee207295e150d6f715a4f6da4afbda02905f64928aaca5736995f61def941623c8390aa29d5425ddd72e540487fdb2e00c3421e425090d59d4204 + checksum: 039e53b2685932770f695982278e8b0ea55b98950175946c40510dbd36cff0672eb407616f5ba06b1ead652035328ee9ae9a392678b48703eec78cd939eff3fe languageName: node linkType: hard @@ -8404,15 +8571,23 @@ __metadata: languageName: node linkType: hard -"react-map-gl@npm:7.0.23": - version: 7.0.23 - resolution: "react-map-gl@npm:7.0.23" +"react-map-gl@npm:7.1.7": + version: 7.1.7 + resolution: "react-map-gl@npm:7.1.7" dependencies: - "@types/mapbox-gl": ^2.6.0 + "@maplibre/maplibre-gl-style-spec": ^19.2.1 + "@types/mapbox-gl": ">=1.0.0" peerDependencies: - mapbox-gl: "*" + mapbox-gl: ">=1.13.0" + maplibre-gl: ">=1.13.0" react: ">=16.3.0" - checksum: 55c771b95f517a4ff3b6f6ec1abb31d968e7467bfe7e45057e4dc2b5a6f9a5c89ee0717781827bd07dd2cc9a60cb2b5240f7e59a3a976d1607f4f26e878e7989 + react-dom: ">=16.3.0" + peerDependenciesMeta: + mapbox-gl: + optional: true + maplibre-gl: + optional: true + checksum: fa53bdcdf2d168a9735a7a9db80257acc206a7893a879e022cfa02ce0cd5cc662a470824968cb5a421a46e5db0f24b339a7fc50ecf98f70e39e787bdbaaed9b0 languageName: node linkType: hard @@ -9057,6 +9232,18 @@ __metadata: languageName: node linkType: hard +"set-value@npm:^2.0.1": + version: 2.0.1 + resolution: "set-value@npm:2.0.1" + dependencies: + extend-shallow: ^2.0.1 + is-extendable: ^0.1.1 + is-plain-object: ^2.0.3 + split-string: ^3.0.1 + checksum: 09a4bc72c94641aeae950eb60dc2755943b863780fcc32e441eda964b64df5e3f50603d5ebdd33394ede722528bd55ed43aae26e9df469b4d32e2292b427b601 + languageName: node + linkType: hard + "shallowequal@npm:^1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -9234,6 +9421,34 @@ __metadata: languageName: node linkType: hard +"sort-asc@npm:^0.2.0": + version: 0.2.0 + resolution: "sort-asc@npm:0.2.0" + checksum: b3610ab695dc8b2cba1c3e6ead06ce97a41f013ed0a002ff7a0d2a39ca297fd2f58c92d3de67dda3a9313ecb1073de4eacc30da3a740ff8d57eb668c9bb151bd + languageName: node + linkType: hard + +"sort-desc@npm:^0.2.0": + version: 0.2.0 + resolution: "sort-desc@npm:0.2.0" + checksum: fb2c02ea38815c79c0127d014f18926a473a1988c01f4c00de467584b99fc7e9f6e4f61c8386f4c2ac3501c60842931c5a499330b3086be6d8cff4d0b8602bed + languageName: node + linkType: hard + +"sort-object@npm:^3.0.3": + version: 3.0.3 + resolution: "sort-object@npm:3.0.3" + dependencies: + bytewise: ^1.1.0 + get-value: ^2.0.2 + is-extendable: ^0.1.1 + sort-asc: ^0.2.0 + sort-desc: ^0.2.0 + union-value: ^1.0.1 + checksum: 381a6b6fe2309d400bd6ae3a8d0188b2b3b3855345d16d953b4bb5875d28fd5512501c85bd4eb951543056cd3095ff8e197ab3efc11389dcfa0e3334bf4a23a5 + languageName: node + linkType: hard + "source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" @@ -9279,6 +9494,15 @@ __metadata: languageName: node linkType: hard +"split-string@npm:^3.0.1": + version: 3.1.0 + resolution: "split-string@npm:3.1.0" + dependencies: + extend-shallow: ^3.0.0 + checksum: ae5af5c91bdc3633628821bde92fdf9492fa0e8a63cf6a0376ed6afde93c701422a1610916f59be61972717070119e848d10dfbbd5024b7729d6a71972d2a84c + languageName: node + linkType: hard + "split2@npm:^4.0.0": version: 4.1.0 resolution: "split2@npm:4.1.0" @@ -9568,12 +9792,12 @@ __metadata: languageName: node linkType: hard -"supercluster@npm:^7.1.5": - version: 7.1.5 - resolution: "supercluster@npm:7.1.5" +"supercluster@npm:^8.0.1": + version: 8.0.1 + resolution: "supercluster@npm:8.0.1" dependencies: - kdbush: ^3.0.0 - checksum: 69863238870093b96617135884721b6343746e14f396b2d67d6b55c52c362ec0516c5e386aa21815e75a9cef2054e831ac34023d0d8b600091d28cea0794f027 + kdbush: ^4.0.2 + checksum: 39d141f768a511efa53260252f9dab9a2ce0228b334e55482c8d3019e151932f05e1a9a0252d681737651b13c741c665542a6ddb40ec27de96159ea7ad41f7f4 languageName: node linkType: hard @@ -10101,6 +10325,22 @@ __metadata: languageName: node linkType: hard +"typewise-core@npm:^1.2, typewise-core@npm:^1.2.0": + version: 1.2.0 + resolution: "typewise-core@npm:1.2.0" + checksum: c21e83544546d1aba2f17377c25ae0eb571c2153b2e3705932515bef103dbe43e05d2286f238ad139341b1000da40583115a44cb5e69a2ef408572b13dab844b + languageName: node + linkType: hard + +"typewise@npm:^1.0.3": + version: 1.0.3 + resolution: "typewise@npm:1.0.3" + dependencies: + typewise-core: ^1.2.0 + checksum: eb3452b1387df8bf8e3b620720d240425a50ce402d7c064c21ac4b5d88c551ee4d1f26cd649b8a17a6d06f7a3675733de841723f8e06bb3edabfeacc4924af4a + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -10113,6 +10353,18 @@ __metadata: languageName: node linkType: hard +"union-value@npm:^1.0.1": + version: 1.0.1 + resolution: "union-value@npm:1.0.1" + dependencies: + arr-union: ^3.1.0 + get-value: ^2.0.6 + is-extendable: ^0.1.1 + set-value: ^2.0.1 + checksum: a3464097d3f27f6aa90cf103ed9387541bccfc006517559381a10e0dffa62f465a9d9a09c9b9c3d26d0f4cbe61d4d010e2fbd710fd4bf1267a768ba8a774b0ba + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -10416,6 +10668,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: ^2.0.0 + bin: + which: ./bin/which + checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 + languageName: node + linkType: hard + "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" From d209b09a0913fdfcb4ab145df0da921feb6147f6 Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 28 Feb 2024 11:02:14 +0100 Subject: [PATCH 068/153] eudr page and layout --- client/src/components/map/types.d.ts | 2 +- .../containers/collapse-button/component.tsx | 17 +-- .../src/containers/collapse-button/index.tsx | 20 ++- client/src/layouts/application/component.tsx | 9 ++ client/src/pages/eudr/index.tsx | 115 ++++++++++++++++++ 5 files changed, 150 insertions(+), 13 deletions(-) create mode 100644 client/src/pages/eudr/index.tsx diff --git a/client/src/components/map/types.d.ts b/client/src/components/map/types.d.ts index 2e19a87a3..2db511b8b 100644 --- a/client/src/components/map/types.d.ts +++ b/client/src/components/map/types.d.ts @@ -10,7 +10,7 @@ export interface CustomMapProps extends MapProps { /** Custom css class for styling */ className?: string; - mapStyle: MapStyle; + mapStyle?: MapStyle; /** An object that defines the viewport * @see https://visgl.github.io/react-map-gl/docs/api-reference/map#initialviewstate diff --git a/client/src/containers/collapse-button/component.tsx b/client/src/containers/collapse-button/component.tsx index 26958d1ad..9b2175e33 100644 --- a/client/src/containers/collapse-button/component.tsx +++ b/client/src/containers/collapse-button/component.tsx @@ -1,18 +1,13 @@ import { useCallback } from 'react'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'; -import { useAppSelector, useAppDispatch } from 'store/hooks'; -import { analysisUI, setSidebarCollapsed } from 'store/features/analysis/ui'; - const ICON_CLASSNAMES = 'h-4 w-4 text-gray-900'; -const CollapseButton: React.FC = () => { - const { isSidebarCollapsed } = useAppSelector(analysisUI); - const dispatch = useAppDispatch(); - - const handleClick = useCallback(() => { - dispatch(setSidebarCollapsed(!isSidebarCollapsed)); - }, [dispatch, isSidebarCollapsed]); +const CollapseButton: React.FC<{ + isCollapsed: boolean; + onClick: (isCollapsed: boolean) => void; +}> = ({ isCollapsed, onClick }) => { + const handleClick = useCallback(() => onClick(!isCollapsed), [isCollapsed, onClick]); return ( + + +
+
Suppliers with deforestation alerts
+
+
+ 36% of suppliers +
+
+
+
+
+
+ +
+
+
+
Suppliers with no location data
+
+
+ 33% of suppliers +
+
+
+
+
+
+ +
+
+
+
+ ); +}; + +export default SuppliersStackedBar; diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/index.ts b/client/src/containers/analysis-eudr/suppliers-stacked-bar/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index cc110951d..4b36c6fda 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -8,6 +8,7 @@ import CollapseButton from 'containers/collapse-button/component'; import TitleTemplate from 'utils/titleTemplate'; import Map from 'components/map'; import LayerManager from 'components/map/layer-manager'; +import SuppliersStackedBar from '@/containers/analysis-eudr/suppliers-stacked-bar'; import type { NextPageWithLayout } from 'pages/_app'; import type { ReactElement } from 'react'; @@ -42,12 +43,7 @@ const MapPage: NextPageWithLayout = () => {

EUDR complience Analysis

-
-
- Total numbers of suppliers: 46.53P -
-

Suppliers by category

-
+
From a4fd6a4c3585e16b1c58d9c1df55c7ccbbab73f5 Mon Sep 17 00:00:00 2001 From: David Inga Date: Fri, 1 Mar 2024 11:49:27 +0100 Subject: [PATCH 070/153] adding filters to eudr page --- .../analysis-eudr/filters/component.tsx | 13 + .../containers/analysis-eudr/filters/index.ts | 1 + .../filters/more-filters/component.tsx | 462 ++++++++++++++++++ .../filters/more-filters/index.tsx | 1 + .../filters/more-filters/types.d.ts | 10 + .../filters/years-range/component.tsx | 99 ++++ .../filters/years-range/constants.ts | 5 + .../filters/years-range/index.ts | 1 + client/src/pages/eudr/index.tsx | 4 +- 9 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 client/src/containers/analysis-eudr/filters/component.tsx create mode 100644 client/src/containers/analysis-eudr/filters/index.ts create mode 100644 client/src/containers/analysis-eudr/filters/more-filters/component.tsx create mode 100644 client/src/containers/analysis-eudr/filters/more-filters/index.tsx create mode 100644 client/src/containers/analysis-eudr/filters/more-filters/types.d.ts create mode 100644 client/src/containers/analysis-eudr/filters/years-range/component.tsx create mode 100644 client/src/containers/analysis-eudr/filters/years-range/constants.ts create mode 100644 client/src/containers/analysis-eudr/filters/years-range/index.ts diff --git a/client/src/containers/analysis-eudr/filters/component.tsx b/client/src/containers/analysis-eudr/filters/component.tsx new file mode 100644 index 000000000..c99d8f2e8 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/component.tsx @@ -0,0 +1,13 @@ +import MoreFilters from './more-filters'; +import YearsRange from './years-range'; + +const EUDRFilters = () => { + return ( +
+ + +
+ ); +}; + +export default EUDRFilters; diff --git a/client/src/containers/analysis-eudr/filters/index.ts b/client/src/containers/analysis-eudr/filters/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/component.tsx b/client/src/containers/analysis-eudr/filters/more-filters/component.tsx new file mode 100644 index 000000000..243d2b5e8 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/more-filters/component.tsx @@ -0,0 +1,462 @@ +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { FilterIcon } from '@heroicons/react/solid'; +import { + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + FloatingPortal, +} from '@floating-ui/react'; +import { Popover, Transition } from '@headlessui/react'; +import { useRouter } from 'next/router'; + +import Materials from '@/containers/analysis-visualization/analysis-filters/materials/component'; +import OriginRegions from '@/containers/analysis-visualization/analysis-filters/origin-regions/component'; +import { flattenTree, recursiveMap, recursiveSort } from 'components/tree-select/utils'; +import Select from 'components/forms/select'; +import Button from 'components/button/component'; +import TreeSelect from 'components/tree-select'; +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { analysisFilters, setFilters } from 'store/features/analysis/filters'; +import { setFilter } from 'store/features/analysis'; +import { useMaterialsTrees } from 'hooks/materials'; +import { useAdminRegionsTrees } from 'hooks/admin-regions'; +import { useSuppliersTypes } from 'hooks/suppliers'; +import { useLocationTypes } from 'hooks/location-types'; +import { useBusinessUnitsOptionsTrees } from 'hooks/business-units'; + +import type { Option } from 'components/forms/select'; +import type { LocationTypes as LocationTyping } from 'containers/interventions/enums'; +import type { TreeSelectOption } from 'components/tree-select/types'; +import type { AnalysisFiltersState } from 'store/features/analysis/filters'; + +type MoreFiltersState = { + materials: AnalysisFiltersState['materials']; + origins: AnalysisFiltersState['origins']; + t1Suppliers: AnalysisFiltersState['t1Suppliers']; + producers: AnalysisFiltersState['producers']; + locationTypes: AnalysisFiltersState['locationTypes']; + businessUnits: AnalysisFiltersState['businessUnits']; +}; + +const INITIAL_FILTERS: MoreFiltersState = { + materials: [], + origins: [], + t1Suppliers: [], + producers: [], + locationTypes: [], + businessUnits: [], +}; + +interface ApiTreeResponse { + id: string; + name: string; + children?: this[]; +} + +const DEFAULT_QUERY_OPTIONS = { + select: (data: ApiTreeResponse[]) => { + const sorted = recursiveSort(data, 'name'); + return sorted.map((item) => recursiveMap(item, ({ id, name }) => ({ label: name, value: id }))); + }, +}; + +const MoreFilters = () => { + const { query } = useRouter(); + const { scenarioId, compareScenarioId } = query; + + const dispatch = useAppDispatch(); + const { materials, origins, t1Suppliers, producers, locationTypes, businessUnits } = + useAppSelector(analysisFilters); + + const moreFilters: MoreFiltersState = useMemo( + () => ({ materials, origins, t1Suppliers, producers, locationTypes, businessUnits }), + [materials, origins, t1Suppliers, producers, locationTypes, businessUnits], + ); + + const [selectedFilters, setSelectedFilters] = useState(moreFilters); + + const materialIds = useMemo( + () => selectedFilters.materials.map(({ value }) => value), + [selectedFilters.materials], + ); + + const originIds = useMemo( + () => selectedFilters.origins.map(({ value }) => value), + [selectedFilters.origins], + ); + + const t1SupplierIds = useMemo( + () => selectedFilters.t1Suppliers.map(({ value }) => value), + [selectedFilters.t1Suppliers], + ); + + const producerIds = useMemo( + () => selectedFilters.producers.map(({ value }) => value), + [selectedFilters.producers], + ); + + const locationTypesIds = useMemo( + () => selectedFilters.locationTypes.map(({ value }) => value), + [selectedFilters.locationTypes], + ); + + const businessUnitIds = useMemo( + () => selectedFilters.businessUnits.map(({ value }) => value), + [selectedFilters.businessUnits], + ); + + const [counter, setCounter] = useState(0); + + // Only the changes are applied when the user clicks on Apply + const handleApply = useCallback(() => { + dispatch(setFilters(selectedFilters)); + }, [dispatch, selectedFilters]); + + // Restoring state from initial state only internally, + // the user have to apply the changes + const handleClearFilters = useCallback(() => { + setSelectedFilters(INITIAL_FILTERS); + }, []); + + // Updating internal state from selectors + const handleChangeFilter = useCallback( + (key: keyof MoreFiltersState, values: TreeSelectOption[] | Option) => { + setSelectedFilters((filters) => ({ ...filters, [key]: values })); + }, + [], + ); + + useEffect(() => { + setSelectedFilters(moreFilters); + }, [moreFilters]); + + const { refs, strategy, x, y, context } = useFloating({ + // open: isOpen, + // onOpenChange: handleOpen, + placement: 'bottom-start', + strategy: 'fixed', + middleware: [offset({ mainAxis: 4 }), shift({ padding: 4 })], + }); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + useClick(context), + useDismiss(context), + ]); + + const scenarioIds = useMemo( + () => [scenarioId, compareScenarioId].filter((id) => id) as string[], + [scenarioId, compareScenarioId], + ); + + const { data: materialOptions, isLoading: materialOptionsIsLoading } = useMaterialsTrees( + { + depth: 1, + withSourcingLocations: true, + scenarioIds, + originIds, + t1SupplierIds, + producerIds, + locationTypes: locationTypesIds, + businessUnitIds, + }, + { + ...DEFAULT_QUERY_OPTIONS, + select: (_materials) => + recursiveSort(_materials, 'name')?.map((item) => + recursiveMap(item, ({ id, name, status }) => ({ + value: id, + label: name, + disabled: status === 'inactive', + })), + ), + }, + ); + + const { data: originOptions, isLoading: originOptionsIsLoading } = useAdminRegionsTrees( + { + withSourcingLocations: true, + materialIds, + t1SupplierIds, + producerIds, + locationTypes: locationTypesIds, + scenarioIds, + businessUnitIds, + }, + DEFAULT_QUERY_OPTIONS, + ); + + const { data: t1SupplierOptions, isLoading: t1SupplierOptionsIsLoading } = useSuppliersTypes( + { + type: 't1supplier', + producerIds, + materialIds, + originIds, + locationTypes: locationTypesIds, + scenarioIds, + businessUnitIds, + }, + DEFAULT_QUERY_OPTIONS, + ); + + const { data: producerOptions, isLoading: producerOptionsIsLoading } = useSuppliersTypes( + { + type: 'producer', + t1SupplierIds, + materialIds, + originIds, + locationTypes: locationTypesIds, + scenarioIds, + businessUnitIds, + }, + DEFAULT_QUERY_OPTIONS, + ); + + const { data: locationTypeOptions, isLoading: locationTypeOptionsIsLoading } = useLocationTypes( + { + materialIds, + originIds, + t1SupplierIds, + producerIds, + scenarioIds, + businessUnitIds, + }, + { + onSuccess: (_locationTypeOptions) => { + // * every time new location types are fetched, we need to validate if the previous location types selected are still + // * available in the new options. Otherwise, we will remove them from the current selection. + setSelectedFilters((filters) => ({ + ...filters, + locationTypes: _locationTypeOptions.filter(({ value }) => + locationTypesIds.includes(value), + ), + })); + }, + }, + ); + + const { data: businessUnitsOptions, isLoading: businessUnitsOptionsIsLoading } = + useBusinessUnitsOptionsTrees({ + depth: 1, + withSourcingLocations: true, + materialIds, + originIds, + t1SupplierIds, + producerIds, + locationTypes: locationTypesIds, + scenarioIds, + }); + + const reviewFilterContent = useCallback( + ( + name: keyof MoreFiltersState, + currentValues: TreeSelectOption[], + allOptions: TreeSelectOption[], + ) => { + const allNodes = allOptions.flatMap((opt) => flattenTree(opt)); + const allKeys = allNodes.map(({ value }) => value); + const currentNodes = currentValues.flatMap(flattenTree); + const validOptions = currentNodes.filter(({ value }) => allKeys.includes(value)); + + if (validOptions.length !== allKeys.length) { + dispatch(setFilter({ id: name, value: validOptions })); + } + }, + [dispatch], + ); + + // Check current values are valid if the scenario changes + const handleScenarioChange = useCallback(() => { + reviewFilterContent('materials', materials, materialOptions); + reviewFilterContent('locationTypes', locationTypes, locationTypes); + reviewFilterContent('origins', origins, origins); + reviewFilterContent('t1Suppliers', t1Suppliers, t1SupplierOptions); + reviewFilterContent('producers', producers, producerOptions); + reviewFilterContent('businessUnits', businessUnits, businessUnitsOptions); + }, [ + businessUnits, + businessUnitsOptions, + locationTypes, + materialOptions, + materials, + origins, + producerOptions, + producers, + reviewFilterContent, + t1SupplierOptions, + t1Suppliers, + ]); + + useEffect(() => { + const counters = Object.values(moreFilters).map((value) => value.length); + const total = counters.reduce((a, b) => a + b); + setCounter(total); + }, [moreFilters]); + + useEffect(() => { + handleScenarioChange(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scenarioId]); + + return ( + + {({ open, close }) => ( + <> + + + + + +
+
Filter by
+ +
+
+
+
Material
+ handleChangeFilter('materials', values)} + id="materials-filter" + /> +
+
+
Business units
+ handleChangeFilter('businessUnits', values)} + id="business-units-filter" + /> +
+
+
Origins
+ handleChangeFilter('origins', values)} + id="origins-filter" + /> +
+
+
T1 Suppliers
+ handleChangeFilter('t1Suppliers', values)} + id="t1-suppliers-filter" + /> +
+
+
Producers
+ handleChangeFilter('producers', values)} + id="producers-filter" + /> +
+
+
Location type
+ + id="location-type-filter" + multiple + loading={locationTypeOptionsIsLoading} + options={locationTypeOptions} + placeholder="Location types" + onChange={(values) => handleChangeFilter('locationTypes', values)} + value={selectedFilters.locationTypes} + /> +
+
+ +
+ + +
+
+
+
+ + )} +
+ ); +}; + +export default MoreFilters; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/index.tsx b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts b/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts new file mode 100644 index 000000000..540cd7215 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts @@ -0,0 +1,10 @@ +export interface BaseTreeSearchParams { + depth?: number; + materialIds?: string[]; + businessUnitIds?: string[]; + originIds?: string[]; + scenarioId?: string; + scenarioIds?: string[]; + producerIds?: string[]; + t1SupplierIds?: string[]; +} diff --git a/client/src/containers/analysis-eudr/filters/years-range/component.tsx b/client/src/containers/analysis-eudr/filters/years-range/component.tsx new file mode 100644 index 000000000..281998178 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/years-range/component.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { isFinite, toNumber, range } from 'lodash-es'; +import toast from 'react-hot-toast'; + +import { DEFAULT_END_YEAR_GAP, MAX_END_YEAR_RANGE } from './constants'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { analysisUI } from 'store/features/analysis/ui'; +import { analysisFilters, setFilters } from 'store/features/analysis/filters'; +import { useYears } from 'hooks/years'; +import YearsRangeFilter, { useYearsRange } from 'containers/filters/years-range'; + +import type { YearsRangeParams } from 'containers/filters/years-range'; + +const YearsRange: React.FC = () => { + const dispatch = useAppDispatch(); + + const [years, setYears] = useState([]); + const { visualizationMode } = useAppSelector(analysisUI); + const filters = useAppSelector(analysisFilters); + const { layer, materials, indicator } = filters; + + const materialIds = useMemo(() => materials.map((mat) => mat.value), [materials]); + + const { data, isLoading } = useYears(layer, materialIds, indicator?.value, { + enabled: !!(layer === 'impact' && indicator?.value) || true, + }); + + const { startYear, endYear, yearsGap, setYearsRange } = useYearsRange({ + years, + yearsGap: 1, + // Map mode only makes use of the endYear and will display the Select, + // not the YearsRangeFilter. + validateRange: visualizationMode !== 'map', + ...filters, + }); + + const lastYearWithData = useMemo(() => data[data.length - 1], [data]); + const defaultLastYear = useMemo( + () => lastYearWithData + DEFAULT_END_YEAR_GAP, + [lastYearWithData], + ); + + useEffect(() => { + setYears(range(data[0], defaultLastYear + 1)); + }, [data, defaultLastYear]); + + useEffect(() => { + dispatch(setFilters({ startYear, endYear })); + }, [startYear, endYear, dispatch]); + + const handleOnEndYearSearch: (searchedYear: string) => void = (searchedYear) => { + const year = toNumber(searchedYear); + + if (!isFinite(year) || year <= data[0]) { + return; + } + if (year > MAX_END_YEAR_RANGE + defaultLastYear) { + toast.error(`Max year limit is ${MAX_END_YEAR_RANGE + defaultLastYear}`); + return; + } + + if (year === lastYearWithData) { + setYears(range(data[0], defaultLastYear + 1)); + } else if (!years.includes(year)) { + setYears(range(data[0], year + 1)); + } + }; + + const handleYearChange = ({ startYear, endYear }: YearsRangeParams) => { + const lastYear = years[years.length - 1]; + // Reduce the years range in case the current selected end year is smaller than the previous and the previous range was larger than the default + if (endYear < lastYear) { + if (endYear > defaultLastYear) { + setYears(range(years[0], toNumber(endYear) + 1)); + } else { + setYears(range(years[0], defaultLastYear + 1)); + } + } + if (endYear) setYearsRange({ startYear, endYear }); + }; + + return ( + + ); +}; + +export default YearsRange; diff --git a/client/src/containers/analysis-eudr/filters/years-range/constants.ts b/client/src/containers/analysis-eudr/filters/years-range/constants.ts new file mode 100644 index 000000000..a97451003 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/years-range/constants.ts @@ -0,0 +1,5 @@ +/** Arbitrary value to define the end year list range */ +export const DEFAULT_END_YEAR_GAP = 5; + +/** Arbitrary value to define the max range of end year options to avoid performance issues */ +export const MAX_END_YEAR_RANGE = 1000; diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.ts b/client/src/containers/analysis-eudr/filters/years-range/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/years-range/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index 4b36c6fda..1e29ec82d 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -9,6 +9,7 @@ import TitleTemplate from 'utils/titleTemplate'; import Map from 'components/map'; import LayerManager from 'components/map/layer-manager'; import SuppliersStackedBar from '@/containers/analysis-eudr/suppliers-stacked-bar'; +import EUDRFilters from '@/containers/analysis-eudr/filters/component'; import type { NextPageWithLayout } from 'pages/_app'; import type { ReactElement } from 'react'; @@ -39,8 +40,9 @@ const MapPage: NextPageWithLayout = () => { ref={scrollRef} >
-
+

EUDR complience Analysis

+
From 3c2b5652ec6be5a61267e07da35d9dae50343497 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 22 Feb 2024 12:15:32 +0100 Subject: [PATCH 071/153] updated test for map --- client/cypress/e2e/analysis/impact-layer.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cypress/e2e/analysis/impact-layer.cy.ts b/client/cypress/e2e/analysis/impact-layer.cy.ts index 583c86016..598c48db5 100644 --- a/client/cypress/e2e/analysis/impact-layer.cy.ts +++ b/client/cypress/e2e/analysis/impact-layer.cy.ts @@ -16,7 +16,7 @@ describe('Analysis: map impact layer', () => { it('request the impact layer', () => { cy.visit('/analysis/map'); - cy.get('canvas.mapboxgl-canvas').should('be.visible'); + cy.get('canvas.maplibregl-canvas').should('be.visible'); cy.wait('@fetchImpactMap').then((interception) => { cy.wrap(JSON.stringify(interception.response.body.data[0])).should( From d56793d42cb6cf2bee673501e38abfce36df42a8 Mon Sep 17 00:00:00 2001 From: David Inga Date: Tue, 5 Mar 2024 13:01:55 +0100 Subject: [PATCH 072/153] fixed image resolution and zoom compatibility --- client/src/components/map/component.tsx | 1 + .../map/styles/map-style-satellite-maplibre.json | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/components/map/component.tsx b/client/src/components/map/component.tsx index f865b23fd..a8afcbbd2 100644 --- a/client/src/components/map/component.tsx +++ b/client/src/components/map/component.tsx @@ -73,6 +73,7 @@ export const Map: FC = ({ {...otherMapProps} {...localViewState} attributionControl + minZoom={1} > {!!mapRef && children(mapRef.getMap())} diff --git a/client/src/components/map/styles/map-style-satellite-maplibre.json b/client/src/components/map/styles/map-style-satellite-maplibre.json index c178f288a..b61de3cdc 100644 --- a/client/src/components/map/styles/map-style-satellite-maplibre.json +++ b/client/src/components/map/styles/map-style-satellite-maplibre.json @@ -7,8 +7,9 @@ "sources": { "esri": { "type": "raster", - "tiles": ["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"], - "attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + "tiles": ["https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"], + "attribution": "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community", + "tileSize": 256 } }, "sprite": "https://tiles.basemaps.cartocdn.com/gl/positron-gl-style/sprite", @@ -28,8 +29,7 @@ { "id": "esri-world-imagery", "type": "raster", - "source": "esri", - "minzoom": 2 + "source": "esri" }, { "id": "custom-layers", From d7f5ebe09a716b2f0cfaa91b2247229935de2a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 4 Mar 2024 18:33:24 +0100 Subject: [PATCH 073/153] eudr - suppliers breakdown foundation --- client/components.json | 17 + client/package.json | 10 +- .../forms/select/autocomplete/component.tsx | 2 +- .../src/components/forms/select/component.tsx | 2 +- client/src/components/table/cell.tsx | 2 +- .../table/pagination/button/component.tsx | 2 +- client/src/components/tabs/component.tsx | 2 +- .../src/components/tree-select/component.tsx | 2 +- client/src/components/ui/collapsible.tsx | 9 + client/src/components/ui/label.tsx | 19 + client/src/components/ui/radio-group.tsx | 36 + .../breakdown/breakdown-item/index.tsx | 32 + .../deforestation-free-suppliers/index.tsx | 16 + .../index.tsx | 5 + .../suppliers-with-no-location-data/index.tsx | 5 + .../analysis-eudr/category-list/index.tsx | 101 +++ .../suppliers-stacked-bar/component.tsx | 139 ++-- .../material-legend-item/component.tsx | 2 +- .../comparison-cell/component.tsx | 2 +- .../containers/mobile-header/component.tsx | 2 +- .../navigation/mobile/component.tsx | 2 +- client/src/lib/utils.ts | 6 + client/src/pages/404.tsx | 2 +- client/src/styles/globals.css | 83 +- client/src/utils/colors.ts | 2 +- client/src/utils/number-format.ts | 4 + client/tailwind.config.ts | 146 ++++ ...lwind.config.js => tailwind.config_old.js} | 0 client/yarn.lock | 763 +++++++++++++++--- 29 files changed, 1234 insertions(+), 181 deletions(-) create mode 100644 client/components.json create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/radio-group.tsx create mode 100644 client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx create mode 100644 client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx create mode 100644 client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx create mode 100644 client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx create mode 100644 client/src/containers/analysis-eudr/category-list/index.tsx create mode 100644 client/src/lib/utils.ts create mode 100644 client/tailwind.config.ts rename client/{tailwind.config.js => tailwind.config_old.js} (100%) diff --git a/client/components.json b/client/components.json new file mode 100644 index 000000000..032a4c479 --- /dev/null +++ b/client/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/client/package.json b/client/package.json index 216213c40..ca08044d5 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,9 @@ "@json2csv/plainjs": "^6.1.3", "@loaders.gl/core": "3.3.1", "@luma.gl/constants": "8.5.18", + "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-label": "2.0.2", + "@radix-ui/react-radio-group": "1.1.3", "@reduxjs/toolkit": "1.8.2", "@tailwindcss/forms": "0.4.0", "@tailwindcss/typography": "0.5.0", @@ -43,7 +46,9 @@ "autoprefixer": "10.2.5", "axios": "1.3.4", "chroma-js": "2.1.2", + "class-variance-authority": "0.7.0", "classnames": "2.3.1", + "clsx": "^2.1.0", "d3-array": "3.0.2", "d3-format": "3.0.1", "d3-scale": "4.0.2", @@ -52,6 +57,7 @@ "jsona": "1.9.2", "lodash-es": "4.17.21", "lottie-react": "2.4.0", + "lucide-react": "0.344.0", "maplibre-gl": "3.6.2", "next": "13.5.5", "next-auth": "4.19.2", @@ -70,7 +76,9 @@ "recharts": "2.9.0", "rooks": "7.14.1", "sharp": "0.32.6", - "tailwindcss": "3.3.1", + "tailwind-merge": "2.2.1", + "tailwindcss": "3.4.1", + "tailwindcss-animate": "1.0.7", "uuid": "8.3.2", "yup": "0.32.11" }, diff --git a/client/src/components/forms/select/autocomplete/component.tsx b/client/src/components/forms/select/autocomplete/component.tsx index a385b38d2..ac43735aa 100644 --- a/client/src/components/forms/select/autocomplete/component.tsx +++ b/client/src/components/forms/select/autocomplete/component.tsx @@ -124,7 +124,7 @@ const AutoCompleteSelect = ({ {({ open }) => ( <> {!!label && ( - + {label} )} diff --git a/client/src/components/forms/select/component.tsx b/client/src/components/forms/select/component.tsx index ce018f19a..53618df35 100644 --- a/client/src/components/forms/select/component.tsx +++ b/client/src/components/forms/select/component.tsx @@ -133,7 +133,7 @@ const Select = ({ {({ open }) => ( <> {!!label && ( - + {label} )} diff --git a/client/src/components/table/cell.tsx b/client/src/components/table/cell.tsx index 3ed99bab0..b8201825f 100644 --- a/client/src/components/table/cell.tsx +++ b/client/src/components/table/cell.tsx @@ -40,7 +40,7 @@ const CellWrapper = ({ children, context }: React.PropsWithChildren = ({ 'flex h-10 min-w-[2.5rem] items-center justify-center text-sm', className, { - 'border-green-700 border-b': active, + 'border-b border-green-700': active, 'cursor-pointer': !disabled, 'opacity-30': disabled, }, diff --git a/client/src/components/tabs/component.tsx b/client/src/components/tabs/component.tsx index c545f6a46..466eae3fb 100644 --- a/client/src/components/tabs/component.tsx +++ b/client/src/components/tabs/component.tsx @@ -11,7 +11,7 @@ const Tabs: React.FC = ({ activeTab, tabs, bottomBorder = true }: Tab href={tab.href} className={classNames('-mb-px py-3', { 'ml-10': index !== 0, - 'text-green-700 border-green-700 border-b-2': activeTab && tab === activeTab, + 'border-b-2 border-green-700 text-green-700': activeTab && tab === activeTab, })} > {tab.name} diff --git a/client/src/components/tree-select/component.tsx b/client/src/components/tree-select/component.tsx index bce8ce91e..1aea66c27 100644 --- a/client/src/components/tree-select/component.tsx +++ b/client/src/components/tree-select/component.tsx @@ -513,7 +513,7 @@ const InnerTreeSelect = ( {theme === 'inline-primary' ? (
diff --git a/client/src/components/ui/collapsible.tsx b/client/src/components/ui/collapsible.tsx new file mode 100644 index 000000000..9605c4e41 --- /dev/null +++ b/client/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/client/src/components/ui/label.tsx b/client/src/components/ui/label.tsx new file mode 100644 index 000000000..470162f1b --- /dev/null +++ b/client/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/client/src/components/ui/radio-group.tsx b/client/src/components/ui/radio-group.tsx new file mode 100644 index 000000000..1cf4b798e --- /dev/null +++ b/client/src/components/ui/radio-group.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; +import { Circle } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ; +}); +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ); +}); +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { RadioGroup, RadioGroupItem }; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx new file mode 100644 index 000000000..c2317c0e9 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx @@ -0,0 +1,32 @@ +import { formatPercentage } from '@/utils/number-format'; + +const BreakdownItem = ({ + name, + color, + icon, + value, +}: { + name: string; + color: string; + icon: string; + value: number; +}): JSX.Element => { + return ( +
+
+
+ {name} +
+
+
+ {formatPercentage(value)} of suppliers +
+
+
+
+
+
+ ); +}; + +export default BreakdownItem; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx new file mode 100644 index 000000000..801873779 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -0,0 +1,16 @@ +const SAMPLE_DATA = { + byMaterial: [ + { name: 'Supplier 1', value: 100 }, + { name: 'Supplier 1', value: 100 }, + ], + byOrigin: [ + { name: 'Supplier 1', value: 100, iso3: 'ITA' }, + { name: 'Supplier 1', value: 100, iso3: 'ITA' }, + ], +}; + +const DeforestationFreeSuppliersBreakdown = () => { + return
DeforestationFreeSuppliersBreakdown
; +}; + +export default DeforestationFreeSuppliersBreakdown; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx new file mode 100644 index 000000000..4a5546533 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -0,0 +1,5 @@ +const SuppliersWithDeforestationAlertsBreakdown = () => { + return
SuppliersWithDeforestationAlertsBreakdown
; +}; + +export default SuppliersWithDeforestationAlertsBreakdown; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx new file mode 100644 index 000000000..dbd62935f --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -0,0 +1,5 @@ +const SuppliersWithNoLocationDataBreakdown = () => { + return
SuppliersWithNoLocationData
; +}; + +export default SuppliersWithNoLocationDataBreakdown; diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx new file mode 100644 index 000000000..98744d88b --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; + +import DeforestationFreeSuppliersBreakdown from './breakdown/deforestation-free-suppliers'; +import SuppliersWithDeforestationAlertsBreakdown from './breakdown/suppliers-with-deforestation-alerts'; +import SuppliersWithNoLocationDataBreakdown from './breakdown/suppliers-with-no-location-data'; + +import { Button } from '@/components/button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { formatPercentage } from '@/utils/number-format'; + +const CATEGORIES = [ + { + name: 'Deforestation-free suppliers', + slug: 'deforestation-free-suppliers', + color: 'bg-[#4AB7F3]', + // todo move this value field to the component + value: 0.3, + }, + { + name: 'Suppliers with deforestation alerts', + slug: 'suppliers-with-deforestation-alerts', + color: 'bg-[#FFC038]', + // todo move this value field to the component + value: 0.6, + }, + { + name: 'Suppliers with no location data', + slug: 'suppliers-with-no-location-data', + color: 'bg-[#8460FF]', + // todo move this value field to the component + value: 0.1, + }, +] as const; + +type CategoryState = Record<(typeof CATEGORIES)[number]['slug'], boolean>; + +export const CategoryList = (): JSX.Element => { + const [categories, toggleCategory] = useState( + CATEGORIES.reduce( + (acc, category) => ({ + ...acc, + [category.slug]: false, + }), + {} as CategoryState, + ), + ); + const categoriesWithValues = CATEGORIES.map((category) => ({ + ...category, + // todo: calculate value field here + })); + + return ( + <> + {categoriesWithValues.map((category) => ( + { + toggleCategory((prev) => ({ + ...prev, + [category.slug]: !prev[category.slug], + })); + }} + > +
+
{category.name}
+
+
+ {formatPercentage(category.value)} of suppliers +
+
+
+
+
+ + + +
+ + {category.slug === 'deforestation-free-suppliers' && ( + + )} + {category.slug === 'suppliers-with-deforestation-alerts' && ( + + )} + {category.slug === 'suppliers-with-no-location-data' && ( + + )} + + + ))} + + ); +}; + +export default CategoryList; diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index b5cfc0a7b..6207e4cd5 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -10,7 +10,23 @@ import { Label, } from 'recharts'; -import { Button } from '@/components/button'; +import CategoryList from '@/containers/analysis-eudr/category-list'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label as RadioLabel } from '@/components/ui/label'; + +const VIEW_BY_OPTIONS = [ + { + label: 'Commodities', + value: 'commodities', + defaultChecked: true, + }, + { + label: 'Countries', + value: 'countries', + }, +]; + +const defaultViewBy = VIEW_BY_OPTIONS.find((option) => option.defaultChecked)?.value; const data = [ { @@ -66,8 +82,16 @@ const SuppliersStackedBar = () => {

Suppliers by category

-
+
View by:
+ + {VIEW_BY_OPTIONS.map((option) => ( +
+ + {option.label} +
+ ))} +
@@ -121,7 +145,7 @@ const SuppliersStackedBar = () => { type="category" width={200} /> - + @@ -129,54 +153,73 @@ const SuppliersStackedBar = () => {
-
-
Deforestation-free suppliers
-
-
- 31% of suppliers -
-
-
-
-
-
- -
-
-
-
Suppliers with deforestation alerts
-
-
- 36% of suppliers -
-
-
+ + {/* + +
+
Deforestation-free suppliers
+
+
+ 31% of suppliers +
+
+
+
+
+ +
-
-
- -
-
-
-
Suppliers with no location data
-
-
- 33% of suppliers + + +
test
+
+ + + +
+
Suppliers with deforestation alerts
+
+
+ 36% of suppliers +
+
+
+
+
+
-
-
+ + +
test
+
+ + + +
+
Suppliers with no location data
+
+
+ 33% of suppliers +
+
+
+
+
+
+ +
-
-
- -
-
+ + +
test
+
+ */}
); diff --git a/client/src/containers/analysis-visualization/analysis-legend/material-legend-item/component.tsx b/client/src/containers/analysis-visualization/analysis-legend/material-legend-item/component.tsx index a9183402a..c1ef5cf8c 100644 --- a/client/src/containers/analysis-visualization/analysis-legend/material-legend-item/component.tsx +++ b/client/src/containers/analysis-visualization/analysis-legend/material-legend-item/component.tsx @@ -127,7 +127,7 @@ const MaterialLayer = () => { /> )} {isError && error.response?.status === 404 && ( -
No data found for this parameters
+
No data found for this parameters
)} ); diff --git a/client/src/containers/analysis-visualization/analysis-table/comparison-cell/component.tsx b/client/src/containers/analysis-visualization/analysis-table/comparison-cell/component.tsx index 2c11ed742..20281e8ee 100644 --- a/client/src/containers/analysis-visualization/analysis-table/comparison-cell/component.tsx +++ b/client/src/containers/analysis-visualization/analysis-table/comparison-cell/component.tsx @@ -49,7 +49,7 @@ const ComparisonCell: React.FC = ({ className={classNames( 'my-auto rounded-[4px] px-1 py-0.5 text-xs font-semibold text-gray-500', { - 'text-green-700 bg-green-400/40': + 'bg-green-400/40 text-green-700': (comparisonMode === 'relative' && percentageDifference <= 0) || (comparisonMode === 'absolute' && absoluteDifference <= 0), 'bg-red-400/20 text-red-800': diff --git a/client/src/containers/mobile-header/component.tsx b/client/src/containers/mobile-header/component.tsx index 4f94411e5..9c80f0904 100644 --- a/client/src/containers/mobile-header/component.tsx +++ b/client/src/containers/mobile-header/component.tsx @@ -9,7 +9,7 @@ const HeaderMobile = () => { return (
-
+
diff --git a/client/src/containers/navigation/mobile/component.tsx b/client/src/containers/navigation/mobile/component.tsx index c5e3e119a..f8eceabd2 100644 --- a/client/src/containers/navigation/mobile/component.tsx +++ b/client/src/containers/navigation/mobile/component.tsx @@ -7,7 +7,7 @@ const MobileNavigation = ({ items }: NavigationProps) => (
diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index d0d35110d..499c8dd51 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -165,75 +165,7 @@ const SuppliersStackedBar = () => {
-
- - {/* - -
-
Deforestation-free suppliers
-
-
- 31% of suppliers -
-
-
-
-
- - -
- - -
test
-
- - - -
-
Suppliers with deforestation alerts
-
-
- 36% of suppliers -
-
-
-
-
- -
- - -
test
-
- - - -
-
Suppliers with no location data
-
-
- 33% of suppliers -
-
-
-
-
-
- -
-
- - -
test
-
- */} -
+
); }; diff --git a/client/yarn.lock b/client/yarn.lock index 55bf1d271..9bc7835d7 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2096,6 +2096,13 @@ __metadata: languageName: node linkType: hard +"@trysound/sax@npm:0.2.0": + version: 0.2.0 + resolution: "@trysound/sax@npm:0.2.0" + checksum: 11226c39b52b391719a2a92e10183e4260d9651f86edced166da1d95f39a0a1eaa470e44d14ac685ccd6d3df7e2002433782872c0feeb260d61e80f21250e65c + languageName: node + linkType: hard + "@types/chroma-js@npm:2.1.3": version: 2.1.3 resolution: "@types/chroma-js@npm:2.1.3" @@ -3343,6 +3350,13 @@ __metadata: languageName: node linkType: hard +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -3810,6 +3824,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + "common-tags@npm:^1.8.0": version: 1.8.2 resolution: "common-tags@npm:1.8.2" @@ -3884,6 +3905,46 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^5.1.0": + version: 5.1.0 + resolution: "css-select@npm:5.1.0" + dependencies: + boolbase: ^1.0.0 + css-what: ^6.1.0 + domhandler: ^5.0.2 + domutils: ^3.0.1 + nth-check: ^2.0.1 + checksum: 2772c049b188d3b8a8159907192e926e11824aea525b8282981f72ba3f349cf9ecd523fdf7734875ee2cb772246c22117fc062da105b6d59afe8dcd5c99c9bda + languageName: node + linkType: hard + +"css-tree@npm:^2.3.1": + version: 2.3.1 + resolution: "css-tree@npm:2.3.1" + dependencies: + mdn-data: 2.0.30 + source-map-js: ^1.0.1 + checksum: 493cc24b5c22b05ee5314b8a0d72d8a5869491c1458017ae5ed75aeb6c3596637dbe1b11dac2548974624adec9f7a1f3a6cf40593dc1f9185eb0e8279543fbc0 + languageName: node + linkType: hard + +"css-tree@npm:~2.2.0": + version: 2.2.1 + resolution: "css-tree@npm:2.2.1" + dependencies: + mdn-data: 2.0.28 + source-map-js: ^1.0.1 + checksum: b94aa8cc2f09e6f66c91548411fcf74badcbad3e150345074715012d16333ce573596ff5dfca03c2a87edf1924716db765120f94247e919d72753628ba3aba27 + languageName: node + linkType: hard + +"css-what@npm:^6.1.0": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -3893,6 +3954,15 @@ __metadata: languageName: node linkType: hard +"csso@npm:^5.0.5": + version: 5.0.5 + resolution: "csso@npm:5.0.5" + dependencies: + css-tree: ~2.2.0 + checksum: 0ad858d36bf5012ed243e9ec69962a867509061986d2ee07cc040a4b26e4d062c00d4c07e5ba8d430706ceb02dd87edd30a52b5937fd45b1b6f2119c4993d59a + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.0 resolution: "csstype@npm:3.1.0" @@ -4336,6 +4406,44 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^2.0.0": + version: 2.0.0 + resolution: "dom-serializer@npm:2.0.0" + dependencies: + domelementtype: ^2.3.0 + domhandler: ^5.0.2 + entities: ^4.2.0 + checksum: cd1810544fd8cdfbd51fa2c0c1128ec3a13ba92f14e61b7650b5de421b88205fd2e3f0cc6ace82f13334114addb90ed1c2f23074a51770a8e9c1273acbc7f3e6 + languageName: node + linkType: hard + +"domelementtype@npm:^2.3.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": + version: 5.0.3 + resolution: "domhandler@npm:5.0.3" + dependencies: + domelementtype: ^2.3.0 + checksum: 0f58f4a6af63e6f3a4320aa446d28b5790a009018707bce2859dcb1d21144c7876482b5188395a188dfa974238c019e0a1e610d2fc269a12b2c192ea2b0b131c + languageName: node + linkType: hard + +"domutils@npm:^3.0.1": + version: 3.1.0 + resolution: "domutils@npm:3.1.0" + dependencies: + dom-serializer: ^2.0.0 + domelementtype: ^2.3.0 + domhandler: ^5.0.3 + checksum: e5757456ddd173caa411cfc02c2bb64133c65546d2c4081381a3bafc8a57411a41eed70494551aa58030be9e58574fcc489828bebd673863d39924fb4878f416 + languageName: node + linkType: hard + "draco3d@npm:1.5.5": version: 1.5.5 resolution: "draco3d@npm:1.5.5" @@ -4442,6 +4550,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.2.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -7141,6 +7256,7 @@ __metadata: react-map-gl: 7.1.7 react-range: 1.8.14 react-redux: 8.0.2 + react-world-flags: 1.6.0 recharts: 2.9.0 rooks: 7.14.1 sharp: 0.32.6 @@ -7499,6 +7615,20 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.0.28": + version: 2.0.28 + resolution: "mdn-data@npm:2.0.28" + checksum: f51d587a6ebe8e426c3376c74ea6df3e19ec8241ed8e2466c9c8a3904d5d04397199ea4f15b8d34d14524b5de926d8724ae85207984be47e165817c26e49e0aa + languageName: node + linkType: hard + +"mdn-data@npm:2.0.30": + version: 2.0.30 + resolution: "mdn-data@npm:2.0.30" + checksum: d6ac5ac7439a1607df44b22738ecf83f48e66a0874e4482d6424a61c52da5cde5750f1d1229b6f5fa1b80a492be89465390da685b11f97d62b8adcc6e88189aa + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -7978,6 +8108,15 @@ __metadata: languageName: node linkType: hard +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: ^1.0.0 + checksum: 5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3 + languageName: node + linkType: hard + "nyc-report-lcov-absolute@npm:1.0.0": version: 1.0.0 resolution: "nyc-report-lcov-absolute@npm:1.0.0" @@ -9159,6 +9298,19 @@ __metadata: languageName: node linkType: hard +"react-world-flags@npm:1.6.0": + version: 1.6.0 + resolution: "react-world-flags@npm:1.6.0" + dependencies: + svg-country-flags: ^1.2.10 + svgo: ^3.0.2 + world-countries: ^5.0.0 + peerDependencies: + react: ">=0.14" + checksum: 29ea43e8ce58c402bac5fe8323bcbc23930c661ed620c050705274ea95227118b62bc8543c636465ea027218c5b3a9dacaf0a10abf795cd7db6eabf41b60bc54 + languageName: node + linkType: hard + "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -9941,7 +10093,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": +"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c @@ -10347,6 +10499,30 @@ __metadata: languageName: node linkType: hard +"svg-country-flags@npm:^1.2.10": + version: 1.2.10 + resolution: "svg-country-flags@npm:1.2.10" + checksum: 52e8a946d5f9edb8f52b2b98754943604e82b465009a01774310b15be018b749ffa8b600b0c78ad18a4efd7c247e80c0cee33ef3930a60b18f751c587706cbdd + languageName: node + linkType: hard + +"svgo@npm:^3.0.2": + version: 3.2.0 + resolution: "svgo@npm:3.2.0" + dependencies: + "@trysound/sax": 0.2.0 + commander: ^7.2.0 + css-select: ^5.1.0 + css-tree: ^2.3.1 + css-what: ^6.1.0 + csso: ^5.0.5 + picocolors: ^1.0.0 + bin: + svgo: ./bin/svgo + checksum: 42168748a5586d85d447bec2867bc19814a4897f973ff023e6aad4ff19ba7408be37cf3736e982bb78e3f1e52df8785da5dca77a8ebc64c0ebd6fcf9915d2895 + languageName: node + linkType: hard + "synckit@npm:^0.8.4": version: 0.8.5 resolution: "synckit@npm:0.8.5" @@ -11232,6 +11408,13 @@ __metadata: languageName: node linkType: hard +"world-countries@npm:^5.0.0": + version: 5.0.0 + resolution: "world-countries@npm:5.0.0" + checksum: 10c58f7fdc7fae180574866c1662defd63c04c828a682aeec13f69ccb9c08d0eb0e328b84f35dc6ebfc3bd5996815bc3cfb102857ef09404751aa29028016fe7 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" From 503da47cb2b1d518546b2a82155e06d3befa0fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 6 Mar 2024 18:41:07 +0100 Subject: [PATCH 076/153] edur - data table --- client/package.json | 4 +- client/src/components/ui/badge.tsx | 33 ++ client/src/components/ui/button.tsx | 49 ++ client/src/components/ui/radio-group.tsx | 4 +- client/src/components/ui/select.tsx | 151 +++++++ client/src/components/ui/table.tsx | 91 ++++ .../analysis-eudr/category-list/index.tsx | 2 +- .../supplier-list-table/index.tsx | 17 + .../table/column-header.tsx | 50 +++ .../supplier-list-table/table/columns.tsx | 108 +++++ .../supplier-list-table/table/index.tsx | 136 ++++++ .../supplier-list-table/table/mock-data.ts | 108 +++++ .../supplier-list-table/table/pagination.tsx | 94 ++++ .../suppliers-stacked-bar/component.tsx | 2 +- client/src/hooks/eudr/index.ts | 26 ++ client/src/pages/eudr/index.tsx | 4 +- client/yarn.lock | 417 +++++++++++++++++- 17 files changed, 1279 insertions(+), 17 deletions(-) create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/table.tsx create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/index.tsx create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/table/column-header.tsx create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts create mode 100644 client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx create mode 100644 client/src/hooks/eudr/index.ts diff --git a/client/package.json b/client/package.json index 61aca4c58..7fc759e1c 100644 --- a/client/package.json +++ b/client/package.json @@ -37,11 +37,13 @@ "@radix-ui/react-collapsible": "1.0.3", "@radix-ui/react-label": "2.0.2", "@radix-ui/react-radio-group": "1.1.3", + "@radix-ui/react-select": "2.0.0", + "@radix-ui/react-slot": "1.0.2", "@reduxjs/toolkit": "1.8.2", "@tailwindcss/forms": "0.4.0", "@tailwindcss/typography": "0.5.0", "@tanstack/react-query": "^4.2.1", - "@tanstack/react-table": "8.5.1", + "@tanstack/react-table": "8.13.2", "@tanstack/react-virtual": "3.0.1", "autoprefixer": "10.2.5", "axios": "1.3.4", diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 000000000..e8e25c504 --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', + secondary: + 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx new file mode 100644 index 000000000..0ecf8afbe --- /dev/null +++ b/client/src/components/ui/button.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/client/src/components/ui/radio-group.tsx b/client/src/components/ui/radio-group.tsx index 1cf4b798e..d41776297 100644 --- a/client/src/components/ui/radio-group.tsx +++ b/client/src/components/ui/radio-group.tsx @@ -20,13 +20,13 @@ const RadioGroupItem = React.forwardRef< - + ); diff --git a/client/src/components/ui/select.tsx b/client/src/components/ui/select.tsx new file mode 100644 index 000000000..989cba607 --- /dev/null +++ b/client/src/components/ui/select.tsx @@ -0,0 +1,151 @@ +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1', + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/client/src/components/ui/table.tsx b/client/src/components/ui/table.tsx new file mode 100644 index 000000000..e538a0fb1 --- /dev/null +++ b/client/src/components/ui/table.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', className)} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = 'TableCaption'; + +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 64938748f..a245616f0 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -102,7 +102,7 @@ export const CategoryList = (): JSX.Element => { size="xs" variant="white" className={cn( - 'w-[98px] rounded-md border-none text-sm shadow-none transition-colors hover:shadow-none', + 'w-[98px] rounded-md border-none text-sm text-gray-500 shadow-none transition-colors hover:shadow-none', { 'bg-navy-400 text-white hover:bg-navy-600': categories[category.slug], }, diff --git a/client/src/containers/analysis-eudr/supplier-list-table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/index.tsx new file mode 100644 index 000000000..b96015508 --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/index.tsx @@ -0,0 +1,17 @@ +import SuppliersListTable from './table'; + +const SupplierListTable = (): JSX.Element => { + return ( +
+
+ All commodities +

Suppliers List

+
+
+ +
+
+ ); +}; + +export default SupplierListTable; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/column-header.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/column-header.tsx new file mode 100644 index 000000000..9b631fc4e --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/column-header.tsx @@ -0,0 +1,50 @@ +import { useCallback } from 'react'; +import { ChevronDown, ChevronUp, ChevronsUpDown } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +import type { Column } from '@tanstack/react-table'; + +interface DataTableColumnHeaderProps extends React.HTMLAttributes { + column: Column; + title: string; +} + +const ICON_CLASSES = 'h-[12px] w-[12px] text-gray-400'; + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + const handleSorting = useCallback(() => { + column.toggleSorting(); + }, [column]); + + const sortingValue = column.getCanSort() ? column.getIsSorted() : null; + + if (!column.getCanSort()) { + return ( +
+ {title} +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx new file mode 100644 index 000000000..b338499dc --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -0,0 +1,108 @@ +'use client'; + +import Link from 'next/link'; + +import { DataTableColumnHeader } from './column-header'; + +import { Badge } from '@/components/ui/badge'; + +import type { Supplier } from '.'; +import type { ColumnDef } from '@tanstack/react-table'; + +export const columns: ColumnDef[] = [ + { + accessorKey: 'supplierName', + header: ({ column }) => , + cell: ({ row }) => { + return ( +
+ + {row.getValue('supplierName')} + +
+ ); + }, + enableSorting: true, + }, + { + accessorKey: 'companyId', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'baseLineVolume', + header: ({ column }) => , + cell: ({ row }) => { + // todo: format number + return {row.getValue('baseLineVolume')}; + }, + }, + { + accessorKey: 'dfs', + header: ({ column }) => , + cell: ({ row }) => { + const dfs = row.getValue('dfs'); + return {`${Number.isNaN(dfs) ? '-' : `${dfs}%`}`}; + }, + }, + { + accessorKey: 'sda', + header: ({ column }) => , + cell: ({ row }) => { + const sda = row.getValue('sda'); + return {`${Number.isNaN(sda) ? '-' : `${sda}%`}`}; + }, + }, + { + accessorKey: 'ttp', + header: ({ column }) => , + cell: ({ row }) => { + const ttp = row.getValue('ttp'); + return {`${Number.isNaN(ttp) ? '-' : `${ttp}%`}`}; + }, + }, + { + accessorKey: 'materials', + header: ({ column }) => , + cell: ({ row }) => { + return ( +
+ {row.original.materials.map(({ name, id }) => ( + + {name} + + ))} +
+ ); + }, + enableSorting: false, + }, + { + accessorKey: 'origins', + header: ({ column }) => , + cell: ({ row }) => { + return ( +
+ {row.original.origins.map(({ name, id }) => ( + + {name} + + ))} +
+ ); + }, + enableSorting: false, + }, +]; + +export default columns; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx new file mode 100644 index 000000000..55793500f --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -0,0 +1,136 @@ +import { + flexRender, + getCoreRowModel, + // getFacetedRowModel, + // getFacetedUniqueValues, + // getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useState } from 'react'; + +import columns from './columns'; +import { MOCK_DATA } from './mock-data'; +import { DataTablePagination, PAGINATION_SIZES } from './pagination'; + +import { + Table, + TableHeader, + TableHead, + TableRow, + TableBody, + TableCell, +} from '@/components/ui/table'; +import { useEUDRSuppliers } from '@/hooks/eudr'; + +import type { + // ColumnFiltersState, + SortingState, + // VisibilityState +} from '@tanstack/react-table'; + +export interface Supplier { + id: number; + supplierName: string; + companyId: string; + baseLineVolume: number; + dfs: number; + sda: number; + ttp: number; + materials: { + name: string; + id: string; + }[]; + origins: { + name: string; + id: string; + }[]; +} + +const SuppliersListTable = (): JSX.Element => { + // const [rowSelection, setRowSelection] = useState({}); + // const [columnVisibility, setColumnVisibility] = useState({}); + // const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); + + const { data } = useEUDRSuppliers(undefined, { + enabled: false, + placeholderData: MOCK_DATA, + }); + + const table = useReactTable({ + data: data, + columns, + initialState: { + pagination: { + pageSize: PAGINATION_SIZES[0], + }, + }, + state: { + sorting, + // columnVisibility, + // rowSelection, + // columnFilters, + }, + enableRowSelection: true, + // onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + // onColumnFiltersChange: setColumnFilters, + // onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + // getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + // getFacetedRowModel: getFacetedRowModel(), + // getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ +
+ ); +}; + +export default SuppliersListTable; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts b/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts new file mode 100644 index 000000000..6d2868458 --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts @@ -0,0 +1,108 @@ +import type { Supplier } from '.'; + +export const MOCK_DATA: Supplier[] = [ + { + id: 2, + supplierName: 'Very long name of supplier Very long name of supplier', + companyId: '123', + baseLineVolume: 1000, + dfs: 100, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 1', id: '1' }, + { name: 'Material 2', id: '2' }, + ], + origins: [ + { name: 'Origin 1', id: '1' }, + { name: 'Origin 2', id: '2' }, + ], + }, + { + id: 4, + supplierName: 'Supplier 2', + companyId: '124', + baseLineVolume: 2000, + dfs: 200, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 3', id: '3' }, + { name: 'Material 4', id: '4' }, + { name: 'Material 4', id: '5' }, + { name: 'Material 4', id: '6' }, + ], + origins: [ + { name: 'Origin 3', id: '3' }, + { name: 'Origin 4', id: '4' }, + ], + }, + { + id: 8, + supplierName: 'Supplier 3', + companyId: '125', + baseLineVolume: 3000, + dfs: 300, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 5', id: '5' }, + { name: 'Material 6', id: '6' }, + ], + origins: [ + { name: 'Origin 5', id: '5' }, + { name: 'Origin 6', id: '6' }, + ], + }, + { + id: 67, + supplierName: 'Supplier 3', + companyId: '125', + baseLineVolume: 3000, + dfs: 300, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 5', id: '5' }, + { name: 'Material 6', id: '6' }, + ], + origins: [ + { name: 'Origin 5', id: '5' }, + { name: 'Origin 6', id: '6' }, + ], + }, + { + id: 33, + supplierName: 'Supplier 3', + companyId: '125', + baseLineVolume: 3000, + dfs: 300, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 5', id: '5' }, + { name: 'Material 6', id: '6' }, + ], + origins: [ + { name: 'Origin 5', id: '5' }, + { name: 'Origin 6', id: '6' }, + ], + }, + { + id: 9, + supplierName: 'Supplier 3', + companyId: '125', + baseLineVolume: 3000, + dfs: 300, + sda: 39.7, + ttp: 40.1, + materials: [ + { name: 'Material 5', id: '5' }, + { name: 'Material 6', id: '6' }, + ], + origins: [ + { name: 'Origin 5', id: '5' }, + { name: 'Origin 6', id: '6' }, + ], + }, +]; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx new file mode 100644 index 000000000..48b335b05 --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx @@ -0,0 +1,94 @@ +import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeft, ChevronsRight } from 'lucide-react'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; + +import type { Table } from '@tanstack/react-table'; + +export const PAGINATION_SIZES = [10, 20, 30, 40, 50]; + +interface DataTablePaginationProps { + table: Table; +} + +export function DataTablePagination({ table }: DataTablePaginationProps) { + return ( +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- + {(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize > + table.getRowCount() + ? table.getRowCount() + : (table.getState().pagination.pageIndex + 1) * + table.getState().pagination.pageSize}{' '} + of {table.getRowCount()} +
+
+ + + + +
+
+
+ ); +} diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index 499c8dd51..a647e2367 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -92,7 +92,7 @@ const SuppliersStackedBar = () => {

Suppliers by category

-
View by:
+
View by:
( + params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-suppliers', params], + () => + apiService + .request<{ data: Supplier[] }>({ + method: 'GET', + url: '/eudr/suppliers', + params, + }) + .then(({ data: responseData }) => responseData.data), + { + ...options, + }, + ); +}; diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index 1e29ec82d..3c7c4dd1f 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -10,6 +10,7 @@ import Map from 'components/map'; import LayerManager from 'components/map/layer-manager'; import SuppliersStackedBar from '@/containers/analysis-eudr/suppliers-stacked-bar'; import EUDRFilters from '@/containers/analysis-eudr/filters/component'; +import SupplierListTable from '@/containers/analysis-eudr/supplier-list-table'; import type { NextPageWithLayout } from 'pages/_app'; import type { ReactElement } from 'react'; @@ -44,8 +45,9 @@ const MapPage: NextPageWithLayout = () => {

EUDR complience Analysis

-
+
+
diff --git a/client/yarn.lock b/client/yarn.lock index 9bc7835d7..2a07a1280 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -551,6 +551,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.0.0": + version: 1.6.0 + resolution: "@floating-ui/core@npm:1.6.0" + dependencies: + "@floating-ui/utils": ^0.2.1 + checksum: 2e25c53b0c124c5c9577972f8ae21d081f2f7895e6695836a53074463e8c65b47722744d6d2b5a993164936da006a268bcfe87fe68fd24dc235b1cb86bed3127 + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.5.1": version: 1.5.3 resolution: "@floating-ui/dom@npm:1.5.3" @@ -561,6 +570,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.6.1": + version: 1.6.3 + resolution: "@floating-ui/dom@npm:1.6.3" + dependencies: + "@floating-ui/core": ^1.0.0 + "@floating-ui/utils": ^0.2.0 + checksum: 81cbb18ece3afc37992f436e469e7fabab2e433248e46fff4302d12493a175b0c64310f8a971e6e1eda7218df28ace6b70237b0f3c22fe12a21bba05b5579555 + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:2.0.2, @floating-ui/react-dom@npm:^2.0.2": version: 2.0.2 resolution: "@floating-ui/react-dom@npm:2.0.2" @@ -573,6 +592,18 @@ __metadata: languageName: node linkType: hard +"@floating-ui/react-dom@npm:^2.0.0": + version: 2.0.8 + resolution: "@floating-ui/react-dom@npm:2.0.8" + dependencies: + "@floating-ui/dom": ^1.6.1 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 5da7f13a69281e38859a3203a608fe9de1d850b332b355c10c0c2427c7b7209a0374c10f6295b6577c1a70237af8b678340bd4cc0a4b1c66436a94755d81e526 + languageName: node + linkType: hard + "@floating-ui/react@npm:0.26.1": version: 0.26.1 resolution: "@floating-ui/react@npm:0.26.1" @@ -594,6 +625,13 @@ __metadata: languageName: node linkType: hard +"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.1": + version: 0.2.1 + resolution: "@floating-ui/utils@npm:0.2.1" + checksum: 9ed4380653c7c217cd6f66ae51f20fdce433730dbc77f95b5abfb5a808f5fdb029c6ae249b4e0490a816f2453aa6e586d9a873cd157fdba4690f65628efc6e06 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -1602,6 +1640,15 @@ __metadata: languageName: node linkType: hard +"@radix-ui/number@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/number@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + checksum: 621ea8b7d4195d1a65a9c0aee918e8335e7f198088eec91577512c89c2ba3a3bab4a767cfb872a2b9c3092a78ff41cad9a924845a939f6bb87fe9356241ea0ea + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/primitive@npm:1.0.1" @@ -1611,6 +1658,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-arrow@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-arrow@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 8cca086f0dbb33360e3c0142adf72f99fc96352d7086d6c2356dbb2ea5944cfb720a87d526fc48087741c602cd8162ca02b0af5e6fdf5f56d20fddb44db8b4c3 + languageName: node + linkType: hard + "@radix-ui/react-collapsible@npm:1.0.3": version: 1.0.3 resolution: "@radix-ui/react-collapsible@npm:1.0.3" @@ -1706,6 +1773,67 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-escape-keydown": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: e73cf4bd3763f4d55b1bea7486a9700384d7d94dc00b1d5a75e222b2f1e4f32bc667a206ca4ed3baaaf7424dce7a239afd0ba59a6f0d89c3462c4e6e8d029a04 + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-focus-scope@npm:1.0.4" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 3481db1a641513a572734f0bcb0e47fefeba7bccd6ec8dde19f520719c783ef0b05a55ef0d5292078ed051cc5eda46b698d5d768da02e26e836022f46b376fd1 + languageName: node + linkType: hard + "@radix-ui/react-id@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-id@npm:1.0.1" @@ -1742,6 +1870,55 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-popper@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-popper@npm:1.1.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@floating-ui/react-dom": ^2.0.0 + "@radix-ui/react-arrow": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-layout-effect": 1.0.1 + "@radix-ui/react-use-rect": 1.0.1 + "@radix-ui/react-use-size": 1.0.1 + "@radix-ui/rect": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: b18a15958623f9222b6ed3e24b9fbcc2ba67b8df5a5272412f261de1592b3f05002af1c8b94c065830c3c74267ce00cf6c1d70d4d507ec92ba639501f98aa348 + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-portal@npm:1.0.4" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64 + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-presence@npm:1.0.1" @@ -1840,6 +2017,46 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-select@npm:2.0.0": + version: 2.0.0 + resolution: "@radix-ui/react-select@npm:2.0.0" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/number": 1.0.1 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-collection": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-direction": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-focus-guards": 1.0.1 + "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-use-layout-effect": 1.0.1 + "@radix-ui/react-use-previous": 1.0.1 + "@radix-ui/react-visually-hidden": 1.0.3 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.5 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 9ebf4a3e70fd5f583cf468e432ff04768b3442c44788eaf415e044f19c900b886e92eb46e19e138c4994d8a361f5e31f93d13b5bcf413469f21899bbe1112d1d + languageName: node + linkType: hard + "@radix-ui/react-slot@npm:1.0.2": version: 1.0.2 resolution: "@radix-ui/react-slot@npm:1.0.2" @@ -1887,6 +2104,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-escape-keydown@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-callback-ref": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: c6ed0d9ce780f67f924980eb305af1f6cce2a8acbaf043a58abe0aa3cc551d9aa76ccee14531df89bbee302ead7ecc7fce330886f82d4672c5eda52f357ef9b8 + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" @@ -1917,6 +2150,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-rect@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/rect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 433f07e61e04eb222349825bb05f3591fca131313a1d03709565d6226d8660bd1d0423635553f95ee4fcc25c8f2050972d848808d753c388e2a9ae191ebf17f3 + languageName: node + linkType: hard + "@radix-ui/react-use-size@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-use-size@npm:1.0.1" @@ -1933,6 +2182,35 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-visually-hidden@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 2e9d0c8253f97e7d6ffb2e52a5cfd40ba719f813b39c3e2e42c496d54408abd09ef66b5aec4af9b8ab0553215e32452a5d0934597a49c51dd90dc39181ed0d57 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/rect@npm:1.0.1" + dependencies: + "@babel/runtime": ^7.13.10 + checksum: aeec13b234a946052512d05239067d2d63422f9ec70bf2fe7acfd6b9196693fc33fbaf43c2667c167f777d90a095c6604eb487e0bce79e230b6df0f6cacd6a55 + languageName: node + linkType: hard + "@reduxjs/toolkit@npm:1.8.2": version: 1.8.2 resolution: "@reduxjs/toolkit@npm:1.8.2" @@ -2051,15 +2329,15 @@ __metadata: languageName: node linkType: hard -"@tanstack/react-table@npm:8.5.1": - version: 8.5.1 - resolution: "@tanstack/react-table@npm:8.5.1" +"@tanstack/react-table@npm:8.13.2": + version: 8.13.2 + resolution: "@tanstack/react-table@npm:8.13.2" dependencies: - "@tanstack/table-core": 8.5.1 + "@tanstack/table-core": 8.13.2 peerDependencies: react: ">=16" react-dom: ">=16" - checksum: 4081d0ecd07924b4969c1de7d5a6e8262737d09b45ba649112da1506169f42f3d00d4566d0b69619d530af264e6f32da9ae27197377fb7dbb76934b51348ffcd + checksum: 5a5ae9cf124a19d5b5ba585359fc51e8999d5418df0bdd14acf8e8644355c540d54d7392041ceec514956d54189a1f7bf5a19ec9ae5a27fa93bd556a5b156a75 languageName: node linkType: hard @@ -2075,10 +2353,10 @@ __metadata: languageName: node linkType: hard -"@tanstack/table-core@npm:8.5.1": - version: 8.5.1 - resolution: "@tanstack/table-core@npm:8.5.1" - checksum: 8505861210325b041c1b8e97b2626d40fe64c5cae7b8edc36c1fc6d609128b4269582b2e67817b084232b301a88761f2b8286c1f65ada8d7a14b9cbba8e585db +"@tanstack/table-core@npm:8.13.2": + version: 8.13.2 + resolution: "@tanstack/table-core@npm:8.13.2" + checksum: 404cb5b1f0976d06c1a560ab895bb85af0846dd2d275a7ab47ca5bb599097b39530cd424259a40668ee6de204b6285fd122cd42b5e501c213a6d464ec45d1f9a languageName: node linkType: hard @@ -2996,6 +3274,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.1.1": + version: 1.2.3 + resolution: "aria-hidden@npm:1.2.3" + dependencies: + tslib: ^2.0.0 + checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9 + languageName: node + linkType: hard + "aria-query@npm:^5.1.3": version: 5.3.0 resolution: "aria-query@npm:5.3.0" @@ -4356,6 +4643,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449 + languageName: node + linkType: hard + "didyoumean@npm:^1.2.2": version: 1.2.2 resolution: "didyoumean@npm:1.2.2" @@ -5801,6 +6095,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: e2614e43b4694c78277bb61b0f04583d45786881289285c73770b07ded246a98be7e1f78b940c80cbe6f2b07f55f0b724e6db6fd6f1bcbd1e8bdac16521074ed + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -6403,6 +6704,15 @@ __metadata: languageName: node linkType: hard +"invariant@npm:^2.2.4": + version: 2.2.4 + resolution: "invariant@npm:2.2.4" + dependencies: + loose-envify: ^1.0.0 + checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14 + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -7198,11 +7508,13 @@ __metadata: "@radix-ui/react-collapsible": 1.0.3 "@radix-ui/react-label": 2.0.2 "@radix-ui/react-radio-group": 1.1.3 + "@radix-ui/react-select": 2.0.0 + "@radix-ui/react-slot": 1.0.2 "@reduxjs/toolkit": 1.8.2 "@tailwindcss/forms": 0.4.0 "@tailwindcss/typography": 0.5.0 "@tanstack/react-query": ^4.2.1 - "@tanstack/react-table": 8.5.1 + "@tanstack/react-table": 8.13.2 "@tanstack/react-virtual": 3.0.1 "@types/chroma-js": 2.1.3 "@types/d3-format": 3.0.1 @@ -7471,7 +7783,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -9257,6 +9569,41 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.3": + version: 2.3.5 + resolution: "react-remove-scroll-bar@npm:2.3.5" + dependencies: + react-style-singleton: ^2.2.1 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 0b6eee6d338085f0c766dc6d780100041a39377bc1a2a1b99a13444832b91885fc632b7521636a29d26710cf8bb0f9f4177123abe088a358597ac0f0e8e42f45 + languageName: node + linkType: hard + +"react-remove-scroll@npm:2.5.5": + version: 2.5.5 + resolution: "react-remove-scroll@npm:2.5.5" + dependencies: + react-remove-scroll-bar: ^2.3.3 + react-style-singleton: ^2.2.1 + tslib: ^2.1.0 + use-callback-ref: ^1.3.0 + use-sidecar: ^1.1.2 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2c7fe9cbd766f5e54beb4bec2e2efb2de3583037b23fef8fa511ab426ed7f1ae992382db5acd8ab5bfb030a4b93a06a2ebca41377d6eeaf0e6791bb0a59616a4 + languageName: node + linkType: hard + "react-resize-detector@npm:^8.0.4": version: 8.1.0 resolution: "react-resize-detector@npm:8.1.0" @@ -9283,6 +9630,23 @@ __metadata: languageName: node linkType: hard +"react-style-singleton@npm:^2.2.1": + version: 2.2.1 + resolution: "react-style-singleton@npm:2.2.1" + dependencies: + get-nonce: ^1.0.0 + invariant: ^2.2.4 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7ee8ef3aab74c7ae1d70ff34a27643d11ba1a8d62d072c767827d9ff9a520905223e567002e0bf6c772929d8ea1c781a3ba0cc4a563e92b1e3dc2eaa817ecbe8 + languageName: node + linkType: hard + "react-transition-group@npm:2.9.0": version: 2.9.0 resolution: "react-transition-group@npm:2.9.0" @@ -11153,6 +11517,37 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.0": + version: 1.3.1 + resolution: "use-callback-ref@npm:1.3.1" + dependencies: + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6a6a3a8bfe88f466eab982b8a92e5da560a7127b3b38815e89bc4d195d4b33aa9a53dba50d93e8138e7502bcc7e39efe9f2735a07a673212630990c73483e8e9 + languageName: node + linkType: hard + +"use-sidecar@npm:^1.1.2": + version: 1.1.2 + resolution: "use-sidecar@npm:1.1.2" + dependencies: + detect-node-es: ^1.1.0 + tslib: ^2.0.0 + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 925d1922f9853e516eaad526b6fed1be38008073067274f0ecc3f56b17bb8ab63480140dd7c271f94150027c996cea4efe83d3e3525e8f3eda22055f6a39220b + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" From 88897986fc044b3aaf2be79be69c39079041781e Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 6 Mar 2024 15:08:53 +0100 Subject: [PATCH 077/153] added geosjon layer --- client/package.json | 2 + .../analysis-eudr/map/component.tsx | 66 ++++++ .../src/containers/analysis-eudr/map/index.ts | 1 + .../analysis-eudr/map/zoom/component.tsx | 51 +++++ .../analysis-eudr/map/zoom/index.ts | 1 + client/src/pages/eudr/index.tsx | 28 +-- client/yarn.lock | 211 +++++++++++++++++- 7 files changed, 330 insertions(+), 30 deletions(-) create mode 100644 client/src/containers/analysis-eudr/map/component.tsx create mode 100644 client/src/containers/analysis-eudr/map/index.ts create mode 100644 client/src/containers/analysis-eudr/map/zoom/component.tsx create mode 100644 client/src/containers/analysis-eudr/map/zoom/index.ts diff --git a/client/package.json b/client/package.json index 7fc759e1c..f688abb13 100644 --- a/client/package.json +++ b/client/package.json @@ -15,12 +15,14 @@ "test": "start-server-and-test 'yarn build && yarn start' http://localhost:3000/auth/signin 'nyc --reporter nyc-report-lcov-absolute yarn cypress:headless'" }, "dependencies": { + "@deck.gl/carto": "^8.9.35", "@deck.gl/core": "8.8.6", "@deck.gl/extensions": "8.8.6", "@deck.gl/geo-layers": "8.8.6", "@deck.gl/layers": "8.8.6", "@deck.gl/mapbox": "8.8.6", "@deck.gl/mesh-layers": "8.8.6", + "@deck.gl/react": "^8.9.35", "@dnd-kit/core": "5.0.3", "@dnd-kit/modifiers": "5.0.0", "@dnd-kit/sortable": "6.0.1", diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx new file mode 100644 index 000000000..42bca1162 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -0,0 +1,66 @@ +import { useState, useCallback } from 'react'; +import DeckGL from '@deck.gl/react/typed'; +import { GeoJsonLayer } from '@deck.gl/layers/typed'; +import Map from 'react-map-gl/maplibre'; +import { type MapViewState } from '@deck.gl/core/typed'; + +import ZoomControl from './zoom'; + +import BasemapControl from '@/components/map/controls/basemap'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; + +import type { BasemapValue } from '@/components/map/controls/basemap/types'; +import type { MapStyle } from '@/components/map/types'; + +const data = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; + +const EUDRMap = () => { + const [mapStyle, setMapStyle] = useState('terrain'); + const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); + + const layer: GeoJsonLayer = new GeoJsonLayer({ + id: 'geojson-layer', + data, + // Styles + filled: true, + pointRadiusMinPixels: 2, + pointRadiusScale: 2000, + getFillColor: [200, 0, 80, 180], + // Interactive props + pickable: true, + autoHighlight: true, + }); + + const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { + setMapStyle(newStyle); + }, []); + + const handleZoomIn = useCallback(() => { + const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; + setViewState({ ...viewState, zoom }); + }, [viewState]); + + const handleZoomOut = useCallback(() => { + const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom - 1; + setViewState({ ...viewState, zoom }); + }, [viewState]); + + return ( + <> + setViewState(viewState as MapViewState)} + controller={{ dragRotate: false }} + layers={[layer]} + > + + +
+ + +
+ + ); +}; + +export default EUDRMap; diff --git a/client/src/containers/analysis-eudr/map/index.ts b/client/src/containers/analysis-eudr/map/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/map/zoom/component.tsx b/client/src/containers/analysis-eudr/map/zoom/component.tsx new file mode 100644 index 000000000..8b2eeeba1 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/zoom/component.tsx @@ -0,0 +1,51 @@ +import { MinusIcon, PlusIcon } from '@heroicons/react/solid'; +import cx from 'classnames'; + +import type { MapViewState } from '@deck.gl/core/typed'; +import type { FC } from 'react'; + +const COMMON_CLASSES = + 'p-2 transition-colors bg-white cursor-pointer hover:bg-gray-100 active:bg-navy-50 disabled:bg-gray-100 disabled:opacity-75 disabled:cursor-default'; + +const ZoomControl: FC<{ + viewState: MapViewState; + className?: string; + onZoomIn: () => void; + onZoomOut: () => void; +}> = ({ viewState, className = null, onZoomIn, onZoomOut }) => { + const { zoom, minZoom, maxZoom } = viewState; + + return ( +
+ + +
+ ); +}; + +export default ZoomControl; diff --git a/client/src/containers/analysis-eudr/map/zoom/index.ts b/client/src/containers/analysis-eudr/map/zoom/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/zoom/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index 3c7c4dd1f..be4350cd2 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -6,8 +6,7 @@ import { tasksSSR } from 'services/ssr'; import ApplicationLayout from 'layouts/application'; import CollapseButton from 'containers/collapse-button/component'; import TitleTemplate from 'utils/titleTemplate'; -import Map from 'components/map'; -import LayerManager from 'components/map/layer-manager'; +import Map from 'containers/analysis-eudr/map'; import SuppliersStackedBar from '@/containers/analysis-eudr/suppliers-stacked-bar'; import EUDRFilters from '@/containers/analysis-eudr/filters/component'; import SupplierListTable from '@/containers/analysis-eudr/supplier-list-table'; @@ -54,30 +53,7 @@ const MapPage: NextPageWithLayout = () => {
- - {() => ( - <> - - {/* {tooltipData && tooltipData.data?.v && ( - -
-
- {tooltipData.data.v} - {tooltipData.data.unit && ` ${tooltipData.data.unit}`} -
-
{tooltipData.data.name}
-
-
- )} */} - - )} -
+
diff --git a/client/yarn.lock b/client/yarn.lock index 2a07a1280..af3287489 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -326,6 +326,37 @@ __metadata: languageName: node linkType: hard +"@deck.gl/carto@npm:^8.9.35": + version: 8.9.35 + resolution: "@deck.gl/carto@npm:8.9.35" + dependencies: + "@babel/runtime": ^7.0.0 + "@loaders.gl/gis": ^3.4.13 + "@loaders.gl/loader-utils": ^3.4.13 + "@loaders.gl/mvt": ^3.4.13 + "@loaders.gl/tiles": ^3.4.13 + "@luma.gl/constants": ^8.5.21 + "@math.gl/web-mercator": ^3.6.2 + cartocolor: ^4.0.2 + d3-array: ^3.2.0 + d3-color: ^3.1.0 + d3-format: ^3.1.0 + d3-scale: ^4.0.0 + h3-js: ^3.7.0 + moment-timezone: ^0.5.33 + pbf: ^3.2.1 + quadbin: ^0.1.9 + peerDependencies: + "@deck.gl/aggregation-layers": ^8.0.0 + "@deck.gl/core": ^8.0.0 + "@deck.gl/extensions": ^8.0.0 + "@deck.gl/geo-layers": ^8.0.0 + "@deck.gl/layers": ^8.0.0 + "@loaders.gl/core": ^3.4.13 + checksum: 2c8edfd7974e4e4ee7874da5c8db2da75dc84074d4d121350228206150071ad82d5a541e79ee3bc059f6d8716929b701d1421407e5df052a4f9a097d016e4065 + languageName: node + linkType: hard + "@deck.gl/core@npm:8.8.6": version: 8.8.6 resolution: "@deck.gl/core@npm:8.8.6" @@ -437,6 +468,20 @@ __metadata: languageName: node linkType: hard +"@deck.gl/react@npm:^8.9.35": + version: 8.9.35 + resolution: "@deck.gl/react@npm:8.9.35" + dependencies: + "@babel/runtime": ^7.0.0 + peerDependencies: + "@deck.gl/core": ^8.0.0 + "@types/react": ">= 16.3" + react: ">=16.3" + react-dom: ">=16.3" + checksum: 98269a5c0d9a644f5fcd2a1eb401001edbd87f99bf6b6dd8c5f0a193dbd6e1f738e3ae94bc3559902b760411340a7f730aa1f674e7fa811c7b7fdb42cd4fc6ca + languageName: node + linkType: hard + "@dnd-kit/accessibility@npm:^3.0.0": version: 3.0.1 resolution: "@dnd-kit/accessibility@npm:3.0.1" @@ -1078,6 +1123,19 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/gis@npm:3.4.15, @loaders.gl/gis@npm:^3.4.13": + version: 3.4.15 + resolution: "@loaders.gl/gis@npm:3.4.15" + dependencies: + "@loaders.gl/loader-utils": 3.4.15 + "@loaders.gl/schema": 3.4.15 + "@mapbox/vector-tile": ^1.3.1 + "@math.gl/polygon": ^3.5.1 + pbf: ^3.2.1 + checksum: dae1ac222e731a1ffad95f6f2176a33790fa097fa95e20817a4ca6325e1d7be5d8a4f69a2019cf0769aede7d73bff79f7ad38533add58978505a49862f42c685 + languageName: node + linkType: hard + "@loaders.gl/gltf@npm:3.3.1, @loaders.gl/gltf@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/gltf@npm:3.3.1" @@ -1100,6 +1158,15 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/images@npm:3.4.15": + version: 3.4.15 + resolution: "@loaders.gl/images@npm:3.4.15" + dependencies: + "@loaders.gl/loader-utils": 3.4.15 + checksum: 7d5ac4948b6f612aed410eafb05b34bea7d067475a07d40c022ed9f09c2de7403f24ab1d694a647917275e5e18c70f3c94d5f50278ef15f7b7bd01375df83f47 + languageName: node + linkType: hard + "@loaders.gl/loader-utils@npm:3.3.1, @loaders.gl/loader-utils@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/loader-utils@npm:3.3.1" @@ -1111,6 +1178,17 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/loader-utils@npm:3.4.15, @loaders.gl/loader-utils@npm:^3.4.13": + version: 3.4.15 + resolution: "@loaders.gl/loader-utils@npm:3.4.15" + dependencies: + "@babel/runtime": ^7.3.1 + "@loaders.gl/worker-utils": 3.4.15 + "@probe.gl/stats": ^3.5.0 + checksum: 04a91e56ecf8b792853a24184a27c3bf9824e62959e8d8c5f3b615724816c6bfdced8c56c02c9e22c6795a030c24009a75ae5b6f924627e2575e039c7234ba96 + languageName: node + linkType: hard + "@loaders.gl/math@npm:3.3.1": version: 3.3.1 resolution: "@loaders.gl/math@npm:3.3.1" @@ -1122,6 +1200,17 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/math@npm:3.4.15": + version: 3.4.15 + resolution: "@loaders.gl/math@npm:3.4.15" + dependencies: + "@loaders.gl/images": 3.4.15 + "@loaders.gl/loader-utils": 3.4.15 + "@math.gl/core": ^3.5.1 + checksum: e0e41e5253f876ecd6a15ec0648954c3d88516794336dd431ab1a82dcd4a4d2fb4a69c7e087f83744c4ef7982e60aa4ee6fdf89c8686772c30291cc399e02beb + languageName: node + linkType: hard + "@loaders.gl/mvt@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/mvt@npm:3.3.1" @@ -1135,6 +1224,19 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/mvt@npm:^3.4.13": + version: 3.4.15 + resolution: "@loaders.gl/mvt@npm:3.4.15" + dependencies: + "@loaders.gl/gis": 3.4.15 + "@loaders.gl/loader-utils": 3.4.15 + "@loaders.gl/schema": 3.4.15 + "@math.gl/polygon": ^3.5.1 + pbf: ^3.2.1 + checksum: f3f32d558e87a28256966c215456d482a5d910dd5e7426b57b65e78df5fef2dfd6869591152c61e49ecc719636c519e0047954227c66a30b240bf161dd13b38d + languageName: node + linkType: hard + "@loaders.gl/schema@npm:3.3.1, @loaders.gl/schema@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/schema@npm:3.3.1" @@ -1144,6 +1246,15 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/schema@npm:3.4.15": + version: 3.4.15 + resolution: "@loaders.gl/schema@npm:3.4.15" + dependencies: + "@types/geojson": ^7946.0.7 + checksum: 948e039848d1a599d6fe60276e2fb44f3ffc27924f944d399f1bd264209bb7409ef288dfed1c4440fd527e8939c7753ddbf7c4f2e4d4ac6f420c8a13ed187ffc + languageName: node + linkType: hard + "@loaders.gl/terrain@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/terrain@npm:3.3.1" @@ -1187,6 +1298,23 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/tiles@npm:^3.4.13": + version: 3.4.15 + resolution: "@loaders.gl/tiles@npm:3.4.15" + dependencies: + "@loaders.gl/loader-utils": 3.4.15 + "@loaders.gl/math": 3.4.15 + "@math.gl/core": ^3.5.1 + "@math.gl/culling": ^3.5.1 + "@math.gl/geospatial": ^3.5.1 + "@math.gl/web-mercator": ^3.5.1 + "@probe.gl/stats": ^3.5.0 + peerDependencies: + "@loaders.gl/core": ^3.4.0 + checksum: 18cc163c621b8ee388b0f78d5a95171a7c3540a5156d687b4d431ffc9f0533c4f220c8b265e7fbeaabc569260615229ed8871a7d2f2a08f7da0a5bcd6f083563 + languageName: node + linkType: hard + "@loaders.gl/worker-utils@npm:3.3.1": version: 3.3.1 resolution: "@loaders.gl/worker-utils@npm:3.3.1" @@ -1196,6 +1324,15 @@ __metadata: languageName: node linkType: hard +"@loaders.gl/worker-utils@npm:3.4.15": + version: 3.4.15 + resolution: "@loaders.gl/worker-utils@npm:3.4.15" + dependencies: + "@babel/runtime": ^7.3.1 + checksum: f4d77444a73b650b92340036af9e5d5a5449ef1e8940a2b33f4800444918c76e7d54f9bafd86c413cb94d777c256a22521bfbbd4e981757d39ecdfbb9994e28d + languageName: node + linkType: hard + "@luma.gl/constants@npm:8.5.16": version: 8.5.16 resolution: "@luma.gl/constants@npm:8.5.16" @@ -1210,6 +1347,13 @@ __metadata: languageName: node linkType: hard +"@luma.gl/constants@npm:^8.5.21": + version: 8.5.21 + resolution: "@luma.gl/constants@npm:8.5.21" + checksum: 2ac0eb0fb368d8a4722f140917de45100af7adde776c5be40935c86c996491e9145464f3dd5be69294a839fa145b456df828425a93f071bf68bf62c1f79c376f + languageName: node + linkType: hard + "@luma.gl/core@npm:^8.5.16": version: 8.5.16 resolution: "@luma.gl/core@npm:8.5.16" @@ -1328,6 +1472,15 @@ __metadata: languageName: node linkType: hard +"@mapbox/tile-cover@npm:3.0.1": + version: 3.0.1 + resolution: "@mapbox/tile-cover@npm:3.0.1" + dependencies: + tilebelt: ^1.0.1 + checksum: 0b28516ca3159a0ec3aca923d7031e2c1ba1e3ea73eead8f06448a7dd904a3a0c3d4ee3a998ae391b0e7444540eb29f79c21cd637a7845f0315c15a9c9455f76 + languageName: node + linkType: hard + "@mapbox/tiny-sdf@npm:^1.1.0": version: 1.2.5 resolution: "@mapbox/tiny-sdf@npm:1.2.5" @@ -3821,6 +3974,15 @@ __metadata: languageName: node linkType: hard +"cartocolor@npm:^4.0.2": + version: 4.0.2 + resolution: "cartocolor@npm:4.0.2" + dependencies: + colorbrewer: 1.0.0 + checksum: 3754e8211acb69e98d489a97dbd7536edcbf466004b3860a175d421296cbe198709c006ac106d0f6cdc379ab2e7e0c6227c88b555a8cf82699bfcc99af78e95a + languageName: node + linkType: hard + "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -4067,6 +4229,13 @@ __metadata: languageName: node linkType: hard +"colorbrewer@npm:1.0.0": + version: 1.0.0 + resolution: "colorbrewer@npm:1.0.0" + checksum: 9513dfe9792824505bda88f1c41f53ad5965a21292d7a2902cbfade0d895e88b15e4b8e00ee08f0710d8d671c46c48f53bb471696a9a24db3cdb6d0862a4ff5d + languageName: node + linkType: hard + "colorette@npm:^1.2.2": version: 1.4.0 resolution: "colorette@npm:1.4.0" @@ -4328,7 +4497,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:^3.1.6": +"d3-array@npm:^3.1.6, d3-array@npm:^3.2.0": version: 3.2.4 resolution: "d3-array@npm:3.2.4" dependencies: @@ -4337,7 +4506,7 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3": +"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b @@ -4351,7 +4520,7 @@ __metadata: languageName: node linkType: hard -"d3-format@npm:1 - 3": +"d3-format@npm:1 - 3, d3-format@npm:^3.1.0": version: 3.1.0 resolution: "d3-format@npm:3.1.0" checksum: f345ec3b8ad3cab19bff5dead395bd9f5590628eb97a389b1dd89f0b204c7c4fc1d9520f13231c2c7cf14b7c9a8cf10f8ef15bde2befbab41454a569bd706ca2 @@ -4381,7 +4550,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:4.0.2, d3-scale@npm:^4.0.2": +"d3-scale@npm:4.0.2, d3-scale@npm:^4.0.0, d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -7486,12 +7655,14 @@ __metadata: version: 0.0.0-use.local resolution: "landgriffon-client@workspace:." dependencies: + "@deck.gl/carto": ^8.9.35 "@deck.gl/core": 8.8.6 "@deck.gl/extensions": 8.8.6 "@deck.gl/geo-layers": 8.8.6 "@deck.gl/layers": 8.8.6 "@deck.gl/mapbox": 8.8.6 "@deck.gl/mesh-layers": 8.8.6 + "@deck.gl/react": ^8.9.35 "@dnd-kit/core": 5.0.3 "@dnd-kit/modifiers": 5.0.0 "@dnd-kit/sortable": 6.0.1 @@ -8148,6 +8319,22 @@ __metadata: languageName: node linkType: hard +"moment-timezone@npm:^0.5.33": + version: 0.5.45 + resolution: "moment-timezone@npm:0.5.45" + dependencies: + moment: ^2.29.4 + checksum: a22e9f983fbe1a01757ce30685bce92e3f6efa692eb682afd47b82da3ff960b3c8c2c3883ec6715c124bc985a342b57cba1f6ba25a1c8b4c7ad766db3cd5e1d0 + languageName: node + linkType: hard + +"moment@npm:^2.29.4": + version: 2.30.1 + resolution: "moment@npm:2.30.1" + checksum: 859236bab1e88c3e5802afcf797fc801acdbd0ee509d34ea3df6eea21eb6bcc2abd4ae4e4e64aa7c986aa6cba563c6e62806218e6412a765010712e5fa121ba6 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -9274,6 +9461,15 @@ __metadata: languageName: node linkType: hard +"quadbin@npm:^0.1.9": + version: 0.1.9 + resolution: "quadbin@npm:0.1.9" + dependencies: + "@mapbox/tile-cover": 3.0.1 + checksum: 318113ac94178659f8e589644b4a0c14817262eb0ab2418f7313052133a53009b52870220e88759efdea7f467448d7fdd063758deb8014be5652208e451a6733 + languageName: node + linkType: hard + "query-string@npm:8.1.0": version: 8.1.0 resolution: "query-string@npm:8.1.0" @@ -11140,6 +11336,13 @@ __metadata: languageName: node linkType: hard +"tilebelt@npm:^1.0.1": + version: 1.0.1 + resolution: "tilebelt@npm:1.0.1" + checksum: f201cf1718f53b8d7b2e89f53f90bbcf6eba5f58fe578da5ce35f93ea4e2302100f019fe2d388a0fb7ec5ceda0b6a0f08d10657b6b54eb5d74db218ad4bad177 + languageName: node + linkType: hard + "tiny-glob@npm:^0.2.9": version: 0.2.9 resolution: "tiny-glob@npm:0.2.9" From 486abad36759917fea9da42a3db8875260372a4a Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 6 Mar 2024 16:08:25 +0100 Subject: [PATCH 078/153] added toogle legend --- client/package.json | 2 + client/src/components/ui/popover.tsx | 29 ++++++ .../analysis-eudr/map/component.tsx | 2 + .../analysis-eudr/map/legend/component.tsx | 31 ++++++ .../analysis-eudr/map/legend/index.ts | 1 + client/yarn.lock | 99 +++++++++++++++++++ 6 files changed, 164 insertions(+) create mode 100644 client/src/components/ui/popover.tsx create mode 100644 client/src/containers/analysis-eudr/map/legend/component.tsx create mode 100644 client/src/containers/analysis-eudr/map/legend/index.ts diff --git a/client/package.json b/client/package.json index f688abb13..2c05f2a00 100644 --- a/client/package.json +++ b/client/package.json @@ -37,7 +37,9 @@ "@loaders.gl/core": "3.3.1", "@luma.gl/constants": "8.5.18", "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "1.1.3", "@radix-ui/react-select": "2.0.0", "@radix-ui/react-slot": "1.0.2", diff --git a/client/src/components/ui/popover.tsx b/client/src/components/ui/popover.tsx new file mode 100644 index 000000000..bbba7e0eb --- /dev/null +++ b/client/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 42bca1162..5aa2189c2 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -5,6 +5,7 @@ import Map from 'react-map-gl/maplibre'; import { type MapViewState } from '@deck.gl/core/typed'; import ZoomControl from './zoom'; +import LegendControl from './legend'; import BasemapControl from '@/components/map/controls/basemap'; import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; @@ -58,6 +59,7 @@ const EUDRMap = () => {
+
); diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx new file mode 100644 index 000000000..53c7d138e --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import classNames from 'classnames'; + +import SandwichIcon from 'components/icons/sandwich'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; + +const EURDLegend = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + + + + Place content for the popover here. + + +
+ ); +}; + +export default EURDLegend; diff --git a/client/src/containers/analysis-eudr/map/legend/index.ts b/client/src/containers/analysis-eudr/map/legend/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/yarn.lock b/client/yarn.lock index af3287489..47369494e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1950,6 +1950,32 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dropdown-menu@npm:^2.0.6": + version: 2.0.6 + resolution: "@radix-ui/react-dropdown-menu@npm:2.0.6" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-menu": 2.0.6 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 1433e04234c29ae688b1d50b4a5ad0fd67e2627a5ea2e5f60fec6e4307e673ef35a703672eae0d61d96156c59084bbb19de9f9b9936b3fc351917dfe41dcf403 + languageName: node + linkType: hard + "@radix-ui/react-focus-guards@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-focus-guards@npm:1.0.1" @@ -2023,6 +2049,77 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-menu@npm:2.0.6": + version: 2.0.6 + resolution: "@radix-ui/react-menu@npm:2.0.6" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-collection": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-direction": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-focus-guards": 1.0.1 + "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-roving-focus": 1.0.4 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-callback-ref": 1.0.1 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.5 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: a43fb560dbb5a4ddc43ea4e2434a9f517bbbcbf8b12e1e74c1e36666ad321aef7e39f91770140c106fe6f34e237102be8a02f3bc5588e6c06a709e20580c5e82 + languageName: node + linkType: hard + +"@radix-ui/react-popover@npm:^1.0.7": + version: 1.0.7 + resolution: "@radix-ui/react-popover@npm:1.0.7" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-focus-guards": 1.0.1 + "@radix-ui/react-focus-scope": 1.0.4 + "@radix-ui/react-id": 1.0.1 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-use-controllable-state": 1.0.1 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.5 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 3ec15c0923ea457f586aa34f77e17fabffa02dffeab622951560ec21c38df2f43718ff088d24bf9fd1d9cd0db62436fc19cae5b122d90f72de4945a1f508dc59 + languageName: node + linkType: hard + "@radix-ui/react-popper@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/react-popper@npm:1.1.3" @@ -7677,7 +7774,9 @@ __metadata: "@loaders.gl/core": 3.3.1 "@luma.gl/constants": 8.5.18 "@radix-ui/react-collapsible": 1.0.3 + "@radix-ui/react-dropdown-menu": ^2.0.6 "@radix-ui/react-label": 2.0.2 + "@radix-ui/react-popover": ^1.0.7 "@radix-ui/react-radio-group": 1.1.3 "@radix-ui/react-select": 2.0.0 "@radix-ui/react-slot": 1.0.2 From 111c9ec1cba7959b167aeb52d14110ddf958a800 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 7 Mar 2024 10:25:20 +0100 Subject: [PATCH 079/153] added legend styles --- client/src/components/ui/popover.tsx | 22 ++++---- .../analysis-eudr/map/component.tsx | 4 +- .../analysis-eudr/map/legend/component.tsx | 51 +++++++++++++++++-- .../analysis-eudr/map/legend/item.tsx | 33 ++++++++++++ 4 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 client/src/containers/analysis-eudr/map/legend/item.tsx diff --git a/client/src/components/ui/popover.tsx b/client/src/components/ui/popover.tsx index bbba7e0eb..325d7b703 100644 --- a/client/src/components/ui/popover.tsx +++ b/client/src/components/ui/popover.tsx @@ -1,29 +1,29 @@ -import * as React from "react" -import * as PopoverPrimitive from "@radix-ui/react-popover" +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Popover = PopoverPrimitive.Root +const Popover = PopoverPrimitive.Root; -const PopoverTrigger = PopoverPrimitive.Trigger +const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent } +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 5aa2189c2..fc4ad8070 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -32,6 +32,8 @@ const EUDRMap = () => { autoHighlight: true, }); + const layers = [layer]; + const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { setMapStyle(newStyle); }, []); @@ -52,7 +54,7 @@ const EUDRMap = () => { viewState={{ ...viewState }} onViewStateChange={({ viewState }) => setViewState(viewState as MapViewState)} controller={{ dragRotate: false }} - layers={[layer]} + layers={layers} > diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index 53c7d138e..54da03733 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -1,11 +1,16 @@ import { useState } from 'react'; import classNames from 'classnames'; +import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; -import SandwichIcon from 'components/icons/sandwich'; +import LegendItem from './item'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import SandwichIcon from '@/components/icons/sandwich'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; const EURDLegend = () => { const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); return (
@@ -14,14 +19,52 @@ const EURDLegend = () => { - - Place content for the popover here. + +
+

Legend

+
+ +
+ + +
+ +
+
+ + + + +
+
diff --git a/client/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx new file mode 100644 index 000000000..3946119f2 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -0,0 +1,33 @@ +import classNames from 'classnames'; + +import type { FC, PropsWithChildren } from 'react'; + +type LegendItemProps = { title: string; description: string; iconClassName?: string }; + +const LegendItem: FC> = ({ + title, + description, + children, + iconClassName, +}) => { + return ( +
+
+
+
+

{title}

+
+
+

{description}

+ {children} +
+
+ ); +}; + +export default LegendItem; From a49dc559c536161d0f0abdec633f94d0f9e8b395 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 7 Mar 2024 11:38:13 +0100 Subject: [PATCH 080/153] added plot geometries from api --- client/package.json | 1 + .../analysis-eudr/map/component.tsx | 56 ++++++++++++++++--- client/src/hooks/eudr/index.ts | 25 +++++++++ client/src/pages/eudr/index.tsx | 2 +- client/yarn.lock | 27 +++++++++ 5 files changed, 102 insertions(+), 9 deletions(-) diff --git a/client/package.json b/client/package.json index 2c05f2a00..370fc9ded 100644 --- a/client/package.json +++ b/client/package.json @@ -49,6 +49,7 @@ "@tanstack/react-query": "^4.2.1", "@tanstack/react-table": "8.13.2", "@tanstack/react-virtual": "3.0.1", + "@turf/bbox": "^6.5.0", "autoprefixer": "10.2.5", "axios": "1.3.4", "chroma-js": "2.1.2", diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index fc4ad8070..8c557d6ee 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -1,35 +1,40 @@ -import { useState, useCallback } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import DeckGL from '@deck.gl/react/typed'; import { GeoJsonLayer } from '@deck.gl/layers/typed'; import Map from 'react-map-gl/maplibre'; -import { type MapViewState } from '@deck.gl/core/typed'; +import { WebMercatorViewport, type MapViewState } from '@deck.gl/core/typed'; +import bbox from '@turf/bbox'; import ZoomControl from './zoom'; import LegendControl from './legend'; import BasemapControl from '@/components/map/controls/basemap'; import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; +import { usePlotGeometries } from '@/hooks/eudr'; import type { BasemapValue } from '@/components/map/controls/basemap/types'; import type { MapStyle } from '@/components/map/types'; -const data = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; - const EUDRMap = () => { const [mapStyle, setMapStyle] = useState('terrain'); const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); + const plotGeometries = usePlotGeometries(); + const layer: GeoJsonLayer = new GeoJsonLayer({ id: 'geojson-layer', - data, + data: plotGeometries.data, // Styles filled: true, - pointRadiusMinPixels: 2, - pointRadiusScale: 2000, - getFillColor: [200, 0, 80, 180], + getFillColor: [255, 176, 0, 84], + stroked: true, + getLineColor: [255, 176, 0, 255], + getLineWidth: 1, + lineWidthUnits: 'pixels', // Interactive props pickable: true, autoHighlight: true, + highlightColor: [255, 176, 0, 255], }); const layers = [layer]; @@ -48,6 +53,40 @@ const EUDRMap = () => { setViewState({ ...viewState, zoom }); }, [viewState]); + const fitToPlotBounds = useCallback(() => { + if (!plotGeometries.data) return; + const [minLng, minLat, maxLng, maxLat] = bbox(plotGeometries.data); + const newViewport = new WebMercatorViewport(viewState); + const { longitude, latitude, zoom } = newViewport.fitBounds( + [ + [minLng, minLat], + [maxLng, maxLat], + ], + { + padding: 10, + }, + ); + if ( + viewState.latitude !== latitude || + viewState.longitude !== longitude || + viewState.zoom !== zoom + ) { + setViewState({ ...viewState, longitude, latitude, zoom }); + } + }, [plotGeometries.data, viewState]); + + // Fit to bounds when data is loaded or changed + useEffect(() => { + if (plotGeometries.data) { + fitToPlotBounds(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data]); + + const handleResize = useCallback(() => { + setTimeout(() => fitToPlotBounds(), 0); + }, [fitToPlotBounds]); + return ( <> { onViewStateChange={({ viewState }) => setViewState(viewState as MapViewState)} controller={{ dragRotate: false }} layers={layers} + onResize={handleResize} > diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 5b369efa7..c39176d69 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -24,3 +24,28 @@ export const useEUDRSuppliers = ( }, ); }; + +export const usePlotGeometries = ( + params?: { + producersIds: string[]; + originsId: string[]; + materialsId: string[]; + geoRegionIds: string[]; + }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-geo-features-collection', params], + () => + apiService + .request<{ geojson }>({ + method: 'GET', + url: '/eudr/geo-features/collection', + params, + }) + .then((response) => response.data.geojson), + { + ...options, + }, + ); +}; diff --git a/client/src/pages/eudr/index.tsx b/client/src/pages/eudr/index.tsx index be4350cd2..8bffe7529 100644 --- a/client/src/pages/eudr/index.tsx +++ b/client/src/pages/eudr/index.tsx @@ -77,7 +77,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, query } } return { props: { query } }; } catch (error) { - if (error.response.status === 401) { + if (error.code === '401' || error.response.status === 401) { return { redirect: { permanent: false, diff --git a/client/yarn.lock b/client/yarn.lock index 47369494e..98c5d3880 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2631,6 +2631,32 @@ __metadata: languageName: node linkType: hard +"@turf/bbox@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/bbox@npm:6.5.0" + dependencies: + "@turf/helpers": ^6.5.0 + "@turf/meta": ^6.5.0 + checksum: 537be56ae0c5ad44e71a691717b35745e947e19a6bd9f20fdac2ab4318caf98cd88472d7dbf576e8b32ead5da034d273ffb3f4559d6d386820ddcb88a1f7fedd + languageName: node + linkType: hard + +"@turf/helpers@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/helpers@npm:6.5.0" + checksum: d57f746351357838c654e0a9b98be3285a14b447504fd6d59753d90c6d437410bb24805d61c65b612827f07f6c2ade823bb7e56e41a1a946217abccfbd64c117 + languageName: node + linkType: hard + +"@turf/meta@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/meta@npm:6.5.0" + dependencies: + "@turf/helpers": ^6.5.0 + checksum: c6bb936aa92bf3365e87a50dc65f248e070c5767a36fac390754c00c89bf2d1583418686ab19a10332bfa9340b8cac6aaf2c55dad7f5fcf77f1a2dda75ccf363 + languageName: node + linkType: hard + "@types/chroma-js@npm:2.1.3": version: 2.1.3 resolution: "@types/chroma-js@npm:2.1.3" @@ -7786,6 +7812,7 @@ __metadata: "@tanstack/react-query": ^4.2.1 "@tanstack/react-table": 8.13.2 "@tanstack/react-virtual": 3.0.1 + "@turf/bbox": ^6.5.0 "@types/chroma-js": 2.1.3 "@types/d3-format": 3.0.1 "@types/d3-scale": 4.0.2 From 3b4702bf782c9d2b1af9cab4671668054bf20d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 7 Mar 2024 16:53:35 +0100 Subject: [PATCH 081/153] starts connecting filters and fetching of filters --- client/package.json | 6 +- client/src/components/ui/calendar.tsx | 57 +++ .../filters/more-filters/component.tsx | 462 ------------------ .../filters/more-filters/index.tsx | 277 ++++++++++- .../filters/more-filters/types.d.ts | 10 - .../filters/years-range/component.tsx | 99 ---- .../filters/years-range/constants.ts | 5 - .../filters/years-range/index.ts | 1 - .../filters/years-range/index.tsx | 62 +++ .../supplier-list-table/table/index.tsx | 11 +- client/src/hooks/eudr/index.ts | 93 +++- client/src/store/features/eudr/index.ts | 32 +- client/yarn.lock | 33 +- 13 files changed, 554 insertions(+), 594 deletions(-) create mode 100644 client/src/components/ui/calendar.tsx delete mode 100644 client/src/containers/analysis-eudr/filters/more-filters/component.tsx delete mode 100644 client/src/containers/analysis-eudr/filters/more-filters/types.d.ts delete mode 100644 client/src/containers/analysis-eudr/filters/years-range/component.tsx delete mode 100644 client/src/containers/analysis-eudr/filters/years-range/constants.ts delete mode 100644 client/src/containers/analysis-eudr/filters/years-range/index.ts create mode 100644 client/src/containers/analysis-eudr/filters/years-range/index.tsx diff --git a/client/package.json b/client/package.json index 370fc9ded..a15ba91ee 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "test": "start-server-and-test 'yarn build && yarn start' http://localhost:3000/auth/signin 'nyc --reporter nyc-report-lcov-absolute yarn cypress:headless'" }, "dependencies": { + "@date-fns/utc": "1.1.1", "@deck.gl/carto": "^8.9.35", "@deck.gl/core": "8.8.6", "@deck.gl/extensions": "8.8.6", @@ -39,7 +40,7 @@ "@radix-ui/react-collapsible": "1.0.3", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "2.0.2", - "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-popover": "1.0.7", "@radix-ui/react-radio-group": "1.1.3", "@radix-ui/react-select": "2.0.0", "@radix-ui/react-slot": "1.0.2", @@ -59,7 +60,7 @@ "d3-array": "3.0.2", "d3-format": "3.0.1", "d3-scale": "4.0.2", - "date-fns": "2.22.1", + "date-fns": "3.3.1", "fuse.js": "6.4.6", "jsona": "1.9.2", "lodash-es": "4.17.21", @@ -73,6 +74,7 @@ "query-string": "8.1.0", "rc-tree": "5.7.0", "react": "18.2.0", + "react-day-picker": "8.10.0", "react-dom": "18.2.0", "react-dropzone": "14.2.2", "react-hook-form": "7.43.1", diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx new file mode 100644 index 000000000..e7fde3fdf --- /dev/null +++ b/client/src/components/ui/calendar.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { DayPicker } from 'react-day-picker'; + +import { cn } from '@/lib/utils'; +import { buttonVariants } from '@/components/ui/button'; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/component.tsx b/client/src/containers/analysis-eudr/filters/more-filters/component.tsx deleted file mode 100644 index 243d2b5e8..000000000 --- a/client/src/containers/analysis-eudr/filters/more-filters/component.tsx +++ /dev/null @@ -1,462 +0,0 @@ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; -import { FilterIcon } from '@heroicons/react/solid'; -import { - offset, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, - FloatingPortal, -} from '@floating-ui/react'; -import { Popover, Transition } from '@headlessui/react'; -import { useRouter } from 'next/router'; - -import Materials from '@/containers/analysis-visualization/analysis-filters/materials/component'; -import OriginRegions from '@/containers/analysis-visualization/analysis-filters/origin-regions/component'; -import { flattenTree, recursiveMap, recursiveSort } from 'components/tree-select/utils'; -import Select from 'components/forms/select'; -import Button from 'components/button/component'; -import TreeSelect from 'components/tree-select'; -import { useAppDispatch, useAppSelector } from 'store/hooks'; -import { analysisFilters, setFilters } from 'store/features/analysis/filters'; -import { setFilter } from 'store/features/analysis'; -import { useMaterialsTrees } from 'hooks/materials'; -import { useAdminRegionsTrees } from 'hooks/admin-regions'; -import { useSuppliersTypes } from 'hooks/suppliers'; -import { useLocationTypes } from 'hooks/location-types'; -import { useBusinessUnitsOptionsTrees } from 'hooks/business-units'; - -import type { Option } from 'components/forms/select'; -import type { LocationTypes as LocationTyping } from 'containers/interventions/enums'; -import type { TreeSelectOption } from 'components/tree-select/types'; -import type { AnalysisFiltersState } from 'store/features/analysis/filters'; - -type MoreFiltersState = { - materials: AnalysisFiltersState['materials']; - origins: AnalysisFiltersState['origins']; - t1Suppliers: AnalysisFiltersState['t1Suppliers']; - producers: AnalysisFiltersState['producers']; - locationTypes: AnalysisFiltersState['locationTypes']; - businessUnits: AnalysisFiltersState['businessUnits']; -}; - -const INITIAL_FILTERS: MoreFiltersState = { - materials: [], - origins: [], - t1Suppliers: [], - producers: [], - locationTypes: [], - businessUnits: [], -}; - -interface ApiTreeResponse { - id: string; - name: string; - children?: this[]; -} - -const DEFAULT_QUERY_OPTIONS = { - select: (data: ApiTreeResponse[]) => { - const sorted = recursiveSort(data, 'name'); - return sorted.map((item) => recursiveMap(item, ({ id, name }) => ({ label: name, value: id }))); - }, -}; - -const MoreFilters = () => { - const { query } = useRouter(); - const { scenarioId, compareScenarioId } = query; - - const dispatch = useAppDispatch(); - const { materials, origins, t1Suppliers, producers, locationTypes, businessUnits } = - useAppSelector(analysisFilters); - - const moreFilters: MoreFiltersState = useMemo( - () => ({ materials, origins, t1Suppliers, producers, locationTypes, businessUnits }), - [materials, origins, t1Suppliers, producers, locationTypes, businessUnits], - ); - - const [selectedFilters, setSelectedFilters] = useState(moreFilters); - - const materialIds = useMemo( - () => selectedFilters.materials.map(({ value }) => value), - [selectedFilters.materials], - ); - - const originIds = useMemo( - () => selectedFilters.origins.map(({ value }) => value), - [selectedFilters.origins], - ); - - const t1SupplierIds = useMemo( - () => selectedFilters.t1Suppliers.map(({ value }) => value), - [selectedFilters.t1Suppliers], - ); - - const producerIds = useMemo( - () => selectedFilters.producers.map(({ value }) => value), - [selectedFilters.producers], - ); - - const locationTypesIds = useMemo( - () => selectedFilters.locationTypes.map(({ value }) => value), - [selectedFilters.locationTypes], - ); - - const businessUnitIds = useMemo( - () => selectedFilters.businessUnits.map(({ value }) => value), - [selectedFilters.businessUnits], - ); - - const [counter, setCounter] = useState(0); - - // Only the changes are applied when the user clicks on Apply - const handleApply = useCallback(() => { - dispatch(setFilters(selectedFilters)); - }, [dispatch, selectedFilters]); - - // Restoring state from initial state only internally, - // the user have to apply the changes - const handleClearFilters = useCallback(() => { - setSelectedFilters(INITIAL_FILTERS); - }, []); - - // Updating internal state from selectors - const handleChangeFilter = useCallback( - (key: keyof MoreFiltersState, values: TreeSelectOption[] | Option) => { - setSelectedFilters((filters) => ({ ...filters, [key]: values })); - }, - [], - ); - - useEffect(() => { - setSelectedFilters(moreFilters); - }, [moreFilters]); - - const { refs, strategy, x, y, context } = useFloating({ - // open: isOpen, - // onOpenChange: handleOpen, - placement: 'bottom-start', - strategy: 'fixed', - middleware: [offset({ mainAxis: 4 }), shift({ padding: 4 })], - }); - - const { getReferenceProps, getFloatingProps } = useInteractions([ - useClick(context), - useDismiss(context), - ]); - - const scenarioIds = useMemo( - () => [scenarioId, compareScenarioId].filter((id) => id) as string[], - [scenarioId, compareScenarioId], - ); - - const { data: materialOptions, isLoading: materialOptionsIsLoading } = useMaterialsTrees( - { - depth: 1, - withSourcingLocations: true, - scenarioIds, - originIds, - t1SupplierIds, - producerIds, - locationTypes: locationTypesIds, - businessUnitIds, - }, - { - ...DEFAULT_QUERY_OPTIONS, - select: (_materials) => - recursiveSort(_materials, 'name')?.map((item) => - recursiveMap(item, ({ id, name, status }) => ({ - value: id, - label: name, - disabled: status === 'inactive', - })), - ), - }, - ); - - const { data: originOptions, isLoading: originOptionsIsLoading } = useAdminRegionsTrees( - { - withSourcingLocations: true, - materialIds, - t1SupplierIds, - producerIds, - locationTypes: locationTypesIds, - scenarioIds, - businessUnitIds, - }, - DEFAULT_QUERY_OPTIONS, - ); - - const { data: t1SupplierOptions, isLoading: t1SupplierOptionsIsLoading } = useSuppliersTypes( - { - type: 't1supplier', - producerIds, - materialIds, - originIds, - locationTypes: locationTypesIds, - scenarioIds, - businessUnitIds, - }, - DEFAULT_QUERY_OPTIONS, - ); - - const { data: producerOptions, isLoading: producerOptionsIsLoading } = useSuppliersTypes( - { - type: 'producer', - t1SupplierIds, - materialIds, - originIds, - locationTypes: locationTypesIds, - scenarioIds, - businessUnitIds, - }, - DEFAULT_QUERY_OPTIONS, - ); - - const { data: locationTypeOptions, isLoading: locationTypeOptionsIsLoading } = useLocationTypes( - { - materialIds, - originIds, - t1SupplierIds, - producerIds, - scenarioIds, - businessUnitIds, - }, - { - onSuccess: (_locationTypeOptions) => { - // * every time new location types are fetched, we need to validate if the previous location types selected are still - // * available in the new options. Otherwise, we will remove them from the current selection. - setSelectedFilters((filters) => ({ - ...filters, - locationTypes: _locationTypeOptions.filter(({ value }) => - locationTypesIds.includes(value), - ), - })); - }, - }, - ); - - const { data: businessUnitsOptions, isLoading: businessUnitsOptionsIsLoading } = - useBusinessUnitsOptionsTrees({ - depth: 1, - withSourcingLocations: true, - materialIds, - originIds, - t1SupplierIds, - producerIds, - locationTypes: locationTypesIds, - scenarioIds, - }); - - const reviewFilterContent = useCallback( - ( - name: keyof MoreFiltersState, - currentValues: TreeSelectOption[], - allOptions: TreeSelectOption[], - ) => { - const allNodes = allOptions.flatMap((opt) => flattenTree(opt)); - const allKeys = allNodes.map(({ value }) => value); - const currentNodes = currentValues.flatMap(flattenTree); - const validOptions = currentNodes.filter(({ value }) => allKeys.includes(value)); - - if (validOptions.length !== allKeys.length) { - dispatch(setFilter({ id: name, value: validOptions })); - } - }, - [dispatch], - ); - - // Check current values are valid if the scenario changes - const handleScenarioChange = useCallback(() => { - reviewFilterContent('materials', materials, materialOptions); - reviewFilterContent('locationTypes', locationTypes, locationTypes); - reviewFilterContent('origins', origins, origins); - reviewFilterContent('t1Suppliers', t1Suppliers, t1SupplierOptions); - reviewFilterContent('producers', producers, producerOptions); - reviewFilterContent('businessUnits', businessUnits, businessUnitsOptions); - }, [ - businessUnits, - businessUnitsOptions, - locationTypes, - materialOptions, - materials, - origins, - producerOptions, - producers, - reviewFilterContent, - t1SupplierOptions, - t1Suppliers, - ]); - - useEffect(() => { - const counters = Object.values(moreFilters).map((value) => value.length); - const total = counters.reduce((a, b) => a + b); - setCounter(total); - }, [moreFilters]); - - useEffect(() => { - handleScenarioChange(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scenarioId]); - - return ( - - {({ open, close }) => ( - <> - - - - - -
-
Filter by
- -
-
-
-
Material
- handleChangeFilter('materials', values)} - id="materials-filter" - /> -
-
-
Business units
- handleChangeFilter('businessUnits', values)} - id="business-units-filter" - /> -
-
-
Origins
- handleChangeFilter('origins', values)} - id="origins-filter" - /> -
-
-
T1 Suppliers
- handleChangeFilter('t1Suppliers', values)} - id="t1-suppliers-filter" - /> -
-
-
Producers
- handleChangeFilter('producers', values)} - id="producers-filter" - /> -
-
-
Location type
- - id="location-type-filter" - multiple - loading={locationTypeOptionsIsLoading} - options={locationTypeOptions} - placeholder="Location types" - onChange={(values) => handleChangeFilter('locationTypes', values)} - value={selectedFilters.locationTypes} - /> -
-
- -
- - -
-
-
-
- - )} -
- ); -}; - -export default MoreFilters; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/index.tsx b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx index b404d7fd4..135628f13 100644 --- a/client/src/containers/analysis-eudr/filters/more-filters/index.tsx +++ b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx @@ -1 +1,276 @@ -export { default } from './component'; +import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import { FilterIcon } from '@heroicons/react/solid'; +import { + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + FloatingPortal, +} from '@floating-ui/react'; +import { Popover, Transition } from '@headlessui/react'; + +import Materials from '@/containers/analysis-visualization/analysis-filters/materials/component'; +import OriginRegions from '@/containers/analysis-visualization/analysis-filters/origin-regions/component'; +import { recursiveMap, recursiveSort } from 'components/tree-select/utils'; +import Button from 'components/button/component'; +import TreeSelect from 'components/tree-select'; +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { eudr, setFilters } from 'store/features/eudr'; +import { + useEUDRMaterialsTree, + useEUDRAdminRegionsTree, + useEUDRSuppliers, + useEUDRPlotsTree, +} from 'hooks/eudr'; + +import type { EUDRState } from 'store/features/eudr'; +import type { Option } from 'components/forms/select'; +import type { TreeSelectOption } from 'components/tree-select/types'; + +type MoreFiltersState = Omit; + +const INITIAL_FILTERS: MoreFiltersState = { + materials: [], + origins: [], + suppliers: [], + plots: [], +}; + +interface ApiTreeResponse { + id: string; + name: string; + children?: this[]; +} + +const DEFAULT_QUERY_OPTIONS = { + select: (data: ApiTreeResponse[]) => { + const sorted = recursiveSort(data, 'name'); + return sorted.map((item) => recursiveMap(item, ({ id, name }) => ({ label: name, value: id }))); + }, +}; + +const MoreFilters = () => { + const dispatch = useAppDispatch(); + const { + filters: { materials, origins, suppliers, plots }, + } = useAppSelector(eudr); + + const filters = useMemo( + () => ({ + materials, + origins, + suppliers, + plots, + }), + [materials, origins, suppliers, plots], + ); + + const [selectedFilters, setSelectedFilters] = useState(filters); + const [counter, setCounter] = useState(0); + + // Only the changes are applied when the user clicks on Apply + const handleApply = useCallback(() => { + dispatch(setFilters(selectedFilters)); + }, [dispatch, selectedFilters]); + + // Restoring state from initial state only internally, + // the user have to apply the changes + const handleClearFilters = useCallback(() => { + setSelectedFilters(INITIAL_FILTERS); + }, []); + + // Updating internal state from selectors + const handleChangeFilter = useCallback( + (key: keyof MoreFiltersState, values: TreeSelectOption[] | Option) => { + setSelectedFilters((filters) => ({ ...filters, [key]: values })); + }, + [], + ); + + const { refs, strategy, x, y, context } = useFloating({ + placement: 'bottom-start', + strategy: 'fixed', + middleware: [offset({ mainAxis: 4 }), shift({ padding: 4 })], + }); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + useClick(context), + useDismiss(context), + ]); + + const { data: materialOptions, isLoading: materialOptionsIsLoading } = useEUDRMaterialsTree( + undefined, + { + ...DEFAULT_QUERY_OPTIONS, + select: (_materials) => { + return recursiveSort(_materials, 'name')?.map((item) => + recursiveMap(item, ({ id, name, status }) => ({ + value: id, + label: name, + disabled: status === 'inactive', + })), + ); + }, + initialData: [], + }, + ); + + const { data: originOptions, isLoading: originOptionsIsLoading } = useEUDRAdminRegionsTree( + undefined, + DEFAULT_QUERY_OPTIONS, + ); + + const { data: supplierOptions, isLoading: supplierOptionsIsLoading } = useEUDRSuppliers( + undefined, + { + ...DEFAULT_QUERY_OPTIONS, + initialData: [], + }, + ); + + const { data: plotOptions, isLoading: plotOptionsIsLoading } = useEUDRPlotsTree(undefined, { + ...DEFAULT_QUERY_OPTIONS, + initialData: [], + }); + + useEffect(() => { + const counters = Object.values(filters).map((value) => value.length); + const total = counters.reduce((a, b) => a + b); + setCounter(total); + }, [filters]); + + return ( + + {({ open, close }) => ( + <> + + + + + +
+
Filter by
+ +
+
+
+
Material
+ handleChangeFilter('materials', values)} + id="materials-filter" + /> +
+
+
Origins
+ handleChangeFilter('origins', values)} + id="origins-filter" + /> +
+
+
Suppliers
+ handleChangeFilter('suppliers', values)} + id="suppliers-filter" + /> +
+
+
Plots
+ handleChangeFilter('plots', values)} + id="plots-filter" + /> +
+
+
+ + +
+
+
+
+ + )} +
+ ); +}; + +export default MoreFilters; diff --git a/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts b/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts deleted file mode 100644 index 540cd7215..000000000 --- a/client/src/containers/analysis-eudr/filters/more-filters/types.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface BaseTreeSearchParams { - depth?: number; - materialIds?: string[]; - businessUnitIds?: string[]; - originIds?: string[]; - scenarioId?: string; - scenarioIds?: string[]; - producerIds?: string[]; - t1SupplierIds?: string[]; -} diff --git a/client/src/containers/analysis-eudr/filters/years-range/component.tsx b/client/src/containers/analysis-eudr/filters/years-range/component.tsx deleted file mode 100644 index 281998178..000000000 --- a/client/src/containers/analysis-eudr/filters/years-range/component.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { isFinite, toNumber, range } from 'lodash-es'; -import toast from 'react-hot-toast'; - -import { DEFAULT_END_YEAR_GAP, MAX_END_YEAR_RANGE } from './constants'; - -import { useAppDispatch, useAppSelector } from 'store/hooks'; -import { analysisUI } from 'store/features/analysis/ui'; -import { analysisFilters, setFilters } from 'store/features/analysis/filters'; -import { useYears } from 'hooks/years'; -import YearsRangeFilter, { useYearsRange } from 'containers/filters/years-range'; - -import type { YearsRangeParams } from 'containers/filters/years-range'; - -const YearsRange: React.FC = () => { - const dispatch = useAppDispatch(); - - const [years, setYears] = useState([]); - const { visualizationMode } = useAppSelector(analysisUI); - const filters = useAppSelector(analysisFilters); - const { layer, materials, indicator } = filters; - - const materialIds = useMemo(() => materials.map((mat) => mat.value), [materials]); - - const { data, isLoading } = useYears(layer, materialIds, indicator?.value, { - enabled: !!(layer === 'impact' && indicator?.value) || true, - }); - - const { startYear, endYear, yearsGap, setYearsRange } = useYearsRange({ - years, - yearsGap: 1, - // Map mode only makes use of the endYear and will display the Select, - // not the YearsRangeFilter. - validateRange: visualizationMode !== 'map', - ...filters, - }); - - const lastYearWithData = useMemo(() => data[data.length - 1], [data]); - const defaultLastYear = useMemo( - () => lastYearWithData + DEFAULT_END_YEAR_GAP, - [lastYearWithData], - ); - - useEffect(() => { - setYears(range(data[0], defaultLastYear + 1)); - }, [data, defaultLastYear]); - - useEffect(() => { - dispatch(setFilters({ startYear, endYear })); - }, [startYear, endYear, dispatch]); - - const handleOnEndYearSearch: (searchedYear: string) => void = (searchedYear) => { - const year = toNumber(searchedYear); - - if (!isFinite(year) || year <= data[0]) { - return; - } - if (year > MAX_END_YEAR_RANGE + defaultLastYear) { - toast.error(`Max year limit is ${MAX_END_YEAR_RANGE + defaultLastYear}`); - return; - } - - if (year === lastYearWithData) { - setYears(range(data[0], defaultLastYear + 1)); - } else if (!years.includes(year)) { - setYears(range(data[0], year + 1)); - } - }; - - const handleYearChange = ({ startYear, endYear }: YearsRangeParams) => { - const lastYear = years[years.length - 1]; - // Reduce the years range in case the current selected end year is smaller than the previous and the previous range was larger than the default - if (endYear < lastYear) { - if (endYear > defaultLastYear) { - setYears(range(years[0], toNumber(endYear) + 1)); - } else { - setYears(range(years[0], defaultLastYear + 1)); - } - } - if (endYear) setYearsRange({ startYear, endYear }); - }; - - return ( - - ); -}; - -export default YearsRange; diff --git a/client/src/containers/analysis-eudr/filters/years-range/constants.ts b/client/src/containers/analysis-eudr/filters/years-range/constants.ts deleted file mode 100644 index a97451003..000000000 --- a/client/src/containers/analysis-eudr/filters/years-range/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Arbitrary value to define the end year list range */ -export const DEFAULT_END_YEAR_GAP = 5; - -/** Arbitrary value to define the max range of end year options to avoid performance issues */ -export const MAX_END_YEAR_RANGE = 1000; diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.ts b/client/src/containers/analysis-eudr/filters/years-range/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/analysis-eudr/filters/years-range/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.tsx b/client/src/containers/analysis-eudr/filters/years-range/index.tsx new file mode 100644 index 000000000..f2ebc53bf --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/years-range/index.tsx @@ -0,0 +1,62 @@ +import React, { useCallback } from 'react'; +import { UTCDate } from '@date-fns/utc'; +import { format } from 'date-fns'; +import { ChevronDown } from 'lucide-react'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { eudr, setFilters } from 'store/features/eudr'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; + +import type { DateRange } from 'react-day-picker'; + +// ! the date range is hardcoded for now +export const DATES_RANGE = [new UTCDate('2020-12-31'), new UTCDate()]; + +const dateFormatter = (date: Date) => format(date, 'dd/MM/yyyy'); + +const DatesRange = (): JSX.Element => { + const dispatch = useAppDispatch(); + const { + filters: { dates }, + } = useAppSelector(eudr); + + const handleDatesChange = useCallback( + (dates: DateRange) => { + if (dates) { + dispatch(setFilters({ dates })); + } + }, + [dispatch], + ); + + return ( + + + + + + + + + ); +}; + +export default DatesRange; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index 55793500f..575060a0a 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -22,7 +22,7 @@ import { TableBody, TableCell, } from '@/components/ui/table'; -import { useEUDRSuppliers } from '@/hooks/eudr'; +// import { useEUDRSuppliers } from '@/hooks/eudr'; import type { // ColumnFiltersState, @@ -54,10 +54,11 @@ const SuppliersListTable = (): JSX.Element => { // const [columnFilters, setColumnFilters] = useState([]); const [sorting, setSorting] = useState([]); - const { data } = useEUDRSuppliers(undefined, { - enabled: false, - placeholderData: MOCK_DATA, - }); + const data = MOCK_DATA; + // const { data } = useEUDRSuppliers(undefined, { + // enabled: false, + // placeholderData: MOCK_DATA, + // }); const table = useReactTable({ data: data, diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index c39176d69..aa86ec2ae 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -2,7 +2,8 @@ import { useQuery } from '@tanstack/react-query'; import { apiService } from 'services/api'; -import type { Supplier } from '@/containers/analysis-eudr/supplier-list-table/table'; +import type { AdminRegionsTreesParams } from '@/hooks/admin-regions'; +import type { MaterialTreeItem, OriginRegion, Supplier } from '@/types'; import type { UseQueryOptions } from '@tanstack/react-query'; export const useEUDRSuppliers = ( @@ -49,3 +50,93 @@ export const usePlotGeometries = ( }, ); }; + +export const useEUDRMaterialsTree = ( + params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-materials', params], + () => + apiService + .request<{ data: MaterialTreeItem[] }>({ + method: 'GET', + url: '/eudr/materials', + params, + }) + .then(({ data: responseData }) => responseData.data), + { + ...options, + }, + ); +}; + +export const useEUDRAdminRegionsTree = ( + params: AdminRegionsTreesParams, + options: UseQueryOptions = {}, +) => { + const query = useQuery( + ['eudr-admin-regions', params], + () => + apiService + .request<{ data: OriginRegion[] }>({ + method: 'GET', + url: '/eudr/admin-regions', + params, + }) + .then(({ data: responseData }) => responseData.data), + { + ...options, + }, + ); + + return query; +}; + +export const useEUDRPlotsTree = ( + params: AdminRegionsTreesParams, + options: UseQueryOptions = {}, +) => { + const query = useQuery( + ['eudr-geo-regions', params], + () => + apiService + .request<{ data: OriginRegion[] }>({ + method: 'GET', + url: '/eudr/geo-regions', + params, + }) + .then(({ data: responseData }) => responseData.data), + { + ...options, + }, + ); + + return query; +}; + +interface Alert { + alertDate: { + value: string; + }; +} + +export const useEUDRAlertDates = ( + params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-dates', params], + () => + apiService + .request({ + method: 'GET', + url: '/eudr/dates', + params, + }) + .then(({ data: responseData }) => responseData.data), + { + ...options, + }, + ); +}; diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index 5b0a934d0..d90397b8e 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -1,15 +1,38 @@ import { createSlice } from '@reduxjs/toolkit'; +import { DATES_RANGE } from 'containers/analysis-eudr/filters/years-range'; + +import type { Option } from '@/components/forms/select'; import type { VIEW_BY_OPTIONS } from 'containers/analysis-eudr/suppliers-stacked-bar'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from 'store'; export type EUDRState = { viewBy: (typeof VIEW_BY_OPTIONS)[number]['value']; + filters: { + materials: Option[]; + origins: Option[]; + plots: Option[]; + suppliers: Option[]; + dates: { + from: Date; + to: Date; + }; + }; }; export const initialState: EUDRState = { viewBy: 'commodities', + filters: { + materials: [], + origins: [], + plots: [], + suppliers: [], + dates: { + from: DATES_RANGE[0], + to: DATES_RANGE[1], + }, + }, }; export const EUDRSlice = createSlice({ @@ -20,10 +43,17 @@ export const EUDRSlice = createSlice({ ...state, viewBy: action.payload, }), + setFilters: (state, action: PayloadAction>) => ({ + ...state, + filters: { + ...state.filters, + ...action.payload, + }, + }), }, }); -export const { setViewBy } = EUDRSlice.actions; +export const { setViewBy, setFilters } = EUDRSlice.actions; export const eudr = (state: RootState) => state['eudr']; diff --git a/client/yarn.lock b/client/yarn.lock index 98c5d3880..f24bb8aa1 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -326,6 +326,13 @@ __metadata: languageName: node linkType: hard +"@date-fns/utc@npm:1.1.1": + version: 1.1.1 + resolution: "@date-fns/utc@npm:1.1.1" + checksum: 8369b27dce2a248d102f4a4756a54263b544a448da6ae621de7951c05e6e66495e38d2bfade103dbb38210cdb1a42faa5741f498404eb965eb49373e78c75d43 + languageName: node + linkType: hard + "@deck.gl/carto@npm:^8.9.35": version: 8.9.35 resolution: "@deck.gl/carto@npm:8.9.35" @@ -2086,7 +2093,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-popover@npm:^1.0.7": +"@radix-ui/react-popover@npm:1.0.7": version: 1.0.7 resolution: "@radix-ui/react-popover@npm:1.0.7" dependencies: @@ -4745,10 +4752,10 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:2.22.1": - version: 2.22.1 - resolution: "date-fns@npm:2.22.1" - checksum: 7ff97cd605af50c02f341687c2cafd218839a1aace67965374989855a13f76dc4fe52e0e38c343c1ad1f8399787cb6839a0b14a669c44b30550c287300b1bb50 +"date-fns@npm:3.3.1": + version: 3.3.1 + resolution: "date-fns@npm:3.3.1" + checksum: 6245e93a47de28ac96dffd4d62877f86e6b64854860ae1e00a4f83174d80bc8e59bd1259cf265223fb2ddce5c8e586dc9cc210f0d052faba2f7660e265877283 languageName: node linkType: hard @@ -7778,6 +7785,7 @@ __metadata: version: 0.0.0-use.local resolution: "landgriffon-client@workspace:." dependencies: + "@date-fns/utc": 1.1.1 "@deck.gl/carto": ^8.9.35 "@deck.gl/core": 8.8.6 "@deck.gl/extensions": 8.8.6 @@ -7802,7 +7810,7 @@ __metadata: "@radix-ui/react-collapsible": 1.0.3 "@radix-ui/react-dropdown-menu": ^2.0.6 "@radix-ui/react-label": 2.0.2 - "@radix-ui/react-popover": ^1.0.7 + "@radix-ui/react-popover": 1.0.7 "@radix-ui/react-radio-group": 1.1.3 "@radix-ui/react-select": 2.0.0 "@radix-ui/react-slot": 1.0.2 @@ -7833,7 +7841,7 @@ __metadata: d3-array: 3.0.2 d3-format: 3.0.1 d3-scale: 4.0.2 - date-fns: 2.22.1 + date-fns: 3.3.1 eslint: 8.23.1 eslint-config-next: 13.5.5 eslint-config-prettier: ^9.1.0 @@ -7858,6 +7866,7 @@ __metadata: query-string: 8.1.0 rc-tree: 5.7.0 react: 18.2.0 + react-day-picker: 8.10.0 react-dom: 18.2.0 react-dropzone: 14.2.2 react-hook-form: 7.43.1 @@ -9762,6 +9771,16 @@ __metadata: languageName: node linkType: hard +"react-day-picker@npm:8.10.0": + version: 8.10.0 + resolution: "react-day-picker@npm:8.10.0" + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a265e8c2f3f0e92e5a23e2edeca40fe67c67c00bae64aa9e1a99c6fe7f58b2f697b538937822b8f68d4b890d7903a06f074f1365deb465a0aeef1a14c701cc65 + languageName: node + linkType: hard + "react-dom@npm:18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" From 09dd50629c54d3c716623b057fb4ec509d22b196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Fri, 8 Mar 2024 09:25:25 +0100 Subject: [PATCH 082/153] minor styling --- .../containers/analysis-eudr/filters/years-range/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.tsx b/client/src/containers/analysis-eudr/filters/years-range/index.tsx index f2ebc53bf..f61f24266 100644 --- a/client/src/containers/analysis-eudr/filters/years-range/index.tsx +++ b/client/src/containers/analysis-eudr/filters/years-range/index.tsx @@ -34,7 +34,10 @@ const DatesRange = (): JSX.Element => { return ( -
- {formatPercentage(category.value)} of suppliers + {`${category.totalPercentage}%`} of suppliers
@@ -104,24 +116,18 @@ export const CategoryList = (): JSX.Element => { className={cn( 'w-[98px] rounded-md border-none text-sm text-gray-500 shadow-none transition-colors hover:shadow-none', { - 'bg-navy-400 text-white hover:bg-navy-600': categories[category.slug], + 'bg-navy-400 text-white hover:bg-navy-600': categories[category.name], }, )} > - {categories[category.slug] ? 'Close detail' : 'View detail'} + {categories[category.name] ? 'Close detail' : 'View detail'}
- {category.slug === 'deforestation-free-suppliers' && ( - - )} - {category.slug === 'suppliers-with-deforestation-alerts' && ( - - )} - {category.slug === 'suppliers-with-no-location-data' && ( - - )} + {category.name === CATEGORIES[0].name && } + {category.name === CATEGORIES[1].name && } + {category.name === CATEGORIES[2].name && } ))} diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.tsx b/client/src/containers/analysis-eudr/filters/years-range/index.tsx index f61f24266..cf9102ee6 100644 --- a/client/src/containers/analysis-eudr/filters/years-range/index.tsx +++ b/client/src/containers/analysis-eudr/filters/years-range/index.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from 'react'; import { UTCDate } from '@date-fns/utc'; -import { format } from 'date-fns'; import { ChevronDown } from 'lucide-react'; import { useAppDispatch, useAppSelector } from 'store/hooks'; @@ -8,14 +7,13 @@ import { eudr, setFilters } from 'store/features/eudr'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { Calendar } from '@/components/ui/calendar'; +import { dateFormatter } from '@/hooks/eudr'; import type { DateRange } from 'react-day-picker'; // ! the date range is hardcoded for now export const DATES_RANGE = [new UTCDate('2020-12-31'), new UTCDate()]; -const dateFormatter = (date: Date) => format(date, 'dd/MM/yyyy'); - const DatesRange = (): JSX.Element => { const dispatch = useAppDispatch(); const { diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx index b338499dc..d863384ba 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -1,10 +1,9 @@ -'use client'; - import Link from 'next/link'; import { DataTableColumnHeader } from './column-header'; import { Badge } from '@/components/ui/badge'; +import { BIG_NUMBER_FORMAT } from 'utils/number-format'; import type { Supplier } from '.'; import type { ColumnDef } from '@tanstack/react-table'; @@ -17,7 +16,7 @@ export const columns: ColumnDef[] = [ return (
{row.getValue('supplierName')} @@ -34,11 +33,15 @@ export const columns: ColumnDef[] = [ enableHiding: false, }, { - accessorKey: 'baseLineVolume', + accessorKey: 'baselineVolume', header: ({ column }) => , cell: ({ row }) => { // todo: format number - return {row.getValue('baseLineVolume')}; + return ( +
+ {BIG_NUMBER_FORMAT(row.getValue('baselineVolume'))} +
+ ); }, }, { @@ -58,11 +61,11 @@ export const columns: ColumnDef[] = [ }, }, { - accessorKey: 'ttp', - header: ({ column }) => , + accessorKey: 'tpl', + header: ({ column }) => , cell: ({ row }) => { - const ttp = row.getValue('ttp'); - return {`${Number.isNaN(ttp) ? '-' : `${ttp}%`}`}; + const tpl = row.getValue('tpl'); + return {`${Number.isNaN(tpl) ? '-' : `${tpl}%`}`}; }, }, { @@ -86,7 +89,7 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'origins', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { return (
diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index 575060a0a..b3b7c48e7 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -11,7 +11,6 @@ import { import { useState } from 'react'; import columns from './columns'; -import { MOCK_DATA } from './mock-data'; import { DataTablePagination, PAGINATION_SIZES } from './pagination'; import { @@ -22,7 +21,9 @@ import { TableBody, TableCell, } from '@/components/ui/table'; -// import { useEUDRSuppliers } from '@/hooks/eudr'; +import { useEUDRData, dateFormatter } from '@/hooks/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { eudr } from '@/store/features/eudr'; import type { // ColumnFiltersState, @@ -31,13 +32,13 @@ import type { } from '@tanstack/react-table'; export interface Supplier { - id: number; + supplierId: number; supplierName: string; companyId: string; - baseLineVolume: number; + baselineVolume: number; dfs: number; sda: number; - ttp: number; + tpl: number; materials: { name: string; id: string; @@ -53,12 +54,22 @@ const SuppliersListTable = (): JSX.Element => { // const [columnVisibility, setColumnVisibility] = useState({}); // const [columnFilters, setColumnFilters] = useState([]); const [sorting, setSorting] = useState([]); + const { + filters: { dates, suppliers, origins, materials }, + } = useAppSelector(eudr); - const data = MOCK_DATA; - // const { data } = useEUDRSuppliers(undefined, { - // enabled: false, - // placeholderData: MOCK_DATA, - // }); + const { data } = useEUDRData( + { + startAlertDate: dateFormatter(dates.from), + endAlertDate: dateFormatter(dates.to), + producerIds: suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + }, + { + select: (data) => data?.table, + }, + ); const table = useReactTable({ data: data, @@ -87,6 +98,8 @@ const SuppliersListTable = (): JSX.Element => { // getFacetedUniqueValues: getFacetedUniqueValues(), }); + if (!data?.length) return null; + return (
diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts b/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts deleted file mode 100644 index 6d2868458..000000000 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/mock-data.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Supplier } from '.'; - -export const MOCK_DATA: Supplier[] = [ - { - id: 2, - supplierName: 'Very long name of supplier Very long name of supplier', - companyId: '123', - baseLineVolume: 1000, - dfs: 100, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 1', id: '1' }, - { name: 'Material 2', id: '2' }, - ], - origins: [ - { name: 'Origin 1', id: '1' }, - { name: 'Origin 2', id: '2' }, - ], - }, - { - id: 4, - supplierName: 'Supplier 2', - companyId: '124', - baseLineVolume: 2000, - dfs: 200, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 3', id: '3' }, - { name: 'Material 4', id: '4' }, - { name: 'Material 4', id: '5' }, - { name: 'Material 4', id: '6' }, - ], - origins: [ - { name: 'Origin 3', id: '3' }, - { name: 'Origin 4', id: '4' }, - ], - }, - { - id: 8, - supplierName: 'Supplier 3', - companyId: '125', - baseLineVolume: 3000, - dfs: 300, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 5', id: '5' }, - { name: 'Material 6', id: '6' }, - ], - origins: [ - { name: 'Origin 5', id: '5' }, - { name: 'Origin 6', id: '6' }, - ], - }, - { - id: 67, - supplierName: 'Supplier 3', - companyId: '125', - baseLineVolume: 3000, - dfs: 300, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 5', id: '5' }, - { name: 'Material 6', id: '6' }, - ], - origins: [ - { name: 'Origin 5', id: '5' }, - { name: 'Origin 6', id: '6' }, - ], - }, - { - id: 33, - supplierName: 'Supplier 3', - companyId: '125', - baseLineVolume: 3000, - dfs: 300, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 5', id: '5' }, - { name: 'Material 6', id: '6' }, - ], - origins: [ - { name: 'Origin 5', id: '5' }, - { name: 'Origin 6', id: '6' }, - ], - }, - { - id: 9, - supplierName: 'Supplier 3', - companyId: '125', - baseLineVolume: 3000, - dfs: 300, - sda: 39.7, - ttp: 40.1, - materials: [ - { name: 'Material 5', id: '5' }, - { name: 'Material 6', id: '6' }, - ], - origins: [ - { name: 'Origin 5', id: '5' }, - { name: 'Origin 6', id: '6' }, - ], - }, -]; diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index a647e2367..daeaab75d 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { BarChart, Bar, @@ -9,71 +9,31 @@ import { ResponsiveContainer, Label, } from 'recharts'; +import { groupBy } from 'lodash-es'; -import CategoryList from '@/containers/analysis-eudr/category-list'; +import CategoryList, { CATEGORIES } from '@/containers/analysis-eudr/category-list'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label as RadioLabel } from '@/components/ui/label'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { eudr, setViewBy } from '@/store/features/eudr'; +import { useEUDRData, dateFormatter } from '@/hooks/eudr'; export const VIEW_BY_OPTIONS = [ { label: 'Commodities', - value: 'commodities', + value: 'materials', }, { label: 'Countries', - value: 'countries', + value: 'origins', }, ] as const; -const data = [ - { - name: 'Cattle', - uv: 40, - pv: 30, - amt: 30, - }, - { - name: 'Cocoa', - uv: 30, - pv: 13, - amt: 57, - }, - { - name: 'Coffee', - uv: 20, - pv: 70, - amt: 10, - }, - { - name: 'Oil palm', - uv: 27, - pv: 39, - amt: 34, - }, - { - name: 'Wood', - uv: 28, - pv: 48, - amt: 24, - }, - { - name: 'Soya', - uv: 23, - pv: 38, - amt: 39, - }, - { - name: 'Rubber', - uv: 34, - pv: 43, - amt: 23, - }, -]; - const SuppliersStackedBar = () => { - const { viewBy } = useAppSelector(eudr); + const { + viewBy, + filters: { dates, suppliers, origins, materials }, + } = useAppSelector(eudr); const dispatch = useAppDispatch(); const handleViewBy = useCallback( @@ -83,11 +43,50 @@ const SuppliersStackedBar = () => { [dispatch], ); + const { data } = useEUDRData( + { + startAlertDate: dateFormatter(dates.from), + endAlertDate: dateFormatter(dates.to), + producerIds: suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + }, + { + select: (data) => data?.breakDown, + }, + ); + + const parsedData = useMemo(() => { + const dataByView = data?.[viewBy] || []; + + const dataRootLevel = Object.keys(dataByView) + .map((key) => ({ + category: key, + ...dataByView[key], + })) + .map(({ detail, category }) => detail.map((x) => ({ ...x, category })).flat()) + .flat(); + + return Object.keys(groupBy(dataRootLevel, 'name')).map((material) => ({ + name: material, + free: dataRootLevel.find( + ({ name, category }) => name === material && category === 'Deforestation-free suppliers', + )?.value, + alerts: dataRootLevel.find( + ({ name, category }) => + name === material && category === 'Suppliers with deforestation alerts', + )?.value, + noData: dataRootLevel.find( + ({ name, category }) => name === material && category === 'Suppliers with no location data', + )?.value, + })); + }, [data, viewBy]); + return (
- Total numbers of suppliers: 46.53P + Total numbers of suppliers: {parsedData?.length || '-'}

Suppliers by category

@@ -113,7 +112,7 @@ const SuppliersStackedBar = () => { { width={200} /> - - - + + +
diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index aa86ec2ae..6d023c807 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -1,11 +1,15 @@ import { useQuery } from '@tanstack/react-query'; +import { format } from 'date-fns'; import { apiService } from 'services/api'; +import type { Supplier as SupplierRow } from '@/containers/analysis-eudr/supplier-list-table/table'; import type { AdminRegionsTreesParams } from '@/hooks/admin-regions'; import type { MaterialTreeItem, OriginRegion, Supplier } from '@/types'; import type { UseQueryOptions } from '@tanstack/react-query'; +export const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd'); + export const useEUDRSuppliers = ( params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, options: UseQueryOptions = {}, @@ -140,3 +144,34 @@ export const useEUDRAlertDates = ( }, ); }; + +interface EUDRData { + table: SupplierRow[]; + breakDown: []; +} + +export const useEUDRData = ( + params?: { + startAlertDate: string; + endAlertDate: string; + producerIds?: string[]; + materialIds?: string[]; + originIds?: string[]; + }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-table', params], + () => + apiService + .request({ + method: 'GET', + url: '/eudr/dashboard', + params, + }) + .then(({ data }) => data), + { + ...options, + }, + ); +}; diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index d90397b8e..83003f3a4 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -22,7 +22,7 @@ export type EUDRState = { }; export const initialState: EUDRState = { - viewBy: 'commodities', + viewBy: 'materials', filters: { materials: [], origins: [], From 29a7e3c7b824cc020d29c876fc0fbe0fb3b42d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 12 Mar 2024 17:21:53 +0100 Subject: [PATCH 084/153] adds georegions param --- .../breakdown/deforestation-free-suppliers/index.tsx | 3 ++- .../breakdown/suppliers-with-deforestation-alerts/index.tsx | 3 ++- .../breakdown/suppliers-with-no-location-data/index.tsx | 3 ++- client/src/containers/analysis-eudr/category-list/index.tsx | 3 ++- client/src/hooks/eudr/index.ts | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx index 1b4d38cf1..8ac96d579 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -14,7 +14,7 @@ import type BreakdownItem from '../breakdown-item'; const DeforestationFreeSuppliersBreakdown = () => { const { viewBy, - filters: { dates, suppliers, origins, materials }, + filters: { dates, suppliers, origins, materials, plots }, } = useAppSelector(eudr); const { data } = useEUDRData( @@ -24,6 +24,7 @@ const DeforestationFreeSuppliersBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), + geoRegiondIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx index 62e82bae4..d27e11075 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -14,7 +14,7 @@ import type BreakdownItem from '../breakdown-item'; const SuppliersWithDeforestationAlertsBreakdown = () => { const { viewBy, - filters: { dates, suppliers, origins, materials }, + filters: { dates, suppliers, origins, materials, plots }, } = useAppSelector(eudr); const { data } = useEUDRData( @@ -24,6 +24,7 @@ const SuppliersWithDeforestationAlertsBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), + geoRegiondIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx index 023c1c66c..ccde9b60c 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -14,7 +14,7 @@ import type BreakdownItem from '../breakdown-item'; const SuppliersWithNoLocationDataBreakdown = () => { const { viewBy, - filters: { dates, suppliers, origins, materials }, + filters: { dates, suppliers, origins, materials, plots }, } = useAppSelector(eudr); const { data } = useEUDRData( @@ -24,6 +24,7 @@ const SuppliersWithNoLocationDataBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), + geoRegiondIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index df17a827c..71ff24180 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -42,7 +42,7 @@ export const CategoryList = (): JSX.Element => { const { viewBy, - filters: { dates, suppliers, origins, materials }, + filters: { dates, suppliers, origins, materials, plots }, } = useAppSelector(eudr); const { data } = useEUDRData( @@ -52,6 +52,7 @@ export const CategoryList = (): JSX.Element => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), + geoRegiondIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 6d023c807..70858cdb3 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -157,6 +157,7 @@ export const useEUDRData = ( producerIds?: string[]; materialIds?: string[]; originIds?: string[]; + geoRegiondIds?: string[]; }, options: UseQueryOptions = {}, ) => { From 0b2f24f95e6a764b02e7794bde4b5099839cb15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 12 Mar 2024 17:38:33 +0100 Subject: [PATCH 085/153] moves Date object away from store --- .../deforestation-free-suppliers/index.tsx | 6 ++-- .../index.tsx | 6 ++-- .../suppliers-with-no-location-data/index.tsx | 6 ++-- .../analysis-eudr/category-list/index.tsx | 10 +++--- .../filters/years-range/index.tsx | 34 +++++++++++++------ .../supplier-list-table/table/index.tsx | 6 ++-- .../suppliers-stacked-bar/component.tsx | 6 ++-- client/src/hooks/eudr/index.ts | 3 -- client/src/store/features/eudr/index.ts | 4 +-- 9 files changed, 46 insertions(+), 35 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx index 8ac96d579..348f11ce1 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -7,7 +7,7 @@ import { CATEGORIES } from '../..'; import { eudr } from '@/store/features/eudr'; import { useAppSelector } from '@/store/hooks'; -import { dateFormatter, useEUDRData } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; import type BreakdownItem from '../breakdown-item'; @@ -19,8 +19,8 @@ const DeforestationFreeSuppliersBreakdown = () => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx index d27e11075..e8e73c48c 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -7,7 +7,7 @@ import { CATEGORIES } from '../..'; import { eudr } from '@/store/features/eudr'; import { useAppSelector } from '@/store/hooks'; -import { dateFormatter, useEUDRData } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; import type BreakdownItem from '../breakdown-item'; @@ -19,8 +19,8 @@ const SuppliersWithDeforestationAlertsBreakdown = () => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx index ccde9b60c..4bdf3d946 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -7,7 +7,7 @@ import { CATEGORIES } from '../..'; import { eudr } from '@/store/features/eudr'; import { useAppSelector } from '@/store/hooks'; -import { dateFormatter, useEUDRData } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; import type BreakdownItem from '../breakdown-item'; @@ -19,8 +19,8 @@ const SuppliersWithNoLocationDataBreakdown = () => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 71ff24180..c8198d2d5 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -7,7 +7,7 @@ import SuppliersWithNoLocationDataBreakdown from './breakdown/suppliers-with-no- import { Button } from '@/components/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { cn } from '@/lib/utils'; -import { useEUDRData, dateFormatter } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; import { useAppSelector } from '@/store/hooks'; import { eudr } from '@/store/features/eudr'; import { themeColors } from '@/utils/colors'; @@ -47,8 +47,8 @@ export const CategoryList = (): JSX.Element => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), @@ -73,7 +73,7 @@ export const CategoryList = (): JSX.Element => { <> {parsedData.map((category) => ( { toggleCategory((prev) => ({ @@ -88,7 +88,7 @@ export const CategoryList = (): JSX.Element => { className="block min-h-4 min-w-4 rounded-full border-2 transition-colors" style={{ borderColor: category.color, - ...(categories[category.slug] && { + ...(categories[category.name] && { backgroundColor: category.color, }), }} diff --git a/client/src/containers/analysis-eudr/filters/years-range/index.tsx b/client/src/containers/analysis-eudr/filters/years-range/index.tsx index cf9102ee6..5f2003469 100644 --- a/client/src/containers/analysis-eudr/filters/years-range/index.tsx +++ b/client/src/containers/analysis-eudr/filters/years-range/index.tsx @@ -1,18 +1,19 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { UTCDate } from '@date-fns/utc'; import { ChevronDown } from 'lucide-react'; +import { format } from 'date-fns'; import { useAppDispatch, useAppSelector } from 'store/hooks'; import { eudr, setFilters } from 'store/features/eudr'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { Calendar } from '@/components/ui/calendar'; -import { dateFormatter } from '@/hooks/eudr'; import type { DateRange } from 'react-day-picker'; +const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd'); // ! the date range is hardcoded for now -export const DATES_RANGE = [new UTCDate('2020-12-31'), new UTCDate()]; +export const DATES_RANGE = ['2020-12-31', dateFormatter(new Date())]; const DatesRange = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -23,12 +24,26 @@ const DatesRange = (): JSX.Element => { const handleDatesChange = useCallback( (dates: DateRange) => { if (dates) { - dispatch(setFilters({ dates })); + dispatch( + setFilters({ + dates: { + from: dateFormatter(dates.from), + to: dateFormatter(dates.to), + }, + }), + ); } }, [dispatch], ); + const datesToDate = useMemo(() => { + return { + from: dates.from ? new UTCDate(dates.from) : undefined, + to: dates.to ? new UTCDate(dates.to) : undefined, + }; + }, [dates]); + return ( @@ -37,9 +52,8 @@ const DatesRange = (): JSX.Element => { className="h-auto space-x-1 border border-gray-200 bg-white shadow-sm" > - from{' '} - {dates.from ? dateFormatter(dates.from) : '-'} to{' '} - {dates.to ? dateFormatter(dates.to) : '-'} + from {dates.from || '-'} to{' '} + {dates.to || '-'} @@ -49,10 +63,10 @@ const DatesRange = (): JSX.Element => { mode="range" numberOfMonths={2} disabled={{ - before: DATES_RANGE[0], - after: DATES_RANGE[1], + before: new UTCDate(DATES_RANGE[0]), + after: new UTCDate(DATES_RANGE[1]), }} - selected={dates} + selected={datesToDate} onSelect={handleDatesChange} /> diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index b3b7c48e7..bfcc9ab7d 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -21,7 +21,7 @@ import { TableBody, TableCell, } from '@/components/ui/table'; -import { useEUDRData, dateFormatter } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; import { useAppSelector } from '@/store/hooks'; import { eudr } from '@/store/features/eudr'; @@ -60,8 +60,8 @@ const SuppliersListTable = (): JSX.Element => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index daeaab75d..e352dbe4c 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -16,7 +16,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label as RadioLabel } from '@/components/ui/label'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { eudr, setViewBy } from '@/store/features/eudr'; -import { useEUDRData, dateFormatter } from '@/hooks/eudr'; +import { useEUDRData } from '@/hooks/eudr'; export const VIEW_BY_OPTIONS = [ { @@ -45,8 +45,8 @@ const SuppliersStackedBar = () => { const { data } = useEUDRData( { - startAlertDate: dateFormatter(dates.from), - endAlertDate: dateFormatter(dates.to), + startAlertDate: dates.from, + endAlertDate: dates.to, producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 70858cdb3..d2e543491 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/react-query'; -import { format } from 'date-fns'; import { apiService } from 'services/api'; @@ -8,8 +7,6 @@ import type { AdminRegionsTreesParams } from '@/hooks/admin-regions'; import type { MaterialTreeItem, OriginRegion, Supplier } from '@/types'; import type { UseQueryOptions } from '@tanstack/react-query'; -export const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd'); - export const useEUDRSuppliers = ( params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, options: UseQueryOptions = {}, diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index 83003f3a4..4c41b1004 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -15,8 +15,8 @@ export type EUDRState = { plots: Option[]; suppliers: Option[]; dates: { - from: Date; - to: Date; + from: string; + to: string; }; }; }; From 5dc6480af06589ced5497817f695c45b5d6d8b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 12 Mar 2024 17:48:43 +0100 Subject: [PATCH 086/153] updates labels of tooltip --- .../suppliers-stacked-bar/component.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index e352dbe4c..297bf6480 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -29,6 +29,12 @@ export const VIEW_BY_OPTIONS = [ }, ] as const; +const TOOLTIP_LABELS = { + free: CATEGORIES[0].name, + alerts: CATEGORIES[1].name, + noData: CATEGORIES[2].name, +} as const; + const SuppliersStackedBar = () => { const { viewBy, @@ -157,15 +163,22 @@ const SuppliersStackedBar = () => { type="category" width={200} /> - + value} + formatter={(value: number, name: keyof typeof TOOLTIP_LABELS) => [ + `${value}%`, + TOOLTIP_LABELS[name], + ]} + /> Date: Tue, 12 Mar 2024 17:49:49 +0100 Subject: [PATCH 087/153] updates ISO field --- .../breakdown/deforestation-free-suppliers/index.tsx | 2 +- .../breakdown/suppliers-with-deforestation-alerts/index.tsx | 2 +- .../breakdown/suppliers-with-no-location-data/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx index 348f11ce1..b2292ac63 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -43,7 +43,7 @@ const DeforestationFreeSuppliersBreakdown = () => { color: CATEGORIES[0].color, icon: getCommodityIconByName(item.name, { fill: CATEGORIES[0].color }), ...(viewBy === 'origins' && { - icon: , + icon: , }), })) .flat(), diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx index e8e73c48c..3ad6a56d3 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -43,7 +43,7 @@ const SuppliersWithDeforestationAlertsBreakdown = () => { color: CATEGORIES[1].color, icon: getCommodityIconByName(item.name, { fill: CATEGORIES[1].color }), ...(viewBy === 'origins' && { - icon: , + icon: , }), })) .flat(), diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx index 4bdf3d946..0a9dba4b9 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -43,7 +43,7 @@ const SuppliersWithNoLocationDataBreakdown = () => { color: CATEGORIES[2].color, icon: getCommodityIconByName(item.name, { fill: CATEGORIES[2].color }), ...(viewBy === 'origins' && { - icon: , + icon: , }), })) .flat(), From 5cde3a9b567db2096784927e8a55733f0eeedec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 12 Mar 2024 18:35:26 +0100 Subject: [PATCH 088/153] EUDR detail page --- client/package.json | 1 + client/src/components/ui/separator.tsx | 24 ++ .../deforestation-alerts/chart/index.tsx | 139 +++++++++++ .../deforestation-alerts/index.tsx | 49 ++++ .../analysis-eudr-detail/filters/index.tsx | 11 + .../filters/years-range/index.tsx | 77 ++++++ .../analysis-eudr-detail/map/component.tsx | 110 +++++++++ .../analysis-eudr-detail/map/index.ts | 1 + .../map/legend/component.tsx | 74 ++++++ .../analysis-eudr-detail/map/legend/index.ts | 1 + .../analysis-eudr-detail/map/legend/item.tsx | 33 +++ .../map/zoom/component.tsx | 51 ++++ .../analysis-eudr-detail/map/zoom/index.ts | 1 + .../sourcing-info/chart/index.tsx | 223 ++++++++++++++++++ .../sourcing-info/index.tsx | 72 ++++++ .../supplier-info/index.tsx | 34 +++ .../breakdown/breakdown-item/index.tsx | 2 +- .../analysis-eudr/category-list/index.tsx | 3 +- .../supplier-list-table/index.tsx | 2 +- .../supplier-list-table/table/columns.tsx | 12 +- .../supplier-list-table/table/pagination.tsx | 2 +- .../suppliers-stacked-bar/component.tsx | 2 +- client/src/hooks/eudr/index.ts | 69 ++++++ .../src/pages/eudr/suppliers/[supplierId].tsx | 117 +++++++++ .../src/store/features/eudr-detail/index.ts | 44 ++++ client/src/store/index.ts | 2 + client/src/utils/colors.ts | 11 + client/yarn.lock | 21 ++ 28 files changed, 1177 insertions(+), 11 deletions(-) create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/filters/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/filters/years-range/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/map/component.tsx create mode 100644 client/src/containers/analysis-eudr-detail/map/index.ts create mode 100644 client/src/containers/analysis-eudr-detail/map/legend/component.tsx create mode 100644 client/src/containers/analysis-eudr-detail/map/legend/index.ts create mode 100644 client/src/containers/analysis-eudr-detail/map/legend/item.tsx create mode 100644 client/src/containers/analysis-eudr-detail/map/zoom/component.tsx create mode 100644 client/src/containers/analysis-eudr-detail/map/zoom/index.ts create mode 100644 client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx create mode 100644 client/src/containers/analysis-eudr-detail/supplier-info/index.tsx create mode 100644 client/src/pages/eudr/suppliers/[supplierId].tsx create mode 100644 client/src/store/features/eudr-detail/index.ts diff --git a/client/package.json b/client/package.json index a15ba91ee..5c9086e6a 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,7 @@ "@radix-ui/react-popover": "1.0.7", "@radix-ui/react-radio-group": "1.1.3", "@radix-ui/react-select": "2.0.0", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "1.0.2", "@reduxjs/toolkit": "1.8.2", "@tailwindcss/forms": "0.4.0", diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx new file mode 100644 index 000000000..37838d623 --- /dev/null +++ b/client/src/components/ui/separator.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; + +import { cn } from '@/lib/utils'; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( + +)); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx new file mode 100644 index 000000000..ca04fe700 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -0,0 +1,139 @@ +import { UTCDate } from '@date-fns/utc'; +import { format } from 'date-fns'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import { useParams } from 'next/navigation'; +import { useMemo, useState } from 'react'; + +import { EUDR_COLOR_RAMP } from '@/utils/colors'; +import { useEUDRSupplier } from '@/hooks/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { eudrDetail } from '@/store/features/eudr-detail'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +const DeforestationAlertsChart = (): JSX.Element => { + const [selectedPlots, setSelectedPlots] = useState([]); + const { supplierId }: { supplierId: string } = useParams(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + const { data } = useEUDRSupplier( + supplierId, + { + startAlertDate: dates.from, + endAlertDate: dates.to, + }, + { + select: (data) => data?.alerts?.values, + }, + ); + + const parsedData = data + ?.map((item) => { + return { + ...item, + ...Object.fromEntries(item.plots.map((plot) => [plot.plotName, plot.alertCount])), + alertDate: new UTCDate(item.alertDate).getTime(), + }; + }) + ?.sort((a, b) => new UTCDate(a.alertDate).getTime() - new UTCDate(b.alertDate).getTime()); + + const plotConfig = useMemo(() => { + if (!parsedData?.[0]) return []; + + return Array.from( + new Set(parsedData.map((item) => item.plots.map((plot) => plot.plotName)).flat()), + ).map((key, index) => ({ + name: key, + color: EUDR_COLOR_RAMP[index] || '#000', + })); + }, [parsedData]); + + return ( + <> +
+ {plotConfig.map(({ name, color }) => ( + { + setSelectedPlots((prev) => { + if (prev.includes(name)) { + return prev.filter((item) => item !== name); + } + return [...prev, name]; + }); + }} + > + + {name} + + ))} +
+ + + + { + if (x === 0) return format(new UTCDate(value), 'LLL yyyy'); + return format(new UTCDate(value), 'LLL'); + }} + tickLine={false} + padding={{ left: 20, right: 20 }} + axisLine={false} + className="text-xs" + tickMargin={15} + /> + + format(new UTCDate(v), 'dd/MM/yyyy')} /> + {plotConfig?.map(({ name, color }) => { + return ( + + ); + })} + + + + ); +}; + +export default DeforestationAlertsChart; diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx new file mode 100644 index 000000000..c1e2257cc --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx @@ -0,0 +1,49 @@ +import { useParams } from 'next/navigation'; +import { format } from 'date-fns'; +import { UTCDate } from '@date-fns/utc'; +import { BellRing } from 'lucide-react'; + +import DeforestationAlertsChart from './chart'; + +import { useEUDRSupplier } from '@/hooks/eudr'; +import { eudrDetail } from '@/store/features/eudr-detail'; +import { useAppSelector } from '@/store/hooks'; + +const dateFormatter = (date: string) => format(new UTCDate(date), "do 'of' MMMM yyyy"); + +const DeforestationAlerts = (): JSX.Element => { + const { supplierId }: { supplierId: string } = useParams(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + const { data } = useEUDRSupplier( + supplierId, + { + startAlertDate: dates.from, + endAlertDate: dates.to, + }, + { + select: (data) => data?.alerts, + }, + ); + + return ( +
+

Deforestation alerts detected within the smallholders

+ {data?.totalAlerts && ( +
+ There were {data?.totalAlerts} deforestation alerts + reported for the supplier between the{' '} + {dateFormatter(data.startAlertDate)} and the{' '} +
+ {dateFormatter(data.endAlertDate)}. + +
+
+ )} + +
+ ); +}; + +export default DeforestationAlerts; diff --git a/client/src/containers/analysis-eudr-detail/filters/index.tsx b/client/src/containers/analysis-eudr-detail/filters/index.tsx new file mode 100644 index 000000000..4d5cfdbb4 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/filters/index.tsx @@ -0,0 +1,11 @@ +import YearsRange from './years-range'; + +const EUDRDetailFilters = () => { + return ( +
+ +
+ ); +}; + +export default EUDRDetailFilters; diff --git a/client/src/containers/analysis-eudr-detail/filters/years-range/index.tsx b/client/src/containers/analysis-eudr-detail/filters/years-range/index.tsx new file mode 100644 index 000000000..ce0f0d1fa --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/filters/years-range/index.tsx @@ -0,0 +1,77 @@ +import React, { useCallback, useMemo } from 'react'; +import { UTCDate } from '@date-fns/utc'; +import { ChevronDown } from 'lucide-react'; +import { format } from 'date-fns'; + +import { useAppDispatch, useAppSelector } from 'store/hooks'; +import { eudrDetail, setFilters } from 'store/features/eudr-detail'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; + +import type { DateRange } from 'react-day-picker'; +const dateFormatter = (date: Date) => format(date, 'yyyy-MM-dd'); + +// ! the date range is hardcoded for now +export const DATES_RANGE = ['2020-12-31', dateFormatter(new Date())]; + +const DatesRange = (): JSX.Element => { + const dispatch = useAppDispatch(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + + const handleDatesChange = useCallback( + (dates: DateRange) => { + if (dates) { + dispatch( + setFilters({ + dates: { + from: dateFormatter(dates.from), + to: dateFormatter(dates.to), + }, + }), + ); + } + }, + [dispatch], + ); + + const datesToDate = useMemo(() => { + return { + from: dates.from ? new UTCDate(dates.from) : undefined, + to: dates.to ? new UTCDate(dates.to) : undefined, + }; + }, [dates]); + + return ( + + + + + + + + + ); +}; + +export default DatesRange; diff --git a/client/src/containers/analysis-eudr-detail/map/component.tsx b/client/src/containers/analysis-eudr-detail/map/component.tsx new file mode 100644 index 000000000..8c557d6ee --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/component.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState, useCallback } from 'react'; +import DeckGL from '@deck.gl/react/typed'; +import { GeoJsonLayer } from '@deck.gl/layers/typed'; +import Map from 'react-map-gl/maplibre'; +import { WebMercatorViewport, type MapViewState } from '@deck.gl/core/typed'; +import bbox from '@turf/bbox'; + +import ZoomControl from './zoom'; +import LegendControl from './legend'; + +import BasemapControl from '@/components/map/controls/basemap'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; +import { usePlotGeometries } from '@/hooks/eudr'; + +import type { BasemapValue } from '@/components/map/controls/basemap/types'; +import type { MapStyle } from '@/components/map/types'; + +const EUDRMap = () => { + const [mapStyle, setMapStyle] = useState('terrain'); + const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); + + const plotGeometries = usePlotGeometries(); + + const layer: GeoJsonLayer = new GeoJsonLayer({ + id: 'geojson-layer', + data: plotGeometries.data, + // Styles + filled: true, + getFillColor: [255, 176, 0, 84], + stroked: true, + getLineColor: [255, 176, 0, 255], + getLineWidth: 1, + lineWidthUnits: 'pixels', + // Interactive props + pickable: true, + autoHighlight: true, + highlightColor: [255, 176, 0, 255], + }); + + const layers = [layer]; + + const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { + setMapStyle(newStyle); + }, []); + + const handleZoomIn = useCallback(() => { + const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; + setViewState({ ...viewState, zoom }); + }, [viewState]); + + const handleZoomOut = useCallback(() => { + const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom - 1; + setViewState({ ...viewState, zoom }); + }, [viewState]); + + const fitToPlotBounds = useCallback(() => { + if (!plotGeometries.data) return; + const [minLng, minLat, maxLng, maxLat] = bbox(plotGeometries.data); + const newViewport = new WebMercatorViewport(viewState); + const { longitude, latitude, zoom } = newViewport.fitBounds( + [ + [minLng, minLat], + [maxLng, maxLat], + ], + { + padding: 10, + }, + ); + if ( + viewState.latitude !== latitude || + viewState.longitude !== longitude || + viewState.zoom !== zoom + ) { + setViewState({ ...viewState, longitude, latitude, zoom }); + } + }, [plotGeometries.data, viewState]); + + // Fit to bounds when data is loaded or changed + useEffect(() => { + if (plotGeometries.data) { + fitToPlotBounds(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data]); + + const handleResize = useCallback(() => { + setTimeout(() => fitToPlotBounds(), 0); + }, [fitToPlotBounds]); + + return ( + <> + setViewState(viewState as MapViewState)} + controller={{ dragRotate: false }} + layers={layers} + onResize={handleResize} + > + + +
+ + + +
+ + ); +}; + +export default EUDRMap; diff --git a/client/src/containers/analysis-eudr-detail/map/index.ts b/client/src/containers/analysis-eudr-detail/map/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/component.tsx b/client/src/containers/analysis-eudr-detail/map/legend/component.tsx new file mode 100644 index 000000000..54da03733 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/legend/component.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import classNames from 'classnames'; +import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; + +import LegendItem from './item'; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import SandwichIcon from '@/components/icons/sandwich'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; + +const EURDLegend = () => { + const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+ + + + + +
+

Legend

+
+ +
+ + +
+ +
+
+ + + + +
+
+
+
+
+ ); +}; + +export default EURDLegend; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/index.ts b/client/src/containers/analysis-eudr-detail/map/legend/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/legend/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/item.tsx b/client/src/containers/analysis-eudr-detail/map/legend/item.tsx new file mode 100644 index 000000000..3946119f2 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/legend/item.tsx @@ -0,0 +1,33 @@ +import classNames from 'classnames'; + +import type { FC, PropsWithChildren } from 'react'; + +type LegendItemProps = { title: string; description: string; iconClassName?: string }; + +const LegendItem: FC> = ({ + title, + description, + children, + iconClassName, +}) => { + return ( +
+
+
+
+

{title}

+
+
+

{description}

+ {children} +
+
+ ); +}; + +export default LegendItem; diff --git a/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx b/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx new file mode 100644 index 000000000..8b2eeeba1 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx @@ -0,0 +1,51 @@ +import { MinusIcon, PlusIcon } from '@heroicons/react/solid'; +import cx from 'classnames'; + +import type { MapViewState } from '@deck.gl/core/typed'; +import type { FC } from 'react'; + +const COMMON_CLASSES = + 'p-2 transition-colors bg-white cursor-pointer hover:bg-gray-100 active:bg-navy-50 disabled:bg-gray-100 disabled:opacity-75 disabled:cursor-default'; + +const ZoomControl: FC<{ + viewState: MapViewState; + className?: string; + onZoomIn: () => void; + onZoomOut: () => void; +}> = ({ viewState, className = null, onZoomIn, onZoomOut }) => { + const { zoom, minZoom, maxZoom } = viewState; + + return ( +
+ + +
+ ); +}; + +export default ZoomControl; diff --git a/client/src/containers/analysis-eudr-detail/map/zoom/index.ts b/client/src/containers/analysis-eudr-detail/map/zoom/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/map/zoom/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx new file mode 100644 index 000000000..7ab89f74a --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx @@ -0,0 +1,223 @@ +import { useParams } from 'next/navigation'; +import { useMemo, useState } from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Label, +} from 'recharts'; +import { format } from 'date-fns'; +import { groupBy } from 'lodash-es'; + +import { useEUDRSupplier } from '@/hooks/eudr'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { useAppSelector } from '@/store/hooks'; +import { eudrDetail } from '@/store/features/eudr-detail'; +import { EUDR_COLOR_RAMP } from '@/utils/colors'; +import { Badge } from '@/components/ui/badge'; + +const SupplierSourcingInfoChart = (): JSX.Element => { + const [showBy, setShowBy] = useState<'byVolume' | 'byArea'>('byVolume'); + const [selectedPlots, setSelectedPlots] = useState([]); + const { supplierId }: { supplierId: string } = useParams(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + + const { data } = useEUDRSupplier( + supplierId, + { + startAlertDate: dates.from, + endAlertDate: dates.to, + }, + { + select: (data) => data?.sourcingInformation, + }, + ); + + const parsedData = useMemo(() => { + if (showBy === 'byVolume') { + const plotsByYear = groupBy(data?.[showBy], 'year'); + + return Object.keys(groupBy(data?.[showBy], 'year')).map((year) => ({ + year: `${year}-01-01`, + ...Object.fromEntries( + plotsByYear[year].map(({ plotName, percentage }) => [plotName, percentage]), + ), + })); + } + + if (showBy === 'byArea') { + const plots = data?.[showBy]?.map(({ plotName, percentage }) => ({ + plotName, + percentage, + })); + + // ! for now, we are hardcoding the date and showing just the baseline (2020) + return [ + { + year: '2020-01-01', + ...Object.fromEntries(plots?.map(({ plotName, percentage }) => [plotName, percentage])), + }, + ]; + } + }, [data, showBy]); + + const plotConfig = useMemo(() => { + if (!parsedData?.[0]) return []; + + return Object.keys(parsedData[0]) + .filter((key) => key !== 'year') + .map((key, index) => ({ + name: key, + color: EUDR_COLOR_RAMP[index], + })); + }, [parsedData]); + + return ( +
+
+

Individual plot contributions to volume accumulation

+
+ Show by +
+ + +
+
+
+
+ {plotConfig.map(({ name, color }) => ( + { + setSelectedPlots((prev) => { + if (prev.includes(name)) { + return prev.filter((item) => item !== name); + } + return [...prev, name]; + }); + }} + > + + {name} + + ))} +
+ +
+ + + + + } + /> + ( + + + {format(new Date(payload.value), 'yyyy')} + + + )} + tickLine={false} + type="category" + width={200} + /> + format(new Date(value), 'yyyy')} + formatter={(value: number, name) => [`${value.toFixed(2)}%`, name]} + /> + {plotConfig.map(({ name, color }) => ( + + ))} + + +
+
+ ); +}; + +export default SupplierSourcingInfoChart; diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx new file mode 100644 index 000000000..87db82d68 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx @@ -0,0 +1,72 @@ +import { useParams } from 'next/navigation'; +import Flag from 'react-world-flags'; + +import SupplierSourcingInfoChart from './chart'; + +import { useEUDRSupplier } from '@/hooks/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { eudrDetail } from '@/store/features/eudr-detail'; +import { Separator } from '@/components/ui/separator'; + +const SupplierSourcingInfo = (): JSX.Element => { + const { supplierId }: { supplierId: string } = useParams(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + const { data } = useEUDRSupplier( + supplierId, + { + startAlertDate: dates.from, + endAlertDate: dates.to, + }, + { + select: (data) => data?.sourcingInformation, + }, + ); + + return ( +
+

Sourcing information

+
+
+

HS code

+ {data?.hsCode || '-'} +
+
+

Commodity sourced from plot

+ {data?.materialName || '-'} +
+
+

Country prod.

+ {data?.country?.name || '-'} + +
+
+
+
+
+

Sourcing volume

+ + {data?.totalVolume + ? `${Intl.NumberFormat(undefined, { style: 'unit', unit: 'kilogram' }).format( + data.totalVolume, + )}` + : '-'} + +
+ +
+

Sourcing area

+ + {data?.totalArea ? `${Intl.NumberFormat(undefined).format(data.totalArea)} Kha` : '-'} + +
+
+ + +
+
+ ); +}; + +export default SupplierSourcingInfo; diff --git a/client/src/containers/analysis-eudr-detail/supplier-info/index.tsx b/client/src/containers/analysis-eudr-detail/supplier-info/index.tsx new file mode 100644 index 000000000..3576e424e --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/supplier-info/index.tsx @@ -0,0 +1,34 @@ +import { useParams } from 'next/navigation'; + +import { useEUDRSupplier } from '@/hooks/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { eudrDetail } from '@/store/features/eudr-detail'; + +const SupplierInfo = (): JSX.Element => { + const { supplierId }: { supplierId: string } = useParams(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + const { data } = useEUDRSupplier(supplierId, { + startAlertDate: dates.from, + endAlertDate: dates.to, + }); + + return ( +
+

Supplier information

+
+
+

Supplier ID

+ {data?.companyId || '-'} +
+
+

Address

+ {data?.address || '-'} +
+
+
+ ); +}; + +export default SupplierInfo; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx index 40b46d5ff..cdb501801 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx @@ -19,7 +19,7 @@ const BreakdownItem = ({
- {`${value}%`} of suppliers + {`${value.toFixed(2)}%`} of suppliers
diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index c8198d2d5..7fce86af5 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -97,7 +97,8 @@ export const CategoryList = (): JSX.Element => {
- {`${category.totalPercentage}%`} of suppliers + {`${category.totalPercentage.toFixed(2)}%`}{' '} + of suppliers
{ return ( -
+
All commodities

Suppliers List

diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx index d863384ba..5209a5eb3 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -48,24 +48,24 @@ export const columns: ColumnDef[] = [ accessorKey: 'dfs', header: ({ column }) => , cell: ({ row }) => { - const dfs = row.getValue('dfs'); - return {`${Number.isNaN(dfs) ? '-' : `${dfs}%`}`}; + const dfs: number = row.getValue('dfs'); + return {`${Number.isNaN(dfs) ? '-' : `${dfs.toFixed(2)}%`}`}; }, }, { accessorKey: 'sda', header: ({ column }) => , cell: ({ row }) => { - const sda = row.getValue('sda'); - return {`${Number.isNaN(sda) ? '-' : `${sda}%`}`}; + const sda: number = row.getValue('sda'); + return {`${Number.isNaN(sda) ? '-' : `${sda.toFixed(2)}%`}`}; }, }, { accessorKey: 'tpl', header: ({ column }) => , cell: ({ row }) => { - const tpl = row.getValue('tpl'); - return {`${Number.isNaN(tpl) ? '-' : `${tpl}%`}`}; + const tpl: number = row.getValue('tpl'); + return {`${Number.isNaN(tpl) ? '-' : `${tpl.toFixed(2)}%`}`}; }, }, { diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx index 48b335b05..a49eda84e 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/pagination.tsx @@ -41,7 +41,7 @@ export function DataTablePagination({ table }: DataTablePaginationProps
-
+
Page {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- {(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize > table.getRowCount() diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index 297bf6480..d9204243d 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -167,7 +167,7 @@ const SuppliersStackedBar = () => { cursor={{ fill: 'transparent' }} labelFormatter={(value: string) => value} formatter={(value: number, name: keyof typeof TOOLTIP_LABELS) => [ - `${value}%`, + `${value.toFixed(2)}%`, TOOLTIP_LABELS[name], ]} /> diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index d2e543491..10e216a82 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -173,3 +173,72 @@ export const useEUDRData = ( }, ); }; + +export interface SupplierDetail { + name: string; + address: string; + companyId: string; + sourcingInformation: { + materialName: string; + hsCode: string; + country: { + name: string; + isoA3: string; + }; + totalArea: number; + totalVolume: number; + byVolume: [ + { + plotName: string; + year: number; + percentage: number; + volume: number; + }, + ]; + byArea: [ + { + plotName: string; + percentage: number; + area: number; + geoRegionId: string; + }, + ]; + }; + alerts: { + startAlertDate: string; + endAlertDate: string; + totalAlerts: number; + values: [ + { + alertDate: string; + plots: [ + { + plotName: string; + alertCount: number; + }, + ]; + }, + ]; + }; +} + +export const useEUDRSupplier = ( + supplierId: string, + params?: { startAlertDate: string; endAlertDate: string }, + options: UseQueryOptions = {}, +) => { + return useQuery( + ['eudr-supplier', supplierId, params], + () => + apiService + .request({ + method: 'GET', + url: `/eudr/dashboard/detail/${supplierId}`, + params, + }) + .then(({ data: responseData }) => responseData), + { + ...options, + }, + ); +}; diff --git a/client/src/pages/eudr/suppliers/[supplierId].tsx b/client/src/pages/eudr/suppliers/[supplierId].tsx new file mode 100644 index 000000000..4888f4e26 --- /dev/null +++ b/client/src/pages/eudr/suppliers/[supplierId].tsx @@ -0,0 +1,117 @@ +import { useRef, useState } from 'react'; +import { Transition } from '@headlessui/react'; +import { MapProvider } from 'react-map-gl/maplibre'; +import Link from 'next/link'; +import { ArrowLeft } from 'lucide-react'; +import { useParams } from 'next/navigation'; + +import { tasksSSR } from 'services/ssr'; +import ApplicationLayout from 'layouts/application'; +import CollapseButton from 'containers/collapse-button/component'; +import TitleTemplate from 'utils/titleTemplate'; +import Map from 'containers/analysis-eudr/map'; +import EUDRFilters from '@/containers/analysis-eudr-detail/filters'; +import { Button } from '@/components/ui/button'; +import { useEUDRSupplier } from '@/hooks/eudr'; +import SupplierInfo from '@/containers/analysis-eudr-detail/supplier-info'; +import SupplierSourcingInfo from '@/containers/analysis-eudr-detail/sourcing-info'; +import { Separator } from '@/components/ui/separator'; +import DeforestationAlerts from '@/containers/analysis-eudr-detail/deforestation-alerts'; + +import type { NextPageWithLayout } from 'pages/_app'; +import type { ReactElement } from 'react'; +import type { GetServerSideProps } from 'next'; + +const MapPage: NextPageWithLayout = () => { + const scrollRef = useRef(null); + const [isCollapsed, setIsCollapsed] = useState(false); + + const { supplierId }: { supplierId: string } = useParams(); + const { data } = useEUDRSupplier(supplierId); + + return ( + + +
+ + +
+ +
+
+
+ ); +}; + +MapPage.Layout = function getLayout(page: ReactElement) { + return {page}; +}; + +export const getServerSideProps: GetServerSideProps = async ({ req, res, query }) => { + try { + const tasks = await tasksSSR({ req, res }); + if (tasks && tasks[0]?.attributes.status === 'processing') { + return { + redirect: { + permanent: false, + destination: '/data', + }, + }; + } + return { props: { query } }; + } catch (error) { + if (error.code === '401' || error.response.status === 401) { + return { + redirect: { + permanent: false, + destination: '/auth/signin', + }, + }; + } + } +}; + +export default MapPage; diff --git a/client/src/store/features/eudr-detail/index.ts b/client/src/store/features/eudr-detail/index.ts new file mode 100644 index 000000000..d5f5ad75d --- /dev/null +++ b/client/src/store/features/eudr-detail/index.ts @@ -0,0 +1,44 @@ +import { createSlice } from '@reduxjs/toolkit'; + +import { DATES_RANGE } from 'containers/analysis-eudr-detail/filters/years-range'; + +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from 'store'; + +export type EUDRDetailState = { + filters: { + dates: { + from: string; + to: string; + }; + }; +}; + +export const initialState: EUDRDetailState = { + filters: { + dates: { + from: DATES_RANGE[0], + to: DATES_RANGE[1], + }, + }, +}; + +export const EUDRSlice = createSlice({ + name: 'eudrDetail', + initialState, + reducers: { + setFilters: (state, action: PayloadAction>) => ({ + ...state, + filters: { + ...state.filters, + ...action.payload, + }, + }), + }, +}); + +export const { setFilters } = EUDRSlice.actions; + +export const eudrDetail = (state: RootState) => state['eudrDetail']; + +export default EUDRSlice.reducer; diff --git a/client/src/store/index.ts b/client/src/store/index.ts index d77c384b5..48f714987 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -17,6 +17,7 @@ import analysisScenarios, { setScenarioToCompare, } from 'store/features/analysis/scenarios'; import eudr from 'store/features/eudr'; +import eudrDetail from 'store/features/eudr-detail'; import type { Action, ReducersMapObject, Middleware } from '@reduxjs/toolkit'; import type { AnalysisState } from './features/analysis'; @@ -28,6 +29,7 @@ const staticReducers = { 'analysis/map': analysisMap, 'analysis/scenarios': analysisScenarios, eudr, + eudrDetail, }; const asyncReducers = {}; diff --git a/client/src/utils/colors.ts b/client/src/utils/colors.ts index 3298c47d7..a89a2b2c9 100644 --- a/client/src/utils/colors.ts +++ b/client/src/utils/colors.ts @@ -23,3 +23,14 @@ export function useColors(layerName: string, colorScale): RGBColor[] { } export const themeColors = resolveConfig(tailwindConfig).theme.colors; + +export const EUDR_COLOR_RAMP = [ + themeColors.navy['600'], + '#50B1F6', + '#E2564F', + '#FAE26C', + '#ED7542', + '#ED75CC', + '#78C679', + '#AB93FF', +]; diff --git a/client/yarn.lock b/client/yarn.lock index f24bb8aa1..971fad216 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2314,6 +2314,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-separator@npm:^1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-separator@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.3 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 42f8c95e404de2ce9387040d78049808a48d423cd4c3bad8cca92c4b0bcbdcb3566b5b52a920d4e939a74b51188697f20a012221f0e630fc7f56de64096c15d2 + languageName: node + linkType: hard + "@radix-ui/react-slot@npm:1.0.2": version: 1.0.2 resolution: "@radix-ui/react-slot@npm:1.0.2" @@ -7813,6 +7833,7 @@ __metadata: "@radix-ui/react-popover": 1.0.7 "@radix-ui/react-radio-group": 1.1.3 "@radix-ui/react-select": 2.0.0 + "@radix-ui/react-separator": ^1.0.3 "@radix-ui/react-slot": 1.0.2 "@reduxjs/toolkit": 1.8.2 "@tailwindcss/forms": 0.4.0 From 1a7f01ed23bf904966272a15e1871b230d6bcdc2 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 7 Mar 2024 16:11:02 +0100 Subject: [PATCH 089/153] wip --- .../src/containers/analysis-eudr/map/component.tsx | 12 ++++++++++++ .../analysis-eudr/map/legend/component.tsx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 8c557d6ee..d58ecdee6 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -16,6 +16,7 @@ import type { BasemapValue } from '@/components/map/controls/basemap/types'; import type { MapStyle } from '@/components/map/types'; const EUDRMap = () => { + const [hoverInfo, setHoverInfo] = useState(null); const [mapStyle, setMapStyle] = useState('terrain'); const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); @@ -35,6 +36,7 @@ const EUDRMap = () => { pickable: true, autoHighlight: true, highlightColor: [255, 176, 0, 255], + onHover: setHoverInfo, }); const layers = [layer]; @@ -87,6 +89,15 @@ const EUDRMap = () => { setTimeout(() => fitToPlotBounds(), 0); }, [fitToPlotBounds]); + const handleTooltip = useCallback(({ object }) => { + console.log(object); + return ( + object && { + html: `
${object.properties.plotName}
`, + } + ); + }, []); + return ( <> { controller={{ dragRotate: false }} layers={layers} onResize={handleResize} + getTooltip={handleTooltip} > diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index 54da03733..326078f0f 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -19,7 +19,7 @@ const EURDLegend = () => { +
+ )} + {showSwitcher && ( +
+ +
+ )} +

{description}

{children} diff --git a/client/yarn.lock b/client/yarn.lock index 55dfb005e..656acf6cd 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2350,6 +2350,32 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-switch@npm:^1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-switch@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-use-previous": 1.0.1 + "@radix-ui/react-use-size": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: de18a802f317804d94315b1035d03a9cabef53317c148027f0f382bc2653723532691b65090596140737bb055e3affff977f5d73fe6caf8c526c6158baa811cc + languageName: node + linkType: hard + "@radix-ui/react-tooltip@npm:^1.0.7": version: 1.0.7 resolution: "@radix-ui/react-tooltip@npm:1.0.7" @@ -7866,6 +7892,7 @@ __metadata: "@radix-ui/react-select": 2.0.0 "@radix-ui/react-separator": ^1.0.3 "@radix-ui/react-slot": 1.0.2 + "@radix-ui/react-switch": ^1.0.3 "@radix-ui/react-tooltip": ^1.0.7 "@reduxjs/toolkit": 1.8.2 "@tailwindcss/forms": 0.4.0 From 13c2a77b01aae7a51ae9dfa720a2e6ff35acc253 Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 13 Mar 2024 16:07:49 +0100 Subject: [PATCH 093/153] basemap selector --- .../analysis-eudr/map/basemap/component.tsx | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/client/src/containers/analysis-eudr/map/basemap/component.tsx b/client/src/containers/analysis-eudr/map/basemap/component.tsx index ce5b8b01d..1654ef4ac 100644 --- a/client/src/containers/analysis-eudr/map/basemap/component.tsx +++ b/client/src/containers/analysis-eudr/map/basemap/component.tsx @@ -1,5 +1,7 @@ import Image from 'next/image'; +import { Switch } from '@/components/ui/switch'; +import InfoModal from '@/components/legend/item/info-modal'; import DefaultImage from '@/components/map/controls/basemap/images/default1.png'; import SatelliteImage from '@/components/map/controls/basemap/images/satellite1.png'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -19,15 +21,61 @@ const EUDRBasemapControl = ({ value, onChange }) => {

Basemaps

-
-
-
-
+
+
+
+
+ +
+
+
+
+
+

Light Map

+
+
+ +
+
+ +
+
+
+
Light basemap version from Carto
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

Planet Satellite Imagery

+
+
+ +
+
+ +
+
+
+
+ Monthly high resolution basemaps (tropics) +
+ +
Select satellite basemap for image comparison option.
); From 7ea012212a09dedcfe08244caf4a8857c81e9631 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 14 Mar 2024 17:13:05 +0100 Subject: [PATCH 094/153] planet comparison and contextual layers --- client/package.json | 5 +- .../analysis-eudr/map/basemap/component.tsx | 111 +++++++- .../analysis-eudr/map/component.tsx | 153 +++++----- .../containers/analysis-eudr/map/layers.json | 2 + .../analysis-eudr/map/legend/component.tsx | 19 +- .../analysis-eudr/map/legend/item.tsx | 12 +- client/src/hooks/eudr/index.ts | 2 + client/src/store/features/eudr/index.ts | 63 ++++- client/yarn.lock | 262 ++++++++---------- 9 files changed, 398 insertions(+), 231 deletions(-) diff --git a/client/package.json b/client/package.json index 44b01a440..b0dee8277 100644 --- a/client/package.json +++ b/client/package.json @@ -16,14 +16,15 @@ }, "dependencies": { "@date-fns/utc": "1.1.1", - "@deck.gl/carto": "^8.9.35", + "@deck.gl/aggregation-layers": "8.8.6", + "@deck.gl/carto": "8.8.6", "@deck.gl/core": "8.8.6", "@deck.gl/extensions": "8.8.6", "@deck.gl/geo-layers": "8.8.6", "@deck.gl/layers": "8.8.6", "@deck.gl/mapbox": "8.8.6", "@deck.gl/mesh-layers": "8.8.6", - "@deck.gl/react": "^8.9.35", + "@deck.gl/react": "8.9.35", "@dnd-kit/core": "5.0.3", "@dnd-kit/modifiers": "5.0.0", "@dnd-kit/sortable": "6.0.1", diff --git a/client/src/containers/analysis-eudr/map/basemap/component.tsx b/client/src/containers/analysis-eudr/map/basemap/component.tsx index 1654ef4ac..1889a43d4 100644 --- a/client/src/containers/analysis-eudr/map/basemap/component.tsx +++ b/client/src/containers/analysis-eudr/map/basemap/component.tsx @@ -1,19 +1,57 @@ +import { useCallback, useEffect } from 'react'; import Image from 'next/image'; +import LayersData from '../layers.json'; + +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { setBasemap, setPlanetCompare } from '@/store/features/eudr'; import { Switch } from '@/components/ui/switch'; import InfoModal from '@/components/legend/item/info-modal'; import DefaultImage from '@/components/map/controls/basemap/images/default1.png'; import SatelliteImage from '@/components/map/controls/basemap/images/satellite1.png'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -const EUDRBasemapControl = ({ value, onChange }) => { +import type { EUDRState } from '@/store/features/eudr'; + +const EUDRBasemapControl = () => { + const dispatch = useAppDispatch(); + const { basemap, planetCompare } = useAppSelector((state) => state.eudr); + const basemapData = LayersData.find((layer) => layer.id === 'planet-data'); + + const handleBasemap = useCallback( + (basemapValue: EUDRState['basemap'], checked: boolean) => { + if (basemapValue === 'light') { + dispatch(setBasemap(checked ? 'light' : 'planet')); + } else if (basemapValue === 'planet') { + dispatch(setBasemap(checked ? 'planet' : 'light')); + } + }, + [dispatch], + ); + + const handleSetCompare = useCallback( + (checked: boolean) => { + dispatch(setPlanetCompare(checked)); + }, + [dispatch], + ); + + useEffect(() => { + if (basemap === 'light') dispatch(setPlanetCompare(false)); + }, [basemap, dispatch]); + return (
- +
@@ -26,7 +64,7 @@ const EUDRBasemapControl = ({ value, onChange }) => {
- +
@@ -35,14 +73,23 @@ const EUDRBasemapControl = ({ value, onChange }) => {

Light Map

- +
- + handleBasemap('light', checked)} + />
-
Light basemap version from Carto
+
Light basemap version from Carto
@@ -60,14 +107,23 @@ const EUDRBasemapControl = ({ value, onChange }) => {

Planet Satellite Imagery

- +
- + handleBasemap('planet', checked)} + />
-
+
Monthly high resolution basemaps (tropics)
@@ -75,7 +131,42 @@ const EUDRBasemapControl = ({ value, onChange }) => {
-
Select satellite basemap for image comparison option.
+
+ {basemap !== 'planet' &&
Select satellite basemap for image comparison option.
} + {basemap === 'planet' && ( +
+
+
+
+ +
+
+
+
+
+

Compare Satellite Images

+
+
+ +
+
+ +
+
+
+
+ Monthly high resolution basemaps (tropics) +
+
+
+ )} +
); diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 23b2dfc8c..d8dad16f2 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -3,35 +3,42 @@ import DeckGL from '@deck.gl/react/typed'; import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; import Map from 'react-map-gl/maplibre'; import { WebMercatorViewport, MapView } from '@deck.gl/core/typed'; -import { View } from '@deck.gl/core'; import { TileLayer } from '@deck.gl/geo-layers/typed'; +import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; import bbox from '@turf/bbox'; import ZoomControl from './zoom'; import LegendControl from './legend'; import BasemapControl from './basemap'; +import { useAppSelector } from '@/store/hooks'; +// import { setBasemap, setPlanetCompare } from '@/store/features/eudr'; import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; import { usePlotGeometries } from '@/hooks/eudr'; import { formatNumber } from '@/utils/number-format'; import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; -import type { BasemapValue } from '@/components/map/controls/basemap/types'; -import type { MapStyle } from '@/components/map/types'; -const PlanetQ32023_ID = 'PlanetQ32023'; -const PlanetQ32019_ID = 'PlanetQ32019'; -const DEFORESTATION_QUADBIN_LAYER_ID = 'deforestationQuadbinLayer'; +setDefaultCredentials({ + // apiBaseUrl: 'https://eudr.carto.com', + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', +}); const EUDRMap = () => { + const { basemap, planetCompare, supplierLayer, contextualLayers } = useAppSelector( + (state) => state.eudr, + ); + const [hoverInfo, setHoverInfo] = useState(null); - const [mapStyle, setMapStyle] = useState('terrain'); const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); const plotGeometries = usePlotGeometries(); + // Supplier plot layer const layer: GeoJsonLayer = new GeoJsonLayer({ - id: 'top-plots-layer', + id: 'full-plots-layer', data: plotGeometries.data, // Styles filled: true, @@ -44,30 +51,13 @@ const EUDRMap = () => { pickable: true, autoHighlight: true, highlightColor: [255, 176, 0, 255], + visible: supplierLayer.active, onHover: setHoverInfo, }); - const layer2: GeoJsonLayer = new GeoJsonLayer({ - id: 'bottom-plots-layer', - data: plotGeometries.data, - // Styles - filled: true, - getFillColor: [255, 0, 0, 255], - stroked: true, - getLineColor: [255, 0, 0, 255], - getLineWidth: 1, - lineWidthUnits: 'pixels', - // Interactive props - pickable: true, - autoHighlight: true, - highlightColor: [255, 176, 0, 255], - onHover: setHoverInfo, - }); - - const planetQ32019Layer = new TileLayer({ - // id: PlanetQ32023_ID, - id: 'top-planet-q3-2019-layer', - data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_quarterly_2019q3_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', + const planetLayer = new TileLayer({ + id: 'top-planet-monthly-layer', + data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_2020_12_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', minZoom: 0, maxZoom: 20, tileSize: 256, @@ -94,10 +84,9 @@ const EUDRMap = () => { }, }); - const planetQ32023Layer = new TileLayer({ - // id: PlanetQ32023_ID, - id: 'bottom-planet-q3-2023-layer', - data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_quarterly_2023q3_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', + const planetCompareLayer = new TileLayer({ + id: 'bottom-planet-monthly-layer', + data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_2024_02_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', minZoom: 0, maxZoom: 20, tileSize: 256, @@ -124,11 +113,35 @@ const EUDRMap = () => { }, }); - const layers = [planetQ32019Layer, planetQ32023Layer, layer, layer2]; + const forestCoverLayer = new CartoLayer({ + id: 'full-forest-cover-2020-ec-jrc', + type: MAP_TYPES.TILESET, + connection: 'eudr', + data: 'cartobq.eudr.JRC_2020_Forest_d_TILE', + pointRadiusMinPixels: 2, + getLineColor: [114, 169, 80], + getFillColor: [114, 169, 80], + lineWidthMinPixels: 1, + visible: contextualLayers['forest-cover-2020-ec-jrc'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjY2JlMjUyYSJ9.LoqzuDp076ESVYmHm1mZNtfhnqOVGmSxzp60Fht8PQw', + }, + }); - const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { - setMapStyle(newStyle); - }, []); + const deforestationLayer = new CartoLayer({ + id: 'full-deforestation-alerts-2020-2022-hansen', + type: MAP_TYPES.QUERY, + connection: 'eudr', + data: 'SELECT * FROM `cartobq.eudr.TCL_hansen_year`', + pointRadiusMinPixels: 2, + getLineColor: [224, 191, 36], + getFillColor: [224, 191, 36], + lineWidthMinPixels: 1, + visible: contextualLayers['deforestation-alerts-2020-2022-hansen'].active, + }); const handleZoomIn = useCallback(() => { const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; @@ -180,36 +193,48 @@ const EUDRMap = () => { viewState={{ ...viewState }} onViewStateChange={({ viewState }) => setViewState(viewState as MapViewState)} controller={{ dragRotate: false }} - layers={layers} - layerFilter={({ layer, viewport }) => viewport.id.startsWith(layer.id.split('-')[0])} - onResize={handleResize} - views={[ - new MapView({ - id: 'full', - controller: true, - }), - new MapView({ - id: 'top', - y: 0, - height: '50%', - padding: { top: '100%' }, - controller: true, - }), - new MapView({ - id: 'bottom', - y: '50%', - height: '50%', - padding: { bottom: '100%' }, - // controller: true, - }), + layers={[ + basemap === 'planet' && !planetCompare ? [planetLayer] : null, + basemap === 'planet' && planetCompare ? [planetLayer, planetCompareLayer] : null, + forestCoverLayer, + deforestationLayer, + layer, ]} + layerFilter={({ layer, viewport }) => { + return !planetCompare || viewport.id.startsWith(layer.id.split('-')[0]); + }} + onResize={handleResize} + {...(planetCompare + ? { + views: [ + new MapView({ + id: 'top', + y: 0, + height: '50%', + padding: { top: '100%' }, + }), + new MapView({ + id: 'bottom', + y: '50%', + height: '50%', + padding: { bottom: '100%' }, + }), + new MapView({ + id: 'full', + y: 0, + x: 0, + width: '100%', + height: '100%', + }), + ], + } + : {})} > - - - - - + + {planetCompare && ( +
+ )} {hoverInfo?.object && (
{ + const dispatch = useAppDispatch(); + const { supplierLayer, contextualLayers } = useAppSelector((state) => state.eudr); + const [isOpen, setIsOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); @@ -39,8 +44,11 @@ const EURDLegend = () => { title={supplierPlotsData.title} content={supplierPlotsData.content} description={supplierPlotsData.description} - iconClassName={supplierPlotsData.legend.iconClassName} showVisibility + isActive={supplierLayer.active} + changeVisibility={() => + dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) + } />
@@ -68,6 +76,15 @@ const EURDLegend = () => { content={layer.content} iconColor={layer.legend?.iconColor} showSwitcher + isActive={contextualLayers[layer.id].active} + changeVisibility={(isVisible) => + dispatch( + setContextualLayer({ + layer: layer.id, + configuration: { active: isVisible }, + }), + ) + } /> ))} diff --git a/client/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx index d39338402..d5394f35c 100644 --- a/client/src/containers/analysis-eudr/map/legend/item.tsx +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -12,11 +12,11 @@ type LegendItemProps = { content: string; description: string; source?: string | string[]; - iconClassName?: string; iconColor?: string; showVisibility?: boolean; showSwitcher?: boolean; isActive?: boolean; + changeVisibility?: (active: boolean) => void; }; const LegendItem: FC> = ({ @@ -25,18 +25,18 @@ const LegendItem: FC> = ({ description, source, children, - iconClassName, - iconColor, + iconColor = null, showVisibility = false, showSwitcher = false, isActive = true, + changeVisibility = () => null, }) => { return (
@@ -50,7 +50,7 @@ const LegendItem: FC> = ({
{showVisibility && (
-
diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 10e216a82..6a65cc4aa 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -48,6 +48,8 @@ export const usePlotGeometries = ( .then((response) => response.data.geojson), { ...options, + refetchOnReconnect: false, + refetchOnWindowFocus: false, }, ); }; diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index 4c41b1004..62c7befcd 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -7,6 +7,13 @@ import type { VIEW_BY_OPTIONS } from 'containers/analysis-eudr/suppliers-stacked import type { PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from 'store'; +type LayerConfiguration = { + active: boolean; + opacity?: number; + month?: number; + year?: number; +}; + export type EUDRState = { viewBy: (typeof VIEW_BY_OPTIONS)[number]['value']; filters: { @@ -19,6 +26,11 @@ export type EUDRState = { to: string; }; }; + // map + basemap: 'light' | 'planet'; + planetCompare: boolean; + supplierLayer: LayerConfiguration; + contextualLayers: Record; }; export const initialState: EUDRState = { @@ -33,6 +45,26 @@ export const initialState: EUDRState = { to: DATES_RANGE[1], }, }, + basemap: 'light', + planetCompare: false, + supplierLayer: { + active: true, + opacity: 1, + }, + contextualLayers: { + ['forest-cover-2020-ec-jrc']: { + active: false, + opacity: 1, + }, + ['deforestation-alerts-2020-2022-hansen']: { + active: false, + opacity: 1, + }, + ['real-time-deforestation-alerts-since-2020-radd']: { + active: false, + opacity: 1, + }, + }, }; export const EUDRSlice = createSlice({ @@ -50,10 +82,39 @@ export const EUDRSlice = createSlice({ ...action.payload, }, }), + setBasemap: (state, action: PayloadAction) => ({ + ...state, + basemap: action.payload, + }), + setPlanetCompare: (state, action: PayloadAction) => ({ + ...state, + planetCompare: action.payload, + }), + setSupplierLayer: (state, action: PayloadAction) => ({ + ...state, + supplierLayer: action.payload, + }), + setContextualLayer: ( + state, + action: PayloadAction<{ layer: string; configuration: LayerConfiguration }>, + ) => ({ + ...state, + contextualLayers: { + ...state.contextualLayers, + [action.payload.layer]: action.payload.configuration, + }, + }), }, }); -export const { setViewBy, setFilters } = EUDRSlice.actions; +export const { + setViewBy, + setFilters, + setBasemap, + setPlanetCompare, + setSupplierLayer, + setContextualLayer, +} = EUDRSlice.actions; export const eudr = (state: RootState) => state['eudr']; diff --git a/client/yarn.lock b/client/yarn.lock index 656acf6cd..84b0b258b 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -333,34 +333,48 @@ __metadata: languageName: node linkType: hard -"@deck.gl/carto@npm:^8.9.35": - version: 8.9.35 - resolution: "@deck.gl/carto@npm:8.9.35" +"@deck.gl/aggregation-layers@npm:8.8.6": + version: 8.8.6 + resolution: "@deck.gl/aggregation-layers@npm:8.8.6" dependencies: - "@babel/runtime": ^7.0.0 - "@loaders.gl/gis": ^3.4.13 - "@loaders.gl/loader-utils": ^3.4.13 - "@loaders.gl/mvt": ^3.4.13 - "@loaders.gl/tiles": ^3.4.13 - "@luma.gl/constants": ^8.5.21 + "@luma.gl/constants": ^8.5.16 + "@luma.gl/shadertools": ^8.5.16 + "@math.gl/web-mercator": ^3.6.2 + d3-hexbin: ^0.2.1 + peerDependencies: + "@deck.gl/core": ^8.0.0 + "@deck.gl/layers": ^8.0.0 + "@luma.gl/core": ^8.0.0 + checksum: 369613f0c6b09c1c697165f04c42d83e709b296af940b8a7b7198be1a603ac0aa729d37f10e3ac716cc0c7b2389358c1290fb9729301a626c8818eb532a22909 + languageName: node + linkType: hard + +"@deck.gl/carto@npm:8.8.6": + version: 8.8.6 + resolution: "@deck.gl/carto@npm:8.8.6" + dependencies: + "@loaders.gl/gis": ^3.2.5 + "@loaders.gl/loader-utils": ^3.2.5 + "@loaders.gl/mvt": ^3.2.5 + "@loaders.gl/tiles": ^3.2.5 + "@luma.gl/constants": ^8.5.16 "@math.gl/web-mercator": ^3.6.2 cartocolor: ^4.0.2 - d3-array: ^3.2.0 - d3-color: ^3.1.0 - d3-format: ^3.1.0 - d3-scale: ^4.0.0 + d3-array: ^2.8.0 + d3-color: ^2.0.0 + d3-format: ^2.0.0 + d3-scale: ^3.2.3 h3-js: ^3.7.0 moment-timezone: ^0.5.33 pbf: ^3.2.1 - quadbin: ^0.1.9 peerDependencies: "@deck.gl/aggregation-layers": ^8.0.0 "@deck.gl/core": ^8.0.0 "@deck.gl/extensions": ^8.0.0 "@deck.gl/geo-layers": ^8.0.0 "@deck.gl/layers": ^8.0.0 - "@loaders.gl/core": ^3.4.13 - checksum: 2c8edfd7974e4e4ee7874da5c8db2da75dc84074d4d121350228206150071ad82d5a541e79ee3bc059f6d8716929b701d1421407e5df052a4f9a097d016e4065 + "@loaders.gl/core": ^3.0.0 + checksum: f27abb24ae5a77c0debcbd32c5a0ee1f255ffdeec7d0b0dc0bdc43ad78acf53b599aacaabae735e03c1c7d22cf29aa212821782f006f6c20345f17265b243c90 languageName: node linkType: hard @@ -475,7 +489,7 @@ __metadata: languageName: node linkType: hard -"@deck.gl/react@npm:^8.9.35": +"@deck.gl/react@npm:8.9.35": version: 8.9.35 resolution: "@deck.gl/react@npm:8.9.35" dependencies: @@ -1130,19 +1144,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/gis@npm:3.4.15, @loaders.gl/gis@npm:^3.4.13": - version: 3.4.15 - resolution: "@loaders.gl/gis@npm:3.4.15" - dependencies: - "@loaders.gl/loader-utils": 3.4.15 - "@loaders.gl/schema": 3.4.15 - "@mapbox/vector-tile": ^1.3.1 - "@math.gl/polygon": ^3.5.1 - pbf: ^3.2.1 - checksum: dae1ac222e731a1ffad95f6f2176a33790fa097fa95e20817a4ca6325e1d7be5d8a4f69a2019cf0769aede7d73bff79f7ad38533add58978505a49862f42c685 - languageName: node - linkType: hard - "@loaders.gl/gltf@npm:3.3.1, @loaders.gl/gltf@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/gltf@npm:3.3.1" @@ -1165,15 +1166,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/images@npm:3.4.15": - version: 3.4.15 - resolution: "@loaders.gl/images@npm:3.4.15" - dependencies: - "@loaders.gl/loader-utils": 3.4.15 - checksum: 7d5ac4948b6f612aed410eafb05b34bea7d067475a07d40c022ed9f09c2de7403f24ab1d694a647917275e5e18c70f3c94d5f50278ef15f7b7bd01375df83f47 - languageName: node - linkType: hard - "@loaders.gl/loader-utils@npm:3.3.1, @loaders.gl/loader-utils@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/loader-utils@npm:3.3.1" @@ -1185,17 +1177,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/loader-utils@npm:3.4.15, @loaders.gl/loader-utils@npm:^3.4.13": - version: 3.4.15 - resolution: "@loaders.gl/loader-utils@npm:3.4.15" - dependencies: - "@babel/runtime": ^7.3.1 - "@loaders.gl/worker-utils": 3.4.15 - "@probe.gl/stats": ^3.5.0 - checksum: 04a91e56ecf8b792853a24184a27c3bf9824e62959e8d8c5f3b615724816c6bfdced8c56c02c9e22c6795a030c24009a75ae5b6f924627e2575e039c7234ba96 - languageName: node - linkType: hard - "@loaders.gl/math@npm:3.3.1": version: 3.3.1 resolution: "@loaders.gl/math@npm:3.3.1" @@ -1207,17 +1188,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/math@npm:3.4.15": - version: 3.4.15 - resolution: "@loaders.gl/math@npm:3.4.15" - dependencies: - "@loaders.gl/images": 3.4.15 - "@loaders.gl/loader-utils": 3.4.15 - "@math.gl/core": ^3.5.1 - checksum: e0e41e5253f876ecd6a15ec0648954c3d88516794336dd431ab1a82dcd4a4d2fb4a69c7e087f83744c4ef7982e60aa4ee6fdf89c8686772c30291cc399e02beb - languageName: node - linkType: hard - "@loaders.gl/mvt@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/mvt@npm:3.3.1" @@ -1231,19 +1201,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/mvt@npm:^3.4.13": - version: 3.4.15 - resolution: "@loaders.gl/mvt@npm:3.4.15" - dependencies: - "@loaders.gl/gis": 3.4.15 - "@loaders.gl/loader-utils": 3.4.15 - "@loaders.gl/schema": 3.4.15 - "@math.gl/polygon": ^3.5.1 - pbf: ^3.2.1 - checksum: f3f32d558e87a28256966c215456d482a5d910dd5e7426b57b65e78df5fef2dfd6869591152c61e49ecc719636c519e0047954227c66a30b240bf161dd13b38d - languageName: node - linkType: hard - "@loaders.gl/schema@npm:3.3.1, @loaders.gl/schema@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/schema@npm:3.3.1" @@ -1253,15 +1210,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/schema@npm:3.4.15": - version: 3.4.15 - resolution: "@loaders.gl/schema@npm:3.4.15" - dependencies: - "@types/geojson": ^7946.0.7 - checksum: 948e039848d1a599d6fe60276e2fb44f3ffc27924f944d399f1bd264209bb7409ef288dfed1c4440fd527e8939c7753ddbf7c4f2e4d4ac6f420c8a13ed187ffc - languageName: node - linkType: hard - "@loaders.gl/terrain@npm:^3.2.5": version: 3.3.1 resolution: "@loaders.gl/terrain@npm:3.3.1" @@ -1305,23 +1253,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/tiles@npm:^3.4.13": - version: 3.4.15 - resolution: "@loaders.gl/tiles@npm:3.4.15" - dependencies: - "@loaders.gl/loader-utils": 3.4.15 - "@loaders.gl/math": 3.4.15 - "@math.gl/core": ^3.5.1 - "@math.gl/culling": ^3.5.1 - "@math.gl/geospatial": ^3.5.1 - "@math.gl/web-mercator": ^3.5.1 - "@probe.gl/stats": ^3.5.0 - peerDependencies: - "@loaders.gl/core": ^3.4.0 - checksum: 18cc163c621b8ee388b0f78d5a95171a7c3540a5156d687b4d431ffc9f0533c4f220c8b265e7fbeaabc569260615229ed8871a7d2f2a08f7da0a5bcd6f083563 - languageName: node - linkType: hard - "@loaders.gl/worker-utils@npm:3.3.1": version: 3.3.1 resolution: "@loaders.gl/worker-utils@npm:3.3.1" @@ -1331,15 +1262,6 @@ __metadata: languageName: node linkType: hard -"@loaders.gl/worker-utils@npm:3.4.15": - version: 3.4.15 - resolution: "@loaders.gl/worker-utils@npm:3.4.15" - dependencies: - "@babel/runtime": ^7.3.1 - checksum: f4d77444a73b650b92340036af9e5d5a5449ef1e8940a2b33f4800444918c76e7d54f9bafd86c413cb94d777c256a22521bfbbd4e981757d39ecdfbb9994e28d - languageName: node - linkType: hard - "@luma.gl/constants@npm:8.5.16": version: 8.5.16 resolution: "@luma.gl/constants@npm:8.5.16" @@ -1354,13 +1276,6 @@ __metadata: languageName: node linkType: hard -"@luma.gl/constants@npm:^8.5.21": - version: 8.5.21 - resolution: "@luma.gl/constants@npm:8.5.21" - checksum: 2ac0eb0fb368d8a4722f140917de45100af7adde776c5be40935c86c996491e9145464f3dd5be69294a839fa145b456df828425a93f071bf68bf62c1f79c376f - languageName: node - linkType: hard - "@luma.gl/core@npm:^8.5.16": version: 8.5.16 resolution: "@luma.gl/core@npm:8.5.16" @@ -1479,15 +1394,6 @@ __metadata: languageName: node linkType: hard -"@mapbox/tile-cover@npm:3.0.1": - version: 3.0.1 - resolution: "@mapbox/tile-cover@npm:3.0.1" - dependencies: - tilebelt: ^1.0.1 - checksum: 0b28516ca3159a0ec3aca923d7031e2c1ba1e3ea73eead8f06448a7dd904a3a0c3d4ee3a998ae391b0e7444540eb29f79c21cd637a7845f0315c15a9c9455f76 - languageName: node - linkType: hard - "@mapbox/tiny-sdf@npm:^1.1.0": version: 1.2.5 resolution: "@mapbox/tiny-sdf@npm:1.2.5" @@ -4695,6 +4601,15 @@ __metadata: languageName: node linkType: hard +"d3-array@npm:2, d3-array@npm:^2.3.0, d3-array@npm:^2.8.0": + version: 2.12.1 + resolution: "d3-array@npm:2.12.1" + dependencies: + internmap: ^1.0.0 + checksum: 97853b7b523aded17078f37c67742f45d81e88dda2107ae9994c31b9e36c5fa5556c4c4cf39650436f247813602dfe31bf7ad067ff80f127a16903827f10c6eb + languageName: node + linkType: hard + "d3-array@npm:3.0.2": version: 3.0.2 resolution: "d3-array@npm:3.0.2" @@ -4704,7 +4619,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:^3.1.6, d3-array@npm:^3.2.0": +"d3-array@npm:^3.1.6": version: 3.2.4 resolution: "d3-array@npm:3.2.4" dependencies: @@ -4713,7 +4628,14 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": +"d3-color@npm:1 - 2, d3-color@npm:^2.0.0": + version: 2.0.0 + resolution: "d3-color@npm:2.0.0" + checksum: b887354aa383937abd04fbffed3e26e5d6a788472cd3737fb10735930e427763e69fe93398663bccf88c0b53ee3e638ac6fcf0c02226b00ed9e4327c2dfbf3dc + languageName: node + linkType: hard + +"d3-color@npm:1 - 3": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b @@ -4727,7 +4649,14 @@ __metadata: languageName: node linkType: hard -"d3-format@npm:1 - 3, d3-format@npm:^3.1.0": +"d3-format@npm:1 - 2, d3-format@npm:^2.0.0": + version: 2.0.0 + resolution: "d3-format@npm:2.0.0" + checksum: c4d3c8f9941d097d514d3986f54f21434e08e5876dc08d1d65226447e8e167600d5b9210235bb03fd45327225f04f32d6e365f08f76d2f4b8bff81594851aaf7 + languageName: node + linkType: hard + +"d3-format@npm:1 - 3": version: 3.1.0 resolution: "d3-format@npm:3.1.0" checksum: f345ec3b8ad3cab19bff5dead395bd9f5590628eb97a389b1dd89f0b204c7c4fc1d9520f13231c2c7cf14b7c9a8cf10f8ef15bde2befbab41454a569bd706ca2 @@ -4741,6 +4670,22 @@ __metadata: languageName: node linkType: hard +"d3-hexbin@npm:^0.2.1": + version: 0.2.2 + resolution: "d3-hexbin@npm:0.2.2" + checksum: 44c31270d98bff7eb8ad198e1fa690559b64ff79f3aa22e390ee82747d73146a33c2544ea31c46220a63fe14a1bd4e7ad03f95a2608b56c3a2398ae23e9c0019 + languageName: node + linkType: hard + +"d3-interpolate@npm:1.2.0 - 2": + version: 2.0.1 + resolution: "d3-interpolate@npm:2.0.1" + dependencies: + d3-color: 1 - 2 + checksum: 4a2018ac34fbcc3e0e7241e117087ca1b2274b8b33673913658623efacc5db013b8d876586d167b23e3145bdb34ec8e441d301299b082e1a90985b2f18d4299c + languageName: node + linkType: hard + "d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" @@ -4757,7 +4702,7 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:4.0.2, d3-scale@npm:^4.0.0, d3-scale@npm:^4.0.2": +"d3-scale@npm:4.0.2, d3-scale@npm:^4.0.2": version: 4.0.2 resolution: "d3-scale@npm:4.0.2" dependencies: @@ -4770,6 +4715,19 @@ __metadata: languageName: node linkType: hard +"d3-scale@npm:^3.2.3": + version: 3.3.0 + resolution: "d3-scale@npm:3.3.0" + dependencies: + d3-array: ^2.3.0 + d3-format: 1 - 2 + d3-interpolate: 1.2.0 - 2 + d3-time: ^2.1.1 + d3-time-format: 2 - 3 + checksum: f77e73f0fb422292211d0687914c30d26e29011a936ad2a535a868ae92f306c3545af1fe7ea5db1b3e67dbce7a6c6cd952e53d02d1d557543e7e5d30e30e52f2 + languageName: node + linkType: hard + "d3-shape@npm:^3.1.0": version: 3.2.0 resolution: "d3-shape@npm:3.2.0" @@ -4779,6 +4737,15 @@ __metadata: languageName: node linkType: hard +"d3-time-format@npm:2 - 3": + version: 3.0.0 + resolution: "d3-time-format@npm:3.0.0" + dependencies: + d3-time: 1 - 2 + checksum: c20c1667dbea653f81d923e741f84c23e4b966002ba0d6ed94cbc70692105566e55e89d18d175404534a879383fd1123300bd12885a3c924fe924032bb0060db + languageName: node + linkType: hard + "d3-time-format@npm:2 - 4": version: 4.1.0 resolution: "d3-time-format@npm:4.1.0" @@ -4788,6 +4755,15 @@ __metadata: languageName: node linkType: hard +"d3-time@npm:1 - 2, d3-time@npm:^2.1.1": + version: 2.1.1 + resolution: "d3-time@npm:2.1.1" + dependencies: + d3-array: 2 + checksum: d1c7b9658c20646e46c3dd19e11c38e02dec098e8baa7d2cd868af8eb01953668f5da499fa33dc63541cf74a26e788786f8828c4381dbbf475a76b95972979a6 + languageName: node + linkType: hard + "d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3": version: 3.0.0 resolution: "d3-time@npm:3.0.0" @@ -7080,6 +7056,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:^1.0.0": + version: 1.0.1 + resolution: "internmap@npm:1.0.1" + checksum: 9d00f8c0cf873a24a53a5a937120dab634c41f383105e066bb318a61864e6292d24eb9516e8e7dccfb4420ec42ca474a0f28ac9a6cc82536898fa09bbbe53813 + languageName: node + linkType: hard + "invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -7863,14 +7846,15 @@ __metadata: resolution: "landgriffon-client@workspace:." dependencies: "@date-fns/utc": 1.1.1 - "@deck.gl/carto": ^8.9.35 + "@deck.gl/aggregation-layers": 8.8.6 + "@deck.gl/carto": 8.8.6 "@deck.gl/core": 8.8.6 "@deck.gl/extensions": 8.8.6 "@deck.gl/geo-layers": 8.8.6 "@deck.gl/layers": 8.8.6 "@deck.gl/mapbox": 8.8.6 "@deck.gl/mesh-layers": 8.8.6 - "@deck.gl/react": ^8.9.35 + "@deck.gl/react": 8.9.35 "@dnd-kit/core": 5.0.3 "@dnd-kit/modifiers": 5.0.0 "@dnd-kit/sortable": 6.0.1 @@ -9676,15 +9660,6 @@ __metadata: languageName: node linkType: hard -"quadbin@npm:^0.1.9": - version: 0.1.9 - resolution: "quadbin@npm:0.1.9" - dependencies: - "@mapbox/tile-cover": 3.0.1 - checksum: 318113ac94178659f8e589644b4a0c14817262eb0ab2418f7313052133a53009b52870220e88759efdea7f467448d7fdd063758deb8014be5652208e451a6733 - languageName: node - linkType: hard - "query-string@npm:8.1.0": version: 8.1.0 resolution: "query-string@npm:8.1.0" @@ -11561,13 +11536,6 @@ __metadata: languageName: node linkType: hard -"tilebelt@npm:^1.0.1": - version: 1.0.1 - resolution: "tilebelt@npm:1.0.1" - checksum: f201cf1718f53b8d7b2e89f53f90bbcf6eba5f58fe578da5ce35f93ea4e2302100f019fe2d388a0fb7ec5ceda0b6a0f08d10657b6b54eb5d74db218ad4bad177 - languageName: node - linkType: hard - "tiny-glob@npm:^0.2.9": version: 0.2.9 resolution: "tiny-glob@npm:0.2.9" From 17f6aec2e4f2d396f6d9bc44ea125dc80be7ab87 Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 14 Mar 2024 18:17:44 +0100 Subject: [PATCH 095/153] fixed types for layer --- client/src/components/ui/calendar.tsx | 4 ++-- .../analysis-eudr/map/component.tsx | 22 ++----------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx index e7fde3fdf..3fbd65201 100644 --- a/client/src/components/ui/calendar.tsx +++ b/client/src/components/ui/calendar.tsx @@ -45,8 +45,8 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C ...classNames, }} components={{ - IconLeft: ({ ...props }) => , - IconRight: ({ ...props }) => , + IconLeft: () => , + IconRight: () => , }} {...props} /> diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index d8dad16f2..0b6e282e0 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -62,19 +62,10 @@ const EUDRMap = () => { maxZoom: 20, tileSize: 256, visible: true, - // onTileLoad: (data) => { - // dispatch( - // updateLayer({ - // id: PlanetQ32023_ID, - // layerAttributes: { ...layerConfig }, - // }) - // ); - // //cartoLayerProps.onDataLoad(data); - // }, renderSubLayers: (props) => { const { bbox: { west, south, east, north }, - } = props.tile; + } = props.tile as { bbox: { west: number; south: number; east: number; north: number } }; return new BitmapLayer(props, { data: null, @@ -91,19 +82,10 @@ const EUDRMap = () => { maxZoom: 20, tileSize: 256, visible: true, - // onTileLoad: (data) => { - // dispatch( - // updateLayer({ - // id: PlanetQ32023_ID, - // layerAttributes: { ...layerConfig }, - // }) - // ); - // //cartoLayerProps.onDataLoad(data); - // }, renderSubLayers: (props) => { const { bbox: { west, south, east, north }, - } = props.tile; + } = props.tile as { bbox: { west: number; south: number; east: number; north: number } }; return new BitmapLayer(props, { data: null, From 6146feecba7534f32c5e9bb6e932b13fc6c25660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 18:24:36 +0100 Subject: [PATCH 096/153] redirects to eudr page after signin --- client/src/pages/auth/signin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/auth/signin.tsx b/client/src/pages/auth/signin.tsx index ddcce85aa..e76c015b4 100644 --- a/client/src/pages/auth/signin.tsx +++ b/client/src/pages/auth/signin.tsx @@ -44,7 +44,7 @@ const SignIn: NextPageWithLayout = () => { }); if (ok) { - router.push((router.query?.callbackUrl as string) || '/analysis/map', undefined, { + router.push((router.query?.callbackUrl as string) || '/eudr', undefined, { shallow: true, }); } else { From a7b4d8ee2f13f0ca6187c5c54b52bbae95a7a38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 18:28:41 +0100 Subject: [PATCH 097/153] removes old tailwind.config.js --- client/tailwind.config_old.js | 87 ----------------------------------- 1 file changed, 87 deletions(-) delete mode 100644 client/tailwind.config_old.js diff --git a/client/tailwind.config_old.js b/client/tailwind.config_old.js deleted file mode 100644 index d136a516c..000000000 --- a/client/tailwind.config_old.js +++ /dev/null @@ -1,87 +0,0 @@ -const forms = require('@tailwindcss/forms'); -const typography = require('@tailwindcss/typography'); -const colors = require('tailwindcss/colors'); - -/** @type import('tailwindcss').Config */ -module.exports = { - content: ['./src/**/*.{ts,tsx}'], - darkMode: 'media', - theme: { - extend: { - fontFamily: { - sans: ['Public Sans', 'sans-serif'], - }, - fontSize: { - '2xs': '0.625rem', // 10px - xs: ['0.75rem', '1rem'], // 12px - sm: ['0.875rem', '1.25rem'], // 14px - base: ['1rem', '1.5rem'], // 16px - lg: ['1.125rem', '1.75rem'], // 18px - '2xl': ['1.5rem', '2rem'], // 20px - '3xl': ['1.875rem', '2.25rem'], // 24px - '4xl': ['2.25rem', '2.5rem'], // 28px - }, - height: { - 'screen-minus-header': "calc(100vh - theme('spacing.16'))", - }, - spacing: { - 125: '30.875rem', - 250: '48.75rem', - }, - backgroundImage: { - auth: 'linear-gradient(240.36deg, #2E34B0 0%, #0C1063 68.13%)', - }, - boxShadow: { - menu: '0 4px 4px 0px rgba(0, 0, 0, 0.25)', - 'button-hovered': - '0px 0px 0px 6px rgba(0, 0, 0, 0.26), 0px 4px 4px 0px rgba(0, 0, 0, 0.25)', - 'button-focused': '0px 0px 0px 4px rgba(63, 89, 224, 0.20), 0px 0px 0px 1px #FFF', - }, - }, - colors: { - black: colors.black, - white: colors.white, - transparent: colors.transparent, - inherit: colors.inherit, - gray: { - 900: '#15181F', - 600: '#40424B', - 500: '#60626A', - 400: '#8F9195', - 300: '#AEB1B5', - 200: '#D1D5DB', - 100: '#F3F4F6', - 50: '#F9FAFB', - }, - navy: { - 50: '#F0F2FD', // light navy - 200: '#C7CDEA', // light mid navy - 400: '#3F59E0', // navy - 600: '#2E34B0', // mid navy - 900: '#152269', // dark navy - }, - orange: { - 500: '#FFA000', // orange - 300: '#F0B957', // light mid orange - 100: '#F9DFB1', // light orange - 50: '#FFF1D9', // lightest orange - }, - blue: { - 400: '#4AB7F3', // blue - 200: '#C7F0FF', // soft blue - }, - green: { - 800: '#006D2C', - 400: '#078A3C', - 200: '#CDE8D8', - 50: '#E6F9EE', - }, - red: { - 800: '#BA1809', // dark red - 400: '#E93323', // red - 50: '#FEF2F2', // light red - }, - }, - }, - plugins: [forms, typography], -}; From edab98fc0f5a290943ee4d04195d3d4b3278b9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 18:39:28 +0100 Subject: [PATCH 098/153] updates redirect of signin test --- client/cypress/e2e/sign-in.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cypress/e2e/sign-in.cy.ts b/client/cypress/e2e/sign-in.cy.ts index dbf955da7..26b37b638 100644 --- a/client/cypress/e2e/sign-in.cy.ts +++ b/client/cypress/e2e/sign-in.cy.ts @@ -10,6 +10,6 @@ describe('Sign in', () => { cy.get('[name="password"]').type(Cypress.env('PASSWORD')); cy.get('button[type="submit"]').click(); cy.wait('@signInRequest'); - cy.url().should('contain', 'analysis'); + cy.url().should('contain', 'eudr'); }); }); From b6cffc93d4702a83937f3625bf3b9b83673a0057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 14 Mar 2024 18:44:52 +0100 Subject: [PATCH 099/153] navigation menu: updates EUDR icon, disables other links --- client/src/components/icons/report.tsx | 38 ++++++++++++++++++++ client/src/layouts/application/component.tsx | 18 +++++----- 2 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 client/src/components/icons/report.tsx diff --git a/client/src/components/icons/report.tsx b/client/src/components/icons/report.tsx new file mode 100644 index 000000000..dd1dc1686 --- /dev/null +++ b/client/src/components/icons/report.tsx @@ -0,0 +1,38 @@ +import type { SVGAttributes } from 'react'; + +const ReportSVG = (props?: SVGAttributes) => { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ReportSVG; diff --git a/client/src/layouts/application/component.tsx b/client/src/layouts/application/component.tsx index eceabffe5..788cc93e8 100644 --- a/client/src/layouts/application/component.tsx +++ b/client/src/layouts/application/component.tsx @@ -9,6 +9,7 @@ import Navigation from 'containers/navigation/desktop'; import UserDropdown from 'containers/user-dropdown'; import LandgriffonLogo from 'containers/logo'; import ToastContainer from 'containers/toaster'; +import ReportSVG from '@/components/icons/report'; import type { NavigationList } from 'containers/navigation/types'; @@ -22,23 +23,22 @@ const ApplicationLayout: React.FC = ({ children }) => { default: CollectionIconOutline, active: CollectionIconSolid, }, + disabled: true, }, { name: 'Analysis', href: '/analysis', icon: { default: ChartBarIconOutline, active: ChartBarIconSolid }, - disabled: !!(!lastTask || lastTask?.status === 'processing'), + disabled: true, }, ]; - if (process.env.NEXT_PUBLIC_MODULE_EUDR === 'true') { - navigationItems.push({ - name: 'EUDR', - href: '/eudr', - icon: { default: ChartBarIconOutline, active: ChartBarIconSolid }, - disabled: !!(!lastTask || lastTask?.status === 'processing'), - }); - } + navigationItems.push({ + name: 'EUDR', + href: '/eudr', + icon: { default: ReportSVG, active: ReportSVG }, + disabled: !!(!lastTask || lastTask?.status === 'processing'), + }); return (
From 911dfdaf5fc22028c65dfb21ec354fb536af8260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Fri, 15 Mar 2024 09:33:20 +0100 Subject: [PATCH 100/153] fixes svg properties --- client/src/components/icons/report.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/icons/report.tsx b/client/src/components/icons/report.tsx index dd1dc1686..a1d6021a7 100644 --- a/client/src/components/icons/report.tsx +++ b/client/src/components/icons/report.tsx @@ -4,7 +4,7 @@ const ReportSVG = (props?: SVGAttributes) => { return ( - + @@ -12,16 +12,16 @@ const ReportSVG = (props?: SVGAttributes) => { - + From 8f7a898666fec1877445225ba64dac67e0654220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Fri, 15 Mar 2024 12:00:46 +0100 Subject: [PATCH 101/153] adds info buttons to charts --- client/src/components/legend/item/info-modal.tsx | 2 +- .../deforestation-alerts/index.tsx | 12 +++++++++++- .../sourcing-info/chart/index.tsx | 13 ++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/src/components/legend/item/info-modal.tsx b/client/src/components/legend/item/info-modal.tsx index 24e11e5ec..8a193da17 100644 --- a/client/src/components/legend/item/info-modal.tsx +++ b/client/src/components/legend/item/info-modal.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Modal from 'components/modal/component'; export type InfoModalProps = { - info: { title: string; description: string; source: string | string[] }; + info: { title: string; description: string; source?: string | string[] }; }; const NO_DATA = 'No data available'; diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx index c1e2257cc..c314aa1ad 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx @@ -8,6 +8,7 @@ import DeforestationAlertsChart from './chart'; import { useEUDRSupplier } from '@/hooks/eudr'; import { eudrDetail } from '@/store/features/eudr-detail'; import { useAppSelector } from '@/store/hooks'; +import InfoModal from '@/components/legend/item/info-modal'; const dateFormatter = (date: string) => format(new UTCDate(date), "do 'of' MMMM yyyy"); @@ -29,7 +30,16 @@ const DeforestationAlerts = (): JSX.Element => { return (
-

Deforestation alerts detected within the smallholders

+
+

Deforestation alerts detected within the smallholders

+ +
{data?.totalAlerts && (
There were {data?.totalAlerts} deforestation alerts diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx index 7ab89f74a..7c9ec6892 100644 --- a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx @@ -20,6 +20,8 @@ import { useAppSelector } from '@/store/hooks'; import { eudrDetail } from '@/store/features/eudr-detail'; import { EUDR_COLOR_RAMP } from '@/utils/colors'; import { Badge } from '@/components/ui/badge'; +import InfoModal from '@/components/legend/item/info-modal'; +import InfoTooltip from '@/components/info-tooltip'; const SupplierSourcingInfoChart = (): JSX.Element => { const [showBy, setShowBy] = useState<'byVolume' | 'byArea'>('byVolume'); @@ -82,7 +84,16 @@ const SupplierSourcingInfoChart = (): JSX.Element => { return (
-

Individual plot contributions to volume accumulation

+
+

Individual plot contributions to volume accumulation

+ +
Show by
From 88a07f6a014a6871f92755532ad25ce2fc87a2e6 Mon Sep 17 00:00:00 2001 From: David Inga Date: Fri, 15 Mar 2024 09:09:41 +0100 Subject: [PATCH 102/153] fixed bug navigating to detail; removed fitbounsd; added radd layer --- client/package.json | 1 - .../analysis-eudr-detail/map/component.tsx | 110 ------------ .../analysis-eudr-detail/map/index.ts | 1 - .../map/legend/component.tsx | 74 -------- .../analysis-eudr-detail/map/legend/index.ts | 1 - .../analysis-eudr-detail/map/legend/item.tsx | 33 ---- .../map/zoom/component.tsx | 51 ------ .../analysis-eudr-detail/map/zoom/index.ts | 1 - .../analysis-eudr/map/component.tsx | 163 +++++++++--------- client/src/pages/eudr/index.tsx | 2 +- .../src/pages/eudr/suppliers/[supplierId].tsx | 4 +- client/yarn.lock | 27 --- 12 files changed, 80 insertions(+), 388 deletions(-) delete mode 100644 client/src/containers/analysis-eudr-detail/map/component.tsx delete mode 100644 client/src/containers/analysis-eudr-detail/map/index.ts delete mode 100644 client/src/containers/analysis-eudr-detail/map/legend/component.tsx delete mode 100644 client/src/containers/analysis-eudr-detail/map/legend/index.ts delete mode 100644 client/src/containers/analysis-eudr-detail/map/legend/item.tsx delete mode 100644 client/src/containers/analysis-eudr-detail/map/zoom/component.tsx delete mode 100644 client/src/containers/analysis-eudr-detail/map/zoom/index.ts diff --git a/client/package.json b/client/package.json index b0dee8277..b1a8ca79d 100644 --- a/client/package.json +++ b/client/package.json @@ -54,7 +54,6 @@ "@tanstack/react-query": "^4.2.1", "@tanstack/react-table": "8.13.2", "@tanstack/react-virtual": "3.0.1", - "@turf/bbox": "^6.5.0", "autoprefixer": "10.2.5", "axios": "1.3.4", "chroma-js": "2.1.2", diff --git a/client/src/containers/analysis-eudr-detail/map/component.tsx b/client/src/containers/analysis-eudr-detail/map/component.tsx deleted file mode 100644 index 8c557d6ee..000000000 --- a/client/src/containers/analysis-eudr-detail/map/component.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import DeckGL from '@deck.gl/react/typed'; -import { GeoJsonLayer } from '@deck.gl/layers/typed'; -import Map from 'react-map-gl/maplibre'; -import { WebMercatorViewport, type MapViewState } from '@deck.gl/core/typed'; -import bbox from '@turf/bbox'; - -import ZoomControl from './zoom'; -import LegendControl from './legend'; - -import BasemapControl from '@/components/map/controls/basemap'; -import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; -import { usePlotGeometries } from '@/hooks/eudr'; - -import type { BasemapValue } from '@/components/map/controls/basemap/types'; -import type { MapStyle } from '@/components/map/types'; - -const EUDRMap = () => { - const [mapStyle, setMapStyle] = useState('terrain'); - const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); - - const plotGeometries = usePlotGeometries(); - - const layer: GeoJsonLayer = new GeoJsonLayer({ - id: 'geojson-layer', - data: plotGeometries.data, - // Styles - filled: true, - getFillColor: [255, 176, 0, 84], - stroked: true, - getLineColor: [255, 176, 0, 255], - getLineWidth: 1, - lineWidthUnits: 'pixels', - // Interactive props - pickable: true, - autoHighlight: true, - highlightColor: [255, 176, 0, 255], - }); - - const layers = [layer]; - - const handleMapStyleChange = useCallback((newStyle: BasemapValue) => { - setMapStyle(newStyle); - }, []); - - const handleZoomIn = useCallback(() => { - const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; - setViewState({ ...viewState, zoom }); - }, [viewState]); - - const handleZoomOut = useCallback(() => { - const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom - 1; - setViewState({ ...viewState, zoom }); - }, [viewState]); - - const fitToPlotBounds = useCallback(() => { - if (!plotGeometries.data) return; - const [minLng, minLat, maxLng, maxLat] = bbox(plotGeometries.data); - const newViewport = new WebMercatorViewport(viewState); - const { longitude, latitude, zoom } = newViewport.fitBounds( - [ - [minLng, minLat], - [maxLng, maxLat], - ], - { - padding: 10, - }, - ); - if ( - viewState.latitude !== latitude || - viewState.longitude !== longitude || - viewState.zoom !== zoom - ) { - setViewState({ ...viewState, longitude, latitude, zoom }); - } - }, [plotGeometries.data, viewState]); - - // Fit to bounds when data is loaded or changed - useEffect(() => { - if (plotGeometries.data) { - fitToPlotBounds(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [plotGeometries.data]); - - const handleResize = useCallback(() => { - setTimeout(() => fitToPlotBounds(), 0); - }, [fitToPlotBounds]); - - return ( - <> - setViewState(viewState as MapViewState)} - controller={{ dragRotate: false }} - layers={layers} - onResize={handleResize} - > - - -
- - - -
- - ); -}; - -export default EUDRMap; diff --git a/client/src/containers/analysis-eudr-detail/map/index.ts b/client/src/containers/analysis-eudr-detail/map/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/analysis-eudr-detail/map/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/component.tsx b/client/src/containers/analysis-eudr-detail/map/legend/component.tsx deleted file mode 100644 index 54da03733..000000000 --- a/client/src/containers/analysis-eudr-detail/map/legend/component.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useState } from 'react'; -import classNames from 'classnames'; -import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; - -import LegendItem from './item'; - -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import SandwichIcon from '@/components/icons/sandwich'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; - -const EURDLegend = () => { - const [isOpen, setIsOpen] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); - - return ( -
- - - - - -
-

Legend

-
- -
- - -
- -
-
- - - - -
-
-
-
-
- ); -}; - -export default EURDLegend; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/index.ts b/client/src/containers/analysis-eudr-detail/map/legend/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/analysis-eudr-detail/map/legend/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/analysis-eudr-detail/map/legend/item.tsx b/client/src/containers/analysis-eudr-detail/map/legend/item.tsx deleted file mode 100644 index 3946119f2..000000000 --- a/client/src/containers/analysis-eudr-detail/map/legend/item.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import classNames from 'classnames'; - -import type { FC, PropsWithChildren } from 'react'; - -type LegendItemProps = { title: string; description: string; iconClassName?: string }; - -const LegendItem: FC> = ({ - title, - description, - children, - iconClassName, -}) => { - return ( -
-
-
-
-

{title}

-
-
-

{description}

- {children} -
-
- ); -}; - -export default LegendItem; diff --git a/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx b/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx deleted file mode 100644 index 8b2eeeba1..000000000 --- a/client/src/containers/analysis-eudr-detail/map/zoom/component.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { MinusIcon, PlusIcon } from '@heroicons/react/solid'; -import cx from 'classnames'; - -import type { MapViewState } from '@deck.gl/core/typed'; -import type { FC } from 'react'; - -const COMMON_CLASSES = - 'p-2 transition-colors bg-white cursor-pointer hover:bg-gray-100 active:bg-navy-50 disabled:bg-gray-100 disabled:opacity-75 disabled:cursor-default'; - -const ZoomControl: FC<{ - viewState: MapViewState; - className?: string; - onZoomIn: () => void; - onZoomOut: () => void; -}> = ({ viewState, className = null, onZoomIn, onZoomOut }) => { - const { zoom, minZoom, maxZoom } = viewState; - - return ( -
- - -
- ); -}; - -export default ZoomControl; diff --git a/client/src/containers/analysis-eudr-detail/map/zoom/index.ts b/client/src/containers/analysis-eudr-detail/map/zoom/index.ts deleted file mode 100644 index b404d7fd4..000000000 --- a/client/src/containers/analysis-eudr-detail/map/zoom/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 0b6e282e0..40361bfe6 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -1,26 +1,31 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import DeckGL from '@deck.gl/react/typed'; import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; import Map from 'react-map-gl/maplibre'; -import { WebMercatorViewport, MapView } from '@deck.gl/core/typed'; +import { MapView } from '@deck.gl/core/typed'; import { TileLayer } from '@deck.gl/geo-layers/typed'; import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; -import bbox from '@turf/bbox'; import ZoomControl from './zoom'; import LegendControl from './legend'; import BasemapControl from './basemap'; import { useAppSelector } from '@/store/hooks'; -// import { setBasemap, setPlanetCompare } from '@/store/features/eudr'; import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; import { usePlotGeometries } from '@/hooks/eudr'; import { formatNumber } from '@/utils/number-format'; import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; +const DEFAULT_VIEW_STATE: MapViewState = { + ...INITIAL_VIEW_STATE, + latitude: -8.461844239054608, + longitude: -74.96226240479487, + zoom: 9, + maxZoom: 20, +}; + setDefaultCredentials({ - // apiBaseUrl: 'https://eudr.carto.com', apiBaseUrl: 'https://gcp-us-east1.api.carto.com', accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', @@ -32,7 +37,7 @@ const EUDRMap = () => { ); const [hoverInfo, setHoverInfo] = useState(null); - const [viewState, setViewState] = useState(INITIAL_VIEW_STATE); + const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); const plotGeometries = usePlotGeometries(); @@ -125,6 +130,24 @@ const EUDRMap = () => { visible: contextualLayers['deforestation-alerts-2020-2022-hansen'].active, }); + const raddLayer = new CartoLayer({ + id: 'real-time-deforestation-alerts-since-2020-radd', + type: MAP_TYPES.QUERY, + connection: 'eudr', + data: 'SELECT * FROM `cartobq.eudr.RADD_date_confidence_3`', + pointRadiusMinPixels: 2, + getLineColor: [201, 42, 109], + getFillColor: [201, 42, 109], + lineWidthMinPixels: 1, + visible: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiI3NTFkNzA1YSJ9.jrVugV7HYfhmjxj-p2Iks8nL_AjHR91Q37JVP2fNmtc', + }, + }); + const handleZoomIn = useCallback(() => { const zoom = viewState.maxZoom === viewState.zoom ? viewState.zoom : viewState.zoom + 1; setViewState({ ...viewState, zoom }); @@ -135,88 +158,56 @@ const EUDRMap = () => { setViewState({ ...viewState, zoom }); }, [viewState]); - const fitToPlotBounds = useCallback(() => { - if (!plotGeometries.data) return; - const [minLng, minLat, maxLng, maxLat] = bbox(plotGeometries.data); - const newViewport = new WebMercatorViewport(viewState); - const { longitude, latitude, zoom } = newViewport.fitBounds( - [ - [minLng, minLat], - [maxLng, maxLat], - ], - { - padding: 10, - }, - ); - if ( - viewState.latitude !== latitude || - viewState.longitude !== longitude || - viewState.zoom !== zoom - ) { - setViewState({ ...viewState, longitude, latitude, zoom }); - } - }, [plotGeometries.data, viewState]); - - // Fit to bounds when data is loaded or changed - useEffect(() => { - if (plotGeometries.data) { - fitToPlotBounds(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [plotGeometries.data]); - - const handleResize = useCallback(() => { - setTimeout(() => fitToPlotBounds(), 0); - }, [fitToPlotBounds]); - return ( <> - setViewState(viewState as MapViewState)} - controller={{ dragRotate: false }} - layers={[ - basemap === 'planet' && !planetCompare ? [planetLayer] : null, - basemap === 'planet' && planetCompare ? [planetLayer, planetCompareLayer] : null, - forestCoverLayer, - deforestationLayer, - layer, - ]} - layerFilter={({ layer, viewport }) => { - return !planetCompare || viewport.id.startsWith(layer.id.split('-')[0]); - }} - onResize={handleResize} - {...(planetCompare - ? { - views: [ - new MapView({ - id: 'top', - y: 0, - height: '50%', - padding: { top: '100%' }, - }), - new MapView({ - id: 'bottom', - y: '50%', - height: '50%', - padding: { bottom: '100%' }, - }), - new MapView({ - id: 'full', - y: 0, - x: 0, - width: '100%', - height: '100%', - }), - ], - } - : {})} - > - - - {planetCompare && ( -
- )} +
+ setViewState(viewState as MapViewState)} + controller={{ dragRotate: false }} + layers={[ + basemap === 'planet' && !planetCompare ? [planetLayer] : null, + basemap === 'planet' && planetCompare ? [planetLayer, planetCompareLayer] : null, + forestCoverLayer, + deforestationLayer, + raddLayer, + layer, + ]} + layerFilter={({ layer, viewport }) => { + return !planetCompare || viewport.id.startsWith(layer.id.split('-')[0]); + }} + {...(planetCompare + ? { + views: [ + new MapView({ + id: 'top', + y: 0, + height: '50%', + padding: { top: '100%' }, + }), + new MapView({ + id: 'bottom', + y: '50%', + height: '50%', + padding: { bottom: '100%' }, + }), + new MapView({ + id: 'full', + y: 0, + x: 0, + width: '100%', + height: '100%', + }), + ], + } + : {})} + > + + + {planetCompare && ( +
+ )} +
{hoverInfo?.object && (
{ -
+
diff --git a/client/src/pages/eudr/suppliers/[supplierId].tsx b/client/src/pages/eudr/suppliers/[supplierId].tsx index 4888f4e26..48ac37ba1 100644 --- a/client/src/pages/eudr/suppliers/[supplierId].tsx +++ b/client/src/pages/eudr/suppliers/[supplierId].tsx @@ -9,7 +9,7 @@ import { tasksSSR } from 'services/ssr'; import ApplicationLayout from 'layouts/application'; import CollapseButton from 'containers/collapse-button/component'; import TitleTemplate from 'utils/titleTemplate'; -import Map from 'containers/analysis-eudr/map'; +import Map from '@/containers/analysis-eudr/map'; import EUDRFilters from '@/containers/analysis-eudr-detail/filters'; import { Button } from '@/components/ui/button'; import { useEUDRSupplier } from '@/hooks/eudr'; @@ -78,7 +78,7 @@ const MapPage: NextPageWithLayout = () => { -
+
diff --git a/client/yarn.lock b/client/yarn.lock index 84b0b258b..2e99fd3b2 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2621,32 +2621,6 @@ __metadata: languageName: node linkType: hard -"@turf/bbox@npm:^6.5.0": - version: 6.5.0 - resolution: "@turf/bbox@npm:6.5.0" - dependencies: - "@turf/helpers": ^6.5.0 - "@turf/meta": ^6.5.0 - checksum: 537be56ae0c5ad44e71a691717b35745e947e19a6bd9f20fdac2ab4318caf98cd88472d7dbf576e8b32ead5da034d273ffb3f4559d6d386820ddcb88a1f7fedd - languageName: node - linkType: hard - -"@turf/helpers@npm:^6.5.0": - version: 6.5.0 - resolution: "@turf/helpers@npm:6.5.0" - checksum: d57f746351357838c654e0a9b98be3285a14b447504fd6d59753d90c6d437410bb24805d61c65b612827f07f6c2ade823bb7e56e41a1a946217abccfbd64c117 - languageName: node - linkType: hard - -"@turf/meta@npm:^6.5.0": - version: 6.5.0 - resolution: "@turf/meta@npm:6.5.0" - dependencies: - "@turf/helpers": ^6.5.0 - checksum: c6bb936aa92bf3365e87a50dc65f248e070c5767a36fac390754c00c89bf2d1583418686ab19a10332bfa9340b8cac6aaf2c55dad7f5fcf77f1a2dda75ccf363 - languageName: node - linkType: hard - "@types/chroma-js@npm:2.1.3": version: 2.1.3 resolution: "@types/chroma-js@npm:2.1.3" @@ -7884,7 +7858,6 @@ __metadata: "@tanstack/react-query": ^4.2.1 "@tanstack/react-table": 8.13.2 "@tanstack/react-virtual": 3.0.1 - "@turf/bbox": ^6.5.0 "@types/chroma-js": 2.1.3 "@types/d3-format": 3.0.1 "@types/d3-scale": 4.0.2 From cb15afae6220d4860145c6e181c6b8f9fb1a12f4 Mon Sep 17 00:00:00 2001 From: David Inga Date: Fri, 15 Mar 2024 19:18:17 +0100 Subject: [PATCH 103/153] slider range for legend --- client/package.json | 1 + client/src/components/ui/slider.tsx | 24 ++ .../analysis-eudr/map/component.tsx | 36 ++- .../containers/analysis-eudr/map/layers.json | 299 +++++++++++++++++- .../analysis-eudr/map/legend/component.tsx | 38 ++- .../analysis-eudr/map/legend/item.tsx | 29 +- .../analysis-eudr/map/legend/radd-slider.tsx | 64 ++++ client/src/store/features/eudr/index.ts | 14 +- client/yarn.lock | 31 ++ 9 files changed, 508 insertions(+), 28 deletions(-) create mode 100644 client/src/components/ui/slider.tsx create mode 100644 client/src/containers/analysis-eudr/map/legend/radd-slider.tsx diff --git a/client/package.json b/client/package.json index b1a8ca79d..2d8cdffcf 100644 --- a/client/package.json +++ b/client/package.json @@ -45,6 +45,7 @@ "@radix-ui/react-radio-group": "1.1.3", "@radix-ui/react-select": "2.0.0", "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/client/src/components/ui/slider.tsx b/client/src/components/ui/slider.tsx new file mode 100644 index 000000000..c3beec33e --- /dev/null +++ b/client/src/components/ui/slider.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import { cn } from '@/lib/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 40361bfe6..2cf73f10e 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -47,15 +47,15 @@ const EUDRMap = () => { data: plotGeometries.data, // Styles filled: true, - getFillColor: [255, 176, 0, 84], + getFillColor: [63, 89, 224, 84], stroked: true, - getLineColor: [255, 176, 0, 255], + getLineColor: [63, 89, 224, 255], getLineWidth: 1, lineWidthUnits: 'pixels', // Interactive props pickable: true, autoHighlight: true, - highlightColor: [255, 176, 0, 255], + highlightColor: [63, 89, 224, 255], visible: supplierLayer.active, onHover: setHoverInfo, }); @@ -105,8 +105,7 @@ const EUDRMap = () => { type: MAP_TYPES.TILESET, connection: 'eudr', data: 'cartobq.eudr.JRC_2020_Forest_d_TILE', - pointRadiusMinPixels: 2, - getLineColor: [114, 169, 80], + stroked: false, getFillColor: [114, 169, 80], lineWidthMinPixels: 1, visible: contextualLayers['forest-cover-2020-ec-jrc'].active, @@ -122,22 +121,35 @@ const EUDRMap = () => { id: 'full-deforestation-alerts-2020-2022-hansen', type: MAP_TYPES.QUERY, connection: 'eudr', - data: 'SELECT * FROM `cartobq.eudr.TCL_hansen_year`', - pointRadiusMinPixels: 2, - getLineColor: [224, 191, 36], + data: 'SELECT * FROM `cartobq.eudr.TCL_hansen_year` WHERE year<=?', + queryParameters: [contextualLayers['deforestation-alerts-2020-2022-hansen'].year], + stroked: false, getFillColor: [224, 191, 36], lineWidthMinPixels: 1, visible: contextualLayers['deforestation-alerts-2020-2022-hansen'].active, + credentials: { + apiVersion: API_VERSIONS.V3, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', + }, }); const raddLayer = new CartoLayer({ id: 'real-time-deforestation-alerts-since-2020-radd', type: MAP_TYPES.QUERY, connection: 'eudr', - data: 'SELECT * FROM `cartobq.eudr.RADD_date_confidence_3`', - pointRadiusMinPixels: 2, - getLineColor: [201, 42, 109], - getFillColor: [201, 42, 109], + data: 'SELECT * FROM `cartobq.eudr.RADD_date_confidence_3` WHERE date BETWEEN ? AND ?', + queryParameters: [ + contextualLayers['real-time-deforestation-alerts-since-2020-radd'].dateFrom, + contextualLayers['real-time-deforestation-alerts-since-2020-radd'].dateTo, + ], + stroked: false, + getFillColor: (d) => { + const { confidence } = d.properties; + if (confidence === 'Low') return [237, 164, 195]; + return [201, 42, 109]; + }, lineWidthMinPixels: 1, visible: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].active, credentials: { diff --git a/client/src/containers/analysis-eudr/map/layers.json b/client/src/containers/analysis-eudr/map/layers.json index 8e2e51a62..b18ddc80c 100644 --- a/client/src/containers/analysis-eudr/map/layers.json +++ b/client/src/containers/analysis-eudr/map/layers.json @@ -8,7 +8,8 @@ "source": null, "type": "layer", "legend": { - "iconClassName": "border-2 border-orange-500 bg-orange-500/30" + "iconClassName": "border-2 border-orange-500 bg-orange-500/30", + "items": null } }, { @@ -24,7 +25,8 @@ ], "type": "contextual", "legend": { - "iconColor": "#72A950" + "iconColor": "#72A950", + "items": null } }, { @@ -44,7 +46,8 @@ ], "type": "contextual", "legend": { - "iconColor": "#E0BF24" + "iconColor": "#E0BF24", + "items": null } }, { @@ -64,7 +67,295 @@ ], "type": "contextual", "legend": { - "iconColor": "#C92A6D" + "iconColor": "#C92A6D", + "items": [ + { + "color": "#EDA4C3", + "label": "Detected by a single alert system" + }, + { + "color": "#C92A6D", + "label": "High confidence: detected more than once by a single alert system" + } + ], + "dates": [ + "2020-01-01", + "2020-01-04", + "2020-01-09", + "2020-01-13", + "2020-01-16", + "2020-01-21", + "2020-01-25", + "2020-01-28", + "2020-02-02", + "2020-02-06", + "2020-02-09", + "2020-02-14", + "2020-02-18", + "2020-02-21", + "2020-02-26", + "2020-03-01", + "2020-03-04", + "2020-03-09", + "2020-03-13", + "2020-03-16", + "2020-03-21", + "2020-03-25", + "2020-03-28", + "2020-04-02", + "2020-04-06", + "2020-04-09", + "2020-04-14", + "2020-04-18", + "2020-04-21", + "2020-04-26", + "2020-04-30", + "2020-05-08", + "2020-05-12", + "2020-05-15", + "2020-05-20", + "2020-05-24", + "2020-05-27", + "2020-06-01", + "2020-06-05", + "2020-06-08", + "2020-06-13", + "2020-06-17", + "2020-06-20", + "2020-06-25", + "2020-06-29", + "2020-07-02", + "2020-07-07", + "2020-07-11", + "2020-07-14", + "2020-07-19", + "2020-07-23", + "2020-07-26", + "2020-07-31", + "2020-08-04", + "2020-08-12", + "2020-08-16", + "2020-08-19", + "2020-08-24", + "2020-08-28", + "2020-08-31", + "2020-09-05", + "2020-09-09", + "2020-09-12", + "2020-09-17", + "2020-09-21", + "2020-09-24", + "2020-09-29", + "2020-10-03", + "2020-10-06", + "2020-10-11", + "2020-10-15", + "2020-10-18", + "2020-10-23", + "2020-10-27", + "2020-10-30", + "2020-11-04", + "2020-11-08", + "2020-11-11", + "2020-11-16", + "2020-11-20", + "2020-11-23", + "2020-11-28", + "2020-12-02", + "2020-12-05", + "2020-12-10", + "2020-12-22", + "2020-12-26", + "2020-12-29", + "2021-01-07", + "2021-01-10", + "2021-01-15", + "2021-01-19", + "2021-01-22", + "2021-01-27", + "2021-01-31", + "2021-02-03", + "2021-02-08", + "2021-02-12", + "2021-02-20", + "2021-02-27", + "2021-03-04", + "2021-03-08", + "2021-03-11", + "2021-03-16", + "2021-03-20", + "2021-03-28", + "2021-04-01", + "2021-04-04", + "2021-04-09", + "2021-04-13", + "2021-04-21", + "2021-04-25", + "2021-04-28", + "2021-05-03", + "2021-05-07", + "2021-05-10", + "2021-05-19", + "2021-05-22", + "2021-05-27", + "2021-05-31", + "2021-06-03", + "2021-06-08", + "2021-06-12", + "2021-06-15", + "2021-06-20", + "2021-06-24", + "2021-06-27", + "2021-07-02", + "2021-07-06", + "2021-07-09", + "2021-07-14", + "2021-07-18", + "2021-07-21", + "2021-07-26", + "2021-07-30", + "2021-08-02", + "2021-08-07", + "2021-08-11", + "2021-08-14", + "2021-08-23", + "2021-08-26", + "2021-08-31", + "2021-09-04", + "2021-09-07", + "2021-09-19", + "2021-09-24", + "2021-09-28", + "2021-10-06", + "2021-10-10", + "2021-10-13", + "2021-10-18", + "2021-10-22", + "2021-10-25", + "2021-10-30", + "2021-11-15", + "2021-11-18", + "2021-11-23", + "2021-11-27", + "2021-11-30", + "2021-12-05", + "2021-12-09", + "2021-12-12", + "2021-12-17", + "2021-12-21", + "2022-03-05", + "2022-03-17", + "2022-03-29", + "2022-04-02", + "2022-04-10", + "2022-04-14", + "2022-04-22", + "2022-04-26", + "2022-05-04", + "2022-05-20", + "2022-05-28", + "2022-06-09", + "2022-06-13", + "2022-06-21", + "2022-06-25", + "2022-07-03", + "2022-07-07", + "2022-07-15", + "2022-07-19", + "2022-07-27", + "2022-07-31", + "2022-08-08", + "2022-08-12", + "2022-08-20", + "2022-08-24", + "2022-09-01", + "2022-09-05", + "2022-09-13", + "2022-09-17", + "2022-09-25", + "2022-09-29", + "2022-10-07", + "2022-10-11", + "2022-10-19", + "2022-10-23", + "2022-10-31", + "2022-11-04", + "2022-11-12", + "2022-11-16", + "2022-11-24", + "2022-11-28", + "2022-12-06", + "2022-12-10", + "2022-12-18", + "2022-12-22", + "2022-12-30", + "2023-01-03", + "2023-01-11", + "2023-01-15", + "2023-01-23", + "2023-01-27", + "2023-02-04", + "2023-02-08", + "2023-02-16", + "2023-02-20", + "2023-02-28", + "2023-03-04", + "2023-03-12", + "2023-03-16", + "2023-03-24", + "2023-03-28", + "2023-04-05", + "2023-04-09", + "2023-04-17", + "2023-04-21", + "2023-04-29", + "2023-05-03", + "2023-05-11", + "2023-05-15", + "2023-05-27", + "2023-06-08", + "2023-06-20", + "2023-07-02", + "2023-07-14", + "2023-07-22", + "2023-07-26", + "2023-08-03", + "2023-08-07", + "2023-08-15", + "2023-08-19", + "2023-08-27", + "2023-08-31", + "2023-09-08", + "2023-09-12", + "2023-09-20", + "2023-09-24", + "2023-10-02", + "2023-10-06", + "2023-10-14", + "2023-10-18", + "2023-10-26", + "2023-10-30", + "2023-11-07", + "2023-11-11", + "2023-11-19", + "2023-11-23", + "2023-12-01", + "2023-12-05", + "2023-12-13", + "2023-12-17", + "2023-12-25", + "2023-12-29", + "2024-01-06", + "2024-01-10", + "2024-01-18", + "2024-01-22", + "2024-01-30", + "2024-02-03", + "2024-02-11", + "2024-02-15", + "2024-02-23", + "2024-02-27" + ] } }, { diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index d7485d545..320a22ec5 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -5,9 +5,11 @@ import { MinusIcon, PlusIcon } from '@heroicons/react/outline'; import LayersData from '../layers.json'; import LegendItem from './item'; +import RADDSlider from './radd-slider'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { setContextualLayer, setSupplierLayer } from '@/store/features/eudr'; +import { Slider } from '@/components/ui/slider'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import SandwichIcon from '@/components/icons/sandwich'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -74,7 +76,7 @@ const EURDLegend = () => { description={layer.description} source={layer.source} content={layer.content} - iconColor={layer.legend?.iconColor} + legendConfig={layer.legend} showSwitcher isActive={contextualLayers[layer.id].active} changeVisibility={(isVisible) => @@ -85,7 +87,39 @@ const EURDLegend = () => { }), ) } - /> + > + <> + {layer.id === 'deforestation-alerts-2020-2022-hansen' && + contextualLayers[layer.id].active && ( +
+ + dispatch( + setContextualLayer({ + layer: layer.id, + configuration: { year: yearRange[0] }, + }), + ) + } + /> +
+ {[2020, 2021, 2022].map((year) => ( +
+ {year} +
+ ))} +
+
+ )} + {layer.id === 'real-time-deforestation-alerts-since-2020-radd' && + contextualLayers[layer.id].active && } + + ))} diff --git a/client/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx index d5394f35c..9c12a5282 100644 --- a/client/src/containers/analysis-eudr/map/legend/item.tsx +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -12,7 +12,11 @@ type LegendItemProps = { content: string; description: string; source?: string | string[]; - iconColor?: string; + legendConfig?: { + iconColor?: string; + items?: { label: string; color: string }[]; + dates?: string[]; + }; showVisibility?: boolean; showSwitcher?: boolean; isActive?: boolean; @@ -25,7 +29,7 @@ const LegendItem: FC> = ({ description, source, children, - iconColor = null, + legendConfig = null, showVisibility = false, showSwitcher = false, isActive = true, @@ -34,11 +38,12 @@ const LegendItem: FC> = ({ return (
@@ -67,6 +72,16 @@ const LegendItem: FC> = ({

{description}

+ {legendConfig?.items?.length > 0 && ( +
    + {legendConfig?.items?.map((item) => ( +
  • +
    +
    {item.label}
    +
  • + ))} +
+ )} {children}
diff --git a/client/src/containers/analysis-eudr/map/legend/radd-slider.tsx b/client/src/containers/analysis-eudr/map/legend/radd-slider.tsx new file mode 100644 index 000000000..6aeca7ef1 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/radd-slider.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react'; +import { useDebounce } from 'rooks'; +import { format } from 'date-fns'; + +import layersData from '../layers.json'; + +import { useAppDispatch } from '@/store/hooks'; +import { setContextualLayer } from '@/store/features/eudr'; +import { Slider } from '@/components/ui/slider'; + +const LAYERD_ID = 'real-time-deforestation-alerts-since-2020-radd'; +const dateFormatter = (date: string) => format(date, 'yyyy MMM dd'); + +const RADDSlider = () => { + const dispatch = useAppDispatch(); + const [values, setValues] = useState([0, 0]); + const data = layersData.find((layer) => layer.id === LAYERD_ID); + const dates = data?.legend?.dates; + const handleValueChange = useDebounce((valuesRange) => { + dispatch( + setContextualLayer({ + layer: LAYERD_ID, + configuration: { dateFrom: dates[valuesRange[0]], dateTo: dates[valuesRange[1]] }, + }), + ); + }, 1000); + + return ( +
+
+
From
+
+ {dateFormatter(dates[values[0]])} +
+
to
+
+ {dateFormatter(dates[values[1]])} +
+
+ { + setValues(values); + handleValueChange(values); + }} + minStepsBetweenThumbs={1} + /> +
+ {[dates[0], dates[dates.length - 1]].map((year) => ( +
+ {year} +
+ ))} +
+
+ ); +}; + +export default RADDSlider; diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index 62c7befcd..ccbdb6390 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -8,9 +8,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from 'store'; type LayerConfiguration = { - active: boolean; + active?: boolean; opacity?: number; - month?: number; + dateFrom?: string; + dateTo?: string; + date?: string; year?: number; }; @@ -59,10 +61,13 @@ export const initialState: EUDRState = { ['deforestation-alerts-2020-2022-hansen']: { active: false, opacity: 1, + year: 2020, }, ['real-time-deforestation-alerts-since-2020-radd']: { active: false, opacity: 1, + dateFrom: '2020-01-01', + dateTo: '2024-07-27', }, }, }; @@ -101,7 +106,10 @@ export const EUDRSlice = createSlice({ ...state, contextualLayers: { ...state.contextualLayers, - [action.payload.layer]: action.payload.configuration, + [action.payload.layer]: { + ...state.contextualLayers[action.payload.layer], + ...action.payload.configuration, + }, }, }), }, diff --git a/client/yarn.lock b/client/yarn.lock index 2e99fd3b2..ed158db3e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2240,6 +2240,36 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-slider@npm:^1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-slider@npm:1.1.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/number": 1.0.1 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-collection": 1.0.3 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-direction": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-use-layout-effect": 1.0.1 + "@radix-ui/react-use-previous": 1.0.1 + "@radix-ui/react-use-size": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 2b774f23d90549aa688ee2e500c5325a91ea92db7a5ef245bdf7b5c709078433e6853d4ad84b1367cf701d0f54906979db51baa21e5154b439dde03a365ed270 + languageName: node + linkType: hard + "@radix-ui/react-slot@npm:1.0.2": version: 1.0.2 resolution: "@radix-ui/react-slot@npm:1.0.2" @@ -7849,6 +7879,7 @@ __metadata: "@radix-ui/react-radio-group": 1.1.3 "@radix-ui/react-select": 2.0.0 "@radix-ui/react-separator": ^1.0.3 + "@radix-ui/react-slider": ^1.1.2 "@radix-ui/react-slot": 1.0.2 "@radix-ui/react-switch": ^1.0.3 "@radix-ui/react-tooltip": ^1.0.7 From 4b99dd0f87d137ace4d924c82cfa81cfc5f44af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 08:55:38 +0100 Subject: [PATCH 104/153] ensures x-axis domain is calculated properly in alerts chart --- .../deforestation-alerts/chart/index.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx index ca04fe700..be97665a4 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -57,6 +57,15 @@ const DeforestationAlertsChart = (): JSX.Element => { })); }, [parsedData]); + const xDomain = useMemo(() => { + if (!parsedData?.length) return []; + + return [ + new UTCDate(parsedData[0].alertDate).getTime(), + new UTCDate(parsedData[parsedData?.length - 1].alertDate).getTime(), + ]; + }, [parsedData]); + return ( <>
@@ -102,10 +111,7 @@ const DeforestationAlertsChart = (): JSX.Element => { type="number" scale="time" dataKey="alertDate" - domain={[ - new UTCDate(parsedData?.[0].alertDate).getTime(), - new UTCDate(parsedData?.[parsedData?.length - 1].alertDate).getTime(), - ]} + domain={xDomain} tickFormatter={(value: string | number, x) => { if (x === 0) return format(new UTCDate(value), 'LLL yyyy'); return format(new UTCDate(value), 'LLL'); From 3b157f4cb2b184ed756d358b2467bbf7ecdfc4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 08:58:24 +0100 Subject: [PATCH 105/153] adds CRM column to suppliers table --- .../analysis-eudr/supplier-list-table/table/columns.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx index 5209a5eb3..a12ed8049 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -68,6 +68,14 @@ export const columns: ColumnDef[] = [ return {`${Number.isNaN(tpl) ? '-' : `${tpl.toFixed(2)}%`}`}; }, }, + { + accessorKey: 'crm', + header: ({ column }) => , + cell: ({ row }) => { + const crm: number = row.getValue('crm'); + return {`${Number.isNaN(crm) ? '-' : crm}`}; + }, + }, { accessorKey: 'materials', header: ({ column }) => , From 12f570c39e9f9feb961f99042d2efd194ac72ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 09:05:22 +0100 Subject: [PATCH 106/153] supplier detail: adds carbon removal info to sourcing information --- .../analysis-eudr-detail/sourcing-info/index.tsx | 11 +++++++++++ client/src/hooks/eudr/index.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx index 87db82d68..e12861365 100644 --- a/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx @@ -61,6 +61,17 @@ const SupplierSourcingInfo = (): JSX.Element => { {data?.totalArea ? `${Intl.NumberFormat(undefined).format(data.totalArea)} Kha` : '-'}
+ +
+

Carbon removal

+ + {data?.totalCarbonRemoval + ? `${Intl.NumberFormat(undefined, { style: 'unit', unit: 'kilogram' }).format( + data.totalCarbonRemoval, + )}` + : '-'} + +
diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 6a65cc4aa..0ec5df213 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -189,6 +189,7 @@ export interface SupplierDetail { }; totalArea: number; totalVolume: number; + totalCarbonRemoval: number; byVolume: [ { plotName: string; From 234276558e3ca49d3d8dcafeba8507a5fd73ba1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 09:43:16 +0100 Subject: [PATCH 107/153] passes filters to plot geometries hook --- .../analysis-eudr/map/component.tsx | 22 +++++++++++++++---- client/src/hooks/eudr/index.ts | 6 ++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 2cf73f10e..86b1e39f0 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -5,6 +5,7 @@ import Map from 'react-map-gl/maplibre'; import { MapView } from '@deck.gl/core/typed'; import { TileLayer } from '@deck.gl/geo-layers/typed'; import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; +import { useParams } from 'next/navigation'; import ZoomControl from './zoom'; import LegendControl from './legend'; @@ -32,14 +33,27 @@ setDefaultCredentials({ }); const EUDRMap = () => { - const { basemap, planetCompare, supplierLayer, contextualLayers } = useAppSelector( - (state) => state.eudr, - ); + const { + basemap, + planetCompare, + supplierLayer, + contextualLayers, + filters: { suppliers, materials, origins, plots }, + } = useAppSelector((state) => state.eudr); const [hoverInfo, setHoverInfo] = useState(null); const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); - const plotGeometries = usePlotGeometries(); + const params = useParams(); + + const plotGeometries = usePlotGeometries({ + producerIds: params?.supplierId + ? [params.supplierId as string] + : suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), + }); // Supplier plot layer const layer: GeoJsonLayer = new GeoJsonLayer({ diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 0ec5df213..281bcaf0d 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -29,9 +29,9 @@ export const useEUDRSuppliers = ( export const usePlotGeometries = ( params?: { - producersIds: string[]; - originsId: string[]; - materialsId: string[]; + producerIds: string[]; + originIds: string[]; + materialIds: string[]; geoRegionIds: string[]; }, options: UseQueryOptions = {}, From f35070c34c212743eb687b824bcd7198e461b95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 11:30:21 +0100 Subject: [PATCH 108/153] adds placeholders for feching and no data in charts --- .../deforestation-alerts/chart/index.tsx | 154 +++++++------ .../deforestation-alerts/index.tsx | 2 +- .../sourcing-info/chart/index.tsx | 207 ++++++++++-------- .../supplier-list-table/table/index.tsx | 102 +++++---- .../suppliers-stacked-bar/component.tsx | 174 ++++++++------- 5 files changed, 348 insertions(+), 291 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx index be97665a4..f5c0c74af 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -25,7 +25,7 @@ const DeforestationAlertsChart = (): JSX.Element => { const { filters: { dates }, } = useAppSelector(eudrDetail); - const { data } = useEUDRSupplier( + const { data, isFetching } = useEUDRSupplier( supplierId, { startAlertDate: dates.from, @@ -68,76 +68,92 @@ const DeforestationAlertsChart = (): JSX.Element => { return ( <> -
- {plotConfig.map(({ name, color }) => ( - { - setSelectedPlots((prev) => { - if (prev.includes(name)) { - return prev.filter((item) => item !== name); - } - return [...prev, name]; - }); - }} - > - - {name} - - ))} -
- - - - { - if (x === 0) return format(new UTCDate(value), 'LLL yyyy'); - return format(new UTCDate(value), 'LLL'); - }} - tickLine={false} - padding={{ left: 20, right: 20 }} - axisLine={false} - className="text-xs" - tickMargin={15} - /> - - format(new UTCDate(v), 'dd/MM/yyyy')} /> - {plotConfig?.map(({ name, color }) => { - return ( - + Fetching data... +
+ )} + {!data?.length && !isFetching && ( +
+ No data available +
+ )} + {data?.length > 0 && ( + <> +
+ {plotConfig.map(({ name, color }) => ( + { + setSelectedPlots((prev) => { + if (prev.includes(name)) { + return prev.filter((item) => item !== name); + } + return [...prev, name]; + }); + }} + > + + {name} + + ))} +
+ + + + { + if (x === 0) return format(new UTCDate(value), 'LLL yyyy'); + return format(new UTCDate(value), 'LLL'); + }} + tickLine={false} + padding={{ left: 20, right: 20 }} + axisLine={false} + className="text-xs" + tickMargin={15} /> - ); - })} - - + + format(new UTCDate(v), 'dd/MM/yyyy')} /> + {plotConfig?.map(({ name, color }) => { + return ( + + ); + })} + + + + )} ); }; diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx index c314aa1ad..6ab152fff 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx @@ -40,7 +40,7 @@ const DeforestationAlerts = (): JSX.Element => { }} />
- {data?.totalAlerts && ( + {data?.totalAlerts > 0 && (
There were {data?.totalAlerts} deforestation alerts reported for the supplier between the{' '} diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx index 7c9ec6892..e3c069018 100644 --- a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx @@ -21,7 +21,6 @@ import { eudrDetail } from '@/store/features/eudr-detail'; import { EUDR_COLOR_RAMP } from '@/utils/colors'; import { Badge } from '@/components/ui/badge'; import InfoModal from '@/components/legend/item/info-modal'; -import InfoTooltip from '@/components/info-tooltip'; const SupplierSourcingInfoChart = (): JSX.Element => { const [showBy, setShowBy] = useState<'byVolume' | 'byArea'>('byVolume'); @@ -31,7 +30,7 @@ const SupplierSourcingInfoChart = (): JSX.Element => { filters: { dates }, } = useAppSelector(eudrDetail); - const { data } = useEUDRSupplier( + const { data, isFetching } = useEUDRSupplier( supplierId, { startAlertDate: dates.from, @@ -130,103 +129,119 @@ const SupplierSourcingInfoChart = (): JSX.Element => {
-
- {plotConfig.map(({ name, color }) => ( - { - setSelectedPlots((prev) => { - if (prev.includes(name)) { - return prev.filter((item) => item !== name); - } - return [...prev, name]; - }); - }} - > - - {name} - - ))} -
- -
- - - - - } - /> - ( - - - {format(new Date(payload.value), 'yyyy')} - - - )} - tickLine={false} - type="category" - width={200} - /> - format(new Date(value), 'yyyy')} - formatter={(value: number, name) => [`${value.toFixed(2)}%`, name]} - /> + {!parsedData?.length && isFetching && ( +
+ Fetching data... +
+ )} + {!parsedData?.length && !isFetching && ( +
+ No data available +
+ )} + {parsedData?.length > 0 && ( + <> +
{plotConfig.map(({ name, color }) => ( - + variant="secondary" + className={cn( + 'flex cursor-pointer items-center space-x-1 rounded border border-gray-200 bg-white p-1 text-gray-900', + { + 'bg-secondary/80': selectedPlots.includes(name), + }, + )} + onClick={() => { + setSelectedPlots((prev) => { + if (prev.includes(name)) { + return prev.filter((item) => item !== name); + } + return [...prev, name]; + }); + }} + > + + {name} + ))} - - -
+
+ +
+ + + + + } + /> + ( + + + {format(new Date(payload.value), 'yyyy')} + + + )} + tickLine={false} + type="category" + width={200} + /> + format(new Date(value), 'yyyy')} + formatter={(value: number, name) => [`${value.toFixed(2)}%`, name]} + /> + {plotConfig.map(({ name, color }) => ( + + ))} + + +
+ + )}
); }; diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index bfcc9ab7d..787c688d3 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -58,7 +58,7 @@ const SuppliersListTable = (): JSX.Element => { filters: { dates, suppliers, origins, materials }, } = useAppSelector(eudr); - const { data } = useEUDRData( + const { data, isFetching } = useEUDRData( { startAlertDate: dates.from, endAlertDate: dates.to, @@ -98,52 +98,64 @@ const SuppliersListTable = (): JSX.Element => { // getFacetedUniqueValues: getFacetedUniqueValues(), }); - if (!data?.length) return null; - return ( -
-
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} + <> + {!data?.length && isFetching && ( +
+ Fetching data... +
+ )} + {!data?.length && !isFetching && ( +
+ No data available +
+ )} + {data?.length && ( +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. - ))} - - )) - ) : ( - - - No results. - - - )} - -
- -
+ + )} + +
+ +
+ )} + ); }; diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index d9204243d..329496f90 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -49,7 +49,7 @@ const SuppliersStackedBar = () => { [dispatch], ); - const { data } = useEUDRData( + const { data, isFetching } = useEUDRData( { startAlertDate: dates.from, endAlertDate: dates.to, @@ -113,86 +113,100 @@ const SuppliersStackedBar = () => {
-
- - - - + Fetching data... +
+ )} + {!parsedData?.length && !isFetching && ( +
+ No data available +
+ )} + {parsedData?.length > 0 && ( + <> +
+ + + + + } /> - } - /> - ( - - - {payload.value} - - - )} - tickLine={false} - type="category" - width={200} - /> - value} - formatter={(value: number, name: keyof typeof TOOLTIP_LABELS) => [ - `${value.toFixed(2)}%`, - TOOLTIP_LABELS[name], - ]} - /> - - - - - -
- + ( + + + {payload.value} + + + )} + tickLine={false} + type="category" + width={200} + /> + value} + formatter={(value: number, name: keyof typeof TOOLTIP_LABELS) => [ + `${value.toFixed(2)}%`, + TOOLTIP_LABELS[name], + ]} + /> + + + + + +
+ + + )}
); }; From 913d35e7154596d61c9ff3a66f38343ba18107cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Mon, 18 Mar 2024 11:30:42 +0100 Subject: [PATCH 109/153] fixes client Docker image --- client/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/Dockerfile b/client/Dockerfile index e2cd865b7..3c447ae2b 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -38,7 +38,7 @@ COPY --chown=$USER:$USER public ./public # NextJS required files COPY --chown=$USER:$USER next.config.js local.d.ts \ - postcss.config.js tailwind.config.js entrypoint.sh \ + postcss.config.js tailwind.config.ts entrypoint.sh \ tsconfig.json tsconfig.eslint.json .browserlistrc .eslintrc.js .prettierrc.cjs ./ RUN yarn build From 2a8fe8a892873b510e32803e0efc61ddf0dec05a Mon Sep 17 00:00:00 2001 From: David Inga Date: Mon, 18 Mar 2024 11:48:42 +0100 Subject: [PATCH 110/153] fixed opacity for layers --- client/src/components/legend/item/info-modal.tsx | 2 +- client/src/containers/analysis-eudr/map/component.tsx | 4 ++++ .../containers/analysis-eudr/map/legend/component.tsx | 11 +++++++++++ .../src/containers/analysis-eudr/map/legend/item.tsx | 10 ++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/src/components/legend/item/info-modal.tsx b/client/src/components/legend/item/info-modal.tsx index 8a193da17..e7ad77ae1 100644 --- a/client/src/components/legend/item/info-modal.tsx +++ b/client/src/components/legend/item/info-modal.tsx @@ -13,7 +13,7 @@ const InfoModal = ({ info: { title, description, source } }: InfoModalProps) => const [open, setOpen] = useState(false); return ( <> - setOpen(false)} title={title || NO_DATA} open={open} size="narrow"> diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 86b1e39f0..a79a5edce 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -72,6 +72,7 @@ const EUDRMap = () => { highlightColor: [63, 89, 224, 255], visible: supplierLayer.active, onHover: setHoverInfo, + opacity: supplierLayer.opacity, }); const planetLayer = new TileLayer({ @@ -122,6 +123,7 @@ const EUDRMap = () => { stroked: false, getFillColor: [114, 169, 80], lineWidthMinPixels: 1, + opacity: contextualLayers['forest-cover-2020-ec-jrc'].opacity, visible: contextualLayers['forest-cover-2020-ec-jrc'].active, credentials: { apiVersion: API_VERSIONS.V3, @@ -140,6 +142,7 @@ const EUDRMap = () => { stroked: false, getFillColor: [224, 191, 36], lineWidthMinPixels: 1, + opacity: contextualLayers['deforestation-alerts-2020-2022-hansen'].opacity, visible: contextualLayers['deforestation-alerts-2020-2022-hansen'].active, credentials: { apiVersion: API_VERSIONS.V3, @@ -165,6 +168,7 @@ const EUDRMap = () => { return [201, 42, 109]; }, lineWidthMinPixels: 1, + opacity: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].opacity, visible: contextualLayers['real-time-deforestation-alerts-since-2020-radd'].active, credentials: { apiVersion: API_VERSIONS.V3, diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index 320a22ec5..12871e5b9 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -51,6 +51,9 @@ const EURDLegend = () => { changeVisibility={() => dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) } + changeOpacity={(opacity) => + dispatch(setSupplierLayer({ ...supplierLayer, opacity })) + } />
@@ -87,6 +90,14 @@ const EURDLegend = () => { }), ) } + changeOpacity={(opacity) => + dispatch( + setContextualLayer({ + layer: layer.id, + configuration: { opacity }, + }), + ) + } > <> {layer.id === 'deforestation-alerts-2020-2022-hansen' && diff --git a/client/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx index 9c12a5282..44efbc152 100644 --- a/client/src/containers/analysis-eudr/map/legend/item.tsx +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -21,6 +21,7 @@ type LegendItemProps = { showSwitcher?: boolean; isActive?: boolean; changeVisibility?: (active: boolean) => void; + changeOpacity?: (opacity: number) => void; }; const LegendItem: FC> = ({ @@ -34,6 +35,7 @@ const LegendItem: FC> = ({ showSwitcher = false, isActive = true, changeVisibility = () => null, + changeOpacity = () => null, }) => { return (
@@ -50,12 +52,16 @@ const LegendItem: FC> = ({

{title}

- null} /> +
{showVisibility && (
-
- handleBasemap('light', checked)} - /> +
@@ -116,24 +170,66 @@ const EUDRBasemapControl = () => { />
- handleBasemap('planet', checked)} - /> +
Monthly high resolution basemaps (tropics)
+ {planetLayer.active && ( +
+
Year
+ +
Month
+ +
+ )}
- {basemap !== 'planet' &&
Select satellite basemap for image comparison option.
} - {basemap === 'planet' && ( + {!planetLayer.active &&
Select satellite basemap for image comparison option.
} + {planetLayer.active && (
@@ -156,13 +252,61 @@ const EUDRBasemapControl = () => { />
- +
Monthly high resolution basemaps (tropics)
+ {planetCompareLayer.active && ( +
+
Year
+ +
Month
+ +
+ )}
)} diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index a79a5edce..9131ca3ab 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -6,6 +6,7 @@ import { MapView } from '@deck.gl/core/typed'; import { TileLayer } from '@deck.gl/geo-layers/typed'; import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; import { useParams } from 'next/navigation'; +import { format } from 'date-fns'; import ZoomControl from './zoom'; import LegendControl from './legend'; @@ -18,6 +19,8 @@ import { formatNumber } from '@/utils/number-format'; import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; +const monthFormatter = (date: string) => format(date, 'MM'); + const DEFAULT_VIEW_STATE: MapViewState = { ...INITIAL_VIEW_STATE, latitude: -8.461844239054608, @@ -35,7 +38,8 @@ setDefaultCredentials({ const EUDRMap = () => { const { basemap, - planetCompare, + planetLayer, + planetCompareLayer, supplierLayer, contextualLayers, filters: { suppliers, materials, origins, plots }, @@ -56,7 +60,7 @@ const EUDRMap = () => { }); // Supplier plot layer - const layer: GeoJsonLayer = new GeoJsonLayer({ + const eudrSupplierLayer: GeoJsonLayer = new GeoJsonLayer({ id: 'full-plots-layer', data: plotGeometries.data, // Styles @@ -75,13 +79,17 @@ const EUDRMap = () => { opacity: supplierLayer.opacity, }); - const planetLayer = new TileLayer({ + const basemapPlanetLayer = new TileLayer({ id: 'top-planet-monthly-layer', - data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_2020_12_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', + data: `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${ + planetLayer.year + }_${monthFormatter( + planetLayer.month.toString(), + )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db`, minZoom: 0, maxZoom: 20, tileSize: 256, - visible: true, + visible: planetLayer.active, renderSubLayers: (props) => { const { bbox: { west, south, east, north }, @@ -95,13 +103,17 @@ const EUDRMap = () => { }, }); - const planetCompareLayer = new TileLayer({ + const basemapPlanetCompareLayer = new TileLayer({ id: 'bottom-planet-monthly-layer', - data: 'https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_2024_02_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db', + data: `https://tiles.planet.com/basemaps/v1/planet-tiles/global_monthly_${ + planetCompareLayer.year + }_${monthFormatter( + planetCompareLayer.month.toString(), + )}_mosaic/gmap/{z}/{x}/{y}.png?api_key=PLAK6679039df83f414faf798ba4ad4530db`, minZoom: 0, maxZoom: 20, tileSize: 256, - visible: true, + visible: planetCompareLayer.active, renderSubLayers: (props) => { const { bbox: { west, south, east, north }, @@ -196,17 +208,19 @@ const EUDRMap = () => { onViewStateChange={({ viewState }) => setViewState(viewState as MapViewState)} controller={{ dragRotate: false }} layers={[ - basemap === 'planet' && !planetCompare ? [planetLayer] : null, - basemap === 'planet' && planetCompare ? [planetLayer, planetCompareLayer] : null, + basemap === 'planet' && !planetCompareLayer.active ? [basemapPlanetLayer] : null, + basemap === 'planet' && planetCompareLayer.active + ? [basemapPlanetLayer, basemapPlanetCompareLayer] + : null, forestCoverLayer, deforestationLayer, raddLayer, - layer, + eudrSupplierLayer, ]} layerFilter={({ layer, viewport }) => { - return !planetCompare || viewport.id.startsWith(layer.id.split('-')[0]); + return !planetCompareLayer.active || viewport.id.startsWith(layer.id.split('-')[0]); }} - {...(planetCompare + {...(planetCompareLayer.active ? { views: [ new MapView({ @@ -234,7 +248,7 @@ const EUDRMap = () => { > - {planetCompare && ( + {planetCompareLayer.active && (
)}
diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index ccbdb6390..82b3b0410 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -13,6 +13,7 @@ type LayerConfiguration = { dateFrom?: string; dateTo?: string; date?: string; + month?: number; year?: number; }; @@ -30,7 +31,8 @@ export type EUDRState = { }; // map basemap: 'light' | 'planet'; - planetCompare: boolean; + planetLayer: LayerConfiguration; + planetCompareLayer: LayerConfiguration; supplierLayer: LayerConfiguration; contextualLayers: Record; }; @@ -48,7 +50,6 @@ export const initialState: EUDRState = { }, }, basemap: 'light', - planetCompare: false, supplierLayer: { active: true, opacity: 1, @@ -70,6 +71,16 @@ export const initialState: EUDRState = { dateTo: '2024-07-27', }, }, + planetLayer: { + active: false, + month: 12, + year: 2020, + }, + planetCompareLayer: { + active: false, + month: 2, + year: 2024, + }, }; export const EUDRSlice = createSlice({ @@ -91,13 +102,12 @@ export const EUDRSlice = createSlice({ ...state, basemap: action.payload, }), - setPlanetCompare: (state, action: PayloadAction) => ({ - ...state, - planetCompare: action.payload, - }), setSupplierLayer: (state, action: PayloadAction) => ({ ...state, - supplierLayer: action.payload, + supplierLayer: { + ...state.supplierLayer, + ...action.payload, + }, }), setContextualLayer: ( state, @@ -112,6 +122,20 @@ export const EUDRSlice = createSlice({ }, }, }), + setPlanetLayer: (state, action: PayloadAction) => ({ + ...state, + planetLayer: { + ...state.planetLayer, + ...action.payload, + }, + }), + setPlanetCompareLayer: (state, action: PayloadAction) => ({ + ...state, + planetCompareLayer: { + ...state.planetCompareLayer, + ...action.payload, + }, + }), }, }); @@ -119,9 +143,10 @@ export const { setViewBy, setFilters, setBasemap, - setPlanetCompare, setSupplierLayer, setContextualLayer, + setPlanetLayer, + setPlanetCompareLayer, } = EUDRSlice.actions; export const eudr = (state: RootState) => state['eudr']; From 0ee456d3cf7b4d98e470e6cc627ff128623b8651 Mon Sep 17 00:00:00 2001 From: David Inga Date: Tue, 19 Mar 2024 09:06:46 +0100 Subject: [PATCH 112/153] fixed total number of suppliers --- client/next.config.js | 14 ++++++++++++-- .../supplier-list-table/table/index.tsx | 8 ++++++-- .../suppliers-stacked-bar/component.tsx | 3 ++- client/src/store/features/eudr/index.ts | 7 +++++++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/client/next.config.js b/client/next.config.js index 49f73d605..db7532e5d 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -8,12 +8,22 @@ const nextConfig = { return [ { source: '/', - destination: '/analysis/map', + destination: '/eurd', permanent: false, }, { source: '/analysis', - destination: '/analysis/map', + destination: '/eudr', + permanent: false, + }, + { + source: '/analysis/:id', + destination: '/eudr', + permanent: false, + }, + { + source: '/data', + destination: '/eurd', permanent: false, }, { diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index 787c688d3..17cbff20e 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -22,8 +22,8 @@ import { TableCell, } from '@/components/ui/table'; import { useEUDRData } from '@/hooks/eudr'; -import { useAppSelector } from '@/store/hooks'; -import { eudr } from '@/store/features/eudr'; +import { useAppSelector, useAppDispatch } from '@/store/hooks'; +import { eudr, setTotalSuppliers } from '@/store/features/eudr'; import type { // ColumnFiltersState, @@ -50,6 +50,7 @@ export interface Supplier { } const SuppliersListTable = (): JSX.Element => { + const dispatch = useAppDispatch(); // const [rowSelection, setRowSelection] = useState({}); // const [columnVisibility, setColumnVisibility] = useState({}); // const [columnFilters, setColumnFilters] = useState([]); @@ -68,6 +69,9 @@ const SuppliersListTable = (): JSX.Element => { }, { select: (data) => data?.table, + onSuccess: (data) => { + dispatch(setTotalSuppliers(data?.length || 0)); + }, }, ); diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index 329496f90..ea0c2cd26 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -38,6 +38,7 @@ const TOOLTIP_LABELS = { const SuppliersStackedBar = () => { const { viewBy, + totalSuppliers, filters: { dates, suppliers, origins, materials }, } = useAppSelector(eudr); const dispatch = useAppDispatch(); @@ -92,7 +93,7 @@ const SuppliersStackedBar = () => {
- Total numbers of suppliers: {parsedData?.length || '-'} + Total numbers of suppliers: {totalSuppliers}

Suppliers by category

diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index 82b3b0410..a7e26b46c 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -19,6 +19,7 @@ type LayerConfiguration = { export type EUDRState = { viewBy: (typeof VIEW_BY_OPTIONS)[number]['value']; + totalSuppliers: number; filters: { materials: Option[]; origins: Option[]; @@ -39,6 +40,7 @@ export type EUDRState = { export const initialState: EUDRState = { viewBy: 'materials', + totalSuppliers: 0, filters: { materials: [], origins: [], @@ -91,6 +93,10 @@ export const EUDRSlice = createSlice({ ...state, viewBy: action.payload, }), + setTotalSuppliers: (state, action: PayloadAction) => ({ + ...state, + totalSuppliers: action.payload, + }), setFilters: (state, action: PayloadAction>) => ({ ...state, filters: { @@ -141,6 +147,7 @@ export const EUDRSlice = createSlice({ export const { setViewBy, + setTotalSuppliers, setFilters, setBasemap, setSupplierLayer, From 10837c61b32e561649acdc337ede3b875a2f894d Mon Sep 17 00:00:00 2001 From: David Inga Date: Tue, 19 Mar 2024 09:18:54 +0100 Subject: [PATCH 113/153] fixed typo --- .../breakdown/deforestation-free-suppliers/index.tsx | 2 +- .../breakdown/suppliers-with-deforestation-alerts/index.tsx | 2 +- .../breakdown/suppliers-with-no-location-data/index.tsx | 2 +- client/src/containers/analysis-eudr/category-list/index.tsx | 2 +- client/src/hooks/eudr/index.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx index b2292ac63..06e09e945 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -24,7 +24,7 @@ const DeforestationFreeSuppliersBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), - geoRegiondIds: plots?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx index 3ad6a56d3..cf701755e 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -24,7 +24,7 @@ const SuppliersWithDeforestationAlertsBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), - geoRegiondIds: plots?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx index 0a9dba4b9..75665d596 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -24,7 +24,7 @@ const SuppliersWithNoLocationDataBreakdown = () => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), - geoRegiondIds: plots?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 7fce86af5..9c15de6dd 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -52,7 +52,7 @@ export const CategoryList = (): JSX.Element => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), - geoRegiondIds: plots?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), }, { select: (data) => data?.breakDown, diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 281bcaf0d..309fb19b3 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -156,7 +156,7 @@ export const useEUDRData = ( producerIds?: string[]; materialIds?: string[]; originIds?: string[]; - geoRegiondIds?: string[]; + geoRegionIds?: string[]; }, options: UseQueryOptions = {}, ) => { From 3831dfe684a5c07f605fb4f276d74e80b1b636a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 10:10:35 +0100 Subject: [PATCH 114/153] retrieves carbon removal data correctly --- .../analysis-eudr-detail/sourcing-info/index.tsx | 9 ++++++--- client/src/hooks/eudr/index.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx index e12861365..9753820d5 100644 --- a/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx @@ -20,7 +20,10 @@ const SupplierSourcingInfo = (): JSX.Element => { endAlertDate: dates.to, }, { - select: (data) => data?.sourcingInformation, + select: (data) => ({ + ...data?.sourcingInformation, + totalCarbonRemovals: data?.alerts.totalCarbonRemovals, + }), }, ); @@ -65,9 +68,9 @@ const SupplierSourcingInfo = (): JSX.Element => {

Carbon removal

- {data?.totalCarbonRemoval + {!isNaN(data?.totalCarbonRemovals) ? `${Intl.NumberFormat(undefined, { style: 'unit', unit: 'kilogram' }).format( - data.totalCarbonRemoval, + data.totalCarbonRemovals, )}` : '-'} diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 309fb19b3..30f1fc8b9 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -189,7 +189,6 @@ export interface SupplierDetail { }; totalArea: number; totalVolume: number; - totalCarbonRemoval: number; byVolume: [ { plotName: string; @@ -211,6 +210,7 @@ export interface SupplierDetail { startAlertDate: string; endAlertDate: string; totalAlerts: number; + totalCarbonRemovals: number; values: [ { alertDate: string; From de0f9a479a98a131b447b6b3705fedb67786dc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 10:11:24 +0100 Subject: [PATCH 115/153] passes geoRegionIds parameter correctly --- .../analysis-eudr/supplier-list-table/table/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index 17cbff20e..ca9dcd385 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -56,7 +56,7 @@ const SuppliersListTable = (): JSX.Element => { // const [columnFilters, setColumnFilters] = useState([]); const [sorting, setSorting] = useState([]); const { - filters: { dates, suppliers, origins, materials }, + filters: { dates, suppliers, origins, materials, plots }, } = useAppSelector(eudr); const { data, isFetching } = useEUDRData( @@ -66,6 +66,7 @@ const SuppliersListTable = (): JSX.Element => { producerIds: suppliers?.map(({ value }) => value), materialIds: materials?.map(({ value }) => value), originIds: origins?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), }, { select: (data) => data?.table, From 93188d622e9ab3fc395c5554421ce3a293f8e030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 10:36:23 +0100 Subject: [PATCH 116/153] makes filters reactive --- .../filters/more-filters/index.tsx | 33 +++++++++++++++---- client/src/hooks/eudr/index.ts | 23 ++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/client/src/containers/analysis-eudr/filters/more-filters/index.tsx b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx index 135628f13..12304da5f 100644 --- a/client/src/containers/analysis-eudr/filters/more-filters/index.tsx +++ b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx @@ -101,7 +101,11 @@ const MoreFilters = () => { ]); const { data: materialOptions, isLoading: materialOptionsIsLoading } = useEUDRMaterialsTree( - undefined, + { + producerIds: selectedFilters.suppliers.map((supplier) => supplier.value), + originIds: selectedFilters.origins.map((origin) => origin.value), + geoRegionIds: selectedFilters.plots.map((plot) => plot.value), + }, { ...DEFAULT_QUERY_OPTIONS, select: (_materials) => { @@ -118,22 +122,37 @@ const MoreFilters = () => { ); const { data: originOptions, isLoading: originOptionsIsLoading } = useEUDRAdminRegionsTree( - undefined, + { + producerIds: selectedFilters.suppliers.map((supplier) => supplier.value), + materialIds: selectedFilters.materials.map((material) => material.value), + geoRegionIds: selectedFilters.plots.map((plot) => plot.value), + }, DEFAULT_QUERY_OPTIONS, ); const { data: supplierOptions, isLoading: supplierOptionsIsLoading } = useEUDRSuppliers( - undefined, + { + originIds: selectedFilters.origins.map((origin) => origin.value), + materialIds: selectedFilters.materials.map((material) => material.value), + geoRegionIds: selectedFilters.plots.map((plot) => plot.value), + }, { ...DEFAULT_QUERY_OPTIONS, initialData: [], }, ); - const { data: plotOptions, isLoading: plotOptionsIsLoading } = useEUDRPlotsTree(undefined, { - ...DEFAULT_QUERY_OPTIONS, - initialData: [], - }); + const { data: plotOptions, isLoading: plotOptionsIsLoading } = useEUDRPlotsTree( + { + producerIds: selectedFilters.suppliers.map((supplier) => supplier.value), + originIds: selectedFilters.origins.map((origin) => origin.value), + materialIds: selectedFilters.materials.map((material) => material.value), + }, + { + ...DEFAULT_QUERY_OPTIONS, + initialData: [], + }, + ); useEffect(() => { const counters = Object.values(filters).map((value) => value.length); diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 30f1fc8b9..99b0ec8ab 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -3,12 +3,18 @@ import { useQuery } from '@tanstack/react-query'; import { apiService } from 'services/api'; import type { Supplier as SupplierRow } from '@/containers/analysis-eudr/supplier-list-table/table'; -import type { AdminRegionsTreesParams } from '@/hooks/admin-regions'; import type { MaterialTreeItem, OriginRegion, Supplier } from '@/types'; import type { UseQueryOptions } from '@tanstack/react-query'; +interface EUDRParams { + producerIds?: string[]; + originIds?: string[]; + materialIds?: string[]; + geoRegionIds?: string[]; +} + export const useEUDRSuppliers = ( - params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, + params?: EUDRParams, options: UseQueryOptions = {}, ) => { return useQuery( @@ -28,12 +34,7 @@ export const useEUDRSuppliers = ( }; export const usePlotGeometries = ( - params?: { - producerIds: string[]; - originIds: string[]; - materialIds: string[]; - geoRegionIds: string[]; - }, + params?: EUDRParams, options: UseQueryOptions = {}, ) => { return useQuery( @@ -55,7 +56,7 @@ export const usePlotGeometries = ( }; export const useEUDRMaterialsTree = ( - params?: { producersIds: string[]; originsId: string[]; materialsId: string[] }, + params?: EUDRParams, options: UseQueryOptions = {}, ) => { return useQuery( @@ -75,7 +76,7 @@ export const useEUDRMaterialsTree = ( }; export const useEUDRAdminRegionsTree = ( - params: AdminRegionsTreesParams, + params: EUDRParams, options: UseQueryOptions = {}, ) => { const query = useQuery( @@ -97,7 +98,7 @@ export const useEUDRAdminRegionsTree = ( }; export const useEUDRPlotsTree = ( - params: AdminRegionsTreesParams, + params: EUDRParams, options: UseQueryOptions = {}, ) => { const query = useQuery( From aaf3cbdc5bdf61f3b2b155e1ede9f183a6b35179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 12:05:06 +0100 Subject: [PATCH 117/153] triggers map comparison from alerts chart --- .../deforestation-alerts/chart/index.tsx | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx index f5c0c74af..99c2203ea 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -7,21 +7,29 @@ import { YAxis, CartesianGrid, Tooltip, + Dot, ResponsiveContainer, } from 'recharts'; import { useParams } from 'next/navigation'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { EUDR_COLOR_RAMP } from '@/utils/colors'; import { useEUDRSupplier } from '@/hooks/eudr'; -import { useAppSelector } from '@/store/hooks'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { eudrDetail } from '@/store/features/eudr-detail'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; +import { setBasemap, setPlanetCompareLayer, setPlanetLayer } from '@/store/features/eudr'; + +import type { DotProps } from 'recharts'; + +type DotPropsWithPayload = DotProps & { payload: { alertDate: number } }; const DeforestationAlertsChart = (): JSX.Element => { const [selectedPlots, setSelectedPlots] = useState([]); + const [selectedDate, setSelectedDate] = useState(null); const { supplierId }: { supplierId: string } = useParams(); + const dispatch = useAppDispatch(); const { filters: { dates }, } = useAppSelector(eudrDetail); @@ -36,6 +44,33 @@ const DeforestationAlertsChart = (): JSX.Element => { }, ); + const handleClickDot = useCallback( + (payload: DotPropsWithPayload['payload']) => { + if (payload.alertDate) { + if (selectedDate === payload.alertDate) { + setSelectedDate(null); + return dispatch(setPlanetCompareLayer({ active: false })); + } + + const date = new UTCDate(payload.alertDate); + + setSelectedDate(date.getTime()); + + dispatch(setBasemap('planet')); + dispatch(setPlanetLayer({ active: true })); + + dispatch( + setPlanetCompareLayer({ + active: true, + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + }), + ); + } + }, + [dispatch, selectedDate], + ); + const parsedData = data ?.map((item) => { return { @@ -147,6 +182,32 @@ const DeforestationAlertsChart = (): JSX.Element => { selectedPlots.length ? (selectedPlots.includes(name) ? 1 : 0.2) : 1 } connectNulls + dot={(props: DotPropsWithPayload) => { + const { payload } = props; + return ( + handleClickDot(payload)} + className={cn('cursor-pointer', { + // todo: fill when we have design + '': payload.alertDate === selectedDate, + })} + /> + ); + }} + activeDot={(props: DotPropsWithPayload) => { + const { payload } = props; + return ( + handleClickDot(payload)} + className={cn('cursor-pointer', { + // todo: fill when we have design + '': payload.alertDate === selectedDate, + })} + /> + ); + }} /> ); })} From c54ec5261dd85d7b04a81ee1a0d9146f92cfc51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 12:58:05 +0100 Subject: [PATCH 118/153] adds table filters --- .../analysis-eudr/category-list/index.tsx | 97 ++++++++++++------- .../supplier-list-table/table/columns.tsx | 9 +- .../supplier-list-table/table/index.tsx | 10 +- client/src/store/features/eudr/index.ts | 25 +++++ 4 files changed, 101 insertions(+), 40 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 9c15de6dd..fabb6e405 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import DeforestationFreeSuppliersBreakdown from './breakdown/deforestation-free-suppliers'; import SuppliersWithDeforestationAlertsBreakdown from './breakdown/suppliers-with-deforestation-alerts'; @@ -8,10 +8,12 @@ import { Button } from '@/components/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { cn } from '@/lib/utils'; import { useEUDRData } from '@/hooks/eudr'; -import { useAppSelector } from '@/store/hooks'; -import { eudr } from '@/store/features/eudr'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { eudr, setTableFilters } from '@/store/features/eudr'; import { themeColors } from '@/utils/colors'; +import type { EUDRState } from '@/store/features/eudr'; + export const CATEGORIES = [ { name: 'Deforestation-free suppliers', @@ -27,6 +29,15 @@ export const CATEGORIES = [ }, ] as const; +const CATEGORY_TO_FILTER: Record< + (typeof CATEGORIES)[number]['name'], + Partial +> = { + [CATEGORIES[0].name]: 'dfs', + [CATEGORIES[1].name]: 'sda', + [CATEGORIES[2].name]: 'tpl', +} as const; + type CategoryState = Record<(typeof CATEGORIES)[number]['name'], boolean>; export const CategoryList = (): JSX.Element => { @@ -43,7 +54,25 @@ export const CategoryList = (): JSX.Element => { const { viewBy, filters: { dates, suppliers, origins, materials, plots }, + table: { filters: tableFilters }, } = useAppSelector(eudr); + const dispatch = useAppDispatch(); + + const onClickCategory = useCallback( + (category: (typeof CATEGORIES)[number]) => { + toggleCategory((prev) => ({ + ...prev, + [category.name]: !prev[category.name], + })); + + dispatch( + setTableFilters({ + [CATEGORY_TO_FILTER[category.name]]: !tableFilters[CATEGORY_TO_FILTER[category.name]], + }), + ); + }, + [dispatch, tableFilters], + ); const { data } = useEUDRData( { @@ -75,42 +104,38 @@ export const CategoryList = (): JSX.Element => { { - toggleCategory((prev) => ({ - ...prev, - [category.name]: !prev[category.name], - })); - }} + onOpenChange={() => onClickCategory(category)} > -
-
- - {category.name} -
-
-
- {`${category.totalPercentage.toFixed(2)}%`}{' '} - of suppliers -
-
-
+
+
+ + {category.name}
-
- +
+
+ {`${category.totalPercentage.toFixed(2)}%`}{' '} + of suppliers +
+
+
+
+
+ - -
+
+ {category.name === CATEGORIES[0].name && } {category.name === CATEGORIES[1].name && } diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx index a12ed8049..8e5ff77a8 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -8,6 +8,8 @@ import { BIG_NUMBER_FORMAT } from 'utils/number-format'; import type { Supplier } from '.'; import type { ColumnDef } from '@tanstack/react-table'; +const numberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }); + export const columns: ColumnDef[] = [ { accessorKey: 'supplierName', @@ -49,7 +51,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => , cell: ({ row }) => { const dfs: number = row.getValue('dfs'); - return {`${Number.isNaN(dfs) ? '-' : `${dfs.toFixed(2)}%`}`}; + return {`${Number.isNaN(dfs) ? '-' : `${numberFormat.format(dfs)}%`}`}; }, }, { @@ -57,7 +59,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => , cell: ({ row }) => { const sda: number = row.getValue('sda'); - return {`${Number.isNaN(sda) ? '-' : `${sda.toFixed(2)}%`}`}; + return {`${Number.isNaN(sda) ? '-' : `${numberFormat.format(sda)}%`}`}; }, }, { @@ -65,7 +67,8 @@ export const columns: ColumnDef[] = [ header: ({ column }) => , cell: ({ row }) => { const tpl: number = row.getValue('tpl'); - return {`${Number.isNaN(tpl) ? '-' : `${tpl.toFixed(2)}%`}`}; + + return {`${Number.isNaN(tpl) ? '-' : `${numberFormat.format(tpl)}%`}`}; }, }, { diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index ca9dcd385..73a6ad681 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -57,6 +57,7 @@ const SuppliersListTable = (): JSX.Element => { const [sorting, setSorting] = useState([]); const { filters: { dates, suppliers, origins, materials, plots }, + table: { filters: tableFilters }, } = useAppSelector(eudr); const { data, isFetching } = useEUDRData( @@ -69,7 +70,14 @@ const SuppliersListTable = (): JSX.Element => { geoRegionIds: plots?.map(({ value }) => value), }, { - select: (data) => data?.table, + select: (data) => + data?.table.filter((dataRow) => { + if (Object.values(tableFilters).every((filter) => !filter)) return true; + + if (tableFilters.dfs && dataRow.dfs > 0) return true; + if (tableFilters.sda && dataRow.sda > 0) return true; + if (tableFilters.tpl && dataRow.tpl > 0) return true; + }), onSuccess: (data) => { dispatch(setTotalSuppliers(data?.length || 0)); }, diff --git a/client/src/store/features/eudr/index.ts b/client/src/store/features/eudr/index.ts index a7e26b46c..27dbf16fe 100644 --- a/client/src/store/features/eudr/index.ts +++ b/client/src/store/features/eudr/index.ts @@ -30,6 +30,13 @@ export type EUDRState = { to: string; }; }; + table: { + filters: { + dfs: boolean; + sda: boolean; + tpl: boolean; + }; + }; // map basemap: 'light' | 'planet'; planetLayer: LayerConfiguration; @@ -51,6 +58,13 @@ export const initialState: EUDRState = { to: DATES_RANGE[1], }, }, + table: { + filters: { + dfs: false, + sda: false, + tpl: false, + }, + }, basemap: 'light', supplierLayer: { active: true, @@ -104,6 +118,16 @@ export const EUDRSlice = createSlice({ ...action.payload, }, }), + setTableFilters: (state, action: PayloadAction>) => ({ + ...state, + table: { + ...state.table, + filters: { + ...state.table.filters, + ...action.payload, + }, + }, + }), setBasemap: (state, action: PayloadAction) => ({ ...state, basemap: action.payload, @@ -149,6 +173,7 @@ export const { setViewBy, setTotalSuppliers, setFilters, + setTableFilters, setBasemap, setSupplierLayer, setContextualLayer, From 2a48c9a4a3afede721cebd3881a8367883306472 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 19 Mar 2024 16:37:48 +0300 Subject: [PATCH 119/153] add dfs and sda georegionids by supplier --- .../eudr-alerts/dashboard/dashboard-utils.ts | 26 ++++++++++++++ .../eudr-alerts/dashboard/dashboard.types.ts | 10 ++++++ .../dashboard/eudr-dashboard.service.ts | 36 +++++++++++++++++-- .../modules/eudr-alerts/eudr.controller.ts | 1 + 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts index 2052bc03e..532c692d4 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts @@ -79,3 +79,29 @@ export const groupAlertsByDate = ( plots: alertsByDate[key], })); }; + +export const findNonAlertedGeoRegions = ( + geoRegionMapBySupplier: Record, + alertMap: any, +): Record => { + const missingGeoRegionIds: Record = {} as Record< + string, + string[] + >; + + Object.entries(geoRegionMapBySupplier).forEach( + ([supplierId, geoRegionIds]) => { + const alertGeoRegions = + alertMap.get(supplierId)?.geoRegionIdSet || new Set(); + const missingIds = geoRegionIds.filter( + (geoRegionId) => !alertGeoRegions.has(geoRegionId), + ); + + if (missingIds.length > 0) { + missingGeoRegionIds[supplierId] = missingIds; + } + }, + ); + + return missingGeoRegionIds; +}; diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index 415aa3d07..8cdf1c1d1 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -1,5 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; +class AffectedPlots { + @ApiProperty() + dfs: string[]; + @ApiProperty() + sda: string[]; +} + export class DashBoardTableElements { supplierId: string; @ApiProperty() @@ -31,6 +38,9 @@ export class DashBoardTableElements { @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) origins: EntitiesBySupplier[]; + + @ApiProperty({ type: () => AffectedPlots, isArray: true }) + plots: AffectedPlots; } class EntitiesBySupplier { diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index e4bb1c747..63e5ac381 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -25,6 +25,7 @@ import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service import { groupAlertsByDate, aggregateAndCalculatePercentage, + findNonAlertedGeoRegions, } from './dashboard-utils'; @Injectable() @@ -95,8 +96,6 @@ export class EudrDashboardService { ), }); - console.log(alerts); - const alertMap: Map< string, { geoRegionIdSet: Set; carbonRemovalValuesForSupplier: number[] } @@ -122,6 +121,13 @@ export class EudrDashboardService { .carbonRemovalValuesForSupplier.push(alert.carbonRemovals); }); + const allGeoRegions: Record = + await this.getGeoRegionsMapBySupplier( + entityMetadata.map((e) => e.supplierId), + ); + const nonAlertedGeoregions: Record = + findNonAlertedGeoRegions(allGeoRegions, alertMap); + const suppliersMap = new Map(); const materialsMap = new Map(); const originsMap = new Map(); @@ -167,10 +173,17 @@ export class EudrDashboardService { ) || 0; if (!suppliersMap.has(supplierId)) { + const alertedGeoRegions: string[] = [ + ...(alertMap.get(supplierId)?.geoRegionIdSet || []), + ]; suppliersMap.set(supplierId, { supplierId, supplierName, companyId, + plots: { + dfs: nonAlertedGeoregions[supplierId] || [], + sda: alertedGeoRegions, + }, materials: [], origins: [], totalBaselineVolume: 0, @@ -346,6 +359,7 @@ export class EudrDashboardService { sda: supplier.sda, tpl: supplier.tpl, crm: supplier.crm, + plots: supplier.plots, }); }); @@ -408,6 +422,24 @@ export class EudrDashboardService { return queryBuilder.getRawMany(); } + async getGeoRegionsMapBySupplier(supplierIds: string[]): Promise { + const res = await this.datasource + .createQueryBuilder() + .from(SourcingLocation, 'sl') + .select('sl.geoRegionId', 'geoRegionId') + .addSelect('sl.producerId', 'supplierId') + .distinct(true) + .where('sl.producerId IN (:...supplierIds)', { supplierIds }) + .getRawMany(); + return res.reduce((acc, item) => { + if (!acc[item.supplierId]) { + acc[item.supplierId] = []; + } + acc[item.supplierId].push(item.geoRegionId); + return acc; + }, {} as Record); + } + async buildDashboardDetail( supplierId: string, dto?: GetEUDRAlertDatesDto, diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index a0479b7c4..7fc52c881 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -251,6 +251,7 @@ export class EudrController { async getDashboard( @Query(ValidationPipe) dto: GetDashBoardDTO, ): Promise { + console.log('entra al dashboard', dto); return this.dashboard.buildDashboard(dto); } From 59bb2f0bf38a18820c3ec6d5454f9d5b5a472c10 Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 19 Mar 2024 16:53:46 +0300 Subject: [PATCH 120/153] add dfs * sda georegion ids for dashboard detail --- .../dashboard/dashboard-detail.types.ts | 4 ++++ .../eudr-alerts/dashboard/dashboard.types.ts | 2 +- .../dashboard/eudr-dashboard.service.ts | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts index 6bf7253e6..c4360dfe6 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { AffectedPlots } from './dashboard.types'; export class EUDRDashBoardDetail { @ApiProperty() @@ -11,6 +12,9 @@ export class EUDRDashBoardDetail { type: () => DashBoardDetailSourcingInformation, isArray: true, }) + @ApiProperty({ type: () => AffectedPlots }) + plots: AffectedPlots; + @ApiProperty({ type: () => DashBoardDetailCountry, isArray: true }) sourcingInformation: DashBoardDetailSourcingInformation[]; @ApiProperty({ type: () => DashBoardDetailAlerts, isArray: true }) alerts: DashBoardDetailAlerts[]; diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index 8cdf1c1d1..38b64c236 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -class AffectedPlots { +export class AffectedPlots { @ApiProperty() dfs: string[]; @ApiProperty() diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 63e5ac381..9e392e9e2 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -448,6 +448,7 @@ export class EudrDashboardService { const sourcingInformation: any = {}; let supplier: Supplier; const geoRegionMap: Map = new Map(); + const allGeoRegionsBySupplier: string[] = []; return this.datasource.transaction(async (manager: EntityManager) => { supplier = await manager @@ -514,7 +515,11 @@ export class EudrDashboardService { plotName: string; geoRegionId: string; }[] = []; + for (const geoRegion of geoRegions) { + if (geoRegion.geoRegionId) { + allGeoRegionsBySupplier.push(geoRegion.geoRegionId); + } geoRegion.geoRegionId = geoRegion.geoRegionId ?? null; geoRegion.plotName = geoRegion.plotName ?? 'Unknown'; if (!geoRegionMap.get(geoRegion.geoRegionId)) { @@ -583,6 +588,7 @@ export class EudrDashboardService { endAlertDate: dto?.endAlertDate, }); + const affectedGeoRegionIds: Set = new Set(); const { totalAlerts, totalCarbonRemovals } = alertsOutput.reduce( ( acc: { totalAlerts: number; totalCarbonRemovals: number }, @@ -590,6 +596,8 @@ export class EudrDashboardService { ) => { acc.totalAlerts += cur.alertCount; acc.totalCarbonRemovals += cur.carbonRemovals; + console.log(cur.geoRegionId); + affectedGeoRegionIds.add(cur.geoRegionId); return acc; }, { totalAlerts: 0, totalCarbonRemovals: 0 }, @@ -608,6 +616,14 @@ export class EudrDashboardService { values: groupAlertsByDate(alertsOutput, geoRegionMap), }; + const nonAlertedGeoRegions: string[] = allGeoRegionsBySupplier.filter( + (id: string) => ![...affectedGeoRegionIds].includes(id), + ); + result.plots = { + dfs: nonAlertedGeoRegions, + sda: [...affectedGeoRegionIds], + }; + result.alerts = alerts; return result; From a5f42370a1a7d394735b28fe9f01345c791f3c6c Mon Sep 17 00:00:00 2001 From: alexeh Date: Tue, 19 Mar 2024 17:15:02 +0300 Subject: [PATCH 121/153] fix swagger doc for dfs sda georegion ids --- .../modules/eudr-alerts/dashboard/dashboard-detail.types.ts | 4 ---- api/src/modules/eudr-alerts/dashboard/dashboard.types.ts | 2 +- api/src/modules/eudr-alerts/eudr.controller.ts | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts index c4360dfe6..2d28ce712 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -8,10 +8,6 @@ export class EUDRDashBoardDetail { address: string; @ApiProperty() companyId: string; - @ApiProperty({ - type: () => DashBoardDetailSourcingInformation, - isArray: true, - }) @ApiProperty({ type: () => AffectedPlots }) plots: AffectedPlots; @ApiProperty({ type: () => DashBoardDetailCountry, isArray: true }) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts index 38b64c236..e372a38c1 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -39,7 +39,7 @@ export class DashBoardTableElements { @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) origins: EntitiesBySupplier[]; - @ApiProperty({ type: () => AffectedPlots, isArray: true }) + @ApiProperty({ type: () => AffectedPlots }) plots: AffectedPlots; } diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts index 7fc52c881..a0479b7c4 100644 --- a/api/src/modules/eudr-alerts/eudr.controller.ts +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -251,7 +251,6 @@ export class EudrController { async getDashboard( @Query(ValidationPipe) dto: GetDashBoardDTO, ): Promise { - console.log('entra al dashboard', dto); return this.dashboard.buildDashboard(dto); } From bfe2b45c0999d5afbf0179fc9751fe566fe713fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 16:21:50 +0100 Subject: [PATCH 122/153] skipping tests conditionally --- .github/workflows/testing-client.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testing-client.yml b/.github/workflows/testing-client.yml index d13e8bf7b..301501310 100644 --- a/.github/workflows/testing-client.yml +++ b/.github/workflows/testing-client.yml @@ -16,6 +16,7 @@ jobs: name: Running client tests runs-on: ubuntu-22.04 timeout-minutes: 30 + if: ${{ github.ref_name != 'test' }} strategy: fail-fast: false defaults: From 9eab30b80b054c20361ea83b85303888489d88e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 08:41:46 +0100 Subject: [PATCH 123/153] fixes EUDR redirect --- client/next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/next.config.js b/client/next.config.js index db7532e5d..316c22618 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -8,7 +8,7 @@ const nextConfig = { return [ { source: '/', - destination: '/eurd', + destination: '/eudr', permanent: false, }, { From 427041cc215498af1b1751675e879c8a5efdefa6 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 20 Mar 2024 12:13:10 +0300 Subject: [PATCH 124/153] connect real output, remove carbon_removals from query and set value to 0 --- .../big-query-alerts-query.builder.ts | 4 ++-- .../modules/eudr-alerts/alerts.repository.ts | 20 +++++++++---------- .../dashboard/eudr-dashboard.service.ts | 5 ++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts index 7978a630b..9af1372eb 100644 --- a/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -5,9 +5,9 @@ import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; import { EUDRAlertsFields } from 'modules/eudr-alerts/alerts.repository'; export enum EUDR_ALERTS_DATABASE_FIELDS { - alertDate = 'alert_date', + alertDate = 'date', alertConfidence = 'alert_confidence', - alertCount = 'alert_count', + alertCount = 'pixel_count', geoRegionId = 'georegionid', supplierId = 'supplierid', carbonRemovals = 'carbon_removals', diff --git a/api/src/modules/eudr-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts index ac9019593..762a54690 100644 --- a/api/src/modules/eudr-alerts/alerts.repository.ts +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -26,10 +26,10 @@ import { const projectId: string = 'carto-dw-ac-zk2uhih6'; export enum EUDRAlertsFields { - alertDate = 'alertdate', + alertDate = 'date', alertConfidence = 'alertconfidence', year = 'year', - alertCount = 'alertcount', + alertCount = 'pixel_count', geoRegionId = 'georegionid', supplierId = 'supplierid', } @@ -74,10 +74,10 @@ export class AlertsRepository implements IEUDRAlertsRepository { EUDR_ALERTS_DATABASE_FIELDS.geoRegionId, 'geoRegionId', ); - queryBuilder.addSelect( - EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, - 'carbonRemovals', - ); + // queryBuilder.addSelect( + // EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, + // 'carbonRemovals', + // ); queryBuilder.orderBy(EUDR_ALERTS_DATABASE_FIELDS.alertDate, 'ASC'); return this.query(queryBuilder); } @@ -95,10 +95,10 @@ export class AlertsRepository implements IEUDRAlertsRepository { EUDR_ALERTS_DATABASE_FIELDS.supplierId, 'supplierId', ); - queryBuilder.addSelect( - EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, - 'carbonRemovals', - ); + // queryBuilder.addSelect( + // EUDR_ALERTS_DATABASE_FIELDS.carbonRemovals, + // 'carbonRemovals', + // ); return this.query(queryBuilder); } diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 9e392e9e2..ae1bcba17 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -118,7 +118,7 @@ export class EudrDashboardService { alertMap.get(supplierId)!.geoRegionIdSet.add(geoRegionId); alertMap .get(supplierId)! - .carbonRemovalValuesForSupplier.push(alert.carbonRemovals); + .carbonRemovalValuesForSupplier.push(alert.carbonRemovals ?? 0); }); const allGeoRegions: Record = @@ -595,8 +595,7 @@ export class EudrDashboardService { cur: AlertsOutput, ) => { acc.totalAlerts += cur.alertCount; - acc.totalCarbonRemovals += cur.carbonRemovals; - console.log(cur.geoRegionId); + acc.totalCarbonRemovals += cur.carbonRemovals ?? 0; affectedGeoRegionIds.add(cur.geoRegionId); return acc; }, From edabac5ea4f3cd4be23f99c3508fc3bc26ea3d7e Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 20 Mar 2024 10:56:07 +0100 Subject: [PATCH 125/153] fit plots to bounds when data is fetched or changed --- client/package.json | 1 + .../analysis-eudr/map/component.tsx | 24 +++++++++++++++-- client/yarn.lock | 27 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/client/package.json b/client/package.json index 2d8cdffcf..1c467af1d 100644 --- a/client/package.json +++ b/client/package.json @@ -55,6 +55,7 @@ "@tanstack/react-query": "^4.2.1", "@tanstack/react-table": "8.13.2", "@tanstack/react-virtual": "3.0.1", + "@turf/bbox": "^6.5.0", "autoprefixer": "10.2.5", "axios": "1.3.4", "chroma-js": "2.1.2", diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 9131ca3ab..9a8989636 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -1,12 +1,13 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import DeckGL from '@deck.gl/react/typed'; import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; import Map from 'react-map-gl/maplibre'; -import { MapView } from '@deck.gl/core/typed'; +import { MapView, WebMercatorViewport } from '@deck.gl/core/typed'; import { TileLayer } from '@deck.gl/geo-layers/typed'; import { CartoLayer, setDefaultCredentials, MAP_TYPES, API_VERSIONS } from '@deck.gl/carto/typed'; import { useParams } from 'next/navigation'; import { format } from 'date-fns'; +import bbox from '@turf/bbox'; import ZoomControl from './zoom'; import LegendControl from './legend'; @@ -200,6 +201,25 @@ const EUDRMap = () => { setViewState({ ...viewState, zoom }); }, [viewState]); + useEffect(() => { + if (!plotGeometries.data || !plotGeometries.isLoading) return; + const dataBounds = bbox(plotGeometries.data); + const newViewport = new WebMercatorViewport(viewState); + if (newViewport) { + const { latitude, longitude, zoom } = newViewport.fitBounds( + [ + [dataBounds[0], dataBounds[1]], + [dataBounds[2], dataBounds[3]], + ], + { + padding: 10, + }, + ); + setViewState({ ...viewState, latitude, longitude, zoom }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data, plotGeometries.isLoading]); + return ( <>
diff --git a/client/yarn.lock b/client/yarn.lock index ed158db3e..f78139c60 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2651,6 +2651,32 @@ __metadata: languageName: node linkType: hard +"@turf/bbox@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/bbox@npm:6.5.0" + dependencies: + "@turf/helpers": ^6.5.0 + "@turf/meta": ^6.5.0 + checksum: 537be56ae0c5ad44e71a691717b35745e947e19a6bd9f20fdac2ab4318caf98cd88472d7dbf576e8b32ead5da034d273ffb3f4559d6d386820ddcb88a1f7fedd + languageName: node + linkType: hard + +"@turf/helpers@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/helpers@npm:6.5.0" + checksum: d57f746351357838c654e0a9b98be3285a14b447504fd6d59753d90c6d437410bb24805d61c65b612827f07f6c2ade823bb7e56e41a1a946217abccfbd64c117 + languageName: node + linkType: hard + +"@turf/meta@npm:^6.5.0": + version: 6.5.0 + resolution: "@turf/meta@npm:6.5.0" + dependencies: + "@turf/helpers": ^6.5.0 + checksum: c6bb936aa92bf3365e87a50dc65f248e070c5767a36fac390754c00c89bf2d1583418686ab19a10332bfa9340b8cac6aaf2c55dad7f5fcf77f1a2dda75ccf363 + languageName: node + linkType: hard + "@types/chroma-js@npm:2.1.3": version: 2.1.3 resolution: "@types/chroma-js@npm:2.1.3" @@ -7889,6 +7915,7 @@ __metadata: "@tanstack/react-query": ^4.2.1 "@tanstack/react-table": 8.13.2 "@tanstack/react-virtual": 3.0.1 + "@turf/bbox": ^6.5.0 "@types/chroma-js": 2.1.3 "@types/d3-format": 3.0.1 "@types/d3-scale": 4.0.2 From fc6120382ae82b3e7a61b004df0f1f724f6f3299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 17:26:34 +0100 Subject: [PATCH 126/153] display plots based on dfs/sda values --- .../analysis-eudr/map/component.tsx | 99 ++++++++++++++----- .../supplier-list-table/table/index.tsx | 6 +- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index 9a8989636..c8c124416 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import DeckGL from '@deck.gl/react/typed'; import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers/typed'; import Map from 'react-map-gl/maplibre'; @@ -15,7 +15,7 @@ import BasemapControl from './basemap'; import { useAppSelector } from '@/store/hooks'; import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; -import { usePlotGeometries } from '@/hooks/eudr'; +import { useEUDRData, usePlotGeometries } from '@/hooks/eudr'; import { formatNumber } from '@/utils/number-format'; import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; @@ -43,7 +43,7 @@ const EUDRMap = () => { planetCompareLayer, supplierLayer, contextualLayers, - filters: { suppliers, materials, origins, plots }, + filters: { suppliers, materials, origins, plots, dates }, } = useAppSelector((state) => state.eudr); const [hoverInfo, setHoverInfo] = useState(null); @@ -51,6 +51,38 @@ const EUDRMap = () => { const params = useParams(); + const { data } = useEUDRData( + { + startAlertDate: dates.from, + endAlertDate: dates.to, + producerIds: suppliers?.map(({ value }) => value), + materialIds: materials?.map(({ value }) => value), + originIds: origins?.map(({ value }) => value), + geoRegionIds: plots?.map(({ value }) => value), + }, + { + select: (data) => { + if (params?.supplierId) { + return { + dfs: data.table + .filter((row) => row.supplierId === (params.supplierId as string)) + .map((row) => row.plots.dfs.flat()) + .flat(), + sda: data.table + .filter((row) => row.supplierId === (params.supplierId as string)) + .map((row) => row.plots.sda.flat()) + .flat(), + }; + } + + return { + dfs: data.table.map((row) => row.plots.dfs.flat()).flat(), + sda: data.table.map((row) => row.plots.sda.flat()).flat(), + }; + }, + }, + ); + const plotGeometries = usePlotGeometries({ producerIds: params?.supplierId ? [params.supplierId as string] @@ -60,25 +92,48 @@ const EUDRMap = () => { geoRegionIds: plots?.map(({ value }) => value), }); - // Supplier plot layer - const eudrSupplierLayer: GeoJsonLayer = new GeoJsonLayer({ - id: 'full-plots-layer', - data: plotGeometries.data, - // Styles - filled: true, - getFillColor: [63, 89, 224, 84], - stroked: true, - getLineColor: [63, 89, 224, 255], - getLineWidth: 1, - lineWidthUnits: 'pixels', - // Interactive props - pickable: true, - autoHighlight: true, - highlightColor: [63, 89, 224, 255], - visible: supplierLayer.active, - onHover: setHoverInfo, - opacity: supplierLayer.opacity, - }); + const eudrSupplierLayer = useMemo(() => { + if (!plotGeometries.data || !data) return null; + + return new GeoJsonLayer({ + id: 'full-plots-layer', + data: plotGeometries.data, + // Styles + filled: true, + getFillColor: ({ properties }) => { + if (data.dfs.indexOf(properties.id) > -1) return [74, 183, 243, 84]; + if (data.sda.indexOf(properties.id) > -1) return [255, 192, 56, 84]; + return [0, 0, 0, 84]; + }, + stroked: true, + getLineColor: ({ properties }) => { + if (data.dfs.indexOf(properties.id) > -1) return [74, 183, 243, 255]; + if (data.sda.indexOf(properties.id) > -1) return [255, 192, 56, 255]; + return [0, 0, 0, 84]; + }, + getLineWidth: 1, + lineWidthUnits: 'pixels', + // Interactive props + pickable: true, + autoHighlight: true, + highlightColor: (x: PickingInfo) => { + if (x.object?.properties?.id) { + const { + object: { + properties: { id }, + }, + } = x; + + if (data.dfs.indexOf(id) > -1) return [74, 183, 243, 255]; + if (data.sda.indexOf(id) > -1) return [255, 192, 56, 255]; + } + return [0, 0, 0, 84]; + }, + visible: supplierLayer.active, + onHover: setHoverInfo, + opacity: supplierLayer.opacity, + }); + }, [plotGeometries.data, data, supplierLayer.active, supplierLayer.opacity]); const basemapPlanetLayer = new TileLayer({ id: 'top-planet-monthly-layer', diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx index 73a6ad681..d427f7a07 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -32,7 +32,7 @@ import type { } from '@tanstack/react-table'; export interface Supplier { - supplierId: number; + supplierId: string; supplierName: string; companyId: string; baselineVolume: number; @@ -47,6 +47,10 @@ export interface Supplier { name: string; id: string; }[]; + plots: { + dfs: string[]; + sda: string[]; + }; } const SuppliersListTable = (): JSX.Element => { From 3cac2ad387c9e26dd6b0146f00a7d05182ea28ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Tue, 19 Mar 2024 18:00:31 +0100 Subject: [PATCH 127/153] filter geometries by category - WIP --- client/package.json | 1 + .../analysis-eudr/category-list/index.tsx | 67 ++++++++----------- .../analysis-eudr/map/component.tsx | 39 +++++++++-- client/src/hooks/eudr/index.ts | 7 +- client/yarn.lock | 3 +- 5 files changed, 69 insertions(+), 48 deletions(-) diff --git a/client/package.json b/client/package.json index 1c467af1d..aae160a81 100644 --- a/client/package.json +++ b/client/package.json @@ -101,6 +101,7 @@ "@types/chroma-js": "2.1.3", "@types/d3-format": "3.0.1", "@types/d3-scale": "4.0.2", + "@types/geojson": "7946.0.14", "@types/lodash-es": "4.17.6", "@types/node": "16.11.6", "@types/react": "18.2.28", diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index fabb6e405..0ba6ce815 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -6,68 +6,60 @@ import SuppliersWithNoLocationDataBreakdown from './breakdown/suppliers-with-no- import { Button } from '@/components/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { cn } from '@/lib/utils'; import { useEUDRData } from '@/hooks/eudr'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { eudr, setTableFilters } from '@/store/features/eudr'; import { themeColors } from '@/utils/colors'; -import type { EUDRState } from '@/store/features/eudr'; - export const CATEGORIES = [ { name: 'Deforestation-free suppliers', + key: 'dfs', color: themeColors.blue[400], }, { name: 'Suppliers with deforestation alerts', + key: 'sda', color: '#FFC038', }, { name: 'Suppliers with no location data', + key: 'tpl', color: '#8561FF', }, ] as const; -const CATEGORY_TO_FILTER: Record< - (typeof CATEGORIES)[number]['name'], - Partial -> = { - [CATEGORIES[0].name]: 'dfs', - [CATEGORIES[1].name]: 'sda', - [CATEGORIES[2].name]: 'tpl', -} as const; - -type CategoryState = Record<(typeof CATEGORIES)[number]['name'], boolean>; +type CategoryState = Record<(typeof CATEGORIES)[number]['key'], boolean>; export const CategoryList = (): JSX.Element => { + const { + viewBy, + filters: { dates, suppliers, origins, materials, plots }, + table: { filters: tableFilters }, + } = useAppSelector(eudr); + const [categories, toggleCategory] = useState( CATEGORIES.reduce( (acc, category) => ({ ...acc, - [category.name]: false, + [category.key]: tableFilters[category.key], }), {} as CategoryState, ), ); - const { - viewBy, - filters: { dates, suppliers, origins, materials, plots }, - table: { filters: tableFilters }, - } = useAppSelector(eudr); const dispatch = useAppDispatch(); const onClickCategory = useCallback( (category: (typeof CATEGORIES)[number]) => { toggleCategory((prev) => ({ ...prev, - [category.name]: !prev[category.name], + [category.key]: !prev[category.key], })); dispatch( setTableFilters({ - [CATEGORY_TO_FILTER[category.name]]: !tableFilters[CATEGORY_TO_FILTER[category.name]], + [category.key]: !tableFilters[category.key], }), ); }, @@ -91,19 +83,23 @@ export const CategoryList = (): JSX.Element => { const parsedData = useMemo(() => { const dataByView = data?.[viewBy] || []; - return Object.keys(dataByView).map((key) => ({ - name: key, - ...dataByView[key], - color: CATEGORIES.find((category) => category.name === key)?.color || '#000', - })); + return Object.keys(dataByView).map((key) => { + const category = CATEGORIES.find((category) => category.name === key); + return { + name: key, + ...dataByView[key], + key: category?.key, + color: category?.color || '#000', + }; + }); }, [data, viewBy]); return ( <> {parsedData.map((category) => ( onClickCategory(category)} > @@ -140,21 +136,16 @@ export const CategoryList = (): JSX.Element => { type="button" size="xs" variant="white" - className={cn( - 'w-[98px] rounded-md border-none text-sm text-gray-500 shadow-none transition-colors hover:shadow-none', - { - 'bg-navy-400 text-white hover:bg-navy-600': categories[category.name], - }, - )} + className="w-[98px] rounded-md border-none text-sm text-gray-500 shadow-none transition-colors hover:shadow-none group-data-[state=open]:bg-navy-400 group-data-[state=open]:text-white group-data-[state=open]:hover:bg-navy-600" > - {categories[category.name] ? 'Close detail' : 'View detail'} + {categories[category.key] ? 'Close detail' : 'View detail'}
- {category.name === CATEGORIES[0].name && } - {category.name === CATEGORIES[1].name && } - {category.name === CATEGORIES[2].name && } + {categories['dfs'] && } + {categories['sda'] && } + {categories['tpl'] && } ))} diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index c8c124416..b3618325d 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -44,6 +44,7 @@ const EUDRMap = () => { supplierLayer, contextualLayers, filters: { suppliers, materials, origins, plots, dates }, + table: { filters: tableFilters }, } = useAppSelector((state) => state.eudr); const [hoverInfo, setHoverInfo] = useState(null); @@ -75,9 +76,17 @@ const EUDRMap = () => { }; } + const filteredData = data?.table.filter((dataRow) => { + if (Object.values(tableFilters).every((filter) => !filter)) return true; + + if (tableFilters.dfs && dataRow.dfs > 0) return true; + if (tableFilters.sda && dataRow.sda > 0) return true; + if (tableFilters.tpl && dataRow.tpl > 0) return true; + }); + return { - dfs: data.table.map((row) => row.plots.dfs.flat()).flat(), - sda: data.table.map((row) => row.plots.sda.flat()).flat(), + dfs: filteredData.map((row) => row.plots.dfs.flat()).flat(), + sda: filteredData.map((row) => row.plots.sda.flat()).flat(), }; }, }, @@ -92,12 +101,30 @@ const EUDRMap = () => { geoRegionIds: plots?.map(({ value }) => value), }); - const eudrSupplierLayer = useMemo(() => { + const filteredGeometries: typeof plotGeometries.data = useMemo(() => { if (!plotGeometries.data || !data) return null; - return new GeoJsonLayer({ + if (params?.supplierId) return plotGeometries.data; + + return { + type: 'FeatureCollection', + features: plotGeometries.data.features.filter((feature) => { + if (Object.values(tableFilters).every((filter) => !filter)) return true; + + if (tableFilters.dfs && data.dfs.indexOf(feature.properties.id) > -1) return true; + if (tableFilters.sda && data.sda.indexOf(feature.properties.id) > -1) return true; + return false; + }), + }; + }, [data, plotGeometries.data, tableFilters, params]); + + const eudrSupplierLayer = useMemo(() => { + if (!filteredGeometries?.features || !data) return null; + + return new GeoJsonLayer<(typeof filteredGeometries)['features'][number]>({ id: 'full-plots-layer', - data: plotGeometries.data, + // @ts-expect-error will fix this later... + data: filteredGeometries, // Styles filled: true, getFillColor: ({ properties }) => { @@ -133,7 +160,7 @@ const EUDRMap = () => { onHover: setHoverInfo, opacity: supplierLayer.opacity, }); - }, [plotGeometries.data, data, supplierLayer.active, supplierLayer.opacity]); + }, [filteredGeometries, data, supplierLayer.active, supplierLayer.opacity]); const basemapPlanetLayer = new TileLayer({ id: 'top-planet-monthly-layer', diff --git a/client/src/hooks/eudr/index.ts b/client/src/hooks/eudr/index.ts index 99b0ec8ab..5b5c11746 100644 --- a/client/src/hooks/eudr/index.ts +++ b/client/src/hooks/eudr/index.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { apiService } from 'services/api'; +import type { FeatureCollection, Geometry } from 'geojson'; import type { Supplier as SupplierRow } from '@/containers/analysis-eudr/supplier-list-table/table'; import type { MaterialTreeItem, OriginRegion, Supplier } from '@/types'; import type { UseQueryOptions } from '@tanstack/react-query'; @@ -33,15 +34,15 @@ export const useEUDRSuppliers = ( ); }; -export const usePlotGeometries = ( +export const usePlotGeometries = ( params?: EUDRParams, - options: UseQueryOptions = {}, + options?: UseQueryOptions>, ) => { return useQuery( ['eudr-geo-features-collection', params], () => apiService - .request<{ geojson }>({ + .request<{ geojson: FeatureCollection }>({ method: 'GET', url: '/eudr/geo-features/collection', params, diff --git a/client/yarn.lock b/client/yarn.lock index f78139c60..103d073d5 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2817,7 +2817,7 @@ __metadata: languageName: node linkType: hard -"@types/geojson@npm:^7946.0.13": +"@types/geojson@npm:7946.0.14, @types/geojson@npm:^7946.0.13": version: 7946.0.14 resolution: "@types/geojson@npm:7946.0.14" checksum: ae511bee6488ae3bd5a3a3347aedb0371e997b14225b8983679284e22fa4ebd88627c6e3ff8b08bf4cc35068cb29310c89427311ffc9322c255615821a922e71 @@ -7919,6 +7919,7 @@ __metadata: "@types/chroma-js": 2.1.3 "@types/d3-format": 3.0.1 "@types/d3-scale": 4.0.2 + "@types/geojson": 7946.0.14 "@types/lodash-es": 4.17.6 "@types/node": 16.11.6 "@types/react": 18.2.28 From 8253f26f01fab4d6594682ab8198e74f965c366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 11:10:45 +0100 Subject: [PATCH 128/153] fixes duplicating content of category collapsible --- .../breakdown/breakdown-item/index.tsx | 4 +++- .../category-list/breakdown/index.tsx | 2 +- .../analysis-eudr/category-list/index.tsx | 14 ++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx index cdb501801..2fff1939c 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx @@ -6,6 +6,7 @@ const BreakdownItem = ({ icon, value, }: { + id: string; name: string; color: string; icon: ReactNode; @@ -19,7 +20,8 @@ const BreakdownItem = ({
- {`${value.toFixed(2)}%`} of suppliers + {`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format(value)}%`}{' '} + of suppliers
diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx index baaa74db4..d6fe5ae2e 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx @@ -6,7 +6,7 @@ const Breakdown = ({ data }: { data: ComponentProps[] }): return (
{data.map((item) => ( - + ))}
); diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 0ba6ce815..4f9c5b9f6 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -118,7 +118,9 @@ export const CategoryList = (): JSX.Element => {
- {`${category.totalPercentage.toFixed(2)}%`}{' '} + {`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format( + category.totalPercentage, + )}%`}{' '} of suppliers
@@ -143,9 +145,13 @@ export const CategoryList = (): JSX.Element => {
- {categories['dfs'] && } - {categories['sda'] && } - {categories['tpl'] && } + {category.key === 'dfs' && categories['dfs'] && } + {category.key === 'sda' && categories['sda'] && ( + + )} + {category.key === 'tpl' && categories['tpl'] && ( + + )} ))} From 74bd3590e4e4d2f30345625dd2aae741922b75ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 11:32:32 +0100 Subject: [PATCH 129/153] keeps collapsibles open based on redux values --- client/src/containers/analysis-eudr/category-list/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 4f9c5b9f6..6a876cbc9 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -101,6 +101,7 @@ export const CategoryList = (): JSX.Element => { key={category.key} className="group rounded-xl bg-gray-50 p-5" onOpenChange={() => onClickCategory(category)} + defaultOpen={categories[category.key]} >
From d2a7278421de9f7638745c1b2d7996670ef1276b Mon Sep 17 00:00:00 2001 From: David Inga Date: Wed, 20 Mar 2024 12:33:54 +0100 Subject: [PATCH 130/153] max bounds and max zoom for eudr page --- .../containers/analysis-eudr/map/component.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index b3618325d..b96b2628e 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -22,11 +22,14 @@ import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; const monthFormatter = (date: string) => format(date, 'MM'); +const MAX_BOUNDS = [-76.649412, -10.189886, -73.636411, -7.457082]; + const DEFAULT_VIEW_STATE: MapViewState = { ...INITIAL_VIEW_STATE, latitude: -8.461844239054608, longitude: -74.96226240479487, zoom: 9, + minZoom: 7, maxZoom: 20, }; @@ -307,7 +310,17 @@ const EUDRMap = () => {
setViewState(viewState as MapViewState)} + onViewStateChange={({ viewState }) => { + viewState.longitude = Math.min( + MAX_BOUNDS[2], + Math.max(MAX_BOUNDS[0], viewState.longitude), + ); + viewState.latitude = Math.min( + MAX_BOUNDS[3], + Math.max(MAX_BOUNDS[1], viewState.latitude), + ); + setViewState(viewState as MapViewState); + }} controller={{ dragRotate: false }} layers={[ basemap === 'planet' && !planetCompareLayer.active ? [basemapPlanetLayer] : null, From 97ba366e981ff8951ad36d4113543b4f600c6545 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 20 Mar 2024 16:54:19 +0300 Subject: [PATCH 131/153] group by month and fill in missing months --- .../eudr-alerts/dashboard/dashboard-utils.ts | 54 +++++++++++++++++++ .../dashboard/eudr-dashboard.service.ts | 18 +++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts index 532c692d4..c02a61be4 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts @@ -80,6 +80,60 @@ export const groupAlertsByDate = ( })); }; +export const groupAndFillAlertsByMonth = ( + alerts: AlertsOutput[], + geoRegionMap: Map, + startDate: Date, + endDate: Date, +): any[] => { + const alertsByMonth: any = alerts.reduce((acc: any, cur: AlertsOutput) => { + const date = new Date(cur.alertDate.value); + const yearMonthKey = `${date.getFullYear()}-${(date.getMonth() + 1) + .toString() + .padStart(2, '0')}`; + + if (!acc[yearMonthKey]) { + acc[yearMonthKey] = []; + } + + acc[yearMonthKey].push({ + plotName: geoRegionMap.get(cur.geoRegionId)?.plotName, + geoRegionId: cur.geoRegionId, + alertCount: cur.alertCount, + }); + + return acc; + }, {}); + + const start = new Date(startDate); + const end = new Date(endDate); + const filledMonths: any[] = []; + const existingMonths = new Set(Object.keys(alertsByMonth)); + + for ( + let month = new Date(start); + month <= end; + month.setMonth(month.getMonth() + 1) + ) { + const yearMonthKey = `${month.getFullYear()}-${(month.getMonth() + 1) + .toString() + .padStart(2, '0')}`; + if (!existingMonths.has(yearMonthKey)) { + filledMonths.push({ + alertDate: yearMonthKey, + plots: [{ geoRegionId: null, plotName: null, alertCount: 0 }], + }); + } else { + filledMonths.push({ + alertDate: yearMonthKey, + plots: alertsByMonth[yearMonthKey], + }); + } + } + + return filledMonths.sort((a, b) => a.alertDate.localeCompare(b.alertDate)); +}; + export const findNonAlertedGeoRegions = ( geoRegionMapBySupplier: Record, alertMap: any, diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index ae1bcba17..21203b3fc 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -23,9 +23,9 @@ import { EUDRDashBoardDetail } from 'modules/eudr-alerts/dashboard/dashboard-det import { MaterialsService } from 'modules/materials/materials.service'; import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; import { - groupAlertsByDate, aggregateAndCalculatePercentage, findNonAlertedGeoRegions, + groupAndFillAlertsByMonth, } from './dashboard-utils'; @Injectable() @@ -607,12 +607,22 @@ export class EudrDashboardService { alertsOutput[alertsOutput.length - 1]?.alertDate?.value.toString() || null; + const finalStartDate = + dto?.startAlertDate ?? (startAlertDate as unknown as Date); + const finalEndDate = + dto?.endAlertDate ?? (endAlertDate as unknown as Date); + const alerts = { - startAlertDate: startAlertDate, - endAlertDate: endAlertDate, + startAlertDate: dto?.startAlertDate ?? startAlertDate, + endAlertDate: dto?.endAlertDate ?? endAlertDate, totalAlerts, totalCarbonRemovals, - values: groupAlertsByDate(alertsOutput, geoRegionMap), + values: groupAndFillAlertsByMonth( + alertsOutput, + geoRegionMap, + finalStartDate, + finalEndDate, + ), }; const nonAlertedGeoRegions: string[] = allGeoRegionsBySupplier.filter( From 06e3150faec25e79f73a72a8c56d430c29890ef4 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 20 Mar 2024 17:48:22 +0300 Subject: [PATCH 132/153] add all plots to all alert dates --- .../eudr-alerts/dashboard/dashboard-utils.ts | 36 ++++++++++++------- .../dashboard/eudr-dashboard.service.ts | 2 +- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts index c02a61be4..f8734a825 100644 --- a/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts @@ -97,18 +97,19 @@ export const groupAndFillAlertsByMonth = ( } acc[yearMonthKey].push({ - plotName: geoRegionMap.get(cur.geoRegionId)?.plotName, geoRegionId: cur.geoRegionId, + plotName: geoRegionMap.get(cur.geoRegionId)?.plotName || 'Unknown', alertCount: cur.alertCount, }); return acc; }, {}); + const allGeoRegions = Array.from(geoRegionMap.keys()); + const start = new Date(startDate); const end = new Date(endDate); const filledMonths: any[] = []; - const existingMonths = new Set(Object.keys(alertsByMonth)); for ( let month = new Date(start); @@ -118,17 +119,26 @@ export const groupAndFillAlertsByMonth = ( const yearMonthKey = `${month.getFullYear()}-${(month.getMonth() + 1) .toString() .padStart(2, '0')}`; - if (!existingMonths.has(yearMonthKey)) { - filledMonths.push({ - alertDate: yearMonthKey, - plots: [{ geoRegionId: null, plotName: null, alertCount: 0 }], - }); - } else { - filledMonths.push({ - alertDate: yearMonthKey, - plots: alertsByMonth[yearMonthKey], - }); - } + const monthAlerts = alertsByMonth[yearMonthKey] || []; + + const alertsMap = new Map( + monthAlerts.map((alert: any) => [alert.geoRegionId, alert]), + ); + + const monthPlots = allGeoRegions.map((geoRegionId) => { + return ( + alertsMap.get(geoRegionId) || { + geoRegionId: geoRegionId, + plotName: geoRegionMap.get(geoRegionId)?.plotName || 'Unknown', + alertCount: 0, + } + ); + }); + + filledMonths.push({ + alertDate: yearMonthKey, + plots: monthPlots, + }); } return filledMonths.sort((a, b) => a.alertDate.localeCompare(b.alertDate)); diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 21203b3fc..48cba2a83 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -635,7 +635,7 @@ export class EudrDashboardService { result.alerts = alerts; - return result; + return result.alerts; }); } } From eca96471947e49d220e176a19e50845b91e67815 Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 20 Mar 2024 17:57:57 +0300 Subject: [PATCH 133/153] return full detail result --- api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts index 48cba2a83..21203b3fc 100644 --- a/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -635,7 +635,7 @@ export class EudrDashboardService { result.alerts = alerts; - return result.alerts; + return result; }); } } From 3577dc89e5cacb614c0b876b22b446b49ce8a96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 16:18:35 +0100 Subject: [PATCH 134/153] tracks zero alerts in alerts deforestation chart --- .../deforestation-alerts/chart/index.tsx | 37 +++++++++++++------ .../deforestation-alerts/index.tsx | 17 +++++++-- .../src/pages/eudr/suppliers/[supplierId].tsx | 10 ++++- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx index 99c2203ea..e90696a75 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -11,7 +11,7 @@ import { ResponsiveContainer, } from 'recharts'; import { useParams } from 'next/navigation'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { EUDR_COLOR_RAMP } from '@/utils/colors'; import { useEUDRSupplier } from '@/hooks/eudr'; @@ -28,6 +28,7 @@ type DotPropsWithPayload = DotProps & { payload: { alertDate: number } }; const DeforestationAlertsChart = (): JSX.Element => { const [selectedPlots, setSelectedPlots] = useState([]); const [selectedDate, setSelectedDate] = useState(null); + const ticks = useRef([]); const { supplierId }: { supplierId: string } = useParams(); const dispatch = useAppDispatch(); const { @@ -151,6 +152,7 @@ const DeforestationAlertsChart = (): JSX.Element => { margin={{ top: 20, bottom: 15, + right: 20, }} > @@ -159,9 +161,25 @@ const DeforestationAlertsChart = (): JSX.Element => { scale="time" dataKey="alertDate" domain={xDomain} - tickFormatter={(value: string | number, x) => { - if (x === 0) return format(new UTCDate(value), 'LLL yyyy'); - return format(new UTCDate(value), 'LLL'); + minTickGap={25} + tickFormatter={(value: number, index) => { + ticks.current[index] = value; + + const tickDate = new UTCDate(value); + const tickYear = tickDate.getUTCFullYear(); + + if (!ticks.current[index - 1]) { + return format(tickDate, 'LLL yyyy'); + } + + const prevTickDate = new UTCDate(ticks.current[index - 1]); + const prevTickYear = prevTickDate.getUTCFullYear(); + + if (prevTickYear !== tickYear) { + return format(tickDate, 'LLL yyyy'); + } + + return format(tickDate, 'LLL'); }} tickLine={false} padding={{ left: 20, right: 20 }} @@ -188,10 +206,7 @@ const DeforestationAlertsChart = (): JSX.Element => { handleClickDot(payload)} - className={cn('cursor-pointer', { - // todo: fill when we have design - '': payload.alertDate === selectedDate, - })} + className="cursor-pointer stroke-[3px]" /> ); }} @@ -200,11 +215,9 @@ const DeforestationAlertsChart = (): JSX.Element => { return ( handleClickDot(payload)} - className={cn('cursor-pointer', { - // todo: fill when we have design - '': payload.alertDate === selectedDate, - })} + className="cursor-pointer" /> ); }} diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx index 6ab152fff..a28ee809e 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx @@ -1,5 +1,5 @@ import { useParams } from 'next/navigation'; -import { format } from 'date-fns'; +import { format, endOfMonth, startOfMonth } from 'date-fns'; import { UTCDate } from '@date-fns/utc'; import { BellRing } from 'lucide-react'; @@ -10,7 +10,7 @@ import { eudrDetail } from '@/store/features/eudr-detail'; import { useAppSelector } from '@/store/hooks'; import InfoModal from '@/components/legend/item/info-modal'; -const dateFormatter = (date: string) => format(new UTCDate(date), "do 'of' MMMM yyyy"); +const dateFormatter = (date: UTCDate) => format(date, "do 'of' MMMM yyyy"); const DeforestationAlerts = (): JSX.Element => { const { supplierId }: { supplierId: string } = useParams(); @@ -28,6 +28,14 @@ const DeforestationAlerts = (): JSX.Element => { }, ); + const formattedBeginOfMonth = data?.startAlertDate + ? dateFormatter(startOfMonth(new UTCDate(data.startAlertDate))) + : undefined; + + const formattedEndOfMonth = data?.endAlertDate + ? dateFormatter(endOfMonth(new UTCDate(data.endAlertDate))) + : undefined; + return (
@@ -44,9 +52,10 @@ const DeforestationAlerts = (): JSX.Element => {
There were {data?.totalAlerts} deforestation alerts reported for the supplier between the{' '} - {dateFormatter(data.startAlertDate)} and the{' '} + {formattedBeginOfMonth} and the{' '}
- {dateFormatter(data.endAlertDate)}. + {formattedEndOfMonth} + .
diff --git a/client/src/pages/eudr/suppliers/[supplierId].tsx b/client/src/pages/eudr/suppliers/[supplierId].tsx index 48ac37ba1..d1ee58c78 100644 --- a/client/src/pages/eudr/suppliers/[supplierId].tsx +++ b/client/src/pages/eudr/suppliers/[supplierId].tsx @@ -17,6 +17,8 @@ import SupplierInfo from '@/containers/analysis-eudr-detail/supplier-info'; import SupplierSourcingInfo from '@/containers/analysis-eudr-detail/sourcing-info'; import { Separator } from '@/components/ui/separator'; import DeforestationAlerts from '@/containers/analysis-eudr-detail/deforestation-alerts'; +import { eudrDetail } from '@/store/features/eudr-detail'; +import { useAppSelector } from '@/store/hooks'; import type { NextPageWithLayout } from 'pages/_app'; import type { ReactElement } from 'react'; @@ -25,9 +27,15 @@ import type { GetServerSideProps } from 'next'; const MapPage: NextPageWithLayout = () => { const scrollRef = useRef(null); const [isCollapsed, setIsCollapsed] = useState(false); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); const { supplierId }: { supplierId: string } = useParams(); - const { data } = useEUDRSupplier(supplierId); + const { data } = useEUDRSupplier(supplierId, { + startAlertDate: dates.from, + endAlertDate: dates.to, + }); return ( From 3399d5e260ad25dade90ca546087b9f38a2f8837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 17:25:09 +0100 Subject: [PATCH 135/153] fallback for geometries --- client/src/containers/analysis-eudr/map/component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/map/component.tsx b/client/src/containers/analysis-eudr/map/component.tsx index b96b2628e..936281263 100644 --- a/client/src/containers/analysis-eudr/map/component.tsx +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -111,7 +111,7 @@ const EUDRMap = () => { return { type: 'FeatureCollection', - features: plotGeometries.data.features.filter((feature) => { + features: plotGeometries.data.features?.filter((feature) => { if (Object.values(tableFilters).every((filter) => !filter)) return true; if (tableFilters.dfs && data.dfs.indexOf(feature.properties.id) > -1) return true; From 08cba956b0e957215f78df9600c2517159e9f39f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 17:27:27 +0100 Subject: [PATCH 136/153] hides tooltip for deforestation alerts chart --- .../deforestation-alerts/chart/index.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx index e90696a75..c12bda6af 100644 --- a/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -1,15 +1,6 @@ import { UTCDate } from '@date-fns/utc'; import { format } from 'date-fns'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Dot, - ResponsiveContainer, -} from 'recharts'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Dot, ResponsiveContainer } from 'recharts'; import { useParams } from 'next/navigation'; import { useCallback, useMemo, useRef, useState } from 'react'; @@ -188,7 +179,7 @@ const DeforestationAlertsChart = (): JSX.Element => { tickMargin={15} /> - format(new UTCDate(v), 'dd/MM/yyyy')} /> + {/* format(new UTCDate(v), 'dd/MM/yyyy')} /> */} {plotConfig?.map(({ name, color }) => { return ( Date: Wed, 20 Mar 2024 17:41:38 +0100 Subject: [PATCH 137/153] changes category names --- .../deforestation-free-suppliers/index.tsx | 2 +- .../suppliers-with-deforestation-alerts/index.tsx | 2 +- .../suppliers-with-no-location-data/index.tsx | 2 +- .../analysis-eudr/category-list/index.tsx | 15 +++++++++------ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx index 06e09e945..ced54aa28 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -35,7 +35,7 @@ const DeforestationFreeSuppliersBreakdown = () => { const dataByView = data?.[viewBy] || []; return Object.keys(dataByView) - .filter((key) => key === CATEGORIES[0].name) + .filter((key) => key === CATEGORIES[0].apiName) .map((filteredKey) => dataByView[filteredKey].detail .map((item) => ({ diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx index cf701755e..4372b4580 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -35,7 +35,7 @@ const SuppliersWithDeforestationAlertsBreakdown = () => { const dataByView = data?.[viewBy] || []; return Object.keys(dataByView) - .filter((key) => key === CATEGORIES[1].name) + .filter((key) => key === CATEGORIES[1].apiName) .map((filteredKey) => dataByView[filteredKey].detail .map((item) => ({ diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx index 75665d596..412820758 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -35,7 +35,7 @@ const SuppliersWithNoLocationDataBreakdown = () => { const dataByView = data?.[viewBy] || []; return Object.keys(dataByView) - .filter((key) => key === CATEGORIES[2].name) + .filter((key) => key === CATEGORIES[2].apiName) .map((filteredKey) => dataByView[filteredKey].detail .map((item) => ({ diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 6a876cbc9..995ab4376 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -13,17 +13,20 @@ import { themeColors } from '@/utils/colors'; export const CATEGORIES = [ { - name: 'Deforestation-free suppliers', + name: 'Deforestation-free plots', + apiName: 'Deforestation-free suppliers', key: 'dfs', color: themeColors.blue[400], }, { - name: 'Suppliers with deforestation alerts', + name: 'Plots with defrestation alerts', + apiName: 'Suppliers with deforestation alerts', key: 'sda', color: '#FFC038', }, { - name: 'Suppliers with no location data', + name: 'Plots with no location data', + apiName: 'Suppliers with no location data', key: 'tpl', color: '#8561FF', }, @@ -84,9 +87,9 @@ export const CategoryList = (): JSX.Element => { const dataByView = data?.[viewBy] || []; return Object.keys(dataByView).map((key) => { - const category = CATEGORIES.find((category) => category.name === key); + const category = CATEGORIES.find((category) => category.apiName === key); return { - name: key, + name: category.name, ...dataByView[key], key: category?.key, color: category?.color || '#000', @@ -110,7 +113,7 @@ export const CategoryList = (): JSX.Element => { className="block min-h-4 min-w-4 rounded-full border-2 transition-colors" style={{ borderColor: category.color, - ...(categories[category.name] && { + ...(categories[category.key] && { backgroundColor: category.color, }), }} From ac1d30d66625ccc8503dd9b4477f5e5ba007d0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 17:44:32 +0100 Subject: [PATCH 138/153] of suppliers --- .../category-list/breakdown/breakdown-item/index.tsx | 2 +- client/src/containers/analysis-eudr/category-list/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx index 2fff1939c..4581ea44e 100644 --- a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx @@ -21,7 +21,7 @@ const BreakdownItem = ({
{`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format(value)}%`}{' '} - of suppliers + of plots
diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 995ab4376..97b1c8edc 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -125,7 +125,7 @@ export const CategoryList = (): JSX.Element => { {`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format( category.totalPercentage, )}%`}{' '} - of suppliers + of plots
Date: Wed, 20 Mar 2024 17:45:41 +0100 Subject: [PATCH 139/153] updates column names --- .../analysis-eudr/supplier-list-table/table/columns.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx index 8e5ff77a8..674358619 100644 --- a/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -48,7 +48,7 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'dfs', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const dfs: number = row.getValue('dfs'); return {`${Number.isNaN(dfs) ? '-' : `${numberFormat.format(dfs)}%`}`}; @@ -56,7 +56,7 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'sda', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const sda: number = row.getValue('sda'); return {`${Number.isNaN(sda) ? '-' : `${numberFormat.format(sda)}%`}`}; @@ -64,7 +64,7 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'tpl', - header: ({ column }) => , + header: ({ column }) => , cell: ({ row }) => { const tpl: number = row.getValue('tpl'); From 2526e722a38330b0c88aad04b3a4aac30f1426df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Wed, 20 Mar 2024 18:07:02 +0100 Subject: [PATCH 140/153] updates legend with plots --- .../containers/analysis-eudr/map/layers.json | 21 ++++++++++++--- .../analysis-eudr/map/legend/component.tsx | 26 ++++++++++++++++--- .../analysis-eudr/map/legend/item.tsx | 7 +++-- client/tailwind.config.ts | 2 +- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/client/src/containers/analysis-eudr/map/layers.json b/client/src/containers/analysis-eudr/map/layers.json index b18ddc80c..001cda65b 100644 --- a/client/src/containers/analysis-eudr/map/layers.json +++ b/client/src/containers/analysis-eudr/map/layers.json @@ -1,14 +1,27 @@ [ { "id": "suppliers-plot-of-land", - "title": "Supplier's with specified plot of land", - "description": "Suppliers with necessary geographical data of land to assess deforestation risk linked to a commodity", + "title": "Plots with deforestation alerts", + "description": "Land areas where recent deforestation activities have been detected using near-real-time RADD alerts", "content": "Suppliers’s geometry of the land within a single real estate property, as recognised by the law of the country of production, which possesses sufficiently homogeneous conditions to allow an evaluation of the aggregate level of risk of deforestation and forest degradation associated with relevant commodities produced on that land.", "citation": null, "source": null, "type": "layer", + "legendConfig": { + "iconClass": "border-navy-400 bg-navy-400/30", + "items": null + } + }, + { + "id": "suppliers-plot-of-land", + "title": "Deforestation-free plots", + "description": "Land areas where recent deforestation activities have been detected using near-real-time RADD alerts", + "content": "Deforestation-free plots are areas verified to be free from recent deforestation, monitored using near-real-time RADD alerts. These alerts, adhering to the EUDR definition and following the WHISP methodology developed by FAO and WRI, provide robust monitoring of deforestation activities, enabling proactive measures to maintain forest integrity and sustainability.", + "citation": null, + "source": null, + "type": "layer", "legend": { - "iconClassName": "border-2 border-orange-500 bg-orange-500/30", + "iconClass": "border-[#ffc038] bg-[#ffc038]/30", "items": null } }, @@ -16,7 +29,7 @@ "id": "forest-cover-2020-ec-jrc", "title": "Forest Cover 2020 (EC JRC)", "description": "Spatial explicit description of forest presence and absence in the year 2020.", - "content": "Suppliers’s geometry of the land within a single real estate property, as recognised by the law of the country of production, which possesses sufficiently homogeneous conditions to allow an evaluation of the aggregate level of risk of deforestation and forest degradation associated with relevant commodities produced on that land.", + "content": "Plots with deforestation alerts represent areas where recent deforestation activities have been detected using near-real-time RADD alerts. These alerts adhere to the EUDR definition and follow the WHISP methodology developed by FAO and WRI, providing robust monitoring of deforestation activities and enabling timely intervention measures to address forest degradation and promote sustainable land management practices.", "citation": [ "Bourgoin, Clement; Ameztoy, Iban; Verhegghen, Astrid; Carboni, Silvia; Colditz, Rene R.; Achard, Frederic (2023): Global map of forest cover 2020 - version 1. European Commission, Joint Research Centre (JRC) [Dataset] PID: http://data.europa.eu/89h/10d1b337-b7d1-4938-a048-686c8185b290." ], diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index 12871e5b9..cf65b6db2 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -21,7 +21,9 @@ const EURDLegend = () => { const [isOpen, setIsOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); - const supplierPlotsData = LayersData.find((layer) => layer.id === 'suppliers-plot-of-land'); + // const supplierPlotsData = LayersData.find((layer) => layer.id === 'suppliers-plot-of-land'); + const PDAData = LayersData.find((layer) => layer.title === 'Plots with deforestation alerts'); + const DFPData = LayersData.find((layer) => layer.title === 'Deforestation-free plots'); const contextualLayersData = LayersData.filter((layer) => layer.type === 'contextual'); return ( @@ -43,9 +45,25 @@ const EURDLegend = () => {

Legend

+ dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) + } + changeOpacity={(opacity) => + dispatch(setSupplierLayer({ ...supplierLayer, opacity })) + } + /> +
+
+ diff --git a/client/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx index 44efbc152..ac045468d 100644 --- a/client/src/containers/analysis-eudr/map/legend/item.tsx +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -1,6 +1,6 @@ -import classNames from 'classnames'; import { EyeIcon, EyeOffIcon } from '@heroicons/react/solid'; +import { cn } from '@/lib/utils'; import { Switch } from '@/components/ui/switch'; import OpacityControl from '@/components/legend/item/opacityControl'; import InfoModal from '@/components/legend/item/info-modal'; @@ -14,6 +14,7 @@ type LegendItemProps = { source?: string | string[]; legendConfig?: { iconColor?: string; + iconClass?: string; items?: { label: string; color: string }[]; dates?: string[]; }; @@ -40,7 +41,9 @@ const LegendItem: FC> = ({ return (
Date: Wed, 20 Mar 2024 18:08:28 +0100 Subject: [PATCH 141/153] updates title of sourcing information chart --- .../analysis-eudr-detail/sourcing-info/chart/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx index e3c069018..cb39082e8 100644 --- a/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx @@ -84,10 +84,10 @@ const SupplierSourcingInfoChart = (): JSX.Element => {
-

Individual plot contributions to volume accumulation

+

Percentage of sourcing volume per plot

Date: Wed, 20 Mar 2024 18:27:34 +0100 Subject: [PATCH 142/153] updates label --- .../analysis-eudr/suppliers-stacked-bar/component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx index ea0c2cd26..4f7faca8f 100644 --- a/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -153,7 +153,7 @@ const SuppliersStackedBar = () => { type="number" label={
{hoverInfo?.object && ( diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 39e3f37c1..9b190baf1 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -154,3 +154,8 @@ .recharts-label { @apply text-xs fill-gray-400; } + +/* Maplibre Compare */ +.maplibregl-compare .compare-swiper-horizontal { + @apply bg-navy-400 w-8 h-8 -mt-4 bg-cover; +} From ab5febdbcb148adca947baf22ad7aba98733c89d Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 21 Mar 2024 08:52:41 +0100 Subject: [PATCH 151/153] added temporally env variables cc Alex --- client/next.config.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/next.config.js b/client/next.config.js index 316c22618..00a6b0d18 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -33,6 +33,15 @@ const nextConfig = { }, ]; }, + env: { + NEXT_PUBLIC_PLANET_API_KEY: 'PLAK6679039df83f414faf798ba4ad4530db', + NEXT_PUBLIC_CARTO_FOREST_ACCESS_TOKEN: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjY2JlMjUyYSJ9.LoqzuDp076ESVYmHm1mZNtfhnqOVGmSxzp60Fht8PQw', + NEXT_PUBLIC_CARTO_DEFORESTATION_ACCESS_TOKEN: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiJjZDk0ZWIyZSJ9.oqLagnOEc-j7Z4hY-MTP1yoZA_vJ7WYYAkOz_NUmCJo', + NEXT_PUBLIC_CARTO_RADD_ACCESS_TOKEN: + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfemsydWhpaDYiLCJqdGkiOiI3NTFkNzA1YSJ9.jrVugV7HYfhmjxj-p2Iks8nL_AjHR91Q37JVP2fNmtc', + }, }; module.exports = nextConfig; From bf9022b524ed7b7ce660565043be84ddfccab511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 21 Mar 2024 09:59:33 +0100 Subject: [PATCH 152/153] legend updates --- .../src/containers/analysis-eudr/category-list/index.tsx | 2 +- client/src/containers/analysis-eudr/map/layers.json | 8 ++++---- .../src/containers/analysis-eudr/map/legend/component.tsx | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/containers/analysis-eudr/category-list/index.tsx b/client/src/containers/analysis-eudr/category-list/index.tsx index 97b1c8edc..1dd48ddd7 100644 --- a/client/src/containers/analysis-eudr/category-list/index.tsx +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -19,7 +19,7 @@ export const CATEGORIES = [ color: themeColors.blue[400], }, { - name: 'Plots with defrestation alerts', + name: 'Plots with deforestation alerts', apiName: 'Suppliers with deforestation alerts', key: 'sda', color: '#FFC038', diff --git a/client/src/containers/analysis-eudr/map/layers.json b/client/src/containers/analysis-eudr/map/layers.json index 001cda65b..c0d990822 100644 --- a/client/src/containers/analysis-eudr/map/layers.json +++ b/client/src/containers/analysis-eudr/map/layers.json @@ -7,21 +7,21 @@ "citation": null, "source": null, "type": "layer", - "legendConfig": { - "iconClass": "border-navy-400 bg-navy-400/30", + "legend": { + "iconClass": "border-[#ffc038] bg-[#ffc038]/30", "items": null } }, { "id": "suppliers-plot-of-land", "title": "Deforestation-free plots", - "description": "Land areas where recent deforestation activities have been detected using near-real-time RADD alerts", + "description": "Land areas with free deforestation monitored using near-real-time RADD alerts", "content": "Deforestation-free plots are areas verified to be free from recent deforestation, monitored using near-real-time RADD alerts. These alerts, adhering to the EUDR definition and following the WHISP methodology developed by FAO and WRI, provide robust monitoring of deforestation activities, enabling proactive measures to maintain forest integrity and sustainability.", "citation": null, "source": null, "type": "layer", "legend": { - "iconClass": "border-[#ffc038] bg-[#ffc038]/30", + "iconClass": "border-[#4ab8f3] bg-[#4ab8f3]/30", "items": null } }, diff --git a/client/src/containers/analysis-eudr/map/legend/component.tsx b/client/src/containers/analysis-eudr/map/legend/component.tsx index cf65b6db2..fb3bb673a 100644 --- a/client/src/containers/analysis-eudr/map/legend/component.tsx +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -66,6 +66,7 @@ const EURDLegend = () => { description={PDAData.description} showVisibility isActive={supplierLayer.active} + legendConfig={PDAData.legend} changeVisibility={() => dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) } From b8731eaf65ee7c6532d8ccdd5df885005e14607a Mon Sep 17 00:00:00 2001 From: David Inga Date: Thu, 21 Mar 2024 11:23:06 +0100 Subject: [PATCH 153/153] disabling tree data node in all filters --- client/src/components/tree-select/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/tree-select/utils.ts b/client/src/components/tree-select/utils.ts index 60a0e4e36..3fab069b7 100644 --- a/client/src/components/tree-select/utils.ts +++ b/client/src/components/tree-select/utils.ts @@ -155,6 +155,7 @@ const optionToTreeData = ( const children = option.children?.map((option) => optionToTreeData(option, render, depth + 1)); return render({ ...option, + disabled: true, // added to prevent the node from being selectable only for EUDR demo style: { paddingLeft: 16 * depth }, children, });