From 69bd420d68187ca367991b668454d49370e3eccf Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 20 Mar 2025 23:03:31 -0400 Subject: [PATCH 01/41] [TM-1770] add fields and basic structure for nursery report --- .../src/entities/dto/nursery-report.dto.ts | 232 ++++++++++++++++++ .../src/lib/entities/nursery-report.entity.ts | 94 ++++++- .../src/lib/entities/project.entity.ts | 4 + 3 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 apps/entity-service/src/entities/dto/nursery-report.dto.ts diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts new file mode 100644 index 00000000..0f4f5ec9 --- /dev/null +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -0,0 +1,232 @@ +import { NurseryReport, ProjectReport } from "@terramatch-microservices/database/entities"; +import { EntityDto, AdditionalProps } from "./entity.dto"; +import { pickApiProperties } from "@terramatch-microservices/common/dto/json-api-attributes"; +import { JsonApiDto } from "@terramatch-microservices/common/decorators/json-api-dto.decorator"; +import { ApiProperty } from "@nestjs/swagger"; +import { MediaDto } from "./media.dto"; +import { User } from "@sentry/nestjs"; + +@JsonApiDto({ type: "nursery-reports" }) +export class NurseryReportLightDto extends EntityDto { + constructor(projectReport?: ProjectReport) { + super(); + if (projectReport != null) { + this.populate(NurseryReportLightDto, { + ...pickApiProperties(NurseryReport, NurseryReportLightDto), + lightResource: true, + // these two are untyped and marked optional in the base model. + createdAt: projectReport.createdAt as Date, + updatedAt: projectReport.createdAt as Date + }); + } + } + + @ApiProperty({ + nullable: true, + description: "The associated nursery name" + }) + nurseryName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated nursery uuid" + }) + nurseryUuid: string | null; + + @ApiProperty() + frameworkKey: string | null; + + @ApiProperty() + frameworkUuid: string | null; + + @ApiProperty() + status: string; + + @ApiProperty() + updateRequestStatus: string; + + @ApiProperty({ + nullable: true, + description: "The associated project name" + }) + projectName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated project uuid" + }) + projectUuid: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated organisation name" + }) + organisationName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated organisation uuid" + }) + organisationUuid: string | null; + + @ApiProperty() + updatedAt: Date; + + @ApiProperty({ nullable: true }) + submittedAt: Date | null; + + @ApiProperty({ nullable: true }) + taskId: number | null; + + @ApiProperty() + createdAt: Date; +} + +// export type AdditionalNurseryReportLightProps = Pick; +export type AdditionalNurseryReportFullProps = + /*AdditionalNurseryReportLightProps &*/ + AdditionalProps>; +export type NurseryReportMedia = Pick; + +export class NurseryReportFullDto extends NurseryReportLightDto { + constructor(nurseryReport: NurseryReport, props: AdditionalNurseryReportFullProps) { + super(); + this.populate(NurseryReportFullDto, { + ...pickApiProperties(nurseryReport, NurseryReportFullDto), + lightResource: false, + // these two are untyped and marked optional in the base model. + createdAt: nurseryReport.createdAt as Date, + updatedAt: nurseryReport.createdAt as Date, + ...props + }); + } + + @ApiProperty({ nullable: true }) + reportTitle: string | null; + + @ApiProperty({ nullable: true }) + projectReportTitle: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated nursery name" + }) + nurseryName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated nursery uuid" + }) + nurseryUuid: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated organisation name" + }) + organisationName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated organisation uuid" + }) + organisationUuid: string | null; + + @ApiProperty() + dueAt: Date | null; + + @ApiProperty() + status: string; + + @ApiProperty() + updateRequestStatus: string; + + @ApiProperty({ nullable: true }) + feedback: string | null; + + @ApiProperty({ nullable: true }) + feedbackFields: string[] | null; + + @ApiProperty() + nothingToReport: boolean; + + @ApiProperty() + readableCompletionStatus: string; + + @ApiProperty({ nullable: true }) + title: string | null; + + @ApiProperty({ nullable: true }) + seedlingsYoungTrees: number | null; + + @ApiProperty({ nullable: true }) + interesting_facts: string | null; + + @ApiProperty({ nullable: true }) + sitePrep: string | null; + + @ApiProperty({ nullable: true }) + sharedDriveLink: string | null; + + @ApiProperty({ nullable: true }) + createdBy: number | null; + + @ApiProperty({ nullable: true }) + createdByUser: User | null; + + @ApiProperty({ nullable: true }) + approvedBy: number | null; + + @ApiProperty({ nullable: true }) + approvedByUser: User | null; + + @ApiProperty({ + nullable: true, + description: "The associated project name" + }) + projectName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated project uuid" + }) + projectUuid: string | null; + + @ApiProperty({ nullable: true }) + taskId: number | null; + + @ApiProperty({ nullable: true }) + submittedAt: Date | null; + + @ApiProperty() + migrated: boolean; + + @ApiProperty() + createdAt: Date; + + @ApiProperty() + updatedAt: Date; + + @ApiProperty({ + nullable: true, + description: "The associated project report name" + }) + projectReportName: string | null; + + @ApiProperty({ + nullable: true, + description: "The associated project report uuid" + }) + projectReportUuid: string | null; + + @ApiProperty({ type: () => MediaDto, isArray: true }) + file: MediaDto[]; + + @ApiProperty({ type: () => MediaDto, isArray: true }) + otherAdditionalDocuments: MediaDto[]; + + @ApiProperty({ type: () => MediaDto, isArray: true }) + treeSeedlingContributions: MediaDto[]; + + @ApiProperty({ type: () => MediaDto, isArray: true }) + photos: MediaDto[]; +} diff --git a/libs/database/src/lib/entities/nursery-report.entity.ts b/libs/database/src/lib/entities/nursery-report.entity.ts index e8ce94f6..497a781a 100644 --- a/libs/database/src/lib/entities/nursery-report.entity.ts +++ b/libs/database/src/lib/entities/nursery-report.entity.ts @@ -11,7 +11,7 @@ import { Scopes, Table } from "sequelize-typescript"; -import { BIGINT, DATE, INTEGER, Op, STRING, UUID } from "sequelize"; +import { BIGINT, DATE, INTEGER, Op, STRING, TEXT, TINYINT, UUID } from "sequelize"; import { Nursery } from "./nursery.entity"; import { TreeSpecies } from "./tree-species.entity"; import { COMPLETE_REPORT_STATUSES, ReportStatus, UpdateRequestStatus } from "../constants/status"; @@ -19,6 +19,8 @@ import { FrameworkKey } from "../constants/framework"; import { Literal } from "sequelize/types/utils"; import { chainScope } from "../util/chain-scope"; import { Subquery } from "../util/subquery.builder"; +import { User } from "./user.entity"; +import { JsonColumn } from "../decorators/json-column.decorator"; // Incomplete stub @Scopes(() => ({ @@ -33,6 +35,13 @@ export class NurseryReport extends Model { static readonly PARENT_ID = "nurseryId"; static readonly LARAVEL_TYPE = "App\\Models\\V2\\Nurseries\\NurseryReport"; + static readonly MEDIA = { + file: { dbCollection: "file", multiple: true }, + otherAdditionalDocuments: { dbCollection: "other_additional_documents", multiple: true }, + treeSeedlingContributions: { dbCollection: "tree_seedling_contributions", multiple: true }, + photos: { dbCollection: "photos", multiple: true } + } as const; + static incomplete() { return chainScope(this, "incomplete") as typeof NurseryReport; } @@ -68,9 +77,44 @@ export class NurseryReport extends Model { @Column(BIGINT.UNSIGNED) nurseryId: number; + @ForeignKey(() => User) + @Column(BIGINT.UNSIGNED) + createdBy: number; + + @ForeignKey(() => User) + @Column(BIGINT.UNSIGNED) + approvedBy: number; + @BelongsTo(() => Nursery) nursery: Nursery | null; + @BelongsTo(() => User) + user: User | null; + + get projectName() { + return this.nursery?.project?.name; + } + + get projectUuid() { + return this.nursery?.project?.uuid; + } + + get organisationName() { + return this.nursery?.project?.organisationName; + } + + get organisationUuid() { + return this.nursery?.project?.organisationUuid; + } + + get nurseryName() { + return this.nursery?.name; + } + + get nurseryUuid() { + return this.nursery?.uuid; + } + // TODO foreign key for task @AllowNull @Column(BIGINT.UNSIGNED) @@ -91,6 +135,54 @@ export class NurseryReport extends Model { @Column(INTEGER.UNSIGNED) seedlingsYoungTrees: number | null; + @AllowNull + @Column(TINYINT) + nothingToReport: boolean; + + @AllowNull + @Column(TEXT) + feedback: string | null; + + @AllowNull + @JsonColumn() + feedbackFields: string[] | null; + + @AllowNull + @Column(DATE) + submittedAt: Date | null; + + @AllowNull + @Column(INTEGER) + completion: number | null; + + @AllowNull + @Column(STRING) + title: string | null; + + @AllowNull + @Column(TEXT) + interestingFacts: string | null; + + @AllowNull + @Column(TEXT) + sitePrep: string | null; + + @AllowNull + @Column(TEXT) + sharedDriveLink: string | null; + + @AllowNull + @Column(STRING) + oldModel: string | null; + + @AllowNull + @Column(INTEGER.UNSIGNED) + oldId: number | null; + + @AllowNull + @Column(TEXT("long")) + answers: string | null; + @HasMany(() => TreeSpecies, { foreignKey: "speciesableId", constraints: false, diff --git a/libs/database/src/lib/entities/project.entity.ts b/libs/database/src/lib/entities/project.entity.ts index 6410b721..3f15eea2 100644 --- a/libs/database/src/lib/entities/project.entity.ts +++ b/libs/database/src/lib/entities/project.entity.ts @@ -367,6 +367,10 @@ export class Project extends Model { return this.organisation?.name; } + get organisationUuid() { + return this.organisation?.uuid; + } + @BelongsTo(() => Application) application: Application | null; From 620e655221b3a654217adffb2c12a6f3c0cba289 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Fri, 21 Mar 2025 09:04:58 -0400 Subject: [PATCH 02/41] [TM-1770] add nursery report dto --- .../processors/nursery-report.processor.ts | 163 ++++++++++++++++++ .../database/src/lib/entities/media.entity.ts | 11 ++ 2 files changed, 174 insertions(+) create mode 100644 apps/entity-service/src/entities/processors/nursery-report.processor.ts diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts new file mode 100644 index 00000000..2c8d2fb3 --- /dev/null +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -0,0 +1,163 @@ +import { Media, Nursery, NurseryReport, Project, ProjectUser } from "@terramatch-microservices/database/entities"; +import { NurseryLightDto, NurseryFullDto, AdditionalNurseryFullProps, NurseryMedia } from "../dto/nursery.dto"; +import { EntityProcessor, PaginatedResult } from "./entity-processor"; +import { EntityQueryDto } from "../dto/entity-query.dto"; +import { DocumentBuilder } from "@terramatch-microservices/common/util"; +import { col, fn, Includeable, Op } from "sequelize"; +import { BadRequestException } from "@nestjs/common"; +import { FrameworkKey } from "@terramatch-microservices/database/constants/framework"; +import { NurseryReportFullDto, NurseryReportMedia } from "../dto/nursery-report.dto"; +import { NurseryReportLightDto } from "../dto/nursery-report.dto"; + +export class NurseryReportProcessor extends EntityProcessor< + NurseryReport, + NurseryReportLightDto, + NurseryReportFullDto +> { + readonly LIGHT_DTO = NurseryReportLightDto; + readonly FULL_DTO = NurseryReportFullDto; + + async findOne(uuid: string): Promise { + return await NurseryReport.findOne({ + where: { uuid }, + include: [ + { + association: "nursery", + attributes: ["uuid", "name"], + include: [ + { + association: "project", + attributes: ["uuid", "name"], + include: [{ association: "organisation", attributes: ["uuid", "name"] }] + } + ] + } + ] + }); + } + + async findMany( + query: EntityQueryDto, + userId?: number, + permissions?: string[] + ): Promise> { + const projectAssociation: Includeable = { + association: "nursery", + attributes: ["uuid", "name"], + include: [ + { + association: "project", + attributes: ["uuid", "name"], + include: [{ association: "organisation", attributes: ["uuid", "name"] }] + } + ] + }; + + const builder = await this.entitiesService.buildQuery(NurseryReport, query, [projectAssociation]); + if (query.sort != null) { + if ( + ["name", "status", "updateRequestStatus", "createdAt", "submittedAt", "updatedAt", "frameworkKey"].includes( + query.sort.field + ) + ) { + builder.order([query.sort.field, query.sort.direction ?? "ASC"]); + } else if (query.sort.field === "organisationName") { + builder.order(["project", "organisation", "name", query.sort.direction ?? "ASC"]); + } else if (query.sort.field === "projectName") { + builder.order(["project", "name", query.sort.direction ?? "ASC"]); + } else if (query.sort.field !== "id") { + throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); + } + } + + const frameworkPermissions = permissions + ?.filter(name => name.startsWith("framework-")) + .map(name => name.substring("framework-".length) as FrameworkKey); + if (frameworkPermissions?.length > 0) { + builder.where({ frameworkKey: { [Op.in]: frameworkPermissions } }); + } else if (permissions?.includes("manage-own")) { + builder.where({ "$nursery.project.id$": { [Op.in]: ProjectUser.userProjectsSubquery(userId) } }); + } else if (permissions?.includes("projects-manage")) { + builder.where({ "$nursery.project.id$": { [Op.in]: ProjectUser.projectsManageSubquery(userId) } }); + } + + const associationFieldMap = { + nurseryUuid: "$nursery.uuid$", + organisationUuid: "$project.organisation.uuid$", + country: "$project.country$", + projectUuid: "$project.uuid$" + }; + + for (const term of ["status", "updateRequestStatus", "frameworkKey"]) { + if (query[term] != null) { + const field = associationFieldMap[term] || term; + builder.where({ [field]: query[term] }); + } + } + + if (query.search != null) { + builder.where({ + [Op.or]: [ + { name: { [Op.like]: `%${query.search}%` } }, + { "$project.name$": { [Op.like]: `%${query.search}%` } }, + { "$project.organisation.name$": { [Op.like]: `%${query.search}%` } } + ] + }); + } + + if (query.projectUuid != null) { + const project = await Project.findOne({ where: { uuid: query.projectUuid }, attributes: ["id"] }); + if (project == null) { + throw new BadRequestException(`Project with uuid ${query.projectUuid} not found`); + } + builder.where({ "$nursery.project.id$": project.id }); + } + + return { models: await builder.execute(), paginationTotal: await builder.paginationTotal() }; + } + + async addFullDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { + const nurseryReportId = nurseryReport.id; + + const nurseryReportsTotal = await NurseryReport.nurseries([nurseryReportId]).count(); + const seedlingsGrownCount = await this.getSeedlingsGrownCount(nurseryReportId); + const overdueNurseryReportsTotal = await this.getTotalOverdueReports(nurseryReportId); + const props: AdditionalNurseryFullProps = { + seedlingsGrownCount, + nurseryReportsTotal, + overdueNurseryReportsTotal, + + ...(this.entitiesService.mapMediaCollection( + await Media.nurseryReport(nurseryReportId).findAll(), + NurseryReport.MEDIA + ) as NurseryReportMedia) + }; + + document.addData(nurseryReport.uuid, new NurseryReportFullDto(nurseryReport, props)); + } + + async addLightDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { + const nurseryReportId = nurseryReport.id; + + const seedlingsGrownCount = await this.getSeedlingsGrownCount(nurseryReportId); + document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport, { seedlingsGrownCount })); + } + + protected async getTotalOverdueReports(nurseryId: number) { + const countOpts = { where: { dueAt: { [Op.lt]: new Date() } } }; + return await NurseryReport.incomplete().nurseries([nurseryId]).count(countOpts); + } + + private async getSeedlingsGrownCount(nurseryReportId: number) { + return ( + ( + await NurseryReport.nurseries([nurseryReportId]) + .approved() + .findAll({ + raw: true, + attributes: [[fn("SUM", col("seedlings_young_trees")), "seedlingsYoungTrees"]] + }) + )[0].seedlingsYoungTrees ?? 0 + ); + } +} diff --git a/libs/database/src/lib/entities/media.entity.ts b/libs/database/src/lib/entities/media.entity.ts index 9e488e33..657892c6 100644 --- a/libs/database/src/lib/entities/media.entity.ts +++ b/libs/database/src/lib/entities/media.entity.ts @@ -17,6 +17,7 @@ import { Project } from "./project.entity"; import { chainScope } from "../util/chain-scope"; import { Nursery } from "./nursery.entity"; import { Site } from "./site.entity"; +import { NurseryReport } from "./nursery-report.entity"; @DefaultScope(() => ({ order: ["orderColumn"] })) @Scopes(() => ({ @@ -38,6 +39,12 @@ import { Site } from "./site.entity"; modelType: Site.LARAVEL_TYPE, modelId: id } + }), + nurseryReport: (id: number) => ({ + where: { + modelType: NurseryReport.LARAVEL_TYPE, + modelId: id + } }) })) @Table({ @@ -66,6 +73,10 @@ export class Media extends Model { return chainScope(this, "nursery", id) as typeof Media; } + static nurseryReport(id: number) { + return chainScope(this, "nurseryReport", id) as typeof Media; + } + @PrimaryKey @AutoIncrement @Column(BIGINT.UNSIGNED) From e633d692a31ec3d378a31fe4ed3de72d5799e151 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 20:56:10 -0400 Subject: [PATCH 03/41] [TM-1770] add missing features --- .../src/entities/dto/nursery-report.dto.ts | 42 +- .../src/entities/dto/nursery.dto.ts | 1 + .../src/entities/entities.controller.ts | 13 +- .../src/entities/entities.service.ts | 4 +- .../nursery-report.processor.spec.ts | 360 ++++++++++++++++++ .../processors/nursery-report.processor.ts | 105 +++-- .../src/lib/entities/nursery-report.entity.ts | 10 +- 7 files changed, 465 insertions(+), 70 deletions(-) create mode 100644 apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 0f4f5ec9..aecab1c3 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -6,17 +6,17 @@ import { ApiProperty } from "@nestjs/swagger"; import { MediaDto } from "./media.dto"; import { User } from "@sentry/nestjs"; -@JsonApiDto({ type: "nursery-reports" }) +@JsonApiDto({ type: "nurseryReports" }) export class NurseryReportLightDto extends EntityDto { - constructor(projectReport?: ProjectReport) { + constructor(nurseryReport?: NurseryReport) { super(); - if (projectReport != null) { + if (nurseryReport != null) { this.populate(NurseryReportLightDto, { - ...pickApiProperties(NurseryReport, NurseryReportLightDto), + ...pickApiProperties(nurseryReport, NurseryReportLightDto), lightResource: true, // these two are untyped and marked optional in the base model. - createdAt: projectReport.createdAt as Date, - updatedAt: projectReport.createdAt as Date + createdAt: nurseryReport.createdAt as Date, + updatedAt: nurseryReport.createdAt as Date }); } } @@ -82,14 +82,14 @@ export class NurseryReportLightDto extends EntityDto { createdAt: Date; } -// export type AdditionalNurseryReportLightProps = Pick; -export type AdditionalNurseryReportFullProps = - /*AdditionalNurseryReportLightProps &*/ - AdditionalProps>; +export type AdditionalNurseryReportFullProps = AdditionalProps< + NurseryReportFullDto, + NurseryReportLightDto & Omit +>; export type NurseryReportMedia = Pick; export class NurseryReportFullDto extends NurseryReportLightDto { - constructor(nurseryReport: NurseryReport, props: AdditionalNurseryReportFullProps) { + constructor(nurseryReport: NurseryReport, props?: AdditionalNurseryReportFullProps) { super(); this.populate(NurseryReportFullDto, { ...pickApiProperties(nurseryReport, NurseryReportFullDto), @@ -159,7 +159,7 @@ export class NurseryReportFullDto extends NurseryReportLightDto { seedlingsYoungTrees: number | null; @ApiProperty({ nullable: true }) - interesting_facts: string | null; + interestingFacts: string | null; @ApiProperty({ nullable: true }) sitePrep: string | null; @@ -194,6 +194,12 @@ export class NurseryReportFullDto extends NurseryReportLightDto { @ApiProperty({ nullable: true }) taskId: number | null; + @ApiProperty({ + nullable: true, + description: "The associated task uuid" + }) + taskUuid: string | null; + @ApiProperty({ nullable: true }) submittedAt: Date | null; @@ -206,18 +212,6 @@ export class NurseryReportFullDto extends NurseryReportLightDto { @ApiProperty() updatedAt: Date; - @ApiProperty({ - nullable: true, - description: "The associated project report name" - }) - projectReportName: string | null; - - @ApiProperty({ - nullable: true, - description: "The associated project report uuid" - }) - projectReportUuid: string | null; - @ApiProperty({ type: () => MediaDto, isArray: true }) file: MediaDto[]; diff --git a/apps/entity-service/src/entities/dto/nursery.dto.ts b/apps/entity-service/src/entities/dto/nursery.dto.ts index 49406403..109e510c 100644 --- a/apps/entity-service/src/entities/dto/nursery.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery.dto.ts @@ -81,6 +81,7 @@ export class NurseryLightDto extends EntityDto { export type AdditionalNurseryLightProps = Pick; export type AdditionalNurseryFullProps = AdditionalNurseryLightProps & AdditionalProps>; + export type NurseryMedia = Pick; export class NurseryFullDto extends NurseryLightDto { diff --git a/apps/entity-service/src/entities/entities.controller.ts b/apps/entity-service/src/entities/entities.controller.ts index cd33db7a..789520b8 100644 --- a/apps/entity-service/src/entities/entities.controller.ts +++ b/apps/entity-service/src/entities/entities.controller.ts @@ -23,6 +23,7 @@ import { ProjectReportFullDto, ProjectReportLightDto } from "./dto/project-repor import { NurseryFullDto, NurseryLightDto } from "./dto/nursery.dto"; import { EntityModel } from "@terramatch-microservices/database/constants/entities"; import { JsonApiDeletedResponse } from "@terramatch-microservices/common/decorators/json-api-response.decorator"; +import { NurseryReportFullDto, NurseryReportLightDto } from "./dto/nursery-report.dto"; @Controller("entities/v3") @ApiExtraModels(ANRDto, ProjectApplicationDto, MediaDto) @@ -38,7 +39,8 @@ export class EntitiesController { { data: ProjectLightDto, pagination: "number" }, { data: SiteLightDto, pagination: "number" }, { data: NurseryLightDto, pagination: "number" }, - { data: ProjectReportLightDto, pagination: "number" } + { data: ProjectReportLightDto, pagination: "number" }, + { data: NurseryReportLightDto, pagination: "number" } ]) @ExceptionResponse(BadRequestException, { description: "Query params invalid" }) async entityIndex(@Param() { entity }: EntityIndexParamsDto, @Query() query: EntityQueryDto) { @@ -69,7 +71,14 @@ export class EntitiesController { operationId: "entityGet", summary: "Get a single full entity resource by UUID" }) - @JsonApiResponse([ProjectFullDto, SiteFullDto, NurseryFullDto, ProjectReportFullDto]) + @JsonApiResponse([ + ProjectFullDto, + SiteFullDto, + NurseryFullDto, + NurseryFullDto, + ProjectReportFullDto, + NurseryReportFullDto + ]) @ExceptionResponse(UnauthorizedException, { description: "Authentication failed, or resource unavailable to current user." }) diff --git a/apps/entity-service/src/entities/entities.service.ts b/apps/entity-service/src/entities/entities.service.ts index 8d3cbcf8..1aeeaeeb 100644 --- a/apps/entity-service/src/entities/entities.service.ts +++ b/apps/entity-service/src/entities/entities.service.ts @@ -20,13 +20,15 @@ import { UuidModel } from "@terramatch-microservices/database/types/util"; import { SeedingDto } from "./dto/seeding.dto"; import { TreeSpeciesDto } from "./dto/tree-species.dto"; import { DemographicDto } from "./dto/demographic.dto"; +import { NurseryReportProcessor } from "./processors/nursery-report.processor"; // The keys of this array must match the type in the resulting DTO. const ENTITY_PROCESSORS = { projects: ProjectProcessor, sites: SiteProcessor, nurseries: NurseryProcessor, - projectReports: ProjectReportProcessor + projectReports: ProjectReportProcessor, + nurseryReports: NurseryReportProcessor }; export type ProcessableEntity = keyof typeof ENTITY_PROCESSORS; diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts new file mode 100644 index 00000000..2b616b99 --- /dev/null +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -0,0 +1,360 @@ +import { NurseryReport } from "@terramatch-microservices/database/entities"; +import { Test } from "@nestjs/testing"; +import { MediaService } from "@terramatch-microservices/common/media/media.service"; +import { createMock } from "@golevelup/ts-jest"; +import { EntitiesService } from "../entities.service"; +import { reverse, sortBy } from "lodash"; +import { EntityQueryDto } from "../dto/entity-query.dto"; +import { + NurseryFactory, + NurseryReportFactory, + OrganisationFactory, + ProjectFactory, + ProjectUserFactory, + UserFactory +} from "@terramatch-microservices/database/factories"; +import { buildJsonApi } from "@terramatch-microservices/common/util"; +import { BadRequestException } from "@nestjs/common/exceptions/bad-request.exception"; +import { DateTime } from "luxon"; +import { NurseryReportProcessor } from "./nursery-report.processor"; +import { NurseryReportFullDto, NurseryReportLightDto } from "../dto/nursery-report.dto"; + +describe("NurseryReportProcessor", () => { + let processor: NurseryReportProcessor; + let userId: number; + + beforeAll(async () => { + userId = (await UserFactory.create()).id; + }); + + beforeEach(async () => { + await NurseryReport.truncate(); + + const module = await Test.createTestingModule({ + providers: [{ provide: MediaService, useValue: createMock() }, EntitiesService] + }).compile(); + + processor = module.get(EntitiesService).createEntityProcessor("nurseryReports") as NurseryReportProcessor; + }); + + describe("should return a list of nursery reports when findMany is called with valid parameters", () => { + async function expectNurseryReports( + expected: NurseryReport[], + query: Omit, + { + permissions = [], + sortField = "id", + sortUp = true, + total = expected.length + }: { permissions?: string[]; sortField?: string; sortUp?: boolean; total?: number } = {} + ) { + const { models, paginationTotal } = await processor.findMany(query as EntityQueryDto, userId, permissions); + expect(models.length).toBe(expected.length); + expect(paginationTotal).toBe(total); + + const sorted = sortBy(expected, sortField); + if (!sortUp) reverse(sorted); + expect(models.map(({ id }) => id)).toEqual(sorted.map(({ id }) => id)); + } + + it("should returns nursery reports", async () => { + const project = await ProjectFactory.create(); + const nursery = await NurseryFactory.create({ projectId: project.id }); + await ProjectUserFactory.create({ userId, projectId: project.id }); + const managedNurseryReports = await NurseryReportFactory.createMany(3, { nurseryId: nursery.id }); + await NurseryReportFactory.createMany(5); + await expectNurseryReports(managedNurseryReports, {}, { permissions: ["manage-own"] }); + }); + + it("should returns managed nursery reports", async () => { + const project = await ProjectFactory.create(); + await ProjectUserFactory.create({ userId, projectId: project.id, isMonitoring: false, isManaging: true }); + await ProjectFactory.create(); + const nursery = await NurseryFactory.create({ projectId: project.id }); + const nurseryReports = await NurseryReportFactory.createMany(3, { nurseryId: nursery.id }); + await NurseryReportFactory.createMany(5); + await expectNurseryReports(nurseryReports, {}, { permissions: ["projects-manage"] }); + }); + + it("should returns framework nursery reports", async () => { + const nurseryReports = await NurseryReportFactory.createMany(3, { frameworkKey: "hbf" }); + await NurseryReportFactory.createMany(3, { frameworkKey: "ppc" }); + for (const p of await NurseryReportFactory.createMany(3, { frameworkKey: "terrafund" })) { + nurseryReports.push(p); + } + + await expectNurseryReports(nurseryReports, {}, { permissions: ["framework-hbf", "framework-terrafund"] }); + }); + + it("should return nursery reports that match the search term", async () => { + const project1 = await ProjectFactory.create({ name: "Foo Bar" }); + const project2 = await ProjectFactory.create({ name: "Baz Foo" }); + const nursery1 = await NurseryFactory.create({ projectId: project1.id }); + const nursery2 = await NurseryFactory.create({ projectId: project2.id }); + nursery1.project = await nursery1.$get("project"); + nursery2.project = await nursery2.$get("project"); + const nurseryReport1 = await NurseryReportFactory.create({ nurseryId: nursery1.id }); + const nurseryReport2 = await NurseryReportFactory.create({ nurseryId: nursery2.id }); + nurseryReport1.nursery = await nurseryReport1.$get("nursery"); + nurseryReport2.nursery = await nurseryReport2.$get("nursery"); + await NurseryReportFactory.createMany(3); + + await expectNurseryReports([nurseryReport1, nurseryReport2], { search: "foo" }); + }); + + it("should return nursery reports filtered by the update request status or project", async () => { + const p1 = await ProjectFactory.create({ country: "MX" }); + const p2 = await ProjectFactory.create({ country: "CA" }); + await ProjectUserFactory.create({ userId, projectId: p1.id }); + await ProjectUserFactory.create({ userId, projectId: p2.id }); + const n1 = await NurseryFactory.create({ projectId: p1.id }); + const n2 = await NurseryFactory.create({ projectId: p2.id }); + + const first = await NurseryReportFactory.create({ + title: "first nursery report", + status: "approved", + updateRequestStatus: "awaiting-approval", + frameworkKey: "ppc", + nurseryId: n1.id + }); + const second = await NurseryReportFactory.create({ + title: "second project report", + status: "started", + updateRequestStatus: "awaiting-approval", + frameworkKey: "ppc", + nurseryId: n1.id + }); + const third = await NurseryReportFactory.create({ + title: "third project report", + status: "approved", + updateRequestStatus: "awaiting-approval", + frameworkKey: "ppc", + nurseryId: n1.id + }); + const fourth = await NurseryReportFactory.create({ + title: "fourth project report", + status: "approved", + updateRequestStatus: "approved", + frameworkKey: "terrafund", + nurseryId: n2.id + }); + + await expectNurseryReports([first, second, third], { updateRequestStatus: "awaiting-approval" }); + + await expectNurseryReports([fourth], { projectUuid: p2.uuid }); + + await expectNurseryReports([first, second, third], { country: "MX" }); + }); + + it("should throw an error if the project uuid is not found", async () => { + await expect(processor.findMany({ projectUuid: "123" })).rejects.toThrow(BadRequestException); + }); + + it("should sort nursery reports by update request status", async () => { + const nurseryReportA = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + const nurseryReportB = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + const nurseryReportC = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updateRequestStatus" } }, + { sortField: "updateRequestStatus" } + ); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updateRequestStatus", direction: "ASC" } }, + { sortField: "updateRequestStatus" } + ); + await expectNurseryReports( + [nurseryReportC, nurseryReportB, nurseryReportA], + { sort: { field: "updateRequestStatus", direction: "DESC" } }, + { sortField: "updateRequestStatus", sortUp: false } + ); + }); + + it("should sort nursery reports by project name", async () => { + const projectA = await ProjectFactory.create({ name: "A Project" }); + const projectB = await ProjectFactory.create({ name: "B Project" }); + const projectC = await ProjectFactory.create({ name: "C Project" }); + const nurseryA = await NurseryFactory.create({ projectId: projectA.id }); + const nurseryB = await NurseryFactory.create({ projectId: projectB.id }); + const nurseryC = await NurseryFactory.create({ projectId: projectC.id }); + const nurseryReportA = await NurseryReportFactory.create({ nurseryId: nurseryA.id }); + const nurseryReportB = await NurseryReportFactory.create({ nurseryId: nurseryB.id }); + const nurseryReportC = await NurseryReportFactory.create({ nurseryId: nurseryC.id }); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "projectName" } }, + { sortField: "projectName" } + ); + await expectNurseryReports( + [nurseryReportC, nurseryReportB, nurseryReportA], + { sort: { field: "projectName", direction: "DESC" } }, + { sortField: "projectName" } + ); + }); + + it("should sort project reports by organisation name", async () => { + const org1 = await OrganisationFactory.create({ name: "A Org" }); + const org2 = await OrganisationFactory.create({ name: "B Org" }); + const org3 = await OrganisationFactory.create({ name: "C Org" }); + const projectA = await ProjectFactory.create({ organisationId: org1.id }); + const projectB = await ProjectFactory.create({ organisationId: org2.id }); + const projectC = await ProjectFactory.create({ organisationId: org3.id }); + const nurseryA = await NurseryFactory.create({ projectId: projectA.id }); + const nurseryB = await NurseryFactory.create({ projectId: projectB.id }); + const nurseryC = await NurseryFactory.create({ projectId: projectC.id }); + projectA.organisation = await projectA.$get("organisation"); + projectB.organisation = await projectB.$get("organisation"); + projectC.organisation = await projectC.$get("organisation"); + + const nurseryReports = [ + await NurseryReportFactory.create({ + nurseryId: nurseryA.id + }) + ]; + nurseryReports.push( + await NurseryReportFactory.create({ + nurseryId: nurseryB.id + }) + ); + nurseryReports.push( + await NurseryReportFactory.create({ + nurseryId: nurseryC.id + }) + ); + for (const n of nurseryReports) { + n.nursery = await n.$get("nursery"); + } + + await expectNurseryReports( + nurseryReports, + { sort: { field: "organisationName" } }, + { sortField: "organisationName" } + ); + await expectNurseryReports( + nurseryReports, + { sort: { field: "organisationName", direction: "DESC" } }, + { sortField: "organisationName", sortUp: false } + ); + }); + + it("should sort project reports by status", async () => { + const nurseryReportA = await NurseryReportFactory.create({ status: "started" }); + const nurseryReportB = await NurseryReportFactory.create({ status: "approved" }); + const nurseryReportC = await NurseryReportFactory.create({ status: "approved" }); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "status" } }, + { sortField: "status" } + ); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "status", direction: "ASC" } }, + { sortField: "status" } + ); + await expectNurseryReports( + [nurseryReportC, nurseryReportB, nurseryReportA], + { sort: { field: "status", direction: "DESC" } }, + { sortField: "status", sortUp: false } + ); + }); + + it("should sort nursery reports by due date", async () => { + const nurseryReportA = await NurseryReportFactory.create({ dueAt: DateTime.now().minus({ days: 1 }).toJSDate() }); + const nurseryReportB = await NurseryReportFactory.create({ + dueAt: DateTime.now().minus({ days: 10 }).toJSDate() + }); + const nurseryReportC = await NurseryReportFactory.create({ dueAt: DateTime.now().minus({ days: 5 }).toJSDate() }); + await expectNurseryReports( + [nurseryReportA, nurseryReportC, nurseryReportB], + { sort: { field: "dueAt", direction: "DESC" } }, + { sortField: "dueAt", sortUp: false } + ); + await expectNurseryReports( + [nurseryReportB, nurseryReportC, nurseryReportA], + { sort: { field: "dueAt", direction: "ASC" } }, + { sortField: "dueAt", sortUp: true } + ); + }); + + it("should sort nursery reports by submitted at", async () => { + const now = DateTime.now(); + const nurseryReportA = await NurseryReportFactory.create({ + submittedAt: now.minus({ minutes: 1 }).toJSDate() + }); + const nurseryReportB = await NurseryReportFactory.create({ + submittedAt: now.minus({ minutes: 10 }).toJSDate() + }); + const nurseryReportC = await NurseryReportFactory.create({ + submittedAt: now.minus({ minutes: 5 }).toJSDate() + }); + await expectNurseryReports( + [nurseryReportA, nurseryReportC, nurseryReportB], + { sort: { field: "submittedAt", direction: "DESC" } }, + { sortField: "submittedAt", sortUp: false } + ); + await expectNurseryReports( + [nurseryReportB, nurseryReportC, nurseryReportA], + { sort: { field: "submittedAt", direction: "ASC" } }, + { sortField: "submittedAt", sortUp: true } + ); + }); + + it("should paginate nursery reports", async () => { + const nurseryReports = sortBy(await NurseryReportFactory.createMany(25), "id"); + await expectNurseryReports(nurseryReports.slice(0, 10), { page: { size: 10 } }, { total: nurseryReports.length }); + await expectNurseryReports( + nurseryReports.slice(10, 20), + { page: { size: 10, number: 2 } }, + { total: nurseryReports.length } + ); + await expectNurseryReports( + nurseryReports.slice(20), + { page: { size: 10, number: 3 } }, + { total: nurseryReports.length } + ); + }); + }); + + describe("should return a requested nursery report when findOne is called with a valid uuid", () => { + it("should return a requested nursery report", async () => { + const nurseryReport = await NurseryReportFactory.create(); + const result = await processor.findOne(nurseryReport.uuid); + expect(result.id).toBe(nurseryReport.id); + }); + }); + + describe("should properly map the nursery report data into its respective DTOs", () => { + it("should serialize a Nursery Report as a light resource (NurseryReportLightDto)", async () => { + const { uuid } = await NurseryReportFactory.create(); + const nurseryReport = await processor.findOne(uuid); + const document = buildJsonApi(NurseryReportLightDto, { forceDataArray: true }); + await processor.addLightDto(document, nurseryReport); + const attributes = document.serialize().data[0].attributes as NurseryReportLightDto; + expect(attributes).toMatchObject({ + uuid, + lightResource: true + }); + }); + + it("should include calculated fields in NurseryReportFullDto", async () => { + const project = await ProjectFactory.create(); + const nursery = await NurseryFactory.create({ projectId: project.id }); + + const { uuid } = await NurseryReportFactory.create({ + nurseryId: nursery.id + }); + + const nurseryReport = await processor.findOne(uuid); + const document = buildJsonApi(NurseryReportFullDto, { forceDataArray: true }); + await processor.addFullDto(document, nurseryReport); + const attributes = document.serialize().data[0].attributes as NurseryReportFullDto; + expect(attributes).toMatchObject({ + uuid, + lightResource: false, + projectUuid: project.uuid, + nurseryUuid: nursery.uuid + }); + }); + }); +}); diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 2c8d2fb3..f8e39424 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -1,12 +1,19 @@ -import { Media, Nursery, NurseryReport, Project, ProjectUser } from "@terramatch-microservices/database/entities"; -import { NurseryLightDto, NurseryFullDto, AdditionalNurseryFullProps, NurseryMedia } from "../dto/nursery.dto"; +import { + Media, + Nursery, + NurseryReport, + Project, + ProjectReport, + ProjectUser, + User +} from "@terramatch-microservices/database/entities"; import { EntityProcessor, PaginatedResult } from "./entity-processor"; import { EntityQueryDto } from "../dto/entity-query.dto"; import { DocumentBuilder } from "@terramatch-microservices/common/util"; import { col, fn, Includeable, Op } from "sequelize"; import { BadRequestException } from "@nestjs/common"; import { FrameworkKey } from "@terramatch-microservices/database/constants/framework"; -import { NurseryReportFullDto, NurseryReportMedia } from "../dto/nursery-report.dto"; +import { AdditionalNurseryReportFullProps, NurseryReportFullDto, NurseryReportMedia } from "../dto/nursery-report.dto"; import { NurseryReportLightDto } from "../dto/nursery-report.dto"; export class NurseryReportProcessor extends EntityProcessor< @@ -23,14 +30,18 @@ export class NurseryReportProcessor extends EntityProcessor< include: [ { association: "nursery", - attributes: ["uuid", "name"], + attributes: ["id", "uuid", "name"], include: [ { association: "project", - attributes: ["uuid", "name"], + attributes: ["id", "uuid", "name"], include: [{ association: "organisation", attributes: ["uuid", "name"] }] } ] + }, + { + association: "task", + attributes: ["uuid"] } ] }); @@ -41,22 +52,22 @@ export class NurseryReportProcessor extends EntityProcessor< userId?: number, permissions?: string[] ): Promise> { - const projectAssociation: Includeable = { + const nurseryAssociation: Includeable = { association: "nursery", - attributes: ["uuid", "name"], + attributes: ["id", "uuid", "name"], include: [ { association: "project", - attributes: ["uuid", "name"], - include: [{ association: "organisation", attributes: ["uuid", "name"] }] + attributes: ["id", "uuid", "name"], + include: [{ association: "organisation", attributes: ["id", "uuid", "name"] }] } ] }; - const builder = await this.entitiesService.buildQuery(NurseryReport, query, [projectAssociation]); + const builder = await this.entitiesService.buildQuery(NurseryReport, query, [nurseryAssociation]); if (query.sort != null) { if ( - ["name", "status", "updateRequestStatus", "createdAt", "submittedAt", "updatedAt", "frameworkKey"].includes( + ["status", "updateRequestStatus", "createdAt", "submittedAt", "updatedAt", "frameworkKey"].includes( query.sort.field ) ) { @@ -83,9 +94,9 @@ export class NurseryReportProcessor extends EntityProcessor< const associationFieldMap = { nurseryUuid: "$nursery.uuid$", - organisationUuid: "$project.organisation.uuid$", - country: "$project.country$", - projectUuid: "$project.uuid$" + organisationUuid: "$nursery.project.organisation.uuid$", + country: "$nursery.project.country$", + projectUuid: "$nursery.project.uuid$" }; for (const term of ["status", "updateRequestStatus", "frameworkKey"]) { @@ -99,8 +110,8 @@ export class NurseryReportProcessor extends EntityProcessor< builder.where({ [Op.or]: [ { name: { [Op.like]: `%${query.search}%` } }, - { "$project.name$": { [Op.like]: `%${query.search}%` } }, - { "$project.organisation.name$": { [Op.like]: `%${query.search}%` } } + { "$nursery.project.name$": { [Op.like]: `%${query.search}%` } }, + { "$nursery.project.organisation.name$": { [Op.like]: `%${query.search}%` } } ] }); } @@ -118,15 +129,19 @@ export class NurseryReportProcessor extends EntityProcessor< async addFullDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { const nurseryReportId = nurseryReport.id; - - const nurseryReportsTotal = await NurseryReport.nurseries([nurseryReportId]).count(); - const seedlingsGrownCount = await this.getSeedlingsGrownCount(nurseryReportId); - const overdueNurseryReportsTotal = await this.getTotalOverdueReports(nurseryReportId); - const props: AdditionalNurseryFullProps = { - seedlingsGrownCount, - nurseryReportsTotal, - overdueNurseryReportsTotal, - + const reportTitle = await this.getReportTitle(nurseryReport); + const projectReportTitle = await this.getProjectReportTitle(nurseryReport); + const readableCompletionStatus = await this.getReadableCompletionStatus(nurseryReport.completion); + const createdByUser = await User.findOne({ where: { id: nurseryReport?.createdBy } }); + const approvedByUser = await User.findOne({ where: { id: nurseryReport?.approvedBy } }); + const migrated = nurseryReport.oldModel != null; + const props: AdditionalNurseryReportFullProps = { + reportTitle, + projectReportTitle, + readableCompletionStatus, + createdByUser, + approvedByUser, + migrated, ...(this.entitiesService.mapMediaCollection( await Media.nurseryReport(nurseryReportId).findAll(), NurseryReport.MEDIA @@ -137,27 +152,33 @@ export class NurseryReportProcessor extends EntityProcessor< } async addLightDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { - const nurseryReportId = nurseryReport.id; + document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport)); + } - const seedlingsGrownCount = await this.getSeedlingsGrownCount(nurseryReportId); - document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport, { seedlingsGrownCount })); + protected async getReportTitleBase(dueAt: Date | null, title: string | null, locale: string = "en-GB") { + if (dueAt == null) return title ?? ""; + + const adjustedDate = new Date(dueAt); + adjustedDate.setMonth(adjustedDate.getMonth() - 1); + const wEnd = adjustedDate.toLocaleString(locale, { month: "long", year: "numeric" }); + + adjustedDate.setMonth(adjustedDate.getMonth() - 5); + const wStart = adjustedDate.toLocaleString(locale, { month: "long" }); + + return `Nursery Report for ${wStart} - ${wEnd}`; } - protected async getTotalOverdueReports(nurseryId: number) { - const countOpts = { where: { dueAt: { [Op.lt]: new Date() } } }; - return await NurseryReport.incomplete().nurseries([nurseryId]).count(countOpts); + protected async getReportTitle(nurseryReport: NurseryReport) { + return this.getReportTitleBase(nurseryReport.dueAt, nurseryReport.title, nurseryReport.user?.locale ?? "en-GB"); + } + + protected async getProjectReportTitle(nurseryReport: NurseryReport) { + const projectReport = await ProjectReport.findOne({ where: { taskId: nurseryReport.taskId } }); + + return this.getReportTitleBase(projectReport.dueAt, projectReport.title, projectReport.user?.locale ?? "en-GB"); } - private async getSeedlingsGrownCount(nurseryReportId: number) { - return ( - ( - await NurseryReport.nurseries([nurseryReportId]) - .approved() - .findAll({ - raw: true, - attributes: [[fn("SUM", col("seedlings_young_trees")), "seedlingsYoungTrees"]] - }) - )[0].seedlingsYoungTrees ?? 0 - ); + protected async getReadableCompletionStatus(completion: number) { + return completion === 0 ? "Not Started" : completion === 100 ? "Complete" : "Started"; } } diff --git a/libs/database/src/lib/entities/nursery-report.entity.ts b/libs/database/src/lib/entities/nursery-report.entity.ts index 35d99d4d..f0e70ee9 100644 --- a/libs/database/src/lib/entities/nursery-report.entity.ts +++ b/libs/database/src/lib/entities/nursery-report.entity.ts @@ -21,6 +21,7 @@ import { chainScope } from "../util/chain-scope"; import { Subquery } from "../util/subquery.builder"; import { User } from "./user.entity"; import { JsonColumn } from "../decorators/json-column.decorator"; +import { Task } from "./task.entity"; // Incomplete stub @Scopes(() => ({ @@ -96,6 +97,9 @@ export class NurseryReport extends Model { @BelongsTo(() => User) user: User | null; + @BelongsTo(() => Task) + task: Task | null; + get projectName() { return this.nursery?.project?.name; } @@ -120,7 +124,11 @@ export class NurseryReport extends Model { return this.nursery?.uuid; } - // TODO foreign key for task + get taskUuid() { + return this.task?.uuid; + } + + @ForeignKey(() => Task) @AllowNull @Column(BIGINT.UNSIGNED) taskId: number; From 3cded264370556c09a6c27b59abea68738d3699a Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 21:01:48 -0400 Subject: [PATCH 04/41] [TM-1770] fix lint --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 2 +- .../src/entities/processors/nursery-report.processor.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index aecab1c3..79b7c4e3 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -1,4 +1,4 @@ -import { NurseryReport, ProjectReport } from "@terramatch-microservices/database/entities"; +import { NurseryReport } from "@terramatch-microservices/database/entities"; import { EntityDto, AdditionalProps } from "./entity.dto"; import { pickApiProperties } from "@terramatch-microservices/common/dto/json-api-attributes"; import { JsonApiDto } from "@terramatch-microservices/common/decorators/json-api-dto.decorator"; diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index f8e39424..9555e3cf 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -1,6 +1,5 @@ import { Media, - Nursery, NurseryReport, Project, ProjectReport, @@ -10,7 +9,7 @@ import { import { EntityProcessor, PaginatedResult } from "./entity-processor"; import { EntityQueryDto } from "../dto/entity-query.dto"; import { DocumentBuilder } from "@terramatch-microservices/common/util"; -import { col, fn, Includeable, Op } from "sequelize"; +import { Includeable, Op } from "sequelize"; import { BadRequestException } from "@nestjs/common"; import { FrameworkKey } from "@terramatch-microservices/database/constants/framework"; import { AdditionalNurseryReportFullProps, NurseryReportFullDto, NurseryReportMedia } from "../dto/nursery-report.dto"; From b0030cdc0a982fa26b66acf8ae1ffcbdb59ee58b Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 21:06:06 -0400 Subject: [PATCH 05/41] [TM-1770] fix lint --- .../src/entities/processors/nursery-report.processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 9555e3cf..32b0f6c7 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -154,7 +154,7 @@ export class NurseryReportProcessor extends EntityProcessor< document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport)); } - protected async getReportTitleBase(dueAt: Date | null, title: string | null, locale: string = "en-GB") { + protected async getReportTitleBase(dueAt: Date | null, title: string | null, locale: string | null) { if (dueAt == null) return title ?? ""; const adjustedDate = new Date(dueAt); From f9b4d24bf8ff0c26c06e981ad34bd2811c4b0592 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 22:29:56 -0400 Subject: [PATCH 06/41] [TM-1770] fix --- apps/entity-service/src/entities/dto/nursery.dto.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/entity-service/src/entities/dto/nursery.dto.ts b/apps/entity-service/src/entities/dto/nursery.dto.ts index 109e510c..49406403 100644 --- a/apps/entity-service/src/entities/dto/nursery.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery.dto.ts @@ -81,7 +81,6 @@ export class NurseryLightDto extends EntityDto { export type AdditionalNurseryLightProps = Pick; export type AdditionalNurseryFullProps = AdditionalNurseryLightProps & AdditionalProps>; - export type NurseryMedia = Pick; export class NurseryFullDto extends NurseryLightDto { From dd45cf869595508cac2e5c24d0ca9bb32959877c Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 22:59:44 -0400 Subject: [PATCH 07/41] [TM-1770] update order values --- .../src/entities/processors/nursery-report.processor.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 32b0f6c7..9304cdd3 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -66,15 +66,17 @@ export class NurseryReportProcessor extends EntityProcessor< const builder = await this.entitiesService.buildQuery(NurseryReport, query, [nurseryAssociation]); if (query.sort != null) { if ( - ["status", "updateRequestStatus", "createdAt", "submittedAt", "updatedAt", "frameworkKey"].includes( + ["status", "updateRequestStatus", "dueAt", "submittedAt", "updatedAt", "frameworkKey"].includes( query.sort.field ) ) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else if (query.sort.field === "organisationName") { - builder.order(["project", "organisation", "name", query.sort.direction ?? "ASC"]); + builder.order(["$nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field === "projectName") { - builder.order(["project", "name", query.sort.direction ?? "ASC"]); + builder.order(["$nursery", "project", "name", query.sort.direction ?? "ASC"]); + } else if (query.sort.field === "nurseryName") { + builder.order(["$nursery", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field !== "id") { throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } @@ -108,7 +110,6 @@ export class NurseryReportProcessor extends EntityProcessor< if (query.search != null) { builder.where({ [Op.or]: [ - { name: { [Op.like]: `%${query.search}%` } }, { "$nursery.project.name$": { [Op.like]: `%${query.search}%` } }, { "$nursery.project.organisation.name$": { [Op.like]: `%${query.search}%` } } ] From a600a872e78e8fff675f762760c5fa13020ac71c Mon Sep 17 00:00:00 2001 From: LimberHope Date: Sun, 23 Mar 2025 23:58:44 -0400 Subject: [PATCH 08/41] [TM-1770] add missing terms --- .../entities/processors/nursery-report.processor.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 9304cdd3..2f3814a2 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -100,7 +100,15 @@ export class NurseryReportProcessor extends EntityProcessor< projectUuid: "$nursery.project.uuid$" }; - for (const term of ["status", "updateRequestStatus", "frameworkKey"]) { + for (const term of [ + "status", + "updateRequestStatus", + "frameworkKey", + "nurseryUuid", + "organisationUuid", + "country", + "projectUuid" + ]) { if (query[term] != null) { const field = associationFieldMap[term] || term; builder.where({ [field]: query[term] }); From ebdd6940f7994bc759eaaecbcb4ee017d4ad223f Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 00:21:21 -0400 Subject: [PATCH 09/41] [TM-1770] [TM-1769] add nurseryUuid params in query dto --- .../src/entities/dto/entity-query.dto.ts | 4 ++++ .../processors/nursery-report.processor.spec.ts | 16 ++++++++++++---- .../processors/nursery-report.processor.ts | 12 ++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/entity-service/src/entities/dto/entity-query.dto.ts b/apps/entity-service/src/entities/dto/entity-query.dto.ts index d761fe8a..4159c2fb 100644 --- a/apps/entity-service/src/entities/dto/entity-query.dto.ts +++ b/apps/entity-service/src/entities/dto/entity-query.dto.ts @@ -41,4 +41,8 @@ export class EntityQueryDto extends IntersectionType(QuerySort, NumberPage) { @ApiProperty({ required: false }) @IsOptional() projectUuid?: string; + + @ApiProperty({ required: false }) + @IsOptional() + nurseryUuid?: string; } diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 2b616b99..a8fa8185 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -102,14 +102,15 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([nurseryReport1, nurseryReport2], { search: "foo" }); }); - it("should return nursery reports filtered by the update request status or project", async () => { + it("should return nursery reports filtered by the update request status, nursery, country and project", async () => { const p1 = await ProjectFactory.create({ country: "MX" }); const p2 = await ProjectFactory.create({ country: "CA" }); await ProjectUserFactory.create({ userId, projectId: p1.id }); await ProjectUserFactory.create({ userId, projectId: p2.id }); const n1 = await NurseryFactory.create({ projectId: p1.id }); const n2 = await NurseryFactory.create({ projectId: p2.id }); - + n1.project = await n1.$get("project"); + n2.project = await n2.$get("project"); const first = await NurseryReportFactory.create({ title: "first nursery report", status: "approved", @@ -139,15 +140,22 @@ describe("NurseryReportProcessor", () => { nurseryId: n2.id }); + first.nursery = await first.$get("nursery"); + second.nursery = await second.$get("nursery"); + third.nursery = await third.$get("nursery"); + fourth.nursery = await fourth.$get("nursery"); + await expectNurseryReports([first, second, third], { updateRequestStatus: "awaiting-approval" }); await expectNurseryReports([fourth], { projectUuid: p2.uuid }); await expectNurseryReports([first, second, third], { country: "MX" }); + + await expectNurseryReports([first, second, third], { nurseryUuid: n1.uuid }); }); - it("should throw an error if the project uuid is not found", async () => { - await expect(processor.findMany({ projectUuid: "123" })).rejects.toThrow(BadRequestException); + it("should throw an error if the nursery uuid is not found", async () => { + await expect(processor.findMany({ nurseryUuid: "123" })).rejects.toThrow(BadRequestException); }); it("should sort nursery reports by update request status", async () => { diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 2f3814a2..b9867547 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -1,7 +1,7 @@ import { Media, + Nursery, NurseryReport, - Project, ProjectReport, ProjectUser, User @@ -124,12 +124,12 @@ export class NurseryReportProcessor extends EntityProcessor< }); } - if (query.projectUuid != null) { - const project = await Project.findOne({ where: { uuid: query.projectUuid }, attributes: ["id"] }); - if (project == null) { - throw new BadRequestException(`Project with uuid ${query.projectUuid} not found`); + if (query.nurseryUuid != null) { + const nursery = await Nursery.findOne({ where: { uuid: query.nurseryUuid }, attributes: ["id"] }); + if (nursery == null) { + throw new BadRequestException(`Nursery with uuid ${query.nurseryUuid} not found`); } - builder.where({ "$nursery.project.id$": project.id }); + builder.where({ nurseryId: nursery.id }); } return { models: await builder.execute(), paginationTotal: await builder.paginationTotal() }; From f590496748d5edf7636a7acb52e22d4675fde6ce Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 00:38:24 -0400 Subject: [PATCH 10/41] [TM-1770] add more test cases --- .../processors/nursery-report.processor.spec.ts | 6 ++++-- .../entities/processors/nursery-report.processor.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index a8fa8185..2dbf8f58 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -147,6 +147,8 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([first, second, third], { updateRequestStatus: "awaiting-approval" }); + await expectNurseryReports([first, third, fourth], { status: "approved" }); + await expectNurseryReports([fourth], { projectUuid: p2.uuid }); await expectNurseryReports([first, second, third], { country: "MX" }); @@ -201,7 +203,7 @@ describe("NurseryReportProcessor", () => { ); }); - it("should sort project reports by organisation name", async () => { + it("should sort nursery reports by organisation name", async () => { const org1 = await OrganisationFactory.create({ name: "A Org" }); const org2 = await OrganisationFactory.create({ name: "B Org" }); const org3 = await OrganisationFactory.create({ name: "C Org" }); @@ -246,7 +248,7 @@ describe("NurseryReportProcessor", () => { ); }); - it("should sort project reports by status", async () => { + it("should sort nursery reports by status", async () => { const nurseryReportA = await NurseryReportFactory.create({ status: "started" }); const nurseryReportB = await NurseryReportFactory.create({ status: "approved" }); const nurseryReportC = await NurseryReportFactory.create({ status: "approved" }); diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index b9867547..f7a10323 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -46,11 +46,7 @@ export class NurseryReportProcessor extends EntityProcessor< }); } - async findMany( - query: EntityQueryDto, - userId?: number, - permissions?: string[] - ): Promise> { + async findMany(query: EntityQueryDto, userId?: number, permissions?: string[]) { const nurseryAssociation: Includeable = { association: "nursery", attributes: ["id", "uuid", "name"], @@ -72,11 +68,11 @@ export class NurseryReportProcessor extends EntityProcessor< ) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else if (query.sort.field === "organisationName") { - builder.order(["$nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); + builder.order(["nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field === "projectName") { - builder.order(["$nursery", "project", "name", query.sort.direction ?? "ASC"]); + builder.order(["nursery", "project", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field === "nurseryName") { - builder.order(["$nursery", "name", query.sort.direction ?? "ASC"]); + builder.order(["nursery", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field !== "id") { throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } From 5e52666817e8e46a91856e42bbb03210bcb3fe40 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 00:40:55 -0400 Subject: [PATCH 11/41] [TM-1770] fix lint --- .../src/entities/processors/nursery-report.processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index f7a10323..81b34f11 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -6,7 +6,7 @@ import { ProjectUser, User } from "@terramatch-microservices/database/entities"; -import { EntityProcessor, PaginatedResult } from "./entity-processor"; +import { EntityProcessor } from "./entity-processor"; import { EntityQueryDto } from "../dto/entity-query.dto"; import { DocumentBuilder } from "@terramatch-microservices/common/util"; import { Includeable, Op } from "sequelize"; From d6a898f1d34fc6cfc99b9aacf7ea1ecdfef936f6 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 00:48:35 -0400 Subject: [PATCH 12/41] [TM-1770] add more test cases --- .../nursery-report.processor.spec.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 2dbf8f58..a52383c4 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -87,12 +87,16 @@ describe("NurseryReportProcessor", () => { }); it("should return nursery reports that match the search term", async () => { - const project1 = await ProjectFactory.create({ name: "Foo Bar" }); - const project2 = await ProjectFactory.create({ name: "Baz Foo" }); + const org1 = await OrganisationFactory.create({ name: "A Org" }); + const org2 = await OrganisationFactory.create({ name: "B Org" }); + const project1 = await ProjectFactory.create({ name: "Foo Bar", organisationId: org1.id }); + const project2 = await ProjectFactory.create({ name: "Baz Foo", organisationId: org2.id }); const nursery1 = await NurseryFactory.create({ projectId: project1.id }); const nursery2 = await NurseryFactory.create({ projectId: project2.id }); nursery1.project = await nursery1.$get("project"); nursery2.project = await nursery2.$get("project"); + project1.organisation = await project1.$get("organisation"); + project2.organisation = await project2.$get("organisation"); const nurseryReport1 = await NurseryReportFactory.create({ nurseryId: nursery1.id }); const nurseryReport2 = await NurseryReportFactory.create({ nurseryId: nursery2.id }); nurseryReport1.nursery = await nurseryReport1.$get("nursery"); @@ -100,17 +104,23 @@ describe("NurseryReportProcessor", () => { await NurseryReportFactory.createMany(3); await expectNurseryReports([nurseryReport1, nurseryReport2], { search: "foo" }); + + await expectNurseryReports([nurseryReport1, nurseryReport2], { search: "org" }); }); - it("should return nursery reports filtered by the update request status, nursery, country and project", async () => { - const p1 = await ProjectFactory.create({ country: "MX" }); - const p2 = await ProjectFactory.create({ country: "CA" }); + it("should return nursery reports filtered by the status, update request status, nursery, country, organisation and project", async () => { + const org1 = await OrganisationFactory.create(); + const org2 = await OrganisationFactory.create(); + const p1 = await ProjectFactory.create({ country: "MX", organisationId: org1.id }); + const p2 = await ProjectFactory.create({ country: "CA", organisationId: org2.id }); await ProjectUserFactory.create({ userId, projectId: p1.id }); await ProjectUserFactory.create({ userId, projectId: p2.id }); const n1 = await NurseryFactory.create({ projectId: p1.id }); const n2 = await NurseryFactory.create({ projectId: p2.id }); n1.project = await n1.$get("project"); n2.project = await n2.$get("project"); + n1.project.organisation = await n1.project.$get("organisation"); + n2.project.organisation = await n2.project.$get("organisation"); const first = await NurseryReportFactory.create({ title: "first nursery report", status: "approved", From eca24568639d6c73541f77b7570bfd87572f33ae Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 01:02:10 -0400 Subject: [PATCH 13/41] [TM-1770] remove non-sortable fields --- .../src/entities/processors/nursery-report.processor.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 81b34f11..f2c2a6b5 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -61,18 +61,12 @@ export class NurseryReportProcessor extends EntityProcessor< const builder = await this.entitiesService.buildQuery(NurseryReport, query, [nurseryAssociation]); if (query.sort != null) { - if ( - ["status", "updateRequestStatus", "dueAt", "submittedAt", "updatedAt", "frameworkKey"].includes( - query.sort.field - ) - ) { + if (["status", "updateRequestStatus", "dueAt", "submittedAt", "updatedAt"].includes(query.sort.field)) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else if (query.sort.field === "organisationName") { builder.order(["nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field === "projectName") { builder.order(["nursery", "project", "name", query.sort.direction ?? "ASC"]); - } else if (query.sort.field === "nurseryName") { - builder.order(["nursery", "name", query.sort.direction ?? "ASC"]); } else if (query.sort.field !== "id") { throw new BadRequestException(`Invalid sort field: ${query.sort.field}`); } From 4ebfc4611b6934066c0fe12620eaa379fbcce888 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 01:08:43 -0400 Subject: [PATCH 14/41] [TM-1770] remove non-sortable fields --- .../nursery-report.processor.spec.ts | 42 ------------------- .../processors/nursery-report.processor.ts | 2 +- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index a52383c4..4407e703 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -170,27 +170,6 @@ describe("NurseryReportProcessor", () => { await expect(processor.findMany({ nurseryUuid: "123" })).rejects.toThrow(BadRequestException); }); - it("should sort nursery reports by update request status", async () => { - const nurseryReportA = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); - const nurseryReportB = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); - const nurseryReportC = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); - await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], - { sort: { field: "updateRequestStatus" } }, - { sortField: "updateRequestStatus" } - ); - await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], - { sort: { field: "updateRequestStatus", direction: "ASC" } }, - { sortField: "updateRequestStatus" } - ); - await expectNurseryReports( - [nurseryReportC, nurseryReportB, nurseryReportA], - { sort: { field: "updateRequestStatus", direction: "DESC" } }, - { sortField: "updateRequestStatus", sortUp: false } - ); - }); - it("should sort nursery reports by project name", async () => { const projectA = await ProjectFactory.create({ name: "A Project" }); const projectB = await ProjectFactory.create({ name: "B Project" }); @@ -258,27 +237,6 @@ describe("NurseryReportProcessor", () => { ); }); - it("should sort nursery reports by status", async () => { - const nurseryReportA = await NurseryReportFactory.create({ status: "started" }); - const nurseryReportB = await NurseryReportFactory.create({ status: "approved" }); - const nurseryReportC = await NurseryReportFactory.create({ status: "approved" }); - await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], - { sort: { field: "status" } }, - { sortField: "status" } - ); - await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], - { sort: { field: "status", direction: "ASC" } }, - { sortField: "status" } - ); - await expectNurseryReports( - [nurseryReportC, nurseryReportB, nurseryReportA], - { sort: { field: "status", direction: "DESC" } }, - { sortField: "status", sortUp: false } - ); - }); - it("should sort nursery reports by due date", async () => { const nurseryReportA = await NurseryReportFactory.create({ dueAt: DateTime.now().minus({ days: 1 }).toJSDate() }); const nurseryReportB = await NurseryReportFactory.create({ diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index f2c2a6b5..40b42888 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -61,7 +61,7 @@ export class NurseryReportProcessor extends EntityProcessor< const builder = await this.entitiesService.buildQuery(NurseryReport, query, [nurseryAssociation]); if (query.sort != null) { - if (["status", "updateRequestStatus", "dueAt", "submittedAt", "updatedAt"].includes(query.sort.field)) { + if (["dueAt", "submittedAt", "updatedAt"].includes(query.sort.field)) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else if (query.sort.field === "organisationName") { builder.order(["nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); From 60815c728880ba99b505ae36fe3f77b7bf7fee1a Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 01:20:41 -0400 Subject: [PATCH 15/41] [TM-1770] add test --- .../nursery-report.processor.spec.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 4407e703..6d86d9f2 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -278,6 +278,29 @@ describe("NurseryReportProcessor", () => { ); }); + it("should sort nursery reports by updated at", async () => { + const now = DateTime.now(); + const nurseryReportA = await NurseryReportFactory.create({ + updatedAt: now.minus({ minutes: 1 }).toJSDate() + }); + const nurseryReportB = await NurseryReportFactory.create({ + updatedAt: now.minus({ minutes: 10 }).toJSDate() + }); + const nurseryReportC = await NurseryReportFactory.create({ + updatedAt: now.minus({ minutes: 5 }).toJSDate() + }); + await expectNurseryReports( + [nurseryReportA, nurseryReportC, nurseryReportB], + { sort: { field: "updatedAt", direction: "DESC" } }, + { sortField: "updatedAt", sortUp: false } + ); + await expectNurseryReports( + [nurseryReportB, nurseryReportC, nurseryReportA], + { sort: { field: "updatedAt", direction: "ASC" } }, + { sortField: "updatedAt", sortUp: true } + ); + }); + it("should paginate nursery reports", async () => { const nurseryReports = sortBy(await NurseryReportFactory.createMany(25), "id"); await expectNurseryReports(nurseryReports.slice(0, 10), { page: { size: 10 } }, { total: nurseryReports.length }); From 230981c361cf30e3bc41daca5b695efa07826995 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 01:30:31 -0400 Subject: [PATCH 16/41] [TM-1770] fix test --- .../processors/nursery-report.processor.spec.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 6d86d9f2..9d99d26b 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -279,16 +279,9 @@ describe("NurseryReportProcessor", () => { }); it("should sort nursery reports by updated at", async () => { - const now = DateTime.now(); - const nurseryReportA = await NurseryReportFactory.create({ - updatedAt: now.minus({ minutes: 1 }).toJSDate() - }); - const nurseryReportB = await NurseryReportFactory.create({ - updatedAt: now.minus({ minutes: 10 }).toJSDate() - }); - const nurseryReportC = await NurseryReportFactory.create({ - updatedAt: now.minus({ minutes: 5 }).toJSDate() - }); + const nurseryReportA = await NurseryReportFactory.create(); + const nurseryReportB = await NurseryReportFactory.create(); + const nurseryReportC = await NurseryReportFactory.create(); await expectNurseryReports( [nurseryReportA, nurseryReportC, nurseryReportB], { sort: { field: "updatedAt", direction: "DESC" } }, From 9acd6d3a0b47cbb7e65e53c554f2c9f380bab6ea Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 01:52:33 -0400 Subject: [PATCH 17/41] [TM-1770] update test --- .../entities/processors/nursery-report.processor.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 9d99d26b..6cac1177 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -282,6 +282,12 @@ describe("NurseryReportProcessor", () => { const nurseryReportA = await NurseryReportFactory.create(); const nurseryReportB = await NurseryReportFactory.create(); const nurseryReportC = await NurseryReportFactory.create(); + nurseryReportA.updatedAt = DateTime.now().minus({ minutes: 1 }).toJSDate(); + nurseryReportB.updatedAt = DateTime.now().minus({ minutes: 10 }).toJSDate(); + nurseryReportC.updatedAt = DateTime.now().minus({ minutes: 5 }).toJSDate(); + await nurseryReportA.reload(); + await nurseryReportB.reload(); + await nurseryReportC.reload(); await expectNurseryReports( [nurseryReportA, nurseryReportC, nurseryReportB], { sort: { field: "updatedAt", direction: "DESC" } }, From d615cf728b7f20ba077df1bc11a8b580f7afe8a9 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 02:03:01 -0400 Subject: [PATCH 18/41] [TM-1770] update test --- .../nursery-report.processor.spec.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 6cac1177..ca81334f 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -282,21 +282,22 @@ describe("NurseryReportProcessor", () => { const nurseryReportA = await NurseryReportFactory.create(); const nurseryReportB = await NurseryReportFactory.create(); const nurseryReportC = await NurseryReportFactory.create(); - nurseryReportA.updatedAt = DateTime.now().minus({ minutes: 1 }).toJSDate(); - nurseryReportB.updatedAt = DateTime.now().minus({ minutes: 10 }).toJSDate(); - nurseryReportC.updatedAt = DateTime.now().minus({ minutes: 5 }).toJSDate(); + nurseryReportA.updatedAt = DateTime.now().minus({ days: 1 }).toJSDate(); + nurseryReportB.updatedAt = DateTime.now().minus({ days: 10 }).toJSDate(); + nurseryReportC.updatedAt = DateTime.now().minus({ days: 5 }).toJSDate(); await nurseryReportA.reload(); await nurseryReportB.reload(); await nurseryReportC.reload(); + await expectNurseryReports( - [nurseryReportA, nurseryReportC, nurseryReportB], - { sort: { field: "updatedAt", direction: "DESC" } }, - { sortField: "updatedAt", sortUp: false } + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updatedAt" } }, + { sortField: "updatedAt" } ); await expectNurseryReports( - [nurseryReportB, nurseryReportC, nurseryReportA], - { sort: { field: "updatedAt", direction: "ASC" } }, - { sortField: "updatedAt", sortUp: true } + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updatedAt", direction: "DESC" } }, + { sortField: "updatedAt", sortUp: false } ); }); From 7121d70282a0cb8a50a70ee7a19212047b74e6f2 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 02:12:18 -0400 Subject: [PATCH 19/41] [TM-1770] update test --- .../processors/nursery-report.processor.spec.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index ca81334f..e49edc9a 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -280,22 +280,11 @@ describe("NurseryReportProcessor", () => { it("should sort nursery reports by updated at", async () => { const nurseryReportA = await NurseryReportFactory.create(); - const nurseryReportB = await NurseryReportFactory.create(); - const nurseryReportC = await NurseryReportFactory.create(); nurseryReportA.updatedAt = DateTime.now().minus({ days: 1 }).toJSDate(); - nurseryReportB.updatedAt = DateTime.now().minus({ days: 10 }).toJSDate(); - nurseryReportC.updatedAt = DateTime.now().minus({ days: 5 }).toJSDate(); - await nurseryReportA.reload(); - await nurseryReportB.reload(); - await nurseryReportC.reload(); + await expectNurseryReports([nurseryReportA], { sort: { field: "updatedAt" } }, { sortField: "updatedAt" }); await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], - { sort: { field: "updatedAt" } }, - { sortField: "updatedAt" } - ); - await expectNurseryReports( - [nurseryReportA, nurseryReportB, nurseryReportC], + [nurseryReportA], { sort: { field: "updatedAt", direction: "DESC" } }, { sortField: "updatedAt", sortUp: false } ); From dd76056dcbd1d04a99596479a7928190ce1cd6c3 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 11:39:07 -0400 Subject: [PATCH 20/41] [TM-1770] add missing property --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 79b7c4e3..0222d084 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -78,6 +78,9 @@ export class NurseryReportLightDto extends EntityDto { @ApiProperty({ nullable: true }) taskId: number | null; + @ApiProperty() + dueAt: Date | null; + @ApiProperty() createdAt: Date; } From cdd3781b4706c3dcb623622a215644b2cb5c32af Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 11:50:39 -0400 Subject: [PATCH 21/41] [TM-1770] add missing property --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 0222d084..9a29e678 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -81,6 +81,9 @@ export class NurseryReportLightDto extends EntityDto { @ApiProperty() dueAt: Date | null; + @ApiProperty({ nullable: true }) + title: string | null; + @ApiProperty() createdAt: Date; } From 0fb1bf5aba90f12890927ab8152389379ecac480 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 12:50:28 -0400 Subject: [PATCH 22/41] [TM-1770] add missing sort fields --- .../src/entities/dto/nursery-report.dto.ts | 33 ++++++++------ .../nursery-report.processor.spec.ts | 43 +++++++++++++++++++ .../processors/nursery-report.processor.ts | 5 ++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 9a29e678..cf5b2653 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -8,7 +8,7 @@ import { User } from "@sentry/nestjs"; @JsonApiDto({ type: "nurseryReports" }) export class NurseryReportLightDto extends EntityDto { - constructor(nurseryReport?: NurseryReport) { + constructor(nurseryReport?: NurseryReport, props?: AdditionalNurseryReportLightProps) { super(); if (nurseryReport != null) { this.populate(NurseryReportLightDto, { @@ -16,7 +16,8 @@ export class NurseryReportLightDto extends EntityDto { lightResource: true, // these two are untyped and marked optional in the base model. createdAt: nurseryReport.createdAt as Date, - updatedAt: nurseryReport.createdAt as Date + updatedAt: nurseryReport.createdAt as Date, + ...props }); } } @@ -84,27 +85,31 @@ export class NurseryReportLightDto extends EntityDto { @ApiProperty({ nullable: true }) title: string | null; + @ApiProperty({ nullable: true }) + reportTitle: string | null; + @ApiProperty() createdAt: Date; } -export type AdditionalNurseryReportFullProps = AdditionalProps< - NurseryReportFullDto, - NurseryReportLightDto & Omit ->; +export type AdditionalNurseryReportLightProps = Pick; +export type AdditionalNurseryReportFullProps = AdditionalNurseryReportLightProps & + AdditionalProps>; export type NurseryReportMedia = Pick; export class NurseryReportFullDto extends NurseryReportLightDto { constructor(nurseryReport: NurseryReport, props?: AdditionalNurseryReportFullProps) { super(); - this.populate(NurseryReportFullDto, { - ...pickApiProperties(nurseryReport, NurseryReportFullDto), - lightResource: false, - // these two are untyped and marked optional in the base model. - createdAt: nurseryReport.createdAt as Date, - updatedAt: nurseryReport.createdAt as Date, - ...props - }); + if (nurseryReport != null) { + this.populate(NurseryReportFullDto, { + ...pickApiProperties(nurseryReport, NurseryReportFullDto), + lightResource: false, + // these two are untyped and marked optional in the base model. + createdAt: nurseryReport.createdAt as Date, + updatedAt: nurseryReport.createdAt as Date, + ...props + }); + } } @ApiProperty({ nullable: true }) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index e49edc9a..84b019ac 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -11,6 +11,7 @@ import { OrganisationFactory, ProjectFactory, ProjectUserFactory, + SiteReportFactory, UserFactory } from "@terramatch-microservices/database/factories"; import { buildJsonApi } from "@terramatch-microservices/common/util"; @@ -290,6 +291,48 @@ describe("NurseryReportProcessor", () => { ); }); + it("should sort nursery reports by update request status", async () => { + const nurseryReportA = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + const nurseryReportB = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + const nurseryReportC = await NurseryReportFactory.create({ updateRequestStatus: "awaiting-approval" }); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updateRequestStatus" } }, + { sortField: "updateRequestStatus" } + ); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "updateRequestStatus", direction: "ASC" } }, + { sortField: "updateRequestStatus" } + ); + await expectNurseryReports( + [nurseryReportC, nurseryReportB, nurseryReportA], + { sort: { field: "updateRequestStatus", direction: "DESC" } }, + { sortField: "updateRequestStatus", sortUp: false } + ); + }); + + it("should sort nursery reports by status", async () => { + const nurseryReportA = await NurseryReportFactory.create({ status: "started" }); + const nurseryReportB = await NurseryReportFactory.create({ status: "approved" }); + const nurseryReportC = await NurseryReportFactory.create({ status: "approved" }); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "status" } }, + { sortField: "status" } + ); + await expectNurseryReports( + [nurseryReportA, nurseryReportB, nurseryReportC], + { sort: { field: "status", direction: "ASC" } }, + { sortField: "status" } + ); + await expectNurseryReports( + [nurseryReportC, nurseryReportB, nurseryReportA], + { sort: { field: "status", direction: "DESC" } }, + { sortField: "status", sortUp: false } + ); + }); + it("should paginate nursery reports", async () => { const nurseryReports = sortBy(await NurseryReportFactory.createMany(25), "id"); await expectNurseryReports(nurseryReports.slice(0, 10), { page: { size: 10 } }, { total: nurseryReports.length }); diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 40b42888..890d4d27 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -61,7 +61,7 @@ export class NurseryReportProcessor extends EntityProcessor< const builder = await this.entitiesService.buildQuery(NurseryReport, query, [nurseryAssociation]); if (query.sort != null) { - if (["dueAt", "submittedAt", "updatedAt"].includes(query.sort.field)) { + if (["dueAt", "submittedAt", "updatedAt", "status", "updateRequestStatus"].includes(query.sort.field)) { builder.order([query.sort.field, query.sort.direction ?? "ASC"]); } else if (query.sort.field === "organisationName") { builder.order(["nursery", "project", "organisation", "name", query.sort.direction ?? "ASC"]); @@ -150,7 +150,8 @@ export class NurseryReportProcessor extends EntityProcessor< } async addLightDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { - document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport)); + const reportTitle = await this.getReportTitle(nurseryReport); + document.addData(nurseryReport.uuid, new NurseryReportLightDto(nurseryReport, { reportTitle })); } protected async getReportTitleBase(dueAt: Date | null, title: string | null, locale: string | null) { From fffc6bf2d3327bb4c6b2bed75c08c69895c7ca05 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 15:37:11 -0400 Subject: [PATCH 23/41] [TM-1770] fix lint --- .../src/entities/processors/nursery-report.processor.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 84b019ac..49c7d874 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -11,7 +11,6 @@ import { OrganisationFactory, ProjectFactory, ProjectUserFactory, - SiteReportFactory, UserFactory } from "@terramatch-microservices/database/factories"; import { buildJsonApi } from "@terramatch-microservices/common/util"; From f5c1c2470bb91b079c60ef2b78054798c16df853 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 15:59:59 -0400 Subject: [PATCH 24/41] [TM-1770] add new test case --- .../src/entities/processors/nursery-report.processor.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 49c7d874..44a94e7b 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -164,6 +164,8 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([first, second, third], { country: "MX" }); await expectNurseryReports([first, second, third], { nurseryUuid: n1.uuid }); + + await expectNurseryReports([second], { status: "started", updateRequestStatus: "awaiting-approval" }); }); it("should throw an error if the nursery uuid is not found", async () => { From 7a62e594f9b2c68b0f5ee343693da57808aa0178 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:12:54 -0400 Subject: [PATCH 25/41] [TM-1770] add new test case --- .../src/entities/processors/nursery-report.processor.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 44a94e7b..dc427a30 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -334,6 +334,11 @@ describe("NurseryReportProcessor", () => { ); }); + it("should return an empty list when there are no matches in the search", async () => { + await NurseryReportFactory.createMany(3, { title: "foo" }); + await expectNurseryReports([], { search: "bar" }); + }); + it("should paginate nursery reports", async () => { const nurseryReports = sortBy(await NurseryReportFactory.createMany(25), "id"); await expectNurseryReports(nurseryReports.slice(0, 10), { page: { size: 10 } }, { total: nurseryReports.length }); From bc065051455e68d1f2aff286ed9616d3109a1547 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:20:31 -0400 Subject: [PATCH 26/41] [TM-1770] update test --- .../src/entities/processors/nursery-report.processor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index dc427a30..b0c10e15 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -336,7 +336,7 @@ describe("NurseryReportProcessor", () => { it("should return an empty list when there are no matches in the search", async () => { await NurseryReportFactory.createMany(3, { title: "foo" }); - await expectNurseryReports([], { search: "bar" }); + await expectNurseryReports([], { search: "test" }); }); it("should paginate nursery reports", async () => { From a0f10e8f2e9d23a70f7329fd9bf0017e2ea8c787 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:27:39 -0400 Subject: [PATCH 27/41] [TM-1770] update test --- .../entities/processors/nursery-report.processor.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index b0c10e15..ed632c72 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -357,7 +357,13 @@ describe("NurseryReportProcessor", () => { describe("should return a requested nursery report when findOne is called with a valid uuid", () => { it("should return a requested nursery report", async () => { - const nurseryReport = await NurseryReportFactory.create(); + const org = await OrganisationFactory.create({ name: "foo" }); + const project = await ProjectFactory.create({ organisationId: org.id, name: "bar" }); + const nursery = await NurseryFactory.create({ projectId: project.id, name: "baz" }); + const nurseryReport = await NurseryReportFactory.create({ nurseryId: nursery.id, title: "qux" }); + nurseryReport.nursery = await nurseryReport.$get("nursery"); + nurseryReport.nursery.project = await nurseryReport.nursery.$get("project"); + nurseryReport.nursery.project.organisation = await nurseryReport.nursery.project.$get("organisation"); const result = await processor.findOne(nurseryReport.uuid); expect(result.id).toBe(nurseryReport.id); }); From eb5c2ef0d016e9af379192b995d3c63593c2c6b7 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:34:05 -0400 Subject: [PATCH 28/41] [TM-1770] update test --- .../entities/processors/nursery-report.processor.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index ed632c72..b0c10e15 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -357,13 +357,7 @@ describe("NurseryReportProcessor", () => { describe("should return a requested nursery report when findOne is called with a valid uuid", () => { it("should return a requested nursery report", async () => { - const org = await OrganisationFactory.create({ name: "foo" }); - const project = await ProjectFactory.create({ organisationId: org.id, name: "bar" }); - const nursery = await NurseryFactory.create({ projectId: project.id, name: "baz" }); - const nurseryReport = await NurseryReportFactory.create({ nurseryId: nursery.id, title: "qux" }); - nurseryReport.nursery = await nurseryReport.$get("nursery"); - nurseryReport.nursery.project = await nurseryReport.nursery.$get("project"); - nurseryReport.nursery.project.organisation = await nurseryReport.nursery.project.$get("organisation"); + const nurseryReport = await NurseryReportFactory.create(); const result = await processor.findOne(nurseryReport.uuid); expect(result.id).toBe(nurseryReport.id); }); From 9b02d49eaf7d884a0326297493e5a8f3b1ebf3fa Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:43:24 -0400 Subject: [PATCH 29/41] [TM-1770] comment to test --- .../entities/processors/nursery-report.processor.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index b0c10e15..86c85d15 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -117,10 +117,10 @@ describe("NurseryReportProcessor", () => { await ProjectUserFactory.create({ userId, projectId: p2.id }); const n1 = await NurseryFactory.create({ projectId: p1.id }); const n2 = await NurseryFactory.create({ projectId: p2.id }); - n1.project = await n1.$get("project"); - n2.project = await n2.$get("project"); - n1.project.organisation = await n1.project.$get("organisation"); - n2.project.organisation = await n2.project.$get("organisation"); + // n1.project = await n1.$get("project"); + // n2.project = await n2.$get("project"); + // n1.project.organisation = await n1.project.$get("organisation"); + // n2.project.organisation = await n2.project.$get("organisation"); const first = await NurseryReportFactory.create({ title: "first nursery report", status: "approved", From ba34b2209cf137def34e0fb11d7e26b7c14db405 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 24 Mar 2025 16:48:40 -0400 Subject: [PATCH 30/41] [TM-1770] remove comment --- .../src/entities/processors/nursery-report.processor.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 86c85d15..e62c43de 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -117,10 +117,6 @@ describe("NurseryReportProcessor", () => { await ProjectUserFactory.create({ userId, projectId: p2.id }); const n1 = await NurseryFactory.create({ projectId: p1.id }); const n2 = await NurseryFactory.create({ projectId: p2.id }); - // n1.project = await n1.$get("project"); - // n2.project = await n2.$get("project"); - // n1.project.organisation = await n1.project.$get("organisation"); - // n2.project.organisation = await n2.project.$get("organisation"); const first = await NurseryReportFactory.create({ title: "first nursery report", status: "approved", From 8ce160ccd8815f487ca0dafa694a102ef6abcc33 Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Mon, 24 Mar 2025 21:18:19 -0400 Subject: [PATCH 31/41] [TM-1770] add unit test for project uuid filter on nursery reports --- .../entities/processors/nursery-report.processor.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 86c85d15..24d3b969 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -108,7 +108,7 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([nurseryReport1, nurseryReport2], { search: "org" }); }); - it("should return nursery reports filtered by the status, update request status, nursery, country, organisation and project", async () => { + it("should return nursery reports filtered by the status, update request status, nursery, country, organisation", async () => { const org1 = await OrganisationFactory.create(); const org2 = await OrganisationFactory.create(); const p1 = await ProjectFactory.create({ country: "MX", organisationId: org1.id }); @@ -166,6 +166,12 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([first, second, third], { nurseryUuid: n1.uuid }); await expectNurseryReports([second], { status: "started", updateRequestStatus: "awaiting-approval" }); + + await expectNurseryReports([first], { projectUuid: p1.uuid }); + }); + + it("should throw an error if the project uuid is not found", async () => { + await expect(processor.findMany({ projectUuid: "123" })).rejects.toThrow(BadRequestException); }); it("should throw an error if the nursery uuid is not found", async () => { From 39d45fd27e7b0e57fbc0d1228a5c70f564fd783f Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Mon, 24 Mar 2025 21:29:43 -0400 Subject: [PATCH 32/41] [TM-1770] fix test --- .../src/entities/processors/nursery-report.processor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 24d3b969..98334e57 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -167,7 +167,7 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([second], { status: "started", updateRequestStatus: "awaiting-approval" }); - await expectNurseryReports([first], { projectUuid: p1.uuid }); + await expectNurseryReports([first, second, third], { projectUuid: p1.uuid }); }); it("should throw an error if the project uuid is not found", async () => { From 200127e1605647e677c2539858f9868cab1a011b Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Mon, 24 Mar 2025 21:35:51 -0400 Subject: [PATCH 33/41] [TM-1770] move expectation --- .../src/entities/processors/nursery-report.processor.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 98334e57..50d6c408 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -168,10 +168,8 @@ describe("NurseryReportProcessor", () => { await expectNurseryReports([second], { status: "started", updateRequestStatus: "awaiting-approval" }); await expectNurseryReports([first, second, third], { projectUuid: p1.uuid }); - }); - it("should throw an error if the project uuid is not found", async () => { - await expect(processor.findMany({ projectUuid: "123" })).rejects.toThrow(BadRequestException); + await expectNurseryReports([], { projectUuid: "123" }); }); it("should throw an error if the nursery uuid is not found", async () => { From d35ed0f8f8c472a826c203e230209f1d5e4c6c0d Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Tue, 25 Mar 2025 08:43:51 -0400 Subject: [PATCH 34/41] [TM-1770] fix coverage --- .../processors/nursery-report.processor.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts index 50d6c408..2ed206db 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.spec.ts @@ -198,6 +198,10 @@ describe("NurseryReportProcessor", () => { ); }); + it("should throw an error if the sort field is not valid", async () => { + await expect(processor.findMany({ sort: { field: "invalid" } })).rejects.toThrow(BadRequestException); + }); + it("should sort nursery reports by organisation name", async () => { const org1 = await OrganisationFactory.create({ name: "A Org" }); const org2 = await OrganisationFactory.create({ name: "B Org" }); @@ -385,7 +389,8 @@ describe("NurseryReportProcessor", () => { const nursery = await NurseryFactory.create({ projectId: project.id }); const { uuid } = await NurseryReportFactory.create({ - nurseryId: nursery.id + nurseryId: nursery.id, + dueAt: null }); const nurseryReport = await processor.findOne(uuid); @@ -396,7 +401,8 @@ describe("NurseryReportProcessor", () => { uuid, lightResource: false, projectUuid: project.uuid, - nurseryUuid: nursery.uuid + nurseryUuid: nursery.uuid, + dueAt: null }); }); }); From bb87cc2a9e43931c69eb3f6f5a7f534e1c98e9a5 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 26 Mar 2025 12:20:45 -0400 Subject: [PATCH 35/41] [TM-1770] resolve PR comments --- .../src/entities/dto/entity-query.dto.ts | 7 +++++ .../src/entities/dto/nursery-report.dto.ts | 13 ++++----- .../processors/nursery-report.processor.ts | 28 +++++++++++-------- .../entities/processors/nursery.processor.ts | 4 +-- .../src/lib/entities/nursery-report.entity.ts | 6 ++++ 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/apps/entity-service/src/entities/dto/entity-query.dto.ts b/apps/entity-service/src/entities/dto/entity-query.dto.ts index 4159c2fb..720e1a22 100644 --- a/apps/entity-service/src/entities/dto/entity-query.dto.ts +++ b/apps/entity-service/src/entities/dto/entity-query.dto.ts @@ -26,6 +26,13 @@ export class EntityQueryDto extends IntersectionType(QuerySort, NumberPage) { @IsOptional() search?: string; + @ApiProperty({ + required: false, + description: "Search query used for filtering selectable options in autocomplete fields." + }) + @IsOptional() + searchFilter?: string; + @ApiProperty({ required: false }) @IsOptional() country?: string; diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index cf5b2653..445cb0bd 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -15,8 +15,8 @@ export class NurseryReportLightDto extends EntityDto { ...pickApiProperties(nurseryReport, NurseryReportLightDto), lightResource: true, // these two are untyped and marked optional in the base model. - createdAt: nurseryReport.createdAt as Date, - updatedAt: nurseryReport.createdAt as Date, + createdAt: new Date(nurseryReport.createdAt), + updatedAt: new Date(nurseryReport.updatedAt), ...props }); } @@ -77,7 +77,7 @@ export class NurseryReportLightDto extends EntityDto { submittedAt: Date | null; @ApiProperty({ nullable: true }) - taskId: number | null; + taskUuid: string | null; @ApiProperty() dueAt: Date | null; @@ -105,8 +105,8 @@ export class NurseryReportFullDto extends NurseryReportLightDto { ...pickApiProperties(nurseryReport, NurseryReportFullDto), lightResource: false, // these two are untyped and marked optional in the base model. - createdAt: nurseryReport.createdAt as Date, - updatedAt: nurseryReport.createdAt as Date, + createdAt: new Date(nurseryReport.createdAt), + updatedAt: new Date(nurseryReport.updatedAt), ...props }); } @@ -202,9 +202,6 @@ export class NurseryReportFullDto extends NurseryReportLightDto { }) projectUuid: string | null; - @ApiProperty({ nullable: true }) - taskId: number | null; - @ApiProperty({ nullable: true, description: "The associated task uuid" diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index 890d4d27..eeb6eda7 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -41,6 +41,14 @@ export class NurseryReportProcessor extends EntityProcessor< { association: "task", attributes: ["uuid"] + }, + { + association: "createdByUser", + attributes: ["id", "uuid", "firstName", "lastName"] + }, + { + association: "approvedByUser", + attributes: ["id", "uuid", "firstName", "lastName"] } ] }); @@ -90,7 +98,7 @@ export class NurseryReportProcessor extends EntityProcessor< projectUuid: "$nursery.project.uuid$" }; - for (const term of [ + const termsToFilter = [ "status", "updateRequestStatus", "frameworkKey", @@ -98,12 +106,14 @@ export class NurseryReportProcessor extends EntityProcessor< "organisationUuid", "country", "projectUuid" - ]) { + ]; + + termsToFilter.forEach(term => { + const field = associationFieldMap[term] ?? term; if (query[term] != null) { - const field = associationFieldMap[term] || term; builder.where({ [field]: query[term] }); } - } + }); if (query.search != null) { builder.where({ @@ -127,23 +137,17 @@ export class NurseryReportProcessor extends EntityProcessor< async addFullDto(document: DocumentBuilder, nurseryReport: NurseryReport): Promise { const nurseryReportId = nurseryReport.id; + const mediaCollection = await Media.nurseryReport(nurseryReportId).findAll(); const reportTitle = await this.getReportTitle(nurseryReport); const projectReportTitle = await this.getProjectReportTitle(nurseryReport); const readableCompletionStatus = await this.getReadableCompletionStatus(nurseryReport.completion); - const createdByUser = await User.findOne({ where: { id: nurseryReport?.createdBy } }); - const approvedByUser = await User.findOne({ where: { id: nurseryReport?.approvedBy } }); const migrated = nurseryReport.oldModel != null; const props: AdditionalNurseryReportFullProps = { reportTitle, projectReportTitle, readableCompletionStatus, - createdByUser, - approvedByUser, migrated, - ...(this.entitiesService.mapMediaCollection( - await Media.nurseryReport(nurseryReportId).findAll(), - NurseryReport.MEDIA - ) as NurseryReportMedia) + ...(this.entitiesService.mapMediaCollection(mediaCollection, NurseryReport.MEDIA) as NurseryReportMedia) }; document.addData(nurseryReport.uuid, new NurseryReportFullDto(nurseryReport, props)); diff --git a/apps/entity-service/src/entities/processors/nursery.processor.ts b/apps/entity-service/src/entities/processors/nursery.processor.ts index 9eb81fd0..ada79952 100644 --- a/apps/entity-service/src/entities/processors/nursery.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery.processor.ts @@ -75,10 +75,10 @@ export class NurseryProcessor extends EntityProcessor { @BelongsTo(() => User) user: User | null; + @BelongsTo(() => User, { foreignKey: "createdBy", as: "createdByUser" }) + createdByUser: User | null; + + @BelongsTo(() => User, { foreignKey: "approvedBy", as: "approvedByUser" }) + approvedByUser: User | null; + @BelongsTo(() => Task) task: Task | null; From 7dc344eaa3a4cf0e5545557c8e23eb2de0fa47f6 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 26 Mar 2025 12:31:43 -0400 Subject: [PATCH 36/41] [TM-1770] fix lint --- .../src/entities/processors/nursery-report.processor.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index eeb6eda7..eddcd65a 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -1,11 +1,4 @@ -import { - Media, - Nursery, - NurseryReport, - ProjectReport, - ProjectUser, - User -} from "@terramatch-microservices/database/entities"; +import { Media, Nursery, NurseryReport, ProjectReport, ProjectUser } from "@terramatch-microservices/database/entities"; import { EntityProcessor } from "./entity-processor"; import { EntityQueryDto } from "../dto/entity-query.dto"; import { DocumentBuilder } from "@terramatch-microservices/common/util"; From a6085f626bf95ff8ecfddf04d16198f6bb1b4723 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 26 Mar 2025 12:45:12 -0400 Subject: [PATCH 37/41] [TM-1770] remove unnecessary properties --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 445cb0bd..899b8890 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -178,15 +178,9 @@ export class NurseryReportFullDto extends NurseryReportLightDto { @ApiProperty({ nullable: true }) sharedDriveLink: string | null; - @ApiProperty({ nullable: true }) - createdBy: number | null; - @ApiProperty({ nullable: true }) createdByUser: User | null; - @ApiProperty({ nullable: true }) - approvedBy: number | null; - @ApiProperty({ nullable: true }) approvedByUser: User | null; From 8f4fb7cc179be479ef244b079d09df26bea72105 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 26 Mar 2025 22:31:56 -0400 Subject: [PATCH 38/41] [TM-1770] changes from feedback --- .../entity-service/src/entities/dto/nursery-report.dto.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 899b8890..93823eda 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -15,8 +15,8 @@ export class NurseryReportLightDto extends EntityDto { ...pickApiProperties(nurseryReport, NurseryReportLightDto), lightResource: true, // these two are untyped and marked optional in the base model. - createdAt: new Date(nurseryReport.createdAt), - updatedAt: new Date(nurseryReport.updatedAt), + createdAt: nurseryReport.createdAt as Date, + updatedAt: nurseryReport.updatedAt as Date, ...props }); } @@ -105,8 +105,8 @@ export class NurseryReportFullDto extends NurseryReportLightDto { ...pickApiProperties(nurseryReport, NurseryReportFullDto), lightResource: false, // these two are untyped and marked optional in the base model. - createdAt: new Date(nurseryReport.createdAt), - updatedAt: new Date(nurseryReport.updatedAt), + createdAt: nurseryReport.createdAt as Date, + updatedAt: nurseryReport.updatedAt as Date, ...props }); } From 537f0e39a3ff6c8d797aa2be4a399719401d9657 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 27 Mar 2025 10:10:21 -0400 Subject: [PATCH 39/41] [TM-1770] remove readable completion status property --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 4 ++-- .../src/entities/processors/nursery-report.processor.ts | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index 93823eda..b779b366 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -160,8 +160,8 @@ export class NurseryReportFullDto extends NurseryReportLightDto { @ApiProperty() nothingToReport: boolean; - @ApiProperty() - readableCompletionStatus: string; + @ApiProperty({ nullable: true }) + completion: number | null; @ApiProperty({ nullable: true }) title: string | null; diff --git a/apps/entity-service/src/entities/processors/nursery-report.processor.ts b/apps/entity-service/src/entities/processors/nursery-report.processor.ts index eddcd65a..653df405 100644 --- a/apps/entity-service/src/entities/processors/nursery-report.processor.ts +++ b/apps/entity-service/src/entities/processors/nursery-report.processor.ts @@ -133,12 +133,10 @@ export class NurseryReportProcessor extends EntityProcessor< const mediaCollection = await Media.nurseryReport(nurseryReportId).findAll(); const reportTitle = await this.getReportTitle(nurseryReport); const projectReportTitle = await this.getProjectReportTitle(nurseryReport); - const readableCompletionStatus = await this.getReadableCompletionStatus(nurseryReport.completion); const migrated = nurseryReport.oldModel != null; const props: AdditionalNurseryReportFullProps = { reportTitle, projectReportTitle, - readableCompletionStatus, migrated, ...(this.entitiesService.mapMediaCollection(mediaCollection, NurseryReport.MEDIA) as NurseryReportMedia) }; @@ -173,8 +171,4 @@ export class NurseryReportProcessor extends EntityProcessor< return this.getReportTitleBase(projectReport.dueAt, projectReport.title, projectReport.user?.locale ?? "en-GB"); } - - protected async getReadableCompletionStatus(completion: number) { - return completion === 0 ? "Not Started" : completion === 100 ? "Complete" : "Started"; - } } From 2293ec587dba0ecc19f5e19646e77d22733b31c6 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 27 Mar 2025 14:07:12 -0400 Subject: [PATCH 40/41] [TM-1770] add properties to send the first and lastName --- .../src/entities/dto/nursery-report.dto.ts | 10 ++++++++-- .../src/lib/entities/nursery-report.entity.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index b779b366..c38017a9 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -179,10 +179,16 @@ export class NurseryReportFullDto extends NurseryReportLightDto { sharedDriveLink: string | null; @ApiProperty({ nullable: true }) - createdByUser: User | null; + createdByFirstName: string | null; @ApiProperty({ nullable: true }) - approvedByUser: User | null; + createdByLastName: string | null; + + @ApiProperty({ nullable: true }) + approvedByFirstName: string | null; + + @ApiProperty({ nullable: true }) + approvedByLastName: string | null; @ApiProperty({ nullable: true, diff --git a/libs/database/src/lib/entities/nursery-report.entity.ts b/libs/database/src/lib/entities/nursery-report.entity.ts index e158c72c..3288100f 100644 --- a/libs/database/src/lib/entities/nursery-report.entity.ts +++ b/libs/database/src/lib/entities/nursery-report.entity.ts @@ -134,6 +134,22 @@ export class NurseryReport extends Model { return this.task?.uuid; } + get createdByFirstName() { + return this.createdByUser?.firstName; + } + + get createdByLastName() { + return this.createdByUser?.lastName; + } + + get approvedByFirstName() { + return this.approvedByUser?.firstName; + } + + get approvedByLastName() { + return this.approvedByUser?.lastName; + } + @ForeignKey(() => Task) @AllowNull @Column(BIGINT.UNSIGNED) From 93f91ed1ae831ae6fbfbe3b07e36bd947fff8698 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 27 Mar 2025 14:32:20 -0400 Subject: [PATCH 41/41] [TM-1770] fix lint --- apps/entity-service/src/entities/dto/nursery-report.dto.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/entity-service/src/entities/dto/nursery-report.dto.ts b/apps/entity-service/src/entities/dto/nursery-report.dto.ts index c38017a9..08268970 100644 --- a/apps/entity-service/src/entities/dto/nursery-report.dto.ts +++ b/apps/entity-service/src/entities/dto/nursery-report.dto.ts @@ -4,7 +4,6 @@ import { pickApiProperties } from "@terramatch-microservices/common/dto/json-api import { JsonApiDto } from "@terramatch-microservices/common/decorators/json-api-dto.decorator"; import { ApiProperty } from "@nestjs/swagger"; import { MediaDto } from "./media.dto"; -import { User } from "@sentry/nestjs"; @JsonApiDto({ type: "nurseryReports" }) export class NurseryReportLightDto extends EntityDto {