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: | 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/.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: diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 7c713ec1f..a9b0514db 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -72,5 +72,11 @@ "email": { "sendGridApiKey": "SENDGRID_API_KEY" } + }, + "eudr": { + "apiKey": "CARTO_API_KEY", + "baseUrl": "CARTO_BASE_URL", + "credentials": "EUDR_CREDENTIALS", + "dataset": "EUDR_DATASET" } } diff --git a/api/config/default.json b/api/config/default.json index ed6d80b7f..9f83e9a0d 100644 --- a/api/config/default.json +++ b/api/config/default.json @@ -81,5 +81,11 @@ "email": { "sendGridApiKey": null } + }, + "eudr": { + "apiKey": null, + "baseUrl": "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/package.json b/api/package.json index 2b82db8c7..9028353d7 100644 --- a/api/package.json +++ b/api/package.json @@ -20,12 +20,14 @@ "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": { "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", @@ -69,6 +71,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" }, @@ -85,6 +88,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/app.module.ts b/api/src/app.module.ts index 4b014f13a..651897d42 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-alerts/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/create-swagger-specification.ts b/api/src/create-swagger-specification.ts new file mode 100644 index 000000000..536d9f133 --- /dev/null +++ b/api/src/create-swagger-specification.ts @@ -0,0 +1,35 @@ +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'); +} + +const active: boolean = false; + +export async function createOrUpdateSwaggerSpec(document: any): Promise { + 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); + + 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); + } + } else { + console.log('Swagger spec update is not active.'); + return; + } +} 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/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/src/modules/admin-regions/admin-regions.controller.ts b/api/src/modules/admin-regions/admin-regions.controller.ts index f00b0197e..b44cffd83 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`) 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/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..9af1372eb --- /dev/null +++ b/api/src/modules/eudr-alerts/alerts-query-builder/big-query-alerts-query.builder.ts @@ -0,0 +1,186 @@ +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 enum EUDR_ALERTS_DATABASE_FIELDS { + alertDate = 'date', + alertConfidence = 'alert_confidence', + alertCount = 'pixel_count', + geoRegionId = 'georegionid', + supplierId = 'supplierid', + carbonRemovals = 'carbon_removals', + dataset = 'dataset', +} + +export class BigQueryAlertsQueryBuilder { + queryBuilder: SelectQueryBuilder; + dto?: GetEUDRAlertsDto; + + constructor( + queryBuilder: SelectQueryBuilder, + getAlertsDto?: GetEUDRAlertsDto, + ) { + this.queryBuilder = queryBuilder; + 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; + } + + orderBy(field: string, order: 'ASC' | 'DESC'): this { + this.queryBuilder.orderBy(field, order); + 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( + `${EUDRAlertsFields.supplierId} IN (:...supplierIds)`, + { + supplierIds: this.dto.supplierIds, + }, + ); + } + if (this.dto?.geoRegionIds) { + this.queryBuilder.andWhere( + `${EUDRAlertsFields.geoRegionId} IN (:...geoRegionIds)`, + { + geoRegionIds: this.dto.geoRegionIds, + }, + ); + } + if (this.dto?.alertConfidence) { + this.queryBuilder.andWhere( + `${EUDR_ALERTS_DATABASE_FIELDS.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(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) BETWEEN DATE(:startAlertDate) AND DATE(:endAlertDate)`, + { + startAlertDate: this.dto?.startAlertDate, + endAlertDate: this.dto?.endAlertDate, + }, + ); + } + + addAlertDateGreaterThanOrEqual(): void { + this.queryBuilder.andWhere( + `DATE(${EUDR_ALERTS_DATABASE_FIELDS.alertDate}) >= DATE(:startAlertDate)`, + { + startAlertDate: this.dto?.startAlertDate, + }, + ); + } + + addAlertDateLessThanOrEqual(): void { + this.queryBuilder.andWhere( + `DATE(${EUDR_ALERTS_DATABASE_FIELDS.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.entity.ts b/api/src/modules/eudr-alerts/alerts.entity.ts new file mode 100644 index 000000000..c716caef7 --- /dev/null +++ b/api/src/modules/eudr-alerts/alerts.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-alerts/alerts.repository.ts b/api/src/modules/eudr-alerts/alerts.repository.ts new file mode 100644 index 000000000..762a54690 --- /dev/null +++ b/api/src/modules/eudr-alerts/alerts.repository.ts @@ -0,0 +1,177 @@ +import { + BigQuery, + Query, + SimpleQueryRowsResponse, +} from '@google-cloud/bigquery'; +import { + Inject, + Injectable, + Logger, + ServiceUnavailableException, +} from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { AlertsOutput } from 'modules/eudr-alerts/dto/alerts-output.dto'; +import { + AlertedGeoregionsBySupplier, + EUDRAlertDatabaseResult, + EUDRAlertDates, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; +import { GetEUDRAlertsDto } from 'modules/eudr-alerts/dto/get-alerts.dto'; +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'; + +export enum EUDRAlertsFields { + alertDate = 'date', + alertConfidence = 'alertconfidence', + year = 'year', + alertCount = 'pixel_count', + geoRegionId = 'georegionid', + supplierId = 'supplierid', +} + +@Injectable() +export class AlertsRepository implements IEUDRAlertsRepository { + logger: Logger = new Logger(AlertsRepository.name); + bigQueryClient: BigQuery; + + 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(this.credentials), + projectId, + }); + } + + async getAlerts(dto?: GetEUDRAlertsDto): Promise { + const queryBuilder: BigQueryAlertsQueryBuilder = + this.createQueryBuilder(dto); + // TODO: Make field selection dynamic + queryBuilder.from(this.baseDataset, 'alerts'); + 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); + } + + async getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise { + const queryBuilder: BigQueryAlertsQueryBuilder = + this.createQueryBuilder(dto); + queryBuilder.from(this.baseDataset, 'alerts'); + 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); + } + + async getDates(dto: GetEUDRAlertsDto): Promise { + const queryBuilder: BigQueryAlertsQueryBuilder = + this.createQueryBuilder(dto); + queryBuilder.from(this.baseDataset, 'alerts'); + 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); + } + + async getAlertSummary(dto: any): Promise { + const bigQueryBuilder: BigQueryAlertsQueryBuilder = + this.createQueryBuilder(dto); + bigQueryBuilder + .from(this.baseDataset, 'alerts') + .select('supplierid', 'supplierId') + .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 = + this.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', + ) + .from('(' + bigQueryBuilder.getQuery() + ')', 'alerts_summary') + .setParameters(bigQueryBuilder.getParameters()); + + return this.query(mainQueryBuilder); + } + + private async query(queryBuilder: BigQueryAlertsQueryBuilder): Promise { + try { + const response: SimpleQueryRowsResponse = await this.bigQueryClient.query( + queryBuilder.buildQuery(), + ); + 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.', + ); + } + } + + private createQueryBuilder( + dto?: GetEUDRAlertsDto, + ): BigQueryAlertsQueryBuilder { + return new BigQueryAlertsQueryBuilder( + this.dataSource.createQueryBuilder(), + dto, + ); + } +} diff --git a/api/src/modules/eudr-alerts/carto/carto.connector.ts b/api/src/modules/eudr-alerts/carto/carto.connector.ts new file mode 100644 index 000000000..57b54254d --- /dev/null +++ b/api/src/modules/eudr-alerts/carto/carto.connector.ts @@ -0,0 +1,52 @@ +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; + logger: Logger = new Logger(CartoConnector.name); + + constructor(private readonly httpService: HttpService) { + const { apiKey, baseUrl } = AppConfig.get('carto'); + this.cartoApiKey = apiKey; + this.cartoBaseUrl = baseUrl; + if (!this.cartoApiKey || !this.cartoBaseUrl) { + 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.', + ); + } + + 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-alerts/dashboard/dashboard-detail.types.ts b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts new file mode 100644 index 000000000..2d28ce712 --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-detail.types.ts @@ -0,0 +1,94 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { AffectedPlots } from './dashboard.types'; + +export class EUDRDashBoardDetail { + @ApiProperty() + name: string; + @ApiProperty() + address: string; + @ApiProperty() + companyId: string; + @ApiProperty({ type: () => AffectedPlots }) + plots: AffectedPlots; + @ApiProperty({ type: () => DashBoardDetailCountry, isArray: true }) + sourcingInformation: DashBoardDetailSourcingInformation[]; + @ApiProperty({ type: () => DashBoardDetailAlerts, isArray: true }) + 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() + 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; + @ApiProperty() + geoRegionId: string; + @ApiProperty() + plotName: string; +} + +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() + alertDate: string; + @ApiProperty({ type: () => AlertPlots, isArray: true }) + plots: AlertPlots[]; +} + +class AlertPlots { + @ApiProperty() + geoRegionId: string; + @ApiProperty() + plotName: string; + @ApiProperty() + alertCount: number; +} 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..f8734a825 --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/dashboard-utils.ts @@ -0,0 +1,171 @@ +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], + })); +}; + +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({ + 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[] = []; + + for ( + let month = new Date(start); + month <= end; + month.setMonth(month.getMonth() + 1) + ) { + const yearMonthKey = `${month.getFullYear()}-${(month.getMonth() + 1) + .toString() + .padStart(2, '0')}`; + 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)); +}; + +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 new file mode 100644 index 000000000..e372a38c1 --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/dashboard.types.ts @@ -0,0 +1,115 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AffectedPlots { + @ApiProperty() + dfs: string[]; + @ApiProperty() + sda: string[]; +} + +export class DashBoardTableElements { + supplierId: string; + @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() + crm: number; + + @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) + materials: EntitiesBySupplier[]; + + @ApiProperty({ type: () => EntitiesBySupplier, isArray: true }) + origins: EntitiesBySupplier[]; + + @ApiProperty({ type: () => AffectedPlots }) + plots: AffectedPlots; +} + +class EntitiesBySupplier { + @ApiProperty() + name: string; + + @ApiProperty() + id: 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() + totalPercentage: number; + + @ApiProperty({ type: () => BreakDownByEntity, isArray: true }) + detail: BreakDownByEntity[]; +} + +export type EntityMetadata = { + supplierId: string; + supplierName: string; + companyId: string; + materialId: string; + materialName: string; + adminRegionId: string; + adminRegionName: string; + totalBaselineVolume: number; + knownGeoRegions: number; + totalSourcingLocations: number; + isoA3: string; +}; + +class BreakDownByEUDRCategory { + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS]: CategoryDetail; + + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS]: CategoryDetail; + + @ApiProperty({ type: () => CategoryDetail }) + [EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA]: CategoryDetail; +} + +class BreakDownByEntity { + @ApiProperty() + name: string; + + @ApiProperty() + value: number; +} + +export class EUDRBreakDown { + @ApiProperty({ type: () => BreakDownByEUDRCategory }) + materials: BreakDownByEUDRCategory; + + @ApiProperty({ type: () => BreakDownByEUDRCategory }) + origins: any; +} + +export class EUDRDashboard { + @ApiProperty({ type: () => DashBoardTableElements, isArray: true }) + table: DashBoardTableElements[]; + + @ApiProperty({ type: () => EUDRBreakDown }) + breakDown: EUDRBreakDown; +} 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 new file mode 100644 index 000000000..21203b3fc --- /dev/null +++ b/api/src/modules/eudr-alerts/dashboard/eudr-dashboard.service.ts @@ -0,0 +1,641 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; +import { + AlertedGeoregionsBySupplier, + IEUDRAlertsRepository, +} 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 'modules/eudr-alerts/eudr.controller'; +import { + DashBoardTableElements, + EntityMetadata, + EUDRDashboard, + EUDRDashBoardFields, +} from 'modules/eudr-alerts/dashboard/dashboard.types'; +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 'modules/eudr-alerts/dashboard/dashboard-detail.types'; +import { MaterialsService } from 'modules/materials/materials.service'; +import { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { + aggregateAndCalculatePercentage, + findNonAlertedGeoRegions, + groupAndFillAlertsByMonth, +} from './dashboard-utils'; + +@Injectable() +export class EudrDashboardService { + constructor( + @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 materials: Record = { + [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: Record = { + [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 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: 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)) { + const geoRegionIdSet: Set = new Set(); + const carbonRemovalValuesForSupplier: number[] = []; + alertMap.set(supplierId, { + geoRegionIdSet, + carbonRemovalValuesForSupplier, + }); + } + alertMap.get(supplierId)!.geoRegionIdSet.add(geoRegionId); + alertMap + .get(supplierId)! + .carbonRemovalValuesForSupplier.push(alert.carbonRemovals ?? 0); + }); + + 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(); + + entityMetadata.forEach((entity: EntityMetadata) => { + const { + supplierId, + materialId, + adminRegionId, + totalSourcingLocations, + knownGeoRegions, + supplierName, + companyId, + materialName, + adminRegionName, + totalBaselineVolume, + isoA3, + } = entity; + + const alertedGeoRegionsCount: number = + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; + const nonAlertedGeoRegions: number = + parseInt(String(totalSourcingLocations)) - + parseInt(String(alertedGeoRegionsCount)); + const unknownGeoRegions: number = + parseInt(String(totalSourcingLocations)) - + parseInt(String(knownGeoRegions)); + + const sdaPercentage: number = + (alertedGeoRegionsCount / totalSourcingLocations) * 100; + 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)) { + const alertedGeoRegions: string[] = [ + ...(alertMap.get(supplierId)?.geoRegionIdSet || []), + ]; + suppliersMap.set(supplierId, { + supplierId, + supplierName, + companyId, + plots: { + dfs: nonAlertedGeoregions[supplierId] || [], + sda: alertedGeoRegions, + }, + materials: [], + origins: [], + totalBaselineVolume: 0, + dfs: 0, + sda: 0, + tpl: 0, + crm: 0, + }); + } + const supplier = suppliersMap.get(supplierId); + supplier.totalBaselineVolume = totalBaselineVolume; + supplier.dfs = dfsPercentage; + supplier.sda = sdaPercentage; + supplier.tpl = tplPercentage; + supplier.crm = carbonRemovalSumForSupplier; + supplier.materials.push({ id: materialId, name: materialName }); + supplier.origins.push({ + id: adminRegionId, + name: adminRegionName, + isoA3: isoA3, + }); + + if (!materialsMap.has(materialId)) { + materialsMap.set(materialId, { + materialId, + materialName, + suppliers: new Set(), + totalSourcingLocations: 0, + knownGeoRegions: 0, + alertedGeoRegions: 0, + }); + } + const material = materialsMap.get(materialId); + material.suppliers.add(supplierId); + material.totalSourcingLocations += parseFloat( + String(totalSourcingLocations), + ); + material.knownGeoRegions += parseInt(String(knownGeoRegions)); + material.alertedGeoRegions += + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; + + if (!originsMap.has(adminRegionId)) { + originsMap.set(adminRegionId, { + adminRegionId, + adminRegionName, + isoA3, + suppliers: new Set(), + totalSourcingLocations: 0, + knownGeoRegions: 0, + alertedGeoRegions: 0, + }); + } + const origin = originsMap.get(adminRegionId); + origin.suppliers.add(supplierId); + origin.totalSourcingLocations += parseInt(String(totalSourcingLocations)); + origin.knownGeoRegions += parseInt(String(knownGeoRegions)); + origin.alertedGeoRegions += + alertMap.get(supplierId)?.geoRegionIdSet.size || 0; + }); + + materialsMap.forEach((material, materialId) => { + const { + materialName, + totalSourcingLocations, + knownGeoRegions, + alertedGeoRegions, + } = material; + const nonAlertedGeoRegions: number = knownGeoRegions - alertedGeoRegions; + const unknownGeoRegions = totalSourcingLocations - knownGeoRegions; + + const sdaPercentage: number = + (alertedGeoRegions / totalSourcingLocations) * 100; + const tplPercentage: number = + (unknownGeoRegions / totalSourcingLocations) * 100; + const dfsPercentage: number = + 100 - (sdaPercentage + tplPercentage) > 0 + ? 100 - (sdaPercentage + tplPercentage) + : 0; + + materials[EUDRDashBoardFields.DEFORASTATION_FREE_SUPPLIERS].detail.push({ + name: materialName, + id: materialId, + value: dfsPercentage, + }); + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_DEFORASTATION_ALERTS + ].detail.push({ + name: materialName, + id: materialId, + value: sdaPercentage, + }); + materials[ + EUDRDashBoardFields.SUPPLIERS_WITH_NO_LOCATION_DATA + ].detail.push({ + name: materialName, + id: materialId, + value: tplPercentage, + }); + }); + + // @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; + }); + + 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_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, + }); + }); + + // @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; + }); + + 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, + crm: supplier.crm, + plots: supplier.plots, + }); + }); + + return { + table: table, + breakDown: { materials, origins } as any, + }; + } + + /** + * @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 + .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') + .addSelect('COUNT(sl.geoRegionId)', 'knownGeoRegions') + .addSelect('COUNT(sl.id)', 'totalSourcingLocations') + .addSelect('ar.isoA3', 'isoA3') + .from(SourcingLocation, 'sl') + .leftJoin(Supplier, 's', 's.id = sl.producerId') + .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 }) + .groupBy('s.id') + .addGroupBy('m.id') + .addGroupBy('ar.id'); + if (dto.producerIds) { + queryBuilder.andWhere('s.id IN (:...producerIds)', { + 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(); + } + + 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, + ): Promise { + const result: any = {}; + const sourcingInformation: any = {}; + let supplier: Supplier; + const geoRegionMap: Map = new Map(); + const allGeoRegionsBySupplier: string[] = []; + + 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 sourcingRecords: { + year: number; + volume: number; + 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)) { + geoRegionMap.set(geoRegion.geoRegionId, { + plotName: geoRegion.plotName, + }); + } + const queryBuilder: SelectQueryBuilder = manager + .createQueryBuilder(SourcingRecord, 'sr') + .leftJoin(SourcingLocation, 'sl', 'sr.sourcingLocationId = sl.id') + .leftJoin(GeoRegion, 'gr', 'gr.id = sl.geoRegionId'); + if (!geoRegion.geoRegionId) { + 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, + }) + .select([ + 'sr.year AS year', + 'sr.tonnage AS volume', + 'gr.name as "plotName"', + 'gr.id as "geoRegionId"', + ]); + + 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 + parseFloat(cur.volume), + 0, + ); + const totalArea: number = geoRegions.reduce( + (acc: number, cur: any) => acc + parseFloat(cur.totalArea ?? 0), + 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 = + aggregateAndCalculatePercentage(sourcingRecords); + } + + const alertsOutput: AlertsOutput[] = await this.eudrRepository.getAlerts({ + supplierIds: [supplierId], + startAlertDate: dto?.startAlertDate, + endAlertDate: dto?.endAlertDate, + }); + + const affectedGeoRegionIds: Set = new Set(); + const { totalAlerts, totalCarbonRemovals } = alertsOutput.reduce( + ( + acc: { totalAlerts: number; totalCarbonRemovals: number }, + cur: AlertsOutput, + ) => { + acc.totalAlerts += cur.alertCount; + acc.totalCarbonRemovals += cur.carbonRemovals ?? 0; + affectedGeoRegionIds.add(cur.geoRegionId); + return acc; + }, + { totalAlerts: 0, totalCarbonRemovals: 0 }, + ); + const startAlertDate: string | null = + alertsOutput[0]?.alertDate?.value.toString() || null; + const endAlertDate: string | null = + 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: dto?.startAlertDate ?? startAlertDate, + endAlertDate: dto?.endAlertDate ?? endAlertDate, + totalAlerts, + totalCarbonRemovals, + values: groupAndFillAlertsByMonth( + alertsOutput, + geoRegionMap, + finalStartDate, + finalEndDate, + ), + }; + + const nonAlertedGeoRegions: string[] = allGeoRegionsBySupplier.filter( + (id: string) => ![...affectedGeoRegionIds].includes(id), + ); + result.plots = { + dfs: nonAlertedGeoRegions, + sda: [...affectedGeoRegionIds], + }; + + result.alerts = alerts; + + return result; + }); + } +} diff --git a/api/src/modules/eudr-alerts/dto/alerts-input.dto.ts b/api/src/modules/eudr-alerts/dto/alerts-input.dto.ts new file mode 100644 index 000000000..4e90a0cb6 --- /dev/null +++ b/api/src/modules/eudr-alerts/dto/alerts-input.dto.ts @@ -0,0 +1,10 @@ +// Input DTO to be ingested by Carto + +import { GeoJSON } from 'geojson'; + +export class EudrInput { + supplierId: string; + geoRegionId: string; + geom: GeoJSON; + year: number; +} 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..8c91c9216 --- /dev/null +++ b/api/src/modules/eudr-alerts/dto/alerts-output.dto.ts @@ -0,0 +1,16 @@ +export type AlertsOutput = { + alertCount: number; + alertDate: { + value: Date | string; + }; + year: number; + supplierId: string; + geoRegionId: string; + carbonRemovals: number; +}; + +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 new file mode 100644 index 000000000..df21cbafd --- /dev/null +++ b/api/src/modules/eudr-alerts/dto/get-alerts.dto.ts @@ -0,0 +1,69 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsDate, + IsInt, + IsNumber, + IsOptional, + IsUUID, +} from 'class-validator'; + +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[]; + + @ApiPropertyOptional() + @IsOptional() + @IsArray() + @IsUUID('4', { each: true }) + geoRegionIds?: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @Type(() => Number) + startYear?: number; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @Type(() => Number) + endYear?: number; + + alertConfidence?: 'high' | 'medium' | 'low'; + + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + startAlertDate?: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsDate() + @Type(() => Date) + endAlertDate?: Date; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + limit?: number; +} diff --git a/api/src/modules/eudr-alerts/eudr.controller.ts b/api/src/modules/eudr-alerts/eudr.controller.ts new file mode 100644 index 000000000..ff36d1521 --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.controller.ts @@ -0,0 +1,286 @@ +import { + Controller, + Get, + Param, + Query, + UseInterceptors, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiForbiddenResponse, + ApiOkResponse, + ApiOperation, + ApiPropertyOptional, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; + +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 '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 { EudrService } from 'modules/eudr-alerts/eudr.service'; +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 { + GeoFeatureCollectionResponse, + GeoFeatureResponse, +} from 'modules/geo-regions/dto/geo-feature-response.dto'; +import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; +import { IsDate, IsOptional, IsUUID } from 'class-validator'; +import { Type } from 'class-transformer'; +import { EUDRDashboard } from './dashboard/dashboard.types'; +import { EUDRDashBoardDetail } from './dashboard/dashboard-detail.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[]; + + @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') +@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, + 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('/materials') + 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); + } + + @ApiOperation({ + description: 'Get EUDR alerts dates', + }) + @ApiOkResponse({ + type: EUDRAlertDates, + isArray: true, + }) + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @Get('/dates') + async getAlertDates( + @Query(ValidationPipe) dto: GetEUDRAlertsDto, + ): Promise<{ data: EUDRAlertDates[] }> { + const dates: EUDRAlertDates[] = await this.eudrAlertsService.getDates(dto); + return { + data: dates, + }; + } + + @Get('/alerts') + async getAlerts(@Query(ValidationPipe) dto: GetEUDRAlertsDto): Promise { + return this.eudrAlertsService.getAlerts(dto); + } + + @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 { + if (dto.originIds) { + dto.originIds = await this.adminRegionsService.getAdminRegionDescendants( + dto.originIds, + ); + } + if (dto.materialIds) { + dto.materialIds = await this.materialsService.getMaterialsDescendants( + dto.materialIds, + ); + } + return this.geoRegionsService.getGeoJson({ ...dto, eudr: true }); + } + + @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 { + if (dto.originIds) { + dto.originIds = await this.adminRegionsService.getAdminRegionDescendants( + dto.originIds, + ); + } + if (dto.materialIds) { + dto.materialIds = await this.materialsService.getMaterialsDescendants( + dto.materialIds, + ); + } + return this.geoRegionsService.getGeoJson({ + ...dto, + eudr: true, + collection: true, + }); + } + + @ApiOperation({ description: 'Get EUDR Dashboard' }) + @ApiOkResponse({ type: EUDRDashboard }) + @Get('/dashboard') + async getDashboard( + @Query(ValidationPipe) dto: GetDashBoardDTO, + ): Promise { + return this.dashboard.buildDashboard(dto); + } + + @ApiOperation({ description: 'Get EUDR Dashboard Detail' }) + @ApiOkResponse({ type: EUDRDashBoardDetail }) + @Get('/dashboard/detail/:supplierId') + async getDashboardDetail( + @Param('supplierId') supplierId: string, + @Query(ValidationPipe) dto: GetEUDRAlertDatesDto, + ): Promise { + return this.dashboard.buildDashboardDetail(supplierId, dto); + } +} diff --git a/api/src/modules/eudr-alerts/eudr.module.ts b/api/src/modules/eudr-alerts/eudr.module.ts new file mode 100644 index 000000000..099b8040a --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.module.ts @@ -0,0 +1,42 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +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 'modules/eudr-alerts/alerts.repository'; +import { AppConfig } from 'utils/app.config'; +import { EudrDashboardService } from './dashboard/eudr-dashboard.service'; + +export const IEUDRAlertsRepositoryToken: symbol = Symbol( + 'IEUDRAlertsRepository', +); +export const EUDRDataSetToken: symbol = Symbol('EUDRDataSet'); +export const EUDRCredentialsToken: symbol = Symbol('EUDRCredentials'); + +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({ + imports: [ + HttpModule, + MaterialsModule, + SuppliersModule, + GeoRegionsModule, + AdminRegionsModule, + ], + providers: [ + EudrService, + EudrDashboardService, + { provide: 'IEUDRAlertsRepository', useClass: AlertsRepository }, + { provide: 'EUDRDataset', useValue: dataset }, + { provide: 'EUDRCredentials', useValue: credentials }, + ], + controllers: [EudrController], +}) +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..b1aee2987 --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.repositoty.interface.ts @@ -0,0 +1,46 @@ +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'; + +class DateValue { + @ApiProperty() + value: Date; +} + +export class EUDRAlertDates { + @ApiProperty() + alertDate: DateValue; +} + +export interface EUDRAlertDatabaseResult { + supplierid: string; + dfs: number; + sda: number; +} + +export interface AlertedGeoregionsBySupplier { + supplierId: string; + geoRegionId: string; + carbonRemovals: number; +} + +export type GetAlertSummary = { + alertStartDate?: Date; + alertEnDate?: Date; + supplierIds?: string[]; + geoRegionIds?: string[]; +}; + +export interface IEUDRAlertsRepository { + getAlerts(dto?: GetEUDRAlertsDto): Promise; + + getDates(dto: GetEUDRAlertsDto): Promise; + + getAlertSummary(dto: GetAlertSummary): Promise; + + getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise; +} 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..06e909bf1 --- /dev/null +++ b/api/src/modules/eudr-alerts/eudr.service.ts @@ -0,0 +1,23 @@ +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 { + EUDRAlertDates, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; + +@Injectable() +export class EudrService { + constructor( + @Inject('IEUDRAlertsRepository') + private readonly alertsRepository: IEUDRAlertsRepository, + ) {} + + async getAlerts(dto: GetEUDRAlertsDto): Promise { + return this.alertsRepository.getAlerts(dto); + } + + async getDates(dto: GetEUDRAlertsDto): Promise { + return this.alertsRepository.getDates(dto); + } +} 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..f96f48567 --- /dev/null +++ b/api/src/modules/geo-regions/dto/get-features-geojson.dto.ts @@ -0,0 +1,20 @@ +import { Type } from 'class-transformer'; +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; +} + +export class GetEUDRFeaturesGeoJSONDto extends CommonEUDRFiltersDTO { + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + collection: boolean = false; +} 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-features.service.ts b/api/src/modules/geo-regions/geo-features.service.ts new file mode 100644 index 000000000..727a6deb2 --- /dev/null +++ b/api/src/modules/geo-regions/geo-features.service.ts @@ -0,0 +1,100 @@ +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'; +import { + GetEUDRFeaturesGeoJSONDto, + GetFeaturesGeoJsonDto, +} 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 { + logger: Logger = new Logger(GeoFeaturesService.name); + + constructor(private dataSource: DataSource) { + super(GeoRegion, dataSource.createEntityManager()); + } + + async getGeoFeatures( + dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, + ): Promise { + 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); + + if (dto?.collection) { + return this.selectAsFeatureCollection(filteredQueryBuilder); + } + return this.selectAsFeatures(filteredQueryBuilder); + } + + private async selectAsFeatures( + queryBuilder: SelectQueryBuilder, + ): Promise { + queryBuilder.select( + ` + json_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(gr.theGeom)::json, + 'properties', json_build_object(${this.injectMetaDataQuery()}) + )`, + '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(${this.injectMetaDataQuery()}) + ) + ) + )`, + 'geojson', + ); + const result: FeatureCollection | undefined = + await queryBuilder.getRawOne(); + if (!result) { + throw new NotFoundException(`Could not retrieve geo features`); + } + 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/src/modules/geo-regions/geo-region.entity.ts b/api/src/modules/geo-regions/geo-region.entity.ts index b44e19958..40bd08bda 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,12 @@ 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 + @Column({ type: 'decimal', nullable: true }) + totalArea: number; @Column({ type: 'boolean', default: true }) isCreatedByUser: boolean; diff --git a/api/src/modules/geo-regions/geo-region.repository.ts b/api/src/modules/geo-regions/geo-region.repository.ts index d2ff1049d..8997b65de 100644 --- a/api/src/modules/geo-regions/geo-region.repository.ts +++ b/api/src/modules/geo-regions/geo-region.repository.ts @@ -7,6 +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 { 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 { @@ -128,4 +131,19 @@ export class GeoRegionRepository extends Repository { ], ); } + + async getGeoRegionsFromSourcingLocations( + getGeoRegionsDto: GetEUDRGeoRegions, + ): Promise { + const initialQueryBuilder: SelectQueryBuilder = + this.createQueryBuilder('gr') + .innerJoin(SourcingLocation, 'sl', 'sl.geoRegionId = gr.id') + // Using groupBy instead of distinct to avoid the euality opertor for type json + .groupBy('gr.id'); + const queryBuilder: SelectQueryBuilder = + BaseQueryBuilder.addFilters(initialQueryBuilder, getGeoRegionsDto); + queryBuilder.andWhere('gr.theGeom IS NOT NULL'); + + return queryBuilder.getMany(); + } } diff --git a/api/src/modules/geo-regions/geo-regions.module.ts b/api/src/modules/geo-regions/geo-regions.module.ts index 6bcd35a66..bcf572bcb 100644 --- a/api/src/modules/geo-regions/geo-regions.module.ts +++ b/api/src/modules/geo-regions/geo-regions.module.ts @@ -4,11 +4,20 @@ 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'; +import { GeoFeaturesService } from 'modules/geo-regions/geo-features.service'; @Module({ - imports: [TypeOrmModule.forFeature([GeoRegion])], + imports: [ + TypeOrmModule.forFeature([GeoRegion]), + AdminRegionsModule, + MaterialsModule, + 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 624f96ea8..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, @@ -12,6 +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 { AdminRegionsService } from 'modules/admin-regions/admin-regions.service'; +import { MaterialsService } from 'modules/materials/materials.service'; +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< @@ -20,7 +28,12 @@ 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, + private readonly geoFeatures: GeoFeaturesService, + ) { super( geoRegionRepository, geoRegionResource.name.singular, @@ -30,7 +43,7 @@ export class GeoRegionsService extends AppBaseService< get serializerConfig(): JSONAPISerializerConfig { return { - attributes: ['name'], + attributes: ['name', 'theGeom'], keyForAttribute: 'camelCase', }; } @@ -87,4 +100,28 @@ 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); + } + + async getGeoJson( + dto: GetFeaturesGeoJsonDto | GetEUDRFeaturesGeoJSONDto, + ): Promise { + 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 new file mode 100644 index 000000000..529d2aada --- /dev/null +++ b/api/src/modules/import-data/eudr/eudr.dto-processor.service.ts @@ -0,0 +1,258 @@ +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, 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 + * 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; + company_address: 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[]; + }> { + 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.address = row.company_address; + 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(); + geoRegion.totalArea = row.total_area_ha; + geoRegion.theGeom = row.geometry + ? (wellknown.parse(row.geometry) as Geometry) + : (null as unknown as Geometry); + geoRegion.isCreatedByUser = true; + geoRegion.name = row.plot_name; + 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; + sourcingLocation.locationAddressInput = row.sourcing_district; + sourcingLocation.materialId = row.material_id + .split('.') + .filter(Boolean) + .pop() as string; + sourcingLocation.producer = savedSupplier; + // @ts-ignore + sourcingLocation.geoRegion = savedGeoRegion ?? null; + 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(); + 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 queryRunner.manager + .getRepository(SourcingLocation) + .save(sourcingLocations); + + await queryRunner.commitTransaction(); + return { + sourcingLocations: saved, + }; + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } + } + + 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; + 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 new file mode 100644 index 000000000..91aff4f1d --- /dev/null +++ b/api/src/modules/import-data/eudr/eudr.import.service.ts @@ -0,0 +1,221 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { SourcingLocationGroup } from 'modules/sourcing-location-groups/sourcing-location-group.entity'; +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'; +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', +}; + +@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: EUDRDTOProcessor, + protected readonly geoCodingService: GeoCodingAbstractClass, + protected readonly tasksService: TasksService, + protected readonly scenarioService: ScenariosService, + protected readonly indicatorService: IndicatorsService, + protected readonly indicatorRecordService: IndicatorRecordsService, + protected readonly dataSource: DataSource, + ) {} + + async importEudr(filePath: string, taskId: string): Promise { + this.logger.log(`Starting eudr import process`); + await this.fileService.isFilePresentInFs(filePath); + try { + const parsedEudrData: any = 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(); + + // 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, + ); + + // 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') + + .getRawMany(); + + 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: [ + '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, 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); + 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, + year, + alertCount, + }; + } + + 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 (e: any) { + throw new Error( + `Database could not been cleaned before loading new dataset: ${e.message}`, + ); + } + } +} diff --git a/api/src/modules/import-data/import-data.controller.ts b/api/src/modules/import-data/import-data.controller.ts index adf9c25dc..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({ @@ -60,4 +64,41 @@ 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> { + 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, + // }, + // }; + // } } diff --git a/api/src/modules/import-data/import-data.module.ts b/api/src/modules/import-data/import-data.module.ts index 0f0a88082..c5e913170 100644 --- a/api/src/modules/import-data/import-data.module.ts +++ b/api/src/modules/import-data/import-data.module.ts @@ -25,6 +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 '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: [ @@ -35,6 +39,9 @@ import { ImpactModule } from 'modules/impact/impact.module'; BullModule.registerQueue({ name: importQueueName, }), + BullModule.registerQueue({ + name: 'eudr', + }), MaterialsModule, BusinessUnitsModule, SuppliersModule, @@ -58,6 +65,8 @@ import { ImpactModule } from 'modules/impact/impact.module'; ImportDataProducer, ImportDataConsumer, ImportDataService, + EudrImportService, + EUDRDTOProcessor, { 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..6b668cbbd --- /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-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}`, + ); + } + + @OnQueueCompleted() + async onJobComplete(job: Job): Promise { + this.logger.log( + `Import XLSX with TASK ID: ${job.data.taskId} completed successfully`, + ); + // TODO: Handle eudr-alerts 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, + }); + } } 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 9fa9e54b8..3f2a83add 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'; 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/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/src/modules/sourcing-locations/sourcing-location.entity.ts b/api/src/modules/sourcing-locations/sourcing-location.entity.ts index 79a3b0cca..863e881f4 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 { @@ -103,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; 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..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 @@ -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({ @@ -33,3 +36,8 @@ export class GetSupplierByType extends CommonFiltersDto { }) sort?: 'ASC' | 'DESC'; } + +export class GetSupplierEUDR extends CommonEUDRFiltersDTO { + @IsOptional() + 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/supplier.entity.ts b/api/src/modules/suppliers/supplier.entity.ts index 900334d37..06766a394 100644 --- a/api/src/modules/suppliers/supplier.entity.ts +++ b/api/src/modules/suppliers/supplier.entity.ts @@ -65,6 +65,14 @@ export class Supplier extends TimestampedBaseEntity { @Column({ nullable: true }) description?: string; + @ApiPropertyOptional() + @Column({ type: 'varchar', nullable: true }) + companyId: string; + + @ApiPropertyOptional() + @Column({ type: 'varchar', nullable: true }) + address: string; + @ApiProperty() @Column({ type: 'enum', diff --git a/api/src/modules/suppliers/suppliers.controller.ts b/api/src/modules/suppliers/suppliers.controller.ts index 039a9cf81..a1b4e39e5 100644 --- a/api/src/modules/suppliers/suppliers.controller.ts +++ b/api/src/modules/suppliers/suppliers.controller.ts @@ -36,7 +36,10 @@ 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`) 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/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; diff --git a/api/src/utils/base.query-builder.ts b/api/src/utils/base.query-builder.ts index 3c28a62b5..6e8fb0ee4 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 { @@ -47,12 +47,21 @@ export class BaseQueryBuilder { originIds: filters.originIds, }); } - - if (filters.locationTypes) { + if (filters.geoRegionIds) { + queryBuilder.andWhere('sl.geoRegionId IN (:...geoRegionIds)', { + geoRegionIds: filters.geoRegionIds, + }); + } + 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 +140,22 @@ export class CommonFiltersDto { @IsOptional() @IsUUID('4', { each: true }) scenarioIds?: string[]; + + eudr?: boolean; + + geoRegionIds?: string[]; +} + +export class CommonEUDRFiltersDTO extends OmitType(CommonFiltersDto, [ + 'scenarioIds', + 't1SupplierIds', + 'locationTypes', + 'businessUnitIds', +]) { + eudr?: boolean; + + @ApiPropertyOptional({ name: 'geoRegionIds[]' }) + @IsOptional() + @IsUUID('4', { each: true }) + geoRegionIds?: string[]; } diff --git a/api/swagger-spec.json b/api/swagger-spec.json new file mode 100644 index 000000000..3f7146426 --- /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"]}},"/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 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/eudr/eudr-admin-region-filters.spec.ts b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts new file mode 100644 index 000000000..4c18421d6 --- /dev/null +++ b/api/test/e2e/eudr/eudr-admin-region-filters.spec.ts @@ -0,0 +1,54 @@ +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; + + beforeAll(async () => { + testManager = await TestManager.load(EUDRTestManager); + }); + + beforeEach(async () => { + await testManager.refreshState(); + }); + + afterEach(async () => { + await testManager.clearDatabase(); + }); + + afterAll(async () => { + await testManager.close(); + }); + describe('EUDR Admin Regions Filters', () => { + 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 admin-regions that are part of EUDR data and are filtered', async () => { + const { sourcingLocations } = + await testManager.GivenAdminRegionsOfSourcingLocations(); + await testManager.AndAssociatedMaterials(sourcingLocations); + await testManager.AndAssociatedSuppliers([sourcingLocations[0]]); + const { eudrAdminRegions, eudrSourcingLocations } = + await testManager.GivenEUDRAdminRegions(); + const eudrMaterials = await testManager.AndAssociatedMaterials([ + eudrSourcingLocations[0], + ]); + const eudrSuppliers = await testManager.AndAssociatedSuppliers( + eudrSourcingLocations, + ); + await testManager.WhenIRequestEUDRAdminRegions({ + 'materialIds[]': [eudrMaterials[0].id], + 'producerIds[]': eudrSuppliers.map((s: Supplier) => s.id), + }); + testManager.ThenIShouldOnlyReceiveCorrespondingAdminRegions([ + eudrAdminRegions[0], + ]); + }); + }); +}); 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..5cd0bf731 --- /dev/null +++ b/api/test/e2e/eudr/eudr-geo-region-filters.spec.ts @@ -0,0 +1,35 @@ +import { EUDRTestManager } from './fixtures'; +import { TestManager } from '../../utils/test-manager'; + +describe('GeoRegions Filters (e2e)', () => { + let testManager: EUDRTestManager; + + beforeAll(async () => { + testManager = await TestManager.load(EUDRTestManager); + }); + 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); + }); + 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 new file mode 100644 index 000000000..747b375bc --- /dev/null +++ b/api/test/e2e/eudr/fixtures.ts @@ -0,0 +1,182 @@ +import { + LOCATION_TYPES, + SourcingLocation, +} from 'modules/sourcing-locations/sourcing-location.entity'; +import { + createAdminRegion, + createGeoRegion, + createMaterial, + createSourcingLocation, + createSupplier, +} from '../../entity-mocks'; +import { AdminRegion } from 'modules/admin-regions/admin-region.entity'; +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'; + + constructor(manager: TestManager) { + super(manager.testApp, manager.jwtToken, manager.dataSource); + } + + 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 ( + sourcingLocations: SourcingLocation[], + materialNames?: string[], + ) => { + 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(); + } + return materials; + }; + AndAssociatedSuppliers = async ( + sourcingLocations: SourcingLocation[], + supplierNames?: string[], + ) => { + 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].producerId = suppliers[i].id; + await sourcingLocations[i].save(); + } + return suppliers; + }; + 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 (filters?: { + 'producerIds[]'?: string[]; + 'materialIds[]'?: string[]; + }) => { + return this.getRequest({ + url: `${this.url}/admin-regions`, + query: filters, + }); + }; + + ThenIShouldOnlyReceiveCorrespondingAdminRegions = ( + eudrAdminRegions: AdminRegion[], + ) => { + 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( + (adminRegionResponse: AdminRegion) => + adminRegionResponse.id === adminRegion.id, + ), + ).toBeDefined(); + } + }; + + 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], + }; + }; + + 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[]; + }) => { + return this.getRequest({ 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/e2e/geo-regions/fixtures.ts b/api/test/e2e/geo-regions/fixtures.ts new file mode 100644 index 000000000..1a69e235d --- /dev/null +++ b/api/test/e2e/geo-regions/fixtures.ts @@ -0,0 +1,178 @@ +import { + createAdminRegion, + createGeoRegion, + createMaterial, + createSourcingLocation, + createSourcingRecord, + createSupplier, +} from '../../entity-mocks'; +import * as request from 'supertest'; +import { GeoRegion } from '../../../src/modules/geo-regions/geo-region.entity'; +import { + LOCATION_TYPES, + SourcingLocation, +} from '../../../src/modules/sourcing-locations/sourcing-location.entity'; +import { TestManager } from '../../utils/test-manager'; +import { Feature } from 'geojson'; +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) { + super(manager.testApp, manager.jwtToken, manager.dataSource); + } + + GivenRegularSourcingLocationsWithGeoRegions = async () => { + 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, + }); + const sourcingLocation2 = await createSourcingLocation({ + geoRegionId: geoRegion2.id, + locationType: LOCATION_TYPES.PRODUCTION_AGGREGATION_POINT, + }); + return { + sourcingLocations: [sourcingLocation1, sourcingLocation2], + geoRegions: [geoRegion, geoRegion2], + }; + }; + + GivenEUDRSourcingLocationsWithGeoRegions = async () => { + const geoRegion = await createGeoRegion({ + name: this.generateRandomName(), + }); + 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, + }); + 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], + }; + }; + + WhenIRequestEUDRGeoFeatures = async (filters: { + 'geoRegionIds[]': string[]; + collection?: boolean; + }): Promise => { + this.response = await this.getRequest({ + url: '/api/v1/eudr/geo-features', + query: filters, + }); + }; + + WhenIRequestEUDRGeoFeatureCollection = async (filters?: { + 'geoRegionIds[]'?: string[]; + 'producerIds[]'?: string[]; + 'materialIds[]'?: string[]; + 'originIds[]'?: string[]; + }): Promise => { + this.response = await this.getRequest({ + url: '/api/v1/eudr/geo-features/collection', + query: filters, + }); + }; + + ThenIShouldOnlyRecieveCorrespondingGeoFeatures = ( + eudrGeoRegions: GeoRegion[], + collection?: boolean, + ) => { + 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(); + } + } + }; + + 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 new file mode 100644 index 000000000..152ede171 --- /dev/null +++ b/api/test/e2e/geo-regions/geo-features.spec.ts @@ -0,0 +1,83 @@ +import { GeoRegionsTestManager } from './fixtures'; +import { TestManager } from '../../utils/test-manager'; + +describe('Geo Features tests (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(); + }); + + 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, + ); + }); + + 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('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(); + 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, + ); + }); + }); +}); 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..5202eca5c 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, }); }); @@ -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, }); diff --git a/api/test/entity-mocks.ts b/api/test/entity-mocks.ts index 4bd5670bf..2afb385ed 100644 --- a/api/test/entity-mocks.ts +++ b/api/test/entity-mocks.ts @@ -40,6 +40,8 @@ import { faker } from '@faker-js/faker'; import { genSalt, hash } from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; +const wkt = require('wellknown'); + async function createAdminRegion( additionalData: Partial = {}, ): Promise { @@ -285,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', @@ -306,6 +311,7 @@ async function createGeoRegion( ], h3FlatLength: 7, name: 'ABC', + theGeom: wkt.parse(geomstring), }; const geoRegion = GeoRegion.merge( 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..ad6d725c2 --- /dev/null +++ b/api/test/unit/bigquery-query-builder/bigquery-query-builder.spec.ts @@ -0,0 +1,50 @@ +import { DataSource, SelectQueryBuilder } from 'typeorm'; +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', () => { + 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(${EUDR_ALERTS_DATABASE_FIELDS.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']); + }); +}); diff --git a/api/test/utils/application-manager.ts b/api/test/utils/application-manager.ts index 980e8f134..61dd9fb57 100644 --- a/api/test/utils/application-manager.ts +++ b/api/test/utils/application-manager.ts @@ -9,7 +9,9 @@ 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 { IEmailServiceToken } from '../../src/modules/notifications/notifications.module'; +import { AlertsRepository } from 'modules/eudr-alerts/alerts.repository'; export default class ApplicationManager { static readonly regenerateResourcesOnEachTest: boolean = false; @@ -40,11 +42,13 @@ export default class ApplicationManager { const testingModuleBuilder: TestingModuleBuilder = initTestingModuleBuilder || - (await Test.createTestingModule({ + Test.createTestingModule({ imports: [AppModule], }) - .overrideProvider('IEmailService') - .useClass(MockEmailService)); + .overrideProvider(IEmailServiceToken) + .useClass(MockEmailService) + .overrideProvider('IEUDRAlertsRepository') + .useClass(MockAlertRepository); ApplicationManager.testApplication.moduleFixture = await testingModuleBuilder.compile(); diff --git a/api/test/utils/generate-random-name.ts b/api/test/utils/generate-random-name.ts new file mode 100644 index 000000000..690749949 --- /dev/null +++ b/api/test/utils/generate-random-name.ts @@ -0,0 +1,8 @@ +export const generateRandomName = (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/service-mocks.ts b/api/test/utils/service-mocks.ts index 1cb4351c0..1dfaeae4c 100644 --- a/api/test/utils/service-mocks.ts +++ b/api/test/utils/service-mocks.ts @@ -3,6 +3,13 @@ import { SendMailDTO, } from '../../src/modules/notifications/email/email.service.interface'; import { Logger } from '@nestjs/common'; +import { + AlertedGeoregionsBySupplier, + EUDRAlertDatabaseResult, + EUDRAlertDates, + GetAlertSummary, + IEUDRAlertsRepository, +} from 'modules/eudr-alerts/eudr.repositoty.interface'; export class MockEmailService implements IEmailService { logger: Logger = new Logger(MockEmailService.name); @@ -12,3 +19,32 @@ export class MockEmailService implements IEmailService { return Promise.resolve(); } } + +export class MockAlertRepository implements IEUDRAlertsRepository { + logger: Logger = new Logger(MockAlertRepository.name); + + getAlerts(): any { + this.logger.warn(`Alert Repository Mock called... `); + return new Promise((resolve) => { + resolve([]); + }); + } + + getDates(): Promise { + return new Promise((resolve) => { + resolve([]); + }); + } + + getAlertSummary(dto: GetAlertSummary): Promise { + return Promise.resolve([]); + } + + getAlertedGeoRegionsBySupplier(dto: { + supplierIds: string[]; + startAlertDate: Date; + endAlertDate: Date; + }): Promise { + return Promise.resolve([]); + } +} diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts new file mode 100644 index 000000000..c780046a9 --- /dev/null +++ b/api/test/utils/test-manager.ts @@ -0,0 +1,72 @@ +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 { Material } from 'modules/materials/material.entity'; +import { Supplier } from 'modules/suppliers/supplier.entity'; +import { GeoRegion } from 'modules/geo-regions/geo-region.entity'; +import { generateRandomName } from './generate-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 load(manager: any) { + return new manager(await this.createManager()); + } + + 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 getRequest(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(); + } + + generateRandomName(): string { + return generateRandomName(); + } +} diff --git a/api/test/utils/userAuth.ts b/api/test/utils/userAuth.ts index ac9950874..c3cfbf8a6 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,8 @@ export type TestUser = { jwtToken: string; user: User; password: string }; export async function setupTestUser( applicationManager: TestApplication, roleName: ROLES = ROLES.ADMIN, - extraData: Partial = {}, + extraData: Partial> = {}, + password: string = 'Password123', ): Promise { const salt = await genSalt(); const role = new Role(); @@ -23,26 +23,32 @@ 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(); - 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: extraData.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], + ...extraData, + }); + } + 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( diff --git a/api/yarn.lock b/api/yarn.lock index 06f097711..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" @@ -1257,6 +1322,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" @@ -1407,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" @@ -1440,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" @@ -1726,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" @@ -1908,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" @@ -2010,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== @@ -2023,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" @@ -2477,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== @@ -2550,6 +2657,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" @@ -2814,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== @@ -2853,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== @@ -2876,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" @@ -3173,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" @@ -3345,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" @@ -3440,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" @@ -3546,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" @@ -3556,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" @@ -3627,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" @@ -3635,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" @@ -3708,7 +3908,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== @@ -3851,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" @@ -4382,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" @@ -4469,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" @@ -4477,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" @@ -4746,6 +4975,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" @@ -4946,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" @@ -5429,6 +5670,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" @@ -5552,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" @@ -5561,6 +5816,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" @@ -5683,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" @@ -5983,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" @@ -6017,6 +6305,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" @@ -6051,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" @@ -6140,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" @@ -6372,6 +6681,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" @@ -6457,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" @@ -6553,6 +6872,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" 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 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/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( 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'); }); }); diff --git a/client/next.config.js b/client/next.config.js index 49f73d605..00a6b0d18 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -8,12 +8,22 @@ const nextConfig = { return [ { source: '/', - destination: '/analysis/map', + destination: '/eudr', permanent: false, }, { source: '/analysis', - destination: '/analysis/map', + destination: '/eudr', + permanent: false, + }, + { + source: '/analysis/:id', + destination: '/eudr', + permanent: false, + }, + { + source: '/data', + destination: '/eurd', permanent: false, }, { @@ -23,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; diff --git a/client/package.json b/client/package.json index eaba3fda1..4cde0e44f 100644 --- a/client/package.json +++ b/client/package.json @@ -15,12 +15,16 @@ "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/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", "@dnd-kit/core": "5.0.3", "@dnd-kit/modifiers": "5.0.0", "@dnd-kit/sortable": "6.0.1", @@ -34,25 +38,41 @@ "@json2csv/plainjs": "^6.1.3", "@loaders.gl/core": "3.3.1", "@luma.gl/constants": "8.5.18", + "@maplibre/maplibre-gl-compare": "^0.5.0", + "@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-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", "@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", + "@turf/bbox": "^6.5.0", "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", - "date-fns": "2.22.1", + "date-fns": "3.3.1", "fuse.js": "6.4.6", "jsona": "1.9.2", "lodash-es": "4.17.21", "lottie-react": "2.4.0", - "mapbox-gl": "2.13.0", + "lucide-react": "0.344.0", + "maplibre-gl": "3.6.2", "next": "13.5.5", "next-auth": "4.19.2", "pino": "8.1.0", @@ -60,17 +80,21 @@ "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", "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", + "react-world-flags": "1.6.0", "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" }, @@ -78,6 +102,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/components/button/component.tsx b/client/src/components/button/component.tsx index 32e7c2255..17b5ac428 100644 --- a/client/src/components/button/component.tsx +++ b/client/src/components/button/component.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import classNames from 'classnames'; import Link from 'next/link'; import { pickBy } from 'lodash-es'; import Loading from 'components/loading'; +import { cn } from '@/lib/utils'; import type { LinkProps } from 'next/link'; import type { FC } from 'react'; @@ -53,7 +53,7 @@ const PRIMARY = const BASE_BORDER = 'border bg-transparent focus:outline-offset-2 focus:outline focus:outline-none focus:ring-1'; -const SECONDARY = classNames( +const SECONDARY = cn( BASE_BORDER, 'border border-gray-300 focus:border-navy-400 text-gray-600 hover:bg-gray-50 focus:ring-green-700', ); @@ -61,7 +61,7 @@ const SECONDARY = classNames( const TERTIARY = 'border-transparent shadow-sm text-white bg-gray-500 hover:bg-gray-600 focus:outline-offset-2 focus:outline focus:outline-gray-500/20'; -const PRIMARY_LIGHT = classNames(BASE_BORDER, 'text-navy-400 border-navy-400'); +const PRIMARY_LIGHT = cn(BASE_BORDER, 'text-navy-400 border-navy-400'); export const THEME = { default: COMMON_CLASSNAMES, @@ -106,7 +106,7 @@ const buildClassName = ({ size, variant, }: ButtonProps | AnchorProps) => - classNames( + cn( classes.base, danger ? classes.variant[variant].danger : classes.variant[variant].default, classes.size[size], @@ -121,7 +121,7 @@ const ButtonTemplate: React.FC = ({ danger, icon, loading, si {!loading && icon && (
{React.cloneElement(icon, { - className: classNames( + className: cn( { 'w-3 h-3': size === 'xs', 'w-4 h-4': size !== 'xs', @@ -134,7 +134,7 @@ const ButtonTemplate: React.FC = ({ danger, icon, loading, si )} {loading && ( ({ {({ 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/icons/commodities/cattle.tsx b/client/src/components/icons/commodities/cattle.tsx new file mode 100644 index 000000000..f20733bb3 --- /dev/null +++ b/client/src/components/icons/commodities/cattle.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const CattleSVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default CattleSVG; diff --git a/client/src/components/icons/commodities/cocoa.tsx b/client/src/components/icons/commodities/cocoa.tsx new file mode 100644 index 000000000..49121f464 --- /dev/null +++ b/client/src/components/icons/commodities/cocoa.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const CocoaSVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default CocoaSVG; diff --git a/client/src/components/icons/commodities/coffee.tsx b/client/src/components/icons/commodities/coffee.tsx new file mode 100644 index 000000000..1d437c4f5 --- /dev/null +++ b/client/src/components/icons/commodities/coffee.tsx @@ -0,0 +1,18 @@ +import type { SVGAttributes } from 'react'; + +const CoffeeSVG = (props?: SVGAttributes) => { + return ( + + + + ); +}; + +export default CoffeeSVG; diff --git a/client/src/components/icons/commodities/palm-oil.tsx b/client/src/components/icons/commodities/palm-oil.tsx new file mode 100644 index 000000000..5c9b7b304 --- /dev/null +++ b/client/src/components/icons/commodities/palm-oil.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const PalmOilSVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default PalmOilSVG; diff --git a/client/src/components/icons/commodities/rubber.tsx b/client/src/components/icons/commodities/rubber.tsx new file mode 100644 index 000000000..7829d7293 --- /dev/null +++ b/client/src/components/icons/commodities/rubber.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const RubberSVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default RubberSVG; diff --git a/client/src/components/icons/commodities/soy.tsx b/client/src/components/icons/commodities/soy.tsx new file mode 100644 index 000000000..fa6c7daf8 --- /dev/null +++ b/client/src/components/icons/commodities/soy.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const SoySVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default SoySVG; diff --git a/client/src/components/icons/commodities/wood.tsx b/client/src/components/icons/commodities/wood.tsx new file mode 100644 index 000000000..1def18171 --- /dev/null +++ b/client/src/components/icons/commodities/wood.tsx @@ -0,0 +1,19 @@ +import type { SVGAttributes } from 'react'; + +const WoodSVG = (props?: SVGAttributes) => { + return ( + + + + + ); +}; + +export default WoodSVG; diff --git a/client/src/components/icons/report.tsx b/client/src/components/icons/report.tsx new file mode 100644 index 000000000..a1d6021a7 --- /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/components/legend/item/info-modal.tsx b/client/src/components/legend/item/info-modal.tsx index 24e11e5ec..e7ad77ae1 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'; @@ -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/components/map/component.tsx b/client/src/components/map/component.tsx index 1fcfba454..a8afcbbd2 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,16 @@ 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..b61de3cdc --- /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://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", + "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" + }, + { + "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..2db511b8b 100644 --- a/client/src/components/map/types.d.ts +++ b/client/src/components/map/types.d.ts @@ -1,16 +1,16 @@ 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; - 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/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/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, }); 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/calendar.tsx b/client/src/components/ui/calendar.tsx new file mode 100644 index 000000000..3fbd65201 --- /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} + /> + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; 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/popover.tsx b/client/src/components/ui/popover.tsx new file mode 100644 index 000000000..325d7b703 --- /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/components/ui/radio-group.tsx b/client/src/components/ui/radio-group.tsx new file mode 100644 index 000000000..d41776297 --- /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/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/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/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/components/ui/switch.tsx b/client/src/components/ui/switch.tsx new file mode 100644 index 000000000..c76fcdf03 --- /dev/null +++ b/client/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; + +import { cn } from '@/lib/utils'; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; 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-detail/deforestation-alerts/chart/index.tsx b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx new file mode 100644 index 000000000..c12bda6af --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/chart/index.tsx @@ -0,0 +1,226 @@ +import { UTCDate } from '@date-fns/utc'; +import { format } from 'date-fns'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Dot, ResponsiveContainer } from 'recharts'; +import { useParams } from 'next/navigation'; +import { useCallback, useMemo, useRef, useState } from 'react'; + +import { EUDR_COLOR_RAMP } from '@/utils/colors'; +import { useEUDRSupplier } from '@/hooks/eudr'; +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 ticks = useRef([]); + const { supplierId }: { supplierId: string } = useParams(); + const dispatch = useAppDispatch(); + const { + filters: { dates }, + } = useAppSelector(eudrDetail); + const { data, isFetching } = useEUDRSupplier( + supplierId, + { + startAlertDate: dates.from, + endAlertDate: dates.to, + }, + { + select: (data) => data?.alerts?.values, + }, + ); + + 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 { + ...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]); + + const xDomain = useMemo(() => { + if (!parsedData?.length) return []; + + return [ + new UTCDate(parsedData[0].alertDate).getTime(), + new UTCDate(parsedData[parsedData?.length - 1].alertDate).getTime(), + ]; + }, [parsedData]); + + return ( + <> + {!data?.length && isFetching && ( +
+ 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} + + ))} +
+ + + + { + 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 }} + axisLine={false} + className="text-xs" + tickMargin={15} + /> + + {/* format(new UTCDate(v), 'dd/MM/yyyy')} /> */} + {plotConfig?.map(({ name, color }) => { + return ( + { + const { payload } = props; + return ( + handleClickDot(payload)} + className="cursor-pointer stroke-[3px]" + /> + ); + }} + activeDot={(props: DotPropsWithPayload) => { + const { payload } = props; + return ( + handleClickDot(payload)} + className="cursor-pointer" + /> + ); + }} + /> + ); + })} + + + + )} + + ); +}; + +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..a28ee809e --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/deforestation-alerts/index.tsx @@ -0,0 +1,68 @@ +import { useParams } from 'next/navigation'; +import { format, endOfMonth, startOfMonth } 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'; +import InfoModal from '@/components/legend/item/info-modal'; + +const dateFormatter = (date: UTCDate) => format(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, + }, + ); + + const formattedBeginOfMonth = data?.startAlertDate + ? dateFormatter(startOfMonth(new UTCDate(data.startAlertDate))) + : undefined; + + const formattedEndOfMonth = data?.endAlertDate + ? dateFormatter(endOfMonth(new UTCDate(data.endAlertDate))) + : undefined; + + return ( +
+
+

Deforestation alerts detected within the smallholders

+ +
+ {data?.totalAlerts > 0 && ( +
+ There were {data?.totalAlerts} deforestation alerts + reported for the supplier between the{' '} + {formattedBeginOfMonth} and the{' '} +
+ {formattedEndOfMonth} + . + +
+
+ )} + +
+ ); +}; + +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/sourcing-info/chart/index.tsx b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx new file mode 100644 index 000000000..2e95192fd --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/chart/index.tsx @@ -0,0 +1,249 @@ +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'; +import InfoModal from '@/components/legend/item/info-modal'; + +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, isFetching } = 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 ( +
+
+
+

Percentage of sourcing volume per plot

+ +
+
+ Show by +
+ + +
+
+
+ {!parsedData?.length && isFetching && ( +
+ Fetching data... +
+ )} + {!parsedData?.length && !isFetching && ( +
+ No data available +
+ )} + {parsedData?.length > 0 && ( + <> +
+ {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..9753820d5 --- /dev/null +++ b/client/src/containers/analysis-eudr-detail/sourcing-info/index.tsx @@ -0,0 +1,86 @@ +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, + totalCarbonRemovals: data?.alerts.totalCarbonRemovals, + }), + }, + ); + + 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` : '-'} + +
+ +
+

Carbon removal

+ + {!isNaN(data?.totalCarbonRemovals) + ? `${Intl.NumberFormat(undefined, { style: 'unit', unit: 'kilogram' }).format( + data.totalCarbonRemovals, + )}` + : '-'} + +
+
+ + +
+
+ ); +}; + +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 new file mode 100644 index 000000000..4581ea44e --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/index.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; + +const BreakdownItem = ({ + name, + color, + icon, + value, +}: { + id: string; + name: string; + color: string; + icon: ReactNode; + value: number; +}): JSX.Element => { + return ( +
+
+ {icon ?? null} + {name} +
+
+
+ {`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format(value)}%`}{' '} + of plots +
+
+
+
+
+
+ ); +}; + +export default BreakdownItem; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/utils.ts b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/utils.ts new file mode 100644 index 000000000..fc6402ed4 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/breakdown-item/utils.ts @@ -0,0 +1,36 @@ +import { cloneElement } from 'react'; + +import CocoaSVG from '@/components/icons/commodities/cocoa'; +import SoySVG from '@/components/icons/commodities/soy'; +import RubberSVG from '@/components/icons/commodities/rubber'; +import WoodSVG from '@/components/icons/commodities/wood'; +import CoffeeSVG from '@/components/icons/commodities/coffee'; +import PalmOilSVG from '@/components/icons/commodities/palm-oil'; +import CattleSVG from '@/components/icons/commodities/cattle'; +import { cn } from '@/lib/utils'; + +export type CommodityName = 'cocoa' | 'soy' | 'rubber' | 'wood' | 'coffee' | 'palm-oil' | 'cattle'; + +const SVGS_DICTIONARY = { + cocoa: CocoaSVG, + soy: SoySVG, + rubber: RubberSVG, + wood: WoodSVG, + coffee: CoffeeSVG, + 'Palm oil and its fractions': PalmOilSVG, + cattle: CattleSVG, +}; + +export function getCommodityIconByName( + commodity: CommodityName, + iconProps?: React.SVGProps, +) { + const Icon = SVGS_DICTIONARY[commodity]; + + if (Icon === undefined) return null; + + return cloneElement(Icon(), { + ...iconProps, + className: cn('w-[56px] h-[56px]', iconProps?.className), + }); +} 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..ced54aa28 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/deforestation-free-suppliers/index.tsx @@ -0,0 +1,57 @@ +import { useMemo, type ComponentProps } from 'react'; +import Flag from 'react-world-flags'; + +import { getCommodityIconByName } from '../breakdown-item/utils'; +import Breakdown from '..'; +import { CATEGORIES } from '../..'; + +import { eudr } from '@/store/features/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { useEUDRData } from '@/hooks/eudr'; + +import type BreakdownItem from '../breakdown-item'; + +const DeforestationFreeSuppliersBreakdown = () => { + const { + viewBy, + filters: { dates, suppliers, origins, materials, plots }, + } = useAppSelector(eudr); + + 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) => data?.breakDown, + }, + ); + + const parsedData: ComponentProps[] = useMemo(() => { + const dataByView = data?.[viewBy] || []; + + return Object.keys(dataByView) + .filter((key) => key === CATEGORIES[0].apiName) + .map((filteredKey) => + dataByView[filteredKey].detail + .map((item) => ({ + ...item, + color: CATEGORIES[0].color, + icon: getCommodityIconByName(item.name, { fill: CATEGORIES[0].color }), + ...(viewBy === 'origins' && { + icon: , + }), + })) + .flat(), + ) + .flat(); + }, [data, viewBy]); + + return ; +}; + +export default DeforestationFreeSuppliersBreakdown; diff --git a/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx b/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx new file mode 100644 index 000000000..d6fe5ae2e --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/index.tsx @@ -0,0 +1,15 @@ +import BreakdownItem from './breakdown-item'; + +import type { ComponentProps } from 'react'; + +const Breakdown = ({ data }: { data: ComponentProps[] }): JSX.Element => { + return ( +
+ {data.map((item) => ( + + ))} +
+ ); +}; + +export default 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 new file mode 100644 index 000000000..4372b4580 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-deforestation-alerts/index.tsx @@ -0,0 +1,57 @@ +import { useMemo, type ComponentProps } from 'react'; +import Flag from 'react-world-flags'; + +import { getCommodityIconByName } from '../breakdown-item/utils'; +import Breakdown from '..'; +import { CATEGORIES } from '../..'; + +import { eudr } from '@/store/features/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { useEUDRData } from '@/hooks/eudr'; + +import type BreakdownItem from '../breakdown-item'; + +const SuppliersWithDeforestationAlertsBreakdown = () => { + const { + viewBy, + filters: { dates, suppliers, origins, materials, plots }, + } = useAppSelector(eudr); + + 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) => data?.breakDown, + }, + ); + + const parsedData: ComponentProps[] = useMemo(() => { + const dataByView = data?.[viewBy] || []; + + return Object.keys(dataByView) + .filter((key) => key === CATEGORIES[1].apiName) + .map((filteredKey) => + dataByView[filteredKey].detail + .map((item) => ({ + ...item, + color: CATEGORIES[1].color, + icon: getCommodityIconByName(item.name, { fill: CATEGORIES[1].color }), + ...(viewBy === 'origins' && { + icon: , + }), + })) + .flat(), + ) + .flat(); + }, [data, viewBy]); + + return ; +}; + +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..412820758 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/breakdown/suppliers-with-no-location-data/index.tsx @@ -0,0 +1,57 @@ +import { useMemo, type ComponentProps } from 'react'; +import Flag from 'react-world-flags'; + +import { getCommodityIconByName } from '../breakdown-item/utils'; +import Breakdown from '..'; +import { CATEGORIES } from '../..'; + +import { eudr } from '@/store/features/eudr'; +import { useAppSelector } from '@/store/hooks'; +import { useEUDRData } from '@/hooks/eudr'; + +import type BreakdownItem from '../breakdown-item'; + +const SuppliersWithNoLocationDataBreakdown = () => { + const { + viewBy, + filters: { dates, suppliers, origins, materials, plots }, + } = useAppSelector(eudr); + + 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) => data?.breakDown, + }, + ); + + const parsedData: ComponentProps[] = useMemo(() => { + const dataByView = data?.[viewBy] || []; + + return Object.keys(dataByView) + .filter((key) => key === CATEGORIES[2].apiName) + .map((filteredKey) => + dataByView[filteredKey].detail + .map((item) => ({ + ...item, + color: CATEGORIES[2].color, + icon: getCommodityIconByName(item.name, { fill: CATEGORIES[2].color }), + ...(viewBy === 'origins' && { + icon: , + }), + })) + .flat(), + ) + .flat(); + }, [data, viewBy]); + + return ; +}; + +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..1dd48ddd7 --- /dev/null +++ b/client/src/containers/analysis-eudr/category-list/index.tsx @@ -0,0 +1,166 @@ +import { useCallback, useMemo, 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 { useEUDRData } from '@/hooks/eudr'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { eudr, setTableFilters } from '@/store/features/eudr'; +import { themeColors } from '@/utils/colors'; + +export const CATEGORIES = [ + { + name: 'Deforestation-free plots', + apiName: 'Deforestation-free suppliers', + key: 'dfs', + color: themeColors.blue[400], + }, + { + name: 'Plots with deforestation alerts', + apiName: 'Suppliers with deforestation alerts', + key: 'sda', + color: '#FFC038', + }, + { + name: 'Plots with no location data', + apiName: 'Suppliers with no location data', + key: 'tpl', + color: '#8561FF', + }, +] as const; + +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.key]: tableFilters[category.key], + }), + {} as CategoryState, + ), + ); + + const dispatch = useAppDispatch(); + + const onClickCategory = useCallback( + (category: (typeof CATEGORIES)[number]) => { + toggleCategory((prev) => ({ + ...prev, + [category.key]: !prev[category.key], + })); + + dispatch( + setTableFilters({ + [category.key]: !tableFilters[category.key], + }), + ); + }, + [dispatch, tableFilters], + ); + + 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) => data?.breakDown, + }, + ); + + const parsedData = useMemo(() => { + const dataByView = data?.[viewBy] || []; + + return Object.keys(dataByView).map((key) => { + const category = CATEGORIES.find((category) => category.apiName === key); + return { + name: category.name, + ...dataByView[key], + key: category?.key, + color: category?.color || '#000', + }; + }); + }, [data, viewBy]); + + return ( + <> + {parsedData.map((category) => ( + onClickCategory(category)} + defaultOpen={categories[category.key]} + > + +
+
+ + {category.name} +
+
+
+ {`${Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }).format( + category.totalPercentage, + )}%`}{' '} + of plots +
+
+
+
+
+ + +
+ + + {category.key === 'dfs' && categories['dfs'] && } + {category.key === 'sda' && categories['sda'] && ( + + )} + {category.key === 'tpl' && categories['tpl'] && ( + + )} + + + ))} + + ); +}; + +export default CategoryList; 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/index.tsx b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx new file mode 100644 index 000000000..12304da5f --- /dev/null +++ b/client/src/containers/analysis-eudr/filters/more-filters/index.tsx @@ -0,0 +1,295 @@ +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( + { + 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) => { + 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( + { + 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( + { + 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( + { + 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); + 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/years-range/index.tsx b/client/src/containers/analysis-eudr/filters/years-range/index.tsx new file mode 100644 index 000000000..5f2003469 --- /dev/null +++ b/client/src/containers/analysis-eudr/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 { 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'; +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(eudr); + + 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/map/basemap/component.tsx b/client/src/containers/analysis-eudr/map/basemap/component.tsx new file mode 100644 index 000000000..11d3a215d --- /dev/null +++ b/client/src/containers/analysis-eudr/map/basemap/component.tsx @@ -0,0 +1,322 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import Image from 'next/image'; +import { format } from 'date-fns'; + +import LayersData from '../layers.json'; + +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { setBasemap, setPlanetLayer, setPlanetCompareLayer } from '@/store/features/eudr'; +import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +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 monthFormatter = (date: string) => format(date, 'MMM'); + +const YEARS = [2020, 2021, 2022, 2023, 2024]; + +const EUDRBasemapControl = () => { + const currentDate = useMemo(() => new Date(), []); + const dispatch = useAppDispatch(); + const { basemap, planetLayer, planetCompareLayer } = useAppSelector((state) => state.eudr); + const basemapData = LayersData.find((layer) => layer.id === 'planet-data'); + + const handleLightBasemap = useCallback( + (checked: boolean) => { + if (checked) { + dispatch(setBasemap('light')); + dispatch(setPlanetLayer({ active: false })); + } else { + dispatch(setBasemap('planet')); + dispatch(setPlanetLayer({ active: true })); + } + }, + [dispatch], + ); + + const handlePlanetLayer = useCallback( + (checked: boolean) => { + dispatch(setBasemap('planet')); + dispatch(setPlanetLayer({ active: checked })); + if (!checked) { + dispatch(setBasemap('light')); + dispatch(setPlanetCompareLayer({ active: false })); + } + }, + [dispatch], + ); + + const handlePlanetLayerYear = useCallback( + (year: string) => { + const nextYear = Number(year); + if (nextYear === currentDate.getFullYear()) { + // If the year is the current year, set the month to the previous month + dispatch(setPlanetLayer({ month: currentDate.getMonth() })); + } + dispatch(setPlanetLayer({ year: nextYear })); + }, + [dispatch, currentDate], + ); + + const handlePlanetLayerMonth = useCallback( + (month: string) => { + dispatch(setPlanetLayer({ month: Number(month) })); + }, + [dispatch], + ); + + const handlePlanetCompareLayerYear = useCallback( + (year: string) => { + dispatch(setPlanetCompareLayer({ year: Number(year) })); + }, + [dispatch], + ); + + const handlePlanetCompareLayerMonth = useCallback( + (month: string) => { + dispatch(setPlanetCompareLayer({ month: Number(month) })); + }, + [dispatch], + ); + + const handlePlanetCompareLayer = useCallback( + (checked: boolean) => { + dispatch(setPlanetCompareLayer({ active: checked })); + }, + [dispatch], + ); + + useEffect(() => { + if (basemap === 'light') { + dispatch(setPlanetCompareLayer({ active: false })); + } + }, [basemap, dispatch]); + + return ( + + +
+
+
+ Change of basemap +
+
+
+
+ +
+

Basemaps

+ +
+
+
+
+ Change of basemap +
+
+
+
+
+

Light Map

+
+
+ +
+
+ +
+
+
+
Light basemap version from Carto
+
+
+ +
+
+
+
+
+ Change of basemap +
+
+
+
+
+

Planet Satellite Imagery

+
+
+ +
+
+ +
+
+
+
+ Monthly high resolution basemaps (tropics) +
+ {planetLayer.active && ( +
+
Year
+ +
Month
+ +
+ )} +
+
+
+
+ +
+ {!planetLayer.active &&
Select satellite basemap for image comparison option.
} + {planetLayer.active && ( +
+
+
+
+ Change of basemap +
+
+
+
+
+

Compare Satellite Images

+
+
+ +
+
+ +
+
+
+
+ Monthly high resolution basemaps (tropics) +
+ {planetCompareLayer.active && ( +
+
Year
+ +
Month
+ +
+ )} +
+
+ )} +
+
+
+ ); +}; + +export default EUDRBasemapControl; diff --git a/client/src/containers/analysis-eudr/map/basemap/index.ts b/client/src/containers/analysis-eudr/map/basemap/index.ts new file mode 100644 index 000000000..b404d7fd4 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/basemap/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/client/src/containers/analysis-eudr/map/compare.tsx b/client/src/containers/analysis-eudr/map/compare.tsx new file mode 100644 index 000000000..88de837a8 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/compare.tsx @@ -0,0 +1,365 @@ +import { useState, useCallback, useEffect, useMemo } from 'react'; +import DeckGL from '@deck.gl/react/typed'; +import { GeoJsonLayer } from '@deck.gl/layers/typed'; +import Map, { useMap, Source, Layer } from 'react-map-gl/maplibre'; +import { WebMercatorViewport } from '@deck.gl/core/typed'; +import MapLibreCompare from '@maplibre/maplibre-gl-compare'; +import { CartoLayer, 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'; +import BasemapControl from './basemap'; + +import { useAppSelector } from '@/store/hooks'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; +import { useEUDRData, usePlotGeometries } from '@/hooks/eudr'; +import { formatNumber } from '@/utils/number-format'; + +import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; + +const monthFormatter = (date: string) => format(date, 'MM'); +const friendlyMonthFormatter = (date: string) => format(date, 'MMM'); + +const MAX_BOUNDS = [-75.76238126131099, -9.1712425377296, -74.4412398476887, -7.9871587484823845]; + +const DEFAULT_VIEW_STATE: MapViewState = { + ...INITIAL_VIEW_STATE, + latitude: -8.461844239054608, + longitude: -74.96226240479487, + zoom: 9, + minZoom: 7, + maxZoom: 20, +}; + +const EUDRCompareMap = () => { + const maps = useMap(); + + const { + planetLayer, + planetCompareLayer, + supplierLayer, + contextualLayers, + filters: { suppliers, materials, origins, plots, dates }, + table: { filters: tableFilters }, + } = useAppSelector((state) => state.eudr); + + const [hoverInfo, setHoverInfo] = useState(null); + const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); + + 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(), + }; + } + + 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: filteredData.map((row) => row.plots.dfs.flat()).flat(), + sda: filteredData.map((row) => row.plots.sda.flat()).flat(), + }; + }, + }, + ); + + 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), + }); + + const filteredGeometries: typeof plotGeometries.data = useMemo(() => { + if (!plotGeometries.data || !data) return null; + + 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', + // @ts-expect-error will fix this later... + data: filteredGeometries, + // 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, + }); + }, [filteredGeometries, data, supplierLayer.active, supplierLayer.opacity]); + + 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', + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_FOREST_ACCESS_TOKEN, + }, + }); + + 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` WHERE year<=?', + queryParameters: [contextualLayers['deforestation-alerts-2020-2022-hansen'].year], + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_DEFORESTATION_ACCESS_TOKEN, + }, + }); + + 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` 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, + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_RADD_ACCESS_TOKEN, + }, + }); + + 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]); + + useEffect(() => { + if (plotGeometries.data?.features?.length === 0 || plotGeometries.isLoading) { + return; + } + setTimeout(() => { + const newViewport = new WebMercatorViewport({ ...viewState, width: 800, height: 600 }); + const dataBounds = bbox(plotGeometries.data); + const newViewState = newViewport.fitBounds( + [ + [dataBounds[0], dataBounds[1]], + [dataBounds[2], dataBounds[3]], + ], + { + padding: 50, + }, + ); + const { latitude, longitude, zoom } = newViewState; + setViewState({ ...viewState, latitude, longitude, zoom }); + }, 100); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data, plotGeometries.isLoading]); + + useEffect(() => { + if (!maps.afterMap || !maps.beforeMap) return; + const map = new MapLibreCompare(maps.beforeMap, maps.afterMap, '#comparison-container', { + orientation: 'horizontal', + }); + return () => map?.remove(); + }, [maps.afterMap, maps.beforeMap]); + + return ( + <> +
+ { + 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={[forestCoverLayer, deforestationLayer, raddLayer, eudrSupplierLayer]} + > + +
+ {`${planetLayer.year} ${friendlyMonthFormatter(planetLayer.month.toString())}`} +
+ + + +
+ +
+ {`${planetCompareLayer.year} ${friendlyMonthFormatter( + planetCompareLayer.month.toString(), + )}`} +
+ + + +
+
+
+ {hoverInfo?.object && ( +
+
+
+
Supplier:
+
{hoverInfo.object.properties.supplierName}
+
+
+
Plot:
+
{hoverInfo.object.properties.plotName}
+
+
+
Sourcing volume:
+
+ {formatNumber(hoverInfo.object.properties.baselineVolume)} t +
+
+
+
+ )} +
+ + + +
+ + ); +}; + +export default EUDRCompareMap; 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..5adbb2b5a --- /dev/null +++ b/client/src/containers/analysis-eudr/map/component.tsx @@ -0,0 +1,325 @@ +import { useState, useCallback, useEffect, useMemo } from 'react'; +import DeckGL from '@deck.gl/react/typed'; +import { GeoJsonLayer } from '@deck.gl/layers/typed'; +import Map, { Source, Layer } from 'react-map-gl/maplibre'; +import { WebMercatorViewport } from '@deck.gl/core/typed'; +import { CartoLayer, 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'; +import BasemapControl from './basemap'; + +import { useAppSelector } from '@/store/hooks'; +import { INITIAL_VIEW_STATE, MAP_STYLES } from '@/components/map'; +import { useEUDRData, usePlotGeometries } from '@/hooks/eudr'; +import { formatNumber } from '@/utils/number-format'; + +import type { PickingInfo, MapViewState } from '@deck.gl/core/typed'; + +const monthFormatter = (date: string) => format(date, 'MM'); + +const MAX_BOUNDS = [-75.76238126131099, -9.1712425377296, -74.4412398476887, -7.9871587484823845]; + +const DEFAULT_VIEW_STATE: MapViewState = { + ...INITIAL_VIEW_STATE, + latitude: -8.461844239054608, + longitude: -74.96226240479487, + zoom: 9, + minZoom: 7, + maxZoom: 20, +}; + +const EUDRMap: React.FC<{ supplierId?: string }> = ({ supplierId }) => { + const { + planetLayer, + supplierLayer, + contextualLayers, + filters: { suppliers, materials, origins, plots, dates }, + table: { filters: tableFilters }, + } = useAppSelector((state) => state.eudr); + + const [hoverInfo, setHoverInfo] = useState(null); + const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); + + 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(), + }; + } + + 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: filteredData.map((row) => row.plots.dfs.flat()).flat(), + sda: filteredData.map((row) => row.plots.sda.flat()).flat(), + }; + }, + }, + ); + + 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), + }); + + const filteredGeometries: typeof plotGeometries.data = useMemo(() => { + if (!plotGeometries.data || !data) return null; + + 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', + // @ts-expect-error will fix this later... + data: filteredGeometries, + // 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, + }); + }, [filteredGeometries, data, supplierLayer.active, supplierLayer.opacity]); + + 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', + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_FOREST_ACCESS_TOKEN, + }, + }); + + 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` WHERE year<=?', + queryParameters: [contextualLayers['deforestation-alerts-2020-2022-hansen'].year], + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_DEFORESTATION_ACCESS_TOKEN, + }, + }); + + 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` 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, + 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, + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + accessToken: process.env.NEXT_PUBLIC_CARTO_RADD_ACCESS_TOKEN, + }, + }); + + 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]); + + useEffect(() => { + if (!supplierId || plotGeometries.data?.features?.length === 0 || plotGeometries.isLoading) { + return; + } + const newViewport = new WebMercatorViewport({ ...viewState, width: 800, height: 600 }); + const dataBounds = bbox(plotGeometries.data); + setTimeout(() => { + const newViewState = newViewport.fitBounds( + [ + [dataBounds[0], dataBounds[1]], + [dataBounds[2], dataBounds[3]], + ], + { + padding: 50, + }, + ); + const { latitude, longitude, zoom } = newViewState; + setViewState({ ...viewState, latitude, longitude, zoom }); + }, 160); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plotGeometries.data, plotGeometries.isLoading, supplierId]); + + return ( + <> +
+ { + 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={[forestCoverLayer, deforestationLayer, raddLayer, eudrSupplierLayer]} + > + + {planetLayer.active && ( + + + + )} + + +
+ {hoverInfo?.object && ( +
+
+
+
Supplier:
+
{hoverInfo.object.properties.supplierName}
+
+
+
Plot:
+
{hoverInfo.object.properties.plotName}
+
+
+
Sourcing volume:
+
+ {formatNumber(hoverInfo.object.properties.baselineVolume)} t +
+
+
+
+ )} +
+ + + +
+ + ); +}; + +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/layers.json b/client/src/containers/analysis-eudr/map/layers.json new file mode 100644 index 000000000..c0d990822 --- /dev/null +++ b/client/src/containers/analysis-eudr/map/layers.json @@ -0,0 +1,388 @@ +[ + { + "id": "suppliers-plot-of-land", + "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", + "legend": { + "iconClass": "border-[#ffc038] bg-[#ffc038]/30", + "items": null + } + }, + { + "id": "suppliers-plot-of-land", + "title": "Deforestation-free plots", + "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-[#4ab8f3] bg-[#4ab8f3]/30", + "items": null + } + }, + { + "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": "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." + ], + "source": [ + "EC/JRC" + ], + "type": "contextual", + "legend": { + "iconColor": "#72A950", + "items": null + } + }, + { + "id": "deforestation-alerts-2020-2022-hansen", + "title": "Deforestation alerts since 2020 (Hansen)", + "description": "Identifies annual tree cover loss within forested areas as defined by the EUDR from 2020 to 2022.", + "content": "This dataset employs the Hansen tree cover loss layer to analyze annual tree cover loss occurring within forested regions, as defined by the European Union's Regulation on deforestation-related products (EUDR). Specifically, it focuses on the period spanning from 2020 to 2022. By utilizing this layer, which provides comprehensive information on changes in tree cover over time, it aims to assess and quantify the extent of deforestation or degradation within designated forested areas, thereby contributing to monitoring and understanding forest dynamics and associated environmental impacts over the specified timeframe.", + "citation": [ + "Hansen, M. C., P. V. Potapov, R. Moore, M. Hancher, S. A. Turubanova, A. Tyukavina, D. Thau, S. V. Stehman, S. J. Goetz, T. R. Loveland, A. Kommareddy, A. Egorov, L. Chini, C. O. Justice, and J. R. G. Townshend. 2013. “High-Resolution Global Maps of 21st-Century Forest Cover Change.” Science 342 (15 November): 850–53. Data available on-line from:http://earthenginepartners.appspot.com/science-2013-global-forest. Accessed through Global Forest Watch on 12/03/2024. www.globalforestwatch.org", + "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.", + "Adrià, Descals, Serge, Wich, Erik, Meijaard, David, Gaveau, Stephen, Peedell, & Zoltan, Szantoi. (2021, January 27). High resolution global industrial and smallholder oil palm map for 2019 (Version v1). Zenodo. doi:10.5281/zenodo.4473715" + ], + "source": [ + "Hansen/UMD/Google/USGS/NASA, accessed through Global Forest Watch", + "EC/JRC", + "BIOPAMA" + ], + "type": "contextual", + "legend": { + "iconColor": "#E0BF24", + "items": null + } + }, + { + "id": "real-time-deforestation-alerts-since-2020-radd", + "title": "Real time deforestation alerts since 2020 (RADD)", + "description": "Identifies real-time deforestation alerts from RADD within forested areas as defined by the EUDR, from the 31st of December 2020 to the present.", + "content": "This dataset refines real-time deforestation alerts from the RAdar for Detecting Deforestation (RADD) product by overlaying them with the JRC 2020 forest cover data. By filtering the RADD alerts using the forested areas delineated in the JRC 2020 dataset, this process specifically targets deforestation activities within forested regions as defined by the European Union's Regulation on deforestation-related products (EUDR). This approach ensures that only deforestation alerts occurring within designated forested areas, as per the EUDR definition, are captured and analyzed. The timeframe for this analysis spans from the 31st of December 2020, corresponding to the cut-off date specified by the EUDR, to the present.", + "citation": [ + "Cook-Patton, S.C., Leavitt, S.M., Gibbs, D. et al. Mapping carbon accumulation potential from global natural forest regrowth. Nature 585, 545–550 (2020). https://doi.org/10.1038/s41586-020-2686-x", + "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.", + "Adrià, Descals, Serge, Wich, Erik, Meijaard, David, Gaveau, Stephen, Peedell, & Zoltan, Szantoi. (2021, January 27). High resolution global industrial and smallholder oil palm map for 2019 (Version v1). Zenodo. doi:10.5281/zenodo.4473715" + ], + "source": [ + "WUR", + "EC/JRC", + "BIOPAMA" + ], + "type": "contextual", + "legend": { + "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" + ] + } + }, + { + "id": "planet-data", + "title": "Planet data", + "description": "Planet data provides high-resolution satellite imagery crucial for validating deforestation alerts, reducing false positives, and enhancing reporting accuracy.", + "content": "Planet data offers high-resolution satellite imagery that is indispensable for validating detected deforestation alerts within specific plots. This data helps to eliminate false positives and reduce the need for on-site inspections, significantly enhancing the certainty of reporting. Leveraging Planet's higher resolution layer increases accuracy and is commonly used to investigate potential causes behind near-real-time deforestation alerts. Additionally, this imagery is instrumental in validation protocols, enabling the assessment of accuracy in land and forest cover products, as well as monitoring changes over time.", + "citation": [ + "Image © [year of image] Planet Labs Inc. Accessed through Global Forest Watch on 12/03/2024. www.globalforestwatch.org" + ], + "source": [ + "Planet" + ], + "type": "basemap", + "legend": null + } +] 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..fb3bb673a --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/component.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react'; +import classNames from 'classnames'; +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'; + +const EURDLegend = () => { + const dispatch = useAppDispatch(); + const { supplierLayer, contextualLayers } = useAppSelector((state) => state.eudr); + + const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + + // 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 ( +
+ + + + + +
+

Legend

+
+ + dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) + } + changeOpacity={(opacity) => + dispatch(setSupplierLayer({ ...supplierLayer, opacity })) + } + /> +
+
+ + dispatch(setSupplierLayer({ ...supplierLayer, active: !supplierLayer.active })) + } + changeOpacity={(opacity) => + dispatch(setSupplierLayer({ ...supplierLayer, opacity })) + } + /> +
+ + +
+ +
+
+ + {contextualLayersData.map((layer) => ( + + dispatch( + setContextualLayer({ + layer: layer.id, + configuration: { active: isVisible }, + }), + ) + } + changeOpacity={(opacity) => + dispatch( + setContextualLayer({ + layer: layer.id, + configuration: { opacity }, + }), + ) + } + > + <> + {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 && } + +
+ ))} +
+
+
+
+
+
+ ); +}; + +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/src/containers/analysis-eudr/map/legend/item.tsx b/client/src/containers/analysis-eudr/map/legend/item.tsx new file mode 100644 index 000000000..ac045468d --- /dev/null +++ b/client/src/containers/analysis-eudr/map/legend/item.tsx @@ -0,0 +1,100 @@ +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'; + +import type { FC, PropsWithChildren } from 'react'; + +type LegendItemProps = { + title: string; + content: string; + description: string; + source?: string | string[]; + legendConfig?: { + iconColor?: string; + iconClass?: string; + items?: { label: string; color: string }[]; + dates?: string[]; + }; + showVisibility?: boolean; + showSwitcher?: boolean; + isActive?: boolean; + changeVisibility?: (active: boolean) => void; + changeOpacity?: (opacity: number) => void; +}; + +const LegendItem: FC> = ({ + title, + content, + description, + source, + children, + legendConfig = null, + showVisibility = false, + showSwitcher = false, + isActive = true, + changeVisibility = () => null, + changeOpacity = () => null, +}) => { + return ( +
+
+
+
+

{title}

+
+
+ + +
+ {showVisibility && ( +
+ +
+ )} + {showSwitcher && ( +
+ +
+ )} +
+
+

{description}

+ {legendConfig?.items?.length > 0 && ( +
    + {legendConfig?.items?.map((item) => ( +
  • +
    +
    {item.label}
    +
  • + ))} +
+ )} + {children} +
+
+ ); +}; + +export default LegendItem; 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/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/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..d2dccf5ee --- /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..674358619 --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/columns.tsx @@ -0,0 +1,122 @@ +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'; + +const numberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 3 }); + +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 ( +
+ {BIG_NUMBER_FORMAT(row.getValue('baselineVolume'))} +
+ ); + }, + }, + { + accessorKey: 'dfs', + header: ({ column }) => , + cell: ({ row }) => { + const dfs: number = row.getValue('dfs'); + return {`${Number.isNaN(dfs) ? '-' : `${numberFormat.format(dfs)}%`}`}; + }, + }, + { + accessorKey: 'sda', + header: ({ column }) => , + cell: ({ row }) => { + const sda: number = row.getValue('sda'); + return {`${Number.isNaN(sda) ? '-' : `${numberFormat.format(sda)}%`}`}; + }, + }, + { + accessorKey: 'tpl', + header: ({ column }) => , + cell: ({ row }) => { + const tpl: number = row.getValue('tpl'); + + return {`${Number.isNaN(tpl) ? '-' : `${numberFormat.format(tpl)}%`}`}; + }, + }, + { + accessorKey: 'crm', + header: ({ column }) => , + cell: ({ row }) => { + const crm: number = row.getValue('crm'); + return {`${Number.isNaN(crm) ? '-' : crm}`}; + }, + }, + { + 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..d427f7a07 --- /dev/null +++ b/client/src/containers/analysis-eudr/supplier-list-table/table/index.tsx @@ -0,0 +1,179 @@ +import { + flexRender, + getCoreRowModel, + // getFacetedRowModel, + // getFacetedUniqueValues, + // getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { useState } from 'react'; + +import columns from './columns'; +import { DataTablePagination, PAGINATION_SIZES } from './pagination'; + +import { + Table, + TableHeader, + TableHead, + TableRow, + TableBody, + TableCell, +} from '@/components/ui/table'; +import { useEUDRData } from '@/hooks/eudr'; +import { useAppSelector, useAppDispatch } from '@/store/hooks'; +import { eudr, setTotalSuppliers } from '@/store/features/eudr'; + +import type { + // ColumnFiltersState, + SortingState, + // VisibilityState +} from '@tanstack/react-table'; + +export interface Supplier { + supplierId: string; + supplierName: string; + companyId: string; + baselineVolume: number; + dfs: number; + sda: number; + tpl: number; + materials: { + name: string; + id: string; + }[]; + origins: { + name: string; + id: string; + }[]; + plots: { + dfs: string[]; + sda: string[]; + }; +} + +const SuppliersListTable = (): JSX.Element => { + const dispatch = useAppDispatch(); + // const [rowSelection, setRowSelection] = useState({}); + // const [columnVisibility, setColumnVisibility] = useState({}); + // const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); + const { + filters: { dates, suppliers, origins, materials, plots }, + table: { filters: tableFilters }, + } = useAppSelector(eudr); + + const { data, isFetching } = 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) => + 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)); + }, + }, + ); + + 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 ( + <> + {!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. + + + )} + +
+ +
+ )} + + ); +}; + +export default SuppliersListTable; 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..a49eda84e --- /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 new file mode 100644 index 000000000..5b67868a7 --- /dev/null +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/component.tsx @@ -0,0 +1,215 @@ +import React, { useCallback, useMemo } from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Label, +} from 'recharts'; +import { groupBy } from 'lodash-es'; + +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 } from '@/hooks/eudr'; + +export const VIEW_BY_OPTIONS = [ + { + label: 'Commodities', + value: 'materials', + }, + { + label: 'Countries', + value: 'origins', + }, +] as const; + +const TOOLTIP_LABELS = { + free: CATEGORIES[0].name, + alerts: CATEGORIES[1].name, + noData: CATEGORIES[2].name, +} as const; + +const SuppliersStackedBar = () => { + const { + viewBy, + totalSuppliers, + filters: { dates, suppliers, origins, materials }, + } = useAppSelector(eudr); + const dispatch = useAppDispatch(); + + const handleViewBy = useCallback( + (value: typeof viewBy) => { + dispatch(setViewBy(value)); + }, + [dispatch], + ); + + const { data, isFetching } = useEUDRData( + { + startAlertDate: dates.from, + endAlertDate: 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: {totalSuppliers} +
+
+

Suppliers by category

+
+
View by:
+ + {VIEW_BY_OPTIONS.map((option) => ( +
+ + {option.label} +
+ ))} +
+
+
+
+ {!parsedData?.length && isFetching && ( +
+ 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], + ]} + /> */} + + + + + +
+ + + )} +
+ ); +}; + +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..661b131e0 --- /dev/null +++ b/client/src/containers/analysis-eudr/suppliers-stacked-bar/index.ts @@ -0,0 +1,3 @@ +export { default } from './component'; + +export { VIEW_BY_OPTIONS } from './component'; 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-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/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/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 ( + +
+
+
+ + + + +
+
+ + + +
+ {!planetCompareLayer.active && } + {planetCompareLayer.active && } +
+ + + ); +}; + +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/features/eudr/index.ts b/client/src/store/features/eudr/index.ts new file mode 100644 index 000000000..27dbf16fe --- /dev/null +++ b/client/src/store/features/eudr/index.ts @@ -0,0 +1,186 @@ +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'; + +type LayerConfiguration = { + active?: boolean; + opacity?: number; + dateFrom?: string; + dateTo?: string; + date?: string; + month?: number; + year?: number; +}; + +export type EUDRState = { + viewBy: (typeof VIEW_BY_OPTIONS)[number]['value']; + totalSuppliers: number; + filters: { + materials: Option[]; + origins: Option[]; + plots: Option[]; + suppliers: Option[]; + dates: { + from: string; + to: string; + }; + }; + table: { + filters: { + dfs: boolean; + sda: boolean; + tpl: boolean; + }; + }; + // map + basemap: 'light' | 'planet'; + planetLayer: LayerConfiguration; + planetCompareLayer: LayerConfiguration; + supplierLayer: LayerConfiguration; + contextualLayers: Record; +}; + +export const initialState: EUDRState = { + viewBy: 'materials', + totalSuppliers: 0, + filters: { + materials: [], + origins: [], + plots: [], + suppliers: [], + dates: { + from: DATES_RANGE[0], + to: DATES_RANGE[1], + }, + }, + table: { + filters: { + dfs: false, + sda: false, + tpl: false, + }, + }, + basemap: 'light', + supplierLayer: { + active: true, + opacity: 1, + }, + contextualLayers: { + ['forest-cover-2020-ec-jrc']: { + active: false, + opacity: 1, + }, + ['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', + }, + }, + planetLayer: { + active: false, + month: 12, + year: 2020, + }, + planetCompareLayer: { + active: false, + month: 2, + year: 2024, + }, +}; + +export const EUDRSlice = createSlice({ + name: 'eudr', + initialState, + reducers: { + setViewBy: (state, action: PayloadAction) => ({ + ...state, + viewBy: action.payload, + }), + setTotalSuppliers: (state, action: PayloadAction) => ({ + ...state, + totalSuppliers: action.payload, + }), + setFilters: (state, action: PayloadAction>) => ({ + ...state, + filters: { + ...state.filters, + ...action.payload, + }, + }), + setTableFilters: (state, action: PayloadAction>) => ({ + ...state, + table: { + ...state.table, + filters: { + ...state.table.filters, + ...action.payload, + }, + }, + }), + setBasemap: (state, action: PayloadAction) => ({ + ...state, + basemap: action.payload, + }), + setSupplierLayer: (state, action: PayloadAction) => ({ + ...state, + supplierLayer: { + ...state.supplierLayer, + ...action.payload, + }, + }), + setContextualLayer: ( + state, + action: PayloadAction<{ layer: string; configuration: LayerConfiguration }>, + ) => ({ + ...state, + contextualLayers: { + ...state.contextualLayers, + [action.payload.layer]: { + ...state.contextualLayers[action.payload.layer], + ...action.payload.configuration, + }, + }, + }), + setPlanetLayer: (state, action: PayloadAction) => ({ + ...state, + planetLayer: { + ...state.planetLayer, + ...action.payload, + }, + }), + setPlanetCompareLayer: (state, action: PayloadAction) => ({ + ...state, + planetCompareLayer: { + ...state.planetCompareLayer, + ...action.payload, + }, + }), + }, +}); + +export const { + setViewBy, + setTotalSuppliers, + setFilters, + setTableFilters, + setBasemap, + setSupplierLayer, + setContextualLayer, + setPlanetLayer, + setPlanetCompareLayer, +} = EUDRSlice.actions; + +export const eudr = (state: RootState) => state['eudr']; + +export default EUDRSlice.reducer; diff --git a/client/src/store/index.ts b/client/src/store/index.ts index b38766948..48f714987 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -16,6 +16,8 @@ import analysisScenarios, { initialState as analysisScenariosInitialState, 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'; @@ -26,6 +28,8 @@ const staticReducers = { 'analysis/filters': analysisFilters, 'analysis/map': analysisMap, 'analysis/scenarios': analysisScenarios, + eudr, + eudrDetail, }; const asyncReducers = {}; diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 1ce4c66ed..9b190baf1 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -2,15 +2,76 @@ @tailwind components; @tailwind utilities; -@-webkit-keyframes autofill { - 0%, - 100% { - color: #666; - @apply bg-transparent; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; } -} -@layer base { + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } + + * { + @apply border-border; + } + /* body { + @apply bg-background text-foreground; + } */ + input[type='search']::-webkit-search-decoration, input[type='search']::-webkit-search-cancel-button, input[type='search']::-webkit-search-results-button, @@ -56,9 +117,15 @@ } } -@layer utilities { +@-webkit-keyframes autofill { + 0%, + 100% { + color: #666; + @apply bg-transparent; + } } + .rc-tree .rc-tree-checkbox { @apply hidden; } @@ -87,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; +} diff --git a/client/src/utils/colors.ts b/client/src/utils/colors.ts index 53334129f..a89a2b2c9 100644 --- a/client/src/utils/colors.ts +++ b/client/src/utils/colors.ts @@ -22,4 +22,15 @@ export function useColors(layerName: string, colorScale): RGBColor[] { return colors; } -export const themeColors = resolveConfig(tailwindConfig).theme.colors as Record; +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/src/utils/number-format.ts b/client/src/utils/number-format.ts index 3e35866e8..b14809c04 100644 --- a/client/src/utils/number-format.ts +++ b/client/src/utils/number-format.ts @@ -19,3 +19,7 @@ export function formatNumber(number: number): string { export const PRECISE_NUMBER_FORMAT = format(',.3~r'); export const BIG_NUMBER_FORMAT = format('s'); + +export const formatPercentage = (value: number, options: Intl.NumberFormatOptions = {}) => { + return Intl.NumberFormat(undefined, { style: 'percent', ...options }).format(value); +}; diff --git a/client/tailwind.config.js b/client/tailwind.config.js deleted file mode 100644 index d136a516c..000000000 --- a/client/tailwind.config.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], -}; diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts new file mode 100644 index 000000000..69dde4ab9 --- /dev/null +++ b/client/tailwind.config.ts @@ -0,0 +1,146 @@ +import type { Config } from 'tailwindcss'; + +const config = { + darkMode: ['class'], + content: ['./src/**/*.{ts,tsx,json}'], + prefix: '', + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + 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: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + 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 + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [ + require('tailwindcss-animate'), + require('@tailwindcss/forms'), + require('@tailwindcss/typography'), + ], +} satisfies Config; + +export default config; diff --git a/client/yarn.lock b/client/yarn.lock index 2ff8fa2fc..419d38b2c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 6 cacheKey: 8 +"@alloc/quick-lru@npm:^5.2.0": + version: 5.2.0 + resolution: "@alloc/quick-lru@npm:5.2.0" + checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -209,6 +216,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.23.7": + version: 7.24.0 + resolution: "@babel/runtime@npm:7.24.0" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 7a6a5d40fbdd68491ec183ba2e631c07415119960083b4fd76564cce3751e9acd2f12ab89575e38496fa389fa06d458732776e69ee1858e366cc3fbdb049f847 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.20.0": version: 7.20.1 resolution: "@babel/runtime@npm:7.20.1" @@ -310,6 +326,58 @@ __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/aggregation-layers@npm:8.8.6": + version: 8.8.6 + resolution: "@deck.gl/aggregation-layers@npm:8.8.6" + dependencies: + "@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: ^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 + 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.0.0 + checksum: f27abb24ae5a77c0debcbd32c5a0ee1f255ffdeec7d0b0dc0bdc43ad78acf53b599aacaabae735e03c1c7d22cf29aa212821782f006f6c20345f17265b243c90 + languageName: node + linkType: hard + "@deck.gl/core@npm:8.8.6": version: 8.8.6 resolution: "@deck.gl/core@npm:8.8.6" @@ -421,6 +489,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" @@ -535,6 +617,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" @@ -545,6 +636,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" @@ -557,6 +658,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" @@ -578,6 +691,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" @@ -855,6 +975,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -1239,17 +1373,17 @@ __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 +"@mapbox/mapbox-gl-sync-move@npm:^0.3.0": + version: 0.3.1 + resolution: "@mapbox/mapbox-gl-sync-move@npm:0.3.1" + checksum: 3dcc6ecdb22e3797b3aec0fd36c247a23a485ea6552f2a46c36ba89607fc91babe52fba45eb373a43d35175535f4e08a43a05a0ebd99514c5860c30fec808aaf languageName: node linkType: hard @@ -1304,6 +1438,35 @@ __metadata: languageName: node linkType: hard +"@maplibre/maplibre-gl-compare@npm:^0.5.0": + version: 0.5.0 + resolution: "@maplibre/maplibre-gl-compare@npm:0.5.0" + dependencies: + "@mapbox/mapbox-gl-sync-move": ^0.3.0 + peerDependencies: + maplibre-gl: ">=1.14.0" + checksum: 18194e9f5eceb451ca3a9b3e18781fdb8e39ce5e33f40f9ee0e70721a6943fdbf4ab7a3c2e1606e0f1b86cc71686dbea92bcb74fa34d2df1615f3cc43589994e + 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" @@ -1505,6 +1668,13 @@ __metadata: languageName: node linkType: hard +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + "@pkgr/core@npm:^0.1.0": version: 0.1.0 resolution: "@pkgr/core@npm:0.1.0" @@ -1554,152 +1724,927 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:1.8.2": - version: 1.8.2 - resolution: "@reduxjs/toolkit@npm:1.8.2" +"@radix-ui/number@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/number@npm:1.0.1" dependencies: - immer: ^9.0.7 - redux: ^4.1.2 - redux-thunk: ^2.4.1 - reselect: ^4.1.5 - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.0-beta - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - checksum: bd94e6d5c469f841c59e7d74e3fc60681c023ccdc6367005a9b04c252990d103bba438b2aea82f6e3db697486b32f4a5fb0d4bb6208af6119e5028c2ee626198 - languageName: node - linkType: hard - -"@rushstack/eslint-patch@npm:^1.3.3": - version: 1.5.1 - resolution: "@rushstack/eslint-patch@npm:1.5.1" - checksum: e4c25322312dbaa29e835a7ab4fbac53c8731dd0da65e46646e38945e296429e7fb91c2ef3da5af5d5938d44b0cde1d5290438ebb3dcb015e02b80b5e2530d24 + "@babel/runtime": ^7.13.10 + checksum: 621ea8b7d4195d1a65a9c0aee918e8335e7f198088eec91577512c89c2ba3a3bab4a767cfb872a2b9c3092a78ff41cad9a924845a939f6bb87fe9356241ea0ea languageName: node linkType: hard -"@sideway/address@npm:^4.1.3": - version: 4.1.4 - resolution: "@sideway/address@npm:4.1.4" +"@radix-ui/primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/primitive@npm:1.0.1" dependencies: - "@hapi/hoek": ^9.0.0 - checksum: b9fca2a93ac2c975ba12e0a6d97853832fb1f4fb02393015e012b47fa916a75ca95102d77214b2a29a2784740df2407951af8c5dde054824c65577fd293c4cdb + "@babel/runtime": ^7.13.10 + checksum: 2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1 languageName: node linkType: hard -"@sideway/formula@npm:^3.0.0": - version: 3.0.0 - resolution: "@sideway/formula@npm:3.0.0" - checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 +"@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 -"@sideway/pinpoint@npm:^2.0.0": - version: 2.0.0 - resolution: "@sideway/pinpoint@npm:2.0.0" - checksum: 0f4491e5897fcf5bf02c46f5c359c56a314e90ba243f42f0c100437935daa2488f20482f0f77186bd6bf43345095a95d8143ecf8b1f4d876a7bc0806aba9c3d2 +"@radix-ui/react-collapsible@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-collapsible@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-id": 1.0.1 + "@radix-ui/react-presence": 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 + 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: 26976e4a72a3e0f4b2c62af2898b3e205c3652af46a3b41cda9a43567fe8381d9ef6afb0b29e3214c450b847f4f2099a533cffc5045844ecab290e9fa6114ca9 languageName: node linkType: hard -"@streamparser/json@npm:^0.0.12": - version: 0.0.12 - resolution: "@streamparser/json@npm:0.0.12" - checksum: 976975f0169253c6397240434ca8bb87c09be09597b949a8d473be009a6aac6469d5e0df8c8bc47b5a0a9e679ebb48d415f4bcef3169cb1707e45e9296211197 +"@radix-ui/react-collection@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-collection@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@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-slot": 1.0.2 + 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: acfbc9b0b2c553d343c22f02c9f098bc5cfa99e6e48df91c0d671855013f8b877ade9c657b7420a7aa523b5aceadea32a60dd72c23b1291f415684fb45d00cff languageName: node linkType: hard -"@swc/helpers@npm:0.5.2": - version: 0.5.2 - resolution: "@swc/helpers@npm:0.5.2" +"@radix-ui/react-compose-refs@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-compose-refs@npm:1.0.1" dependencies: - tslib: ^2.4.0 - checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8 languageName: node linkType: hard -"@tailwindcss/forms@npm:0.4.0": - version: 0.4.0 - resolution: "@tailwindcss/forms@npm:0.4.0" +"@radix-ui/react-context@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-context@npm:1.0.1" dependencies: - mini-svg-data-uri: ^1.2.3 + "@babel/runtime": ^7.13.10 peerDependencies: - tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1" - checksum: 881a80b136f22b3da68323166ddfbcd844841f711d75ce4d64479302ae523ac7c1fe8a3bb57370216e6da69303640d70d543ecdb3ec2086f1ddf428a6c13566f + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 60e9b81d364f40c91a6213ec953f7c64fcd9d75721205a494a5815b3e5ae0719193429b62ee6c7002cd6aaf70f8c0e2f08bdbaba9ffcc233044d32b56d2127d1 languageName: node linkType: hard -"@tailwindcss/typography@npm:0.5.0": - version: 0.5.0 - resolution: "@tailwindcss/typography@npm:0.5.0" +"@radix-ui/react-direction@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-direction@npm:1.0.1" dependencies: - lodash.castarray: ^4.4.0 - lodash.isplainobject: ^4.0.6 - lodash.merge: ^4.6.2 - lodash.uniq: ^4.5.0 + "@babel/runtime": ^7.13.10 peerDependencies: - tailwindcss: "*" - checksum: 84986845115cd8bfc0d9df8f2a6d3e86fdba0d54e236a6150782b1340e27218610a5dbb5ab63c34cbc6f8bef4180ff0d1540808895e893b16854c2b0f83094dc + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 5336a8b0d4f1cde585d5c2b4448af7b3d948bb63a1aadb37c77771b0e5902dc6266e409cf35fd0edaca7f33e26424be19e64fb8f9d7f7be2d6f1714ea2764210 languageName: node linkType: hard -"@tanstack/query-core@npm:^4.0.0-beta.1": - version: 4.2.1 - resolution: "@tanstack/query-core@npm:4.2.1" - checksum: f71854969e02de6c2cfbe25e8b11e275b61e1297a902e0d5c4beac580a87db99555c1c21d536d838ce5e0664bc49da7b60a3c6b8de334c7004c5005fe2a48030 +"@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 -"@tanstack/react-query@npm:^4.2.1": - version: 4.2.1 - resolution: "@tanstack/react-query@npm:4.2.1" - dependencies: - "@tanstack/query-core": ^4.0.0-beta.1 - "@types/use-sync-external-store": ^0.0.3 - use-sync-external-store: ^1.2.0 +"@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: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: "*" + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: - react-dom: + "@types/react": optional: true - react-native: + "@types/react-dom": optional: true - checksum: bbf3a808645c26c649971dc182bb9a7ed7a1d89f6456b60685c6081b8be6ae84ae83b39c7eacb96c4f3b6677ca001d8114037329951987b7a8d65de53b28c862 + checksum: 1433e04234c29ae688b1d50b4a5ad0fd67e2627a5ea2e5f60fec6e4307e673ef35a703672eae0d61d96156c59084bbb19de9f9b9936b3fc351917dfe41dcf403 languageName: node linkType: hard -"@tanstack/react-table@npm:8.5.1": - version: 8.5.1 - resolution: "@tanstack/react-table@npm:8.5.1" +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" dependencies: - "@tanstack/table-core": 8.5.1 + "@babel/runtime": ^7.13.10 peerDependencies: - react: ">=16" - react-dom: ">=16" - checksum: 4081d0ecd07924b4969c1de7d5a6e8262737d09b45ba649112da1506169f42f3d00d4566d0b69619d530af264e6f32da9ae27197377fb7dbb76934b51348ffcd + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7 languageName: node linkType: hard -"@tanstack/react-virtual@npm:3.0.1": - version: 3.0.1 - resolution: "@tanstack/react-virtual@npm:3.0.1" +"@radix-ui/react-focus-scope@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-focus-scope@npm:1.0.4" dependencies: - "@tanstack/virtual-core": 3.0.0 + "@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: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 11534a23100de14a7e0a95da667381181e60a24e29a71246aeed174f8d5f6e176216de6639e6e1f403722ae30d8b92b21ed75ea131529b1417fb81f433468ef0 + "@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" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-layout-effect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 446a453d799cc790dd2a1583ff8328da88271bff64530b5a17c102fa7fb35eece3cf8985359d416f65e330cd81aa7b8fe984ea125fc4f4eaf4b3801d698e49fe + languageName: node + linkType: hard + +"@radix-ui/react-label@npm:2.0.2": + version: 2.0.2 + resolution: "@radix-ui/react-label@npm:2.0.2" + 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: fe3bd8902bc523fb5125fa96167a13b8a60d007413787eae9573e4b00b0edff0487c4c0620ea5dc37e6da13833ebc4f8d7e00b6c846f2a5686e7f173672b8dde + 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" + 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" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-use-layout-effect": 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: ed2ff9faf9e4257a4065034d3771459e5a91c2d840b2fcec94661761704dbcb65bcdd927d28177a2a129b3dab5664eb90a9b88309afe0257a9f8ba99338c0d95 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-primitive@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-slot": 1.0.2 + 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: 9402bc22923c8e5c479051974a721c301535c36521c0237b83e5fa213d013174e77f3ad7905e6d60ef07e14f88ec7f4ea69891dc7a2b39047f8d3640e8f8d713 + languageName: node + linkType: hard + +"@radix-ui/react-radio-group@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-radio-group@npm:1.1.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-direction": 1.0.1 + "@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-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: 88f7007610817ab30f471a7e1f6605e94cc507a31fb4bb218116d65cc48c9b3149fce500f386716a3ed5fb0089d65faf32d3e01971322cd4a14b51003ec82bc2 + languageName: node + linkType: hard + +"@radix-ui/react-roving-focus@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-roving-focus@npm:1.0.4" + 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-id": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-callback-ref": 1.0.1 + "@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: 69b1c82c2d9db3ba71549a848f2704200dab1b2cd22d050c1e081a78b9a567dbfdc7fd0403ee010c19b79652de69924d8ca2076cd031d6552901e4213493ffc7 + 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-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-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" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: edf5edf435ff594bea7e198bf16d46caf81b6fb559493acad4fa8c308218896136acb16f9b7238c788fd13e94a904f2fd0b6d834e530e4cae94522cdb8f77ce9 + 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" + 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-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 + "@radix-ui/react-visually-hidden": 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: 894d448c69a3e4d7626759f9f6c7997018fe8ef9cde098393bd83e10743d493dfd284eef041e46accc45486d5a5cd5f76d97f56afbdace7aed6e0cb14007bf15 + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-callback-ref@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: b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" + 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: dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9 + 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" + dependencies: + "@babel/runtime": ^7.13.10 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5 + languageName: node + linkType: hard + +"@radix-ui/react-use-previous@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-previous@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: 66b4312e857c58b75f3bf62a2048ef090b79a159e9da06c19a468c93e62336969c33dbef60ff16969f00b20386cc25d138f6a353f1658b35baac0a6eff4761b9 + 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" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-layout-effect": 1.0.1 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6cc150ad1e9fa85019c225c5a5d50a0af6cdc4653dad0c21b4b40cd2121f36ee076db326c43e6bc91a69766ccff5a84e917d27970176b592577deea3c85a3e26 + 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" + dependencies: + immer: ^9.0.7 + redux: ^4.1.2 + redux-thunk: ^2.4.1 + reselect: ^4.1.5 + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.0-beta + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: bd94e6d5c469f841c59e7d74e3fc60681c023ccdc6367005a9b04c252990d103bba438b2aea82f6e3db697486b32f4a5fb0d4bb6208af6119e5028c2ee626198 + languageName: node + linkType: hard + +"@rushstack/eslint-patch@npm:^1.3.3": + version: 1.5.1 + resolution: "@rushstack/eslint-patch@npm:1.5.1" + checksum: e4c25322312dbaa29e835a7ab4fbac53c8731dd0da65e46646e38945e296429e7fb91c2ef3da5af5d5938d44b0cde1d5290438ebb3dcb015e02b80b5e2530d24 + languageName: node + linkType: hard + +"@sideway/address@npm:^4.1.3": + version: 4.1.4 + resolution: "@sideway/address@npm:4.1.4" + dependencies: + "@hapi/hoek": ^9.0.0 + checksum: b9fca2a93ac2c975ba12e0a6d97853832fb1f4fb02393015e012b47fa916a75ca95102d77214b2a29a2784740df2407951af8c5dde054824c65577fd293c4cdb + languageName: node + linkType: hard + +"@sideway/formula@npm:^3.0.0": + version: 3.0.0 + resolution: "@sideway/formula@npm:3.0.0" + checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 + languageName: node + linkType: hard + +"@sideway/pinpoint@npm:^2.0.0": + version: 2.0.0 + resolution: "@sideway/pinpoint@npm:2.0.0" + checksum: 0f4491e5897fcf5bf02c46f5c359c56a314e90ba243f42f0c100437935daa2488f20482f0f77186bd6bf43345095a95d8143ecf8b1f4d876a7bc0806aba9c3d2 + languageName: node + linkType: hard + +"@streamparser/json@npm:^0.0.12": + version: 0.0.12 + resolution: "@streamparser/json@npm:0.0.12" + checksum: 976975f0169253c6397240434ca8bb87c09be09597b949a8d473be009a6aac6469d5e0df8c8bc47b5a0a9e679ebb48d415f4bcef3169cb1707e45e9296211197 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.2": + version: 0.5.2 + resolution: "@swc/helpers@npm:0.5.2" + dependencies: + tslib: ^2.4.0 + checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 + languageName: node + linkType: hard + +"@tailwindcss/forms@npm:0.4.0": + version: 0.4.0 + resolution: "@tailwindcss/forms@npm:0.4.0" + dependencies: + mini-svg-data-uri: ^1.2.3 + peerDependencies: + tailwindcss: ">=3.0.0 || >= 3.0.0-alpha.1" + checksum: 881a80b136f22b3da68323166ddfbcd844841f711d75ce4d64479302ae523ac7c1fe8a3bb57370216e6da69303640d70d543ecdb3ec2086f1ddf428a6c13566f + languageName: node + linkType: hard + +"@tailwindcss/typography@npm:0.5.0": + version: 0.5.0 + resolution: "@tailwindcss/typography@npm:0.5.0" + dependencies: + lodash.castarray: ^4.4.0 + lodash.isplainobject: ^4.0.6 + lodash.merge: ^4.6.2 + lodash.uniq: ^4.5.0 + peerDependencies: + tailwindcss: "*" + checksum: 84986845115cd8bfc0d9df8f2a6d3e86fdba0d54e236a6150782b1340e27218610a5dbb5ab63c34cbc6f8bef4180ff0d1540808895e893b16854c2b0f83094dc + languageName: node + linkType: hard + +"@tanstack/query-core@npm:^4.0.0-beta.1": + version: 4.2.1 + resolution: "@tanstack/query-core@npm:4.2.1" + checksum: f71854969e02de6c2cfbe25e8b11e275b61e1297a902e0d5c4beac580a87db99555c1c21d536d838ce5e0664bc49da7b60a3c6b8de334c7004c5005fe2a48030 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^4.2.1": + version: 4.2.1 + resolution: "@tanstack/react-query@npm:4.2.1" + dependencies: + "@tanstack/query-core": ^4.0.0-beta.1 + "@types/use-sync-external-store": ^0.0.3 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: bbf3a808645c26c649971dc182bb9a7ed7a1d89f6456b60685c6081b8be6ae84ae83b39c7eacb96c4f3b6677ca001d8114037329951987b7a8d65de53b28c862 + languageName: node + linkType: hard + +"@tanstack/react-table@npm:8.13.2": + version: 8.13.2 + resolution: "@tanstack/react-table@npm:8.13.2" + dependencies: + "@tanstack/table-core": 8.13.2 + peerDependencies: + react: ">=16" + react-dom: ">=16" + checksum: 5a5ae9cf124a19d5b5ba585359fc51e8999d5418df0bdd14acf8e8644355c540d54d7392041ceec514956d54189a1f7bf5a19ec9ae5a27fa93bd556a5b156a75 + languageName: node + linkType: hard + +"@tanstack/react-virtual@npm:3.0.1": + version: 3.0.1 + resolution: "@tanstack/react-virtual@npm:3.0.1" + dependencies: + "@tanstack/virtual-core": 3.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 11534a23100de14a7e0a95da667381181e60a24e29a71246aeed174f8d5f6e176216de6639e6e1f403722ae30d8b92b21ed75ea131529b1417fb81f433468ef0 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 @@ -1717,6 +2662,39 @@ __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 + +"@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" @@ -1857,6 +2835,13 @@ __metadata: languageName: node linkType: hard +"@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 + languageName: node + linkType: hard + "@types/hammerjs@npm:^2.0.41": version: 2.0.41 resolution: "@types/hammerjs@npm:2.0.41" @@ -1918,7 +2903,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 +2921,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 +2967,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 +3040,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" @@ -2448,6 +3476,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -2466,6 +3501,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -2546,6 +3588,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" @@ -2555,6 +3606,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 +3749,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" @@ -2886,6 +3951,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" @@ -2961,6 +4033,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" @@ -3044,6 +4135,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" @@ -3135,6 +4235,15 @@ __metadata: languageName: node linkType: hard +"class-variance-authority@npm:0.7.0": + version: 0.7.0 + resolution: "class-variance-authority@npm:0.7.0" + dependencies: + clsx: 2.0.0 + checksum: e7fd1fab433ef06f52a1b7b241b70b4a185864deef199d3b0a2c3412f1cc179517288264c383f3b971a00d76811625fc8f7ffe709e6170219e88cd7368f08a20 + languageName: node + linkType: hard + "classnames@npm:2.3.1, classnames@npm:2.x, classnames@npm:^2.2.1, classnames@npm:^2.2.6": version: 2.3.1 resolution: "classnames@npm:2.3.1" @@ -3206,6 +4315,20 @@ __metadata: languageName: node linkType: hard +"clsx@npm:2.0.0": + version: 2.0.0 + resolution: "clsx@npm:2.0.0" + checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e + languageName: node + linkType: hard + +"clsx@npm:^2.1.0": + version: 2.1.0 + resolution: "clsx@npm:2.1.0" + checksum: 43fefc29b6b49c9476fbce4f8b1cc75c27b67747738e598e6651dd40d63692135dc60b18fa1c5b78a2a9ba8ae6fd2055a068924b94e20b42039bd53b78b98e1d + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -3231,7 +4354,7 @@ __metadata: languageName: node linkType: hard -"color-name@npm:^1.0.0, color-name@npm:^1.1.4, color-name@npm:~1.1.4": +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 @@ -3267,6 +4390,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" @@ -3311,6 +4441,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" @@ -3385,10 +4522,43 @@ __metadata: languageName: node linkType: hard -"csscolorparser@npm:~1.0.3": - version: 1.0.3 - resolution: "csscolorparser@npm:1.0.3" - checksum: e40f3045ea15c7e7eaa78e110412fe8b820d47b698c1eb1d1e7ecb42703bf447406a24304b891ae9df61e85d947f33fc67bd0120c7f9e3a5183e6e0b9afff92c +"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 @@ -3401,6 +4571,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" @@ -3470,6 +4649,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" @@ -3488,6 +4676,13 @@ __metadata: languageName: node linkType: hard +"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" @@ -3502,6 +4697,13 @@ __metadata: languageName: node linkType: hard +"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" @@ -3516,6 +4718,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" @@ -3545,6 +4763,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" @@ -3554,6 +4785,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" @@ -3563,6 +4803,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" @@ -3604,10 +4853,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 @@ -3794,6 +5043,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" @@ -3844,6 +5100,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" @@ -3865,6 +5159,13 @@ __metadata: languageName: node linkType: hard +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + "ecc-jsbn@npm:~0.1.1": version: 0.1.2 resolution: "ecc-jsbn@npm:0.1.2" @@ -3943,6 +5244,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" @@ -4664,6 +5972,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" @@ -4743,16 +6070,16 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.12": - version: 3.2.12 - resolution: "fast-glob@npm:3.2.12" +"fast-glob@npm:^3.3.0": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" dependencies: "@nodelib/fs.stat": ^2.0.2 "@nodelib/fs.walk": ^1.2.3 glob-parent: ^5.1.2 merge2: ^1.3.0 micromatch: ^4.0.4 - checksum: 0b1990f6ce831c7e28c4d505edcdaad8e27e88ab9fa65eedadb730438cfc7cde4910d6c975d6b7b8dc8a73da4773702ebcfcd6e3518e73938bb1383badfe01c2 + checksum: 900e4979f4dbc3313840078419245621259f349950411ca2fa445a2f9a1a6d98c3b5e7e0660c5ccd563aa61abe133a21765c6c0dec8e57da1ba71d8000b05ec1 languageName: node linkType: hard @@ -4938,6 +6265,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + "forever-agent@npm:~0.6.1": version: 0.6.1 resolution: "forever-agent@npm:0.6.1" @@ -5158,6 +6495,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" @@ -5207,6 +6551,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" @@ -5264,20 +6615,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.6": - version: 7.1.6 - resolution: "glob@npm:7.1.6" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.0.4 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 351d549dd90553b87c2d3f90ce11aed9e1093c74130440e7ae0592e11bbcd2ce7f0ebb8ba6bfe63aaf9b62166a7f4c80cb84490ae5d78408bb2572bf7d4ee0a6 - languageName: node - linkType: hard - "glob@npm:7.1.7": version: 7.1.7 resolution: "glob@npm:7.1.7" @@ -5292,6 +6629,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.10": + version: 10.3.10 + resolution: "glob@npm:10.3.10" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.3.5 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + path-scurry: ^1.10.1 + bin: + glob: dist/esm/bin.mjs + checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -5328,6 +6680,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 +6796,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 +7068,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 @@ -5748,6 +7104,22 @@ __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" + 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" @@ -5882,6 +7254,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 +7365,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 +7517,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" @@ -6224,6 +7628,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 + languageName: node + linkType: hard + "jest-worker@npm:^27.4.5": version: 27.5.1 resolution: "jest-worker@npm:27.5.1" @@ -6235,12 +7652,12 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.17.2": - version: 1.18.2 - resolution: "jiti@npm:1.18.2" +"jiti@npm:^1.19.1": + version: 1.21.0 + resolution: "jiti@npm:1.21.0" bin: jiti: bin/jiti.js - checksum: 46c41cd82d01c6efdee3fc0ae9b3e86ed37457192d6366f19157d863d64961b07982ab04e9d5879576a1af99cc4d132b0b73b336094f86a5ce9fb1029ec2d29f + checksum: a7bd5d63921c170eaec91eecd686388181c7828e1fa0657ab374b9372bfc1f383cf4b039e6b272383d5cb25607509880af814a39abdff967322459cca41f2961 languageName: node linkType: hard @@ -6352,6 +7769,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 +7868,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 @@ -6462,12 +7893,16 @@ __metadata: version: 0.0.0-use.local resolution: "landgriffon-client@workspace:." dependencies: + "@date-fns/utc": 1.1.1 + "@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 "@dnd-kit/core": 5.0.3 "@dnd-kit/modifiers": 5.0.0 "@dnd-kit/sortable": 6.0.1 @@ -6481,15 +7916,29 @@ __metadata: "@json2csv/plainjs": ^6.1.3 "@loaders.gl/core": 3.3.1 "@luma.gl/constants": 8.5.18 + "@maplibre/maplibre-gl-compare": ^0.5.0 + "@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-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 "@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 + "@turf/bbox": ^6.5.0 "@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 @@ -6500,12 +7949,14 @@ __metadata: 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 cypress: 13.2.0 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 @@ -6517,7 +7968,8 @@ __metadata: jsona: 1.9.2 lodash-es: 4.17.21 lottie-react: 2.4.0 - mapbox-gl: 2.13.0 + lucide-react: 0.344.0 + maplibre-gl: 3.6.2 next: 13.5.5 next-auth: 4.19.2 nyc: 15.1.0 @@ -6529,18 +7981,22 @@ __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 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 + react-world-flags: 1.6.0 recharts: 2.9.0 rooks: 7.14.1 sharp: 0.32.6 start-server-and-test: 1.14.0 - tailwindcss: 3.3.1 + tailwind-merge: 2.2.1 + tailwindcss: 3.4.1 + tailwindcss-animate: 1.0.7 typescript: 5.2.2 update-browserslist-db: ^1.0.13 uuid: 8.3.2 @@ -6582,10 +8038,17 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^2.0.5, lilconfig@npm:^2.0.6": - version: 2.0.6 - resolution: "lilconfig@npm:2.0.6" - checksum: 40a3cd72f103b1be5975f2ac1850810b61d4053e20ab09be8d3aeddfe042187e1ba70b4651a7e70f95efa1642e7dc8b2ae395b317b7d7753b241b43cef7c0f7d +"lilconfig@npm:^2.1.0": + version: 2.1.0 + resolution: "lilconfig@npm:2.1.0" + checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117 + languageName: node + linkType: hard + +"lilconfig@npm:^3.0.0": + version: 3.1.1 + resolution: "lilconfig@npm:3.1.1" + checksum: dc8a4f4afde3f0fac6bd36163cc4777a577a90759b8ef1d0d766b19ccf121f723aa79924f32af5b954f3965268215e046d0f237c41c76e5ef01d4e6d1208a15e languageName: node linkType: hard @@ -6741,7 +8204,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: @@ -6787,6 +8250,22 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: eee7ddda4a7475deac51ac81d7dd78709095c6fa46e8350dc2d22462559a1faa3b81ed931d5464b13d48cbd7e08b46100b6f768c76833912bc444b99c37e25db + languageName: node + linkType: hard + +"lucide-react@npm:0.344.0": + version: 0.344.0 + resolution: "lucide-react@npm:0.344.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + checksum: 9fd168fd64e4bb2475a955b60ab7a55d25b610da8e5c2b9cc2c66945d64f6703a8587b40ad960caca98528c6a0d8c9c97acc2c6ae02bd7f42647afeec4605b8f + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -6827,32 +8306,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 @@ -6865,6 +8348,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" @@ -6946,6 +8443,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.6 resolution: "minimist@npm:1.2.6" @@ -7020,6 +8526,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": + version: 7.0.4 + resolution: "minipass@npm:7.0.4" + checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 + languageName: node + linkType: hard + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -7056,6 +8569,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" @@ -7111,6 +8640,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "napi-build-utils@npm:^1.0.1": version: 1.0.2 resolution: "napi-build-utils@npm:1.0.2" @@ -7319,6 +8857,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" @@ -7685,6 +9232,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: ^9.1.1 || ^10.0.0 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -7802,36 +9359,36 @@ __metadata: languageName: node linkType: hard -"postcss-import@npm:^14.1.0": - version: 14.1.0 - resolution: "postcss-import@npm:14.1.0" +"postcss-import@npm:^15.1.0": + version: 15.1.0 + resolution: "postcss-import@npm:15.1.0" dependencies: postcss-value-parser: ^4.0.0 read-cache: ^1.0.0 resolve: ^1.1.7 peerDependencies: postcss: ^8.0.0 - checksum: cd45d406e90f67cdab9524352e573cc6b4462b790934a05954e929a6653ebd31288ceebc8ce3c3ed7117ae672d9ebbec57df0bceec0a56e9b259c2e71d47ca86 + checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3 languageName: node linkType: hard -"postcss-js@npm:^4.0.0": - version: 4.0.0 - resolution: "postcss-js@npm:4.0.0" +"postcss-js@npm:^4.0.1": + version: 4.0.1 + resolution: "postcss-js@npm:4.0.1" dependencies: camelcase-css: ^2.0.1 peerDependencies: - postcss: ^8.3.3 - checksum: 14be8a58670b4c5d037d40f179240a4f736d53530db727e2635638fa296bc4bff18149ca860928398aace422e55d07c9f5729eeccd395340944985199cdc82a5 + postcss: ^8.4.21 + checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491 languageName: node linkType: hard -"postcss-load-config@npm:^3.1.4": - version: 3.1.4 - resolution: "postcss-load-config@npm:3.1.4" +"postcss-load-config@npm:^4.0.1": + version: 4.0.2 + resolution: "postcss-load-config@npm:4.0.2" dependencies: - lilconfig: ^2.0.5 - yaml: ^1.10.2 + lilconfig: ^3.0.0 + yaml: ^2.3.4 peerDependencies: postcss: ">=8.0.9" ts-node: ">=9.0.0" @@ -7840,28 +9397,18 @@ __metadata: optional: true ts-node: optional: true - checksum: 1c589504c2d90b1568aecae8238ab993c17dba2c44f848a8f13619ba556d26a1c09644d5e6361b5784e721e94af37b604992f9f3dc0483e687a0cc1cc5029a34 + checksum: 7c27dd3801db4eae207a5116fed2db6b1ebb780b40c3dd62a3e57e087093a8e6a14ee17ada729fee903152d6ef4826c6339eb135bee6208e0f3140d7e8090185 languageName: node linkType: hard -"postcss-nested@npm:6.0.0": - version: 6.0.0 - resolution: "postcss-nested@npm:6.0.0" +"postcss-nested@npm:^6.0.1": + version: 6.0.1 + resolution: "postcss-nested@npm:6.0.1" dependencies: - postcss-selector-parser: ^6.0.10 + postcss-selector-parser: ^6.0.11 peerDependencies: postcss: ^8.2.14 - checksum: 2105dc52cd19747058f1a46862c9e454b5a365ac2e7135fc1015d67a8fe98ada2a8d9ee578e90f7a093bd55d3994dd913ba5ff1d5e945b4ed9a8a2992ecc8f10 - languageName: node - linkType: hard - -"postcss-selector-parser@npm:^6.0.10": - version: 6.0.10 - resolution: "postcss-selector-parser@npm:6.0.10" - dependencies: - cssesc: ^3.0.0 - util-deprecate: ^1.0.2 - checksum: 46afaa60e3d1998bd7adf6caa374baf857cc58d3ff944e29459c9a9e4680a7fe41597bd5b755fc81d7c388357e9bf67c0251d047c640a09f148e13606b8a8608 + checksum: 7ddb0364cd797de01e38f644879189e0caeb7ea3f78628c933d91cc24f327c56d31269384454fc02ecaf503b44bfa8e08870a7c4cc56b23bc15640e1894523fa languageName: node linkType: hard @@ -7875,7 +9422,7 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": +"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.1.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f @@ -7893,14 +9440,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.9": - version: 8.4.23 - resolution: "postcss@npm:8.4.23" +"postcss@npm:^8.4.23": + version: 8.4.35 + resolution: "postcss@npm:8.4.35" dependencies: - nanoid: ^3.3.6 + nanoid: ^3.3.7 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 8bb9d1b2ea6e694f8987d4f18c94617971b2b8d141602725fedcc2222fdc413b776a6e1b969a25d627d7b2681ca5aabb56f59e727ef94072e1b6ac8412105a2f + checksum: cf3c3124d3912a507603f6d9a49b3783f741075e9aa73eb592a6dd9194f9edab9d20a8875d16d137d4f779fe7b6fbd1f5727e39bfd1c3003724980ee4995e1da languageName: node linkType: hard @@ -8203,13 +9750,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed - languageName: node - linkType: hard - "quickselect@npm:^2.0.0": version: 2.0.0 resolution: "quickselect@npm:2.0.0" @@ -8337,6 +9877,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" @@ -8404,15 +9954,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 @@ -8458,6 +10016,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" @@ -8484,6 +10077,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" @@ -8499,6 +10109,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" @@ -8759,7 +10382,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1": +"resolve@npm:^1.1.7, resolve@npm:^1.20.0, resolve@npm:^1.22.0": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -8772,7 +10395,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.22.4": +"resolve@npm:^1.22.2, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -8798,7 +10421,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": +"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" dependencies: @@ -8811,7 +10434,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.22.4#~builtin": +"resolve@patch:resolve@^1.22.2#~builtin, resolve@patch:resolve@^1.22.4#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -9057,6 +10680,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" @@ -9134,6 +10769,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + "simple-concat@npm:^1.0.0": version: 1.0.1 resolution: "simple-concat@npm:1.0.1" @@ -9234,7 +10876,35 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": +"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.1, source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c @@ -9279,6 +10949,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" @@ -9377,7 +11056,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -9388,6 +11067,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.8": version: 4.0.8 resolution: "string.prototype.matchall@npm:4.0.8" @@ -9490,7 +11180,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -9499,6 +11189,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -9550,13 +11249,13 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.29.0": - version: 3.32.0 - resolution: "sucrase@npm:3.32.0" +"sucrase@npm:^3.32.0": + version: 3.35.0 + resolution: "sucrase@npm:3.35.0" dependencies: "@jridgewell/gen-mapping": ^0.3.2 commander: ^4.0.0 - glob: 7.1.6 + glob: ^10.3.10 lines-and-columns: ^1.1.6 mz: ^2.7.0 pirates: ^4.0.1 @@ -9564,16 +11263,16 @@ __metadata: bin: sucrase: bin/sucrase sucrase-node: bin/sucrase-node - checksum: 79f760aef513adcf22b882d43100296a8afa7f307acef3e8803304b763484cf138a3e2cebc498a6791110ab20c7b8deba097f6ce82f812ca8f1723e3440e5c95 + checksum: 9fc5792a9ab8a14dcf9c47dcb704431d35c1cdff1d17d55d382a31c2e8e3063870ad32ce120a80915498486246d612e30cda44f1624d9d9a10423e1a43487ad1 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 @@ -9611,6 +11310,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" @@ -9638,40 +11361,54 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:3.3.1": - version: 3.3.1 - resolution: "tailwindcss@npm:3.3.1" +"tailwind-merge@npm:2.2.1": + version: 2.2.1 + resolution: "tailwind-merge@npm:2.2.1" + dependencies: + "@babel/runtime": ^7.23.7 + checksum: fd149409b1c18f3bc57aa30ae3a0e0c7f2913636666bd920c1aceeb8798b36766be55dfec8581e63860b83c7e405700ef530952a59c828ccff0c2a401fa67f05 + languageName: node + linkType: hard + +"tailwindcss-animate@npm:1.0.7": + version: 1.0.7 + resolution: "tailwindcss-animate@npm:1.0.7" + peerDependencies: + tailwindcss: "*" + checksum: c1760983eb3fec0c8421e95082bf308e6845df43e2f90862386366e82545c801b26b4d189c4cd23d6915252b76d18005c8e5f591f8b119944c7fb8650d0f8bce + languageName: node + linkType: hard + +"tailwindcss@npm:3.4.1": + version: 3.4.1 + resolution: "tailwindcss@npm:3.4.1" dependencies: + "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 chokidar: ^3.5.3 - color-name: ^1.1.4 didyoumean: ^1.2.2 dlv: ^1.1.3 - fast-glob: ^3.2.12 + fast-glob: ^3.3.0 glob-parent: ^6.0.2 is-glob: ^4.0.3 - jiti: ^1.17.2 - lilconfig: ^2.0.6 + jiti: ^1.19.1 + lilconfig: ^2.1.0 micromatch: ^4.0.5 normalize-path: ^3.0.0 object-hash: ^3.0.0 picocolors: ^1.0.0 - postcss: ^8.0.9 - postcss-import: ^14.1.0 - postcss-js: ^4.0.0 - postcss-load-config: ^3.1.4 - postcss-nested: 6.0.0 + postcss: ^8.4.23 + postcss-import: ^15.1.0 + postcss-js: ^4.0.1 + postcss-load-config: ^4.0.1 + postcss-nested: ^6.0.1 postcss-selector-parser: ^6.0.11 - postcss-value-parser: ^4.2.0 - quick-lru: ^5.1.1 - resolve: ^1.22.1 - sucrase: ^3.29.0 - peerDependencies: - postcss: ^8.0.9 + resolve: ^1.22.2 + sucrase: ^3.32.0 bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 966ba175486fb65ef3dd76aa8ec6929ff1d168531843ca7d5faf680b7097c36bf5f9ca385b563cdfdff935bb2bd37ac5998e877491407867503cc129d118bf93 + checksum: ef5a587dd32bb4e91e1549ead6162f85f0b78d3e6ffd8b4e8eeb15585b7b886cb3af6ae9df5092ed8ccb7e590608d1b3eec79ca08c862b07cd9ff7e72f73104b languageName: node linkType: hard @@ -10101,6 +11838,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 +11866,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" @@ -10199,6 +11964,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" @@ -10416,6 +12212,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" @@ -10443,6 +12250,24 @@ __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" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -10454,14 +12279,14 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 languageName: node linkType: hard @@ -10498,10 +12323,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.10.2": - version: 1.10.2 - resolution: "yaml@npm:1.10.2" - checksum: ce4ada136e8a78a0b08dc10b4b900936912d15de59905b2bf415b4d33c63df1d555d23acb2a41b23cf9fb5da41c256441afca3d6509de7247daa062fd2c5ea5f +"yaml@npm:^2.3.4": + version: 2.4.0 + resolution: "yaml@npm:2.4.0" + bin: + yaml: bin.mjs + checksum: 3c25ebae34ee702af772ebbd1855a980b1487cd21d6220d952592edb4f7d89322aafd14753d99924ba7076eb4c5b3d809c64bb532402b01af280f7af674277f1 languageName: node linkType: hard 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= 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 = {} +