diff --git a/CHANGELOG.md b/CHANGELOG.md index c29eaf0907..aab8531b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. The format - Fixed: - optional fields not being marked as optional in frontend client (missing '?' indicator) ([#1470](https://github.com/bloom-housing/bloom/pull/1470)) + - add duplicates to CSV export ([#1352](https://github.com/bloom-housing/bloom/issues/1352)) - Changed: - User module has been removed and incorporated into Auth module diff --git a/backend/core/src/applications/applications.controller.ts b/backend/core/src/applications/applications.controller.ts index 251b1f231d..44f8dabe65 100644 --- a/backend/core/src/applications/applications.controller.ts +++ b/backend/core/src/applications/applications.controller.ts @@ -203,7 +203,7 @@ export class ApplicationsController { @ApiOperation({ summary: "List applications as csv", operationId: "listAsCsv" }) @Header("Content-Type", "text/csv") async listAsCsv(@Query() queryParams: ApplicationsCsvListQueryParams): Promise { - const applications = await this.applicationsService.list(queryParams) + const applications = await this.applicationsService.listWithFlagged(queryParams) const listing = await this.listingsService.findOne(queryParams.listingId) return this.applicationCsvExporter.export( applications, diff --git a/backend/core/src/applications/applications.service.ts b/backend/core/src/applications/applications.service.ts index 37cf78de2e..e9b4a03688 100644 --- a/backend/core/src/applications/applications.service.ts +++ b/backend/core/src/applications/applications.service.ts @@ -43,6 +43,39 @@ export class ApplicationsService { return result } + public async listWithFlagged(params: PaginatedApplicationListQueryParams) { + const qb = this._getQb(params) + const result = await qb.getMany() + + // Get flagged applications + const flaggedQuery = await this.repository + .createQueryBuilder("applications") + .leftJoin( + "application_flagged_set_applications_applications", + "application_flagged_set_applications_applications", + "application_flagged_set_applications_applications.applications_id = applications.id" + ) + .andWhere("applications.listing_id = :lid", { lid: params.listingId }) + .select( + "applications.id, count(application_flagged_set_applications_applications.applications_id) > 0 as flagged" + ) + .groupBy("applications.id") + .getRawAndEntities() + + // Reorganize flagged to object to make it faster to map + const flagged = flaggedQuery.raw.reduce((obj, application) => { + return { ...obj, [application.id]: application.flagged } + }, {}) + await Promise.all( + result.map(async (application) => { + // Because TypeOrm can't map extra flagged field we need to map it manually + application.flagged = flagged[application.id] + await this.authorizeUserAction(this.req.user, application, authzActions.read) + }) + ) + return result + } + async listPaginated( params: PaginatedApplicationListQueryParams ): Promise> { diff --git a/backend/core/src/applications/dto/application.dto.ts b/backend/core/src/applications/dto/application.dto.ts index 6d677b3c8b..41c51b2aaa 100644 --- a/backend/core/src/applications/dto/application.dto.ts +++ b/backend/core/src/applications/dto/application.dto.ts @@ -43,6 +43,7 @@ export class ApplicationDto extends OmitType(Application, [ "accessibility", "demographics", "householdMembers", + "flagged", ] as const) { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend/core/src/applications/entities/application.entity.ts b/backend/core/src/applications/entities/application.entity.ts index 94e322e77f..3c78ba1962 100644 --- a/backend/core/src/applications/entities/application.entity.ts +++ b/backend/core/src/applications/entities/application.entity.ts @@ -242,4 +242,10 @@ export class Application extends AbstractEntity { @Expose() @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) markedAsDuplicate: boolean + + // This is a 'virtual field' needed for CSV export + @Expose() + @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) + @IsOptional({ groups: [ValidationsGroupsEnum.partners] }) + flagged?: boolean } diff --git a/backend/core/src/csv/formatting/formatters.ts b/backend/core/src/csv/formatting/formatters.ts index a5349aef22..a8de5e0fb9 100644 --- a/backend/core/src/csv/formatting/formatters.ts +++ b/backend/core/src/csv/formatting/formatters.ts @@ -608,3 +608,19 @@ export const formatHOPWAPreference = { ) }, } + +export const formatMarkedAsDuplicate = { + label: "Marked as duplicate", + discriminator: "", + formatter: (application: Application) => { + return booleanFormatter(application.markedAsDuplicate) + }, +} + +export const formatFlagged = { + label: "Flagged", + discriminator: "", + formatter: (application: Application) => { + return booleanFormatter(application.flagged) + }, +} diff --git a/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts b/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts index ef6bc495b5..761aef8e07 100644 --- a/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts +++ b/backend/core/src/csv/formatting/metadata/basic-formatting-metadata.ts @@ -44,6 +44,8 @@ import { formatRequestUnitType, formatVouchersOrSubsidies, formatWorkPreference, + formatMarkedAsDuplicate, + formatFlagged, } from "../formatters" export const basicFormattingMetadata = [ @@ -92,4 +94,6 @@ export const basicFormattingMetadata = [ formatWorkPreference, formatHouseholdSize, formatHoueholdMembers, + formatMarkedAsDuplicate, + formatFlagged, ] diff --git a/backend/core/test/applications/applications.e2e-spec.ts b/backend/core/test/applications/applications.e2e-spec.ts index f0898b166d..02608ed55f 100644 --- a/backend/core/test/applications/applications.e2e-spec.ts +++ b/backend/core/test/applications/applications.e2e-spec.ts @@ -416,7 +416,8 @@ describe("Applications", () => { .get(`/applications/csv/?includeHeaders=true&listingId=${listing1Id}`) .set(...setAuthorization(adminAccessToken)) .expect(200) - expect(typeof res.body === "string") + expect(typeof res.text === "string") + expect(new RegExp(/Flagged/).test(res.text)).toEqual(true) }) it(`should allow an admin to delete user's applications`, async () => {