From 85cb4da406b3d40f46eff7ac48ae8d0b5db7b453 Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Mon, 17 Oct 2022 10:57:42 -0500 Subject: [PATCH] feat: backend changes for what to expect changes (#508) * fix: update email confirmation what to expect copy * fix: email tests (#3095) * feat: new listing type field * 3032/new what to expect core (#3087) * feat: bring over What to Expect and update Markdown styles * feat: show dedicated content on the What to Expect form step * feat: add additional text to terms and confirmation * feat: update form conditionals to use waitlist enum * fix: remove stray import * fix: typos and improved switch statements * fix: confirmation text and remove lottery date * fix: add back in eligibility translations (#3134) * fix: add back in detail page changes (#510) * fix: remove check for whatToExpect in cypress test Co-authored-by: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com> Co-authored-by: Jared White --- backend/core/src/email/email.service.spec.ts | 5 +- backend/core/src/email/email.service.ts | 31 ++-- .../dto/listing-published-create.dto.ts | 6 - .../dto/listing-published-update.dto.ts | 6 - .../src/listings/entities/listing.entity.ts | 11 -- backend/core/src/listings/listings.service.ts | 6 +- .../types/listing-availability-enum.ts | 4 - .../types/listing-review-order-enum.ts | 1 + backend/core/src/listings/views/config.ts | 1 - ...106-new-confirmation-email-translations.ts | 40 +++++ .../1663959354563-new-listing-type-enum.ts | 61 +++++++ .../1665528174645-email-reset-translations.ts | 40 +++++ .../seeds/listings/listing-coliseum-seed.ts | 4 +- .../seeds/listings/listing-triton-seed.ts | 2 - .../core/src/seeder/seeds/listings/shared.ts | 3 +- .../core/src/shared/units-transformations.ts | 4 +- .../core/src/shared/views/confirmation.hbs | 9 +- .../services/translations.service.ts | 1 + backend/core/types/src/backend-swagger.ts | 15 +- shared-helpers/src/summaryTables.tsx | 12 +- .../sections/DetailUnits.test.tsx | 8 +- sites/partners/__tests__/testHelpers.ts | 2 - .../cypress/integration/03-listing.spec.ts | 1 - .../sections/DetailRankingsAndResults.tsx | 52 +++--- .../sections/DetailUnits.tsx | 6 +- .../listings/PaperListingForm/formTypes.ts | 3 +- .../formatters/AdditionalMetadataFormatter.ts | 9 +- .../sections/RankingsAndResults.tsx | 52 +++--- .../PaperListingForm/sections/Units.tsx | 7 +- sites/public/lib/helpers.tsx | 11 +- .../applications/review/confirmation.tsx | 39 ++--- .../pages/applications/review/terms.tsx | 58 +++++-- .../applications/start/what-to-expect.tsx | 66 ++++++- sites/public/src/ListingView.tsx | 15 +- ui-components/src/global/markdown.scss | 162 +++++++++++++++--- ui-components/src/locales/general.json | 22 ++- 36 files changed, 556 insertions(+), 219 deletions(-) delete mode 100644 backend/core/src/listings/types/listing-availability-enum.ts create mode 100644 backend/core/src/migration/1663104141106-new-confirmation-email-translations.ts create mode 100644 backend/core/src/migration/1663959354563-new-listing-type-enum.ts create mode 100644 backend/core/src/migration/1665528174645-email-reset-translations.ts diff --git a/backend/core/src/email/email.service.spec.ts b/backend/core/src/email/email.service.spec.ts index 47d9c715ac..3084f1f808 100644 --- a/backend/core/src/email/email.service.spec.ts +++ b/backend/core/src/email/email.service.spec.ts @@ -94,7 +94,6 @@ const translationServiceMock = { readHowYouCanPrepare: "Read about how you can prepare for next steps", needToMakeUpdates: "Need to make updates?", ifYouNeedToUpdateInformation: "", - shouldBeChosen: "Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", subject: "Your Application Confirmation", @@ -259,7 +258,7 @@ describe("EmailService", () => { expect(emailMock.html).toMatch("Your Confirmation Number") expect(emailMock.html).toMatch("Marisela Baca") expect(emailMock.html).toMatch( - /If you are contacted for an interview, you will need to fill out a more detailed application and provide supporting documents./ + /Eligible applicants will be contacted on a first come first serve basis until vacancies are filled./ ) expect(emailMock.html).toMatch(/http:\/\/localhost:3000\/listing\/Uvbk5qurpB2WI9V6WnNdH/) // contains application id @@ -279,7 +278,7 @@ describe("EmailService", () => { const emailMock = sendMock.mock.calls[0][0] expect(emailMock.html).toMatch( - /Eligible applicants will be placed in order based on preference and lottery rank<\/strong>./ + /Once the application period closes, eligible applicants will be placed in order based on lottery rank order./ ) }) diff --git a/backend/core/src/email/email.service.ts b/backend/core/src/email/email.service.ts index 548df90bba..c2f8aed885 100644 --- a/backend/core/src/email/email.service.ts +++ b/backend/core/src/email/email.service.ts @@ -132,7 +132,6 @@ export class EmailService { public async confirmation(listing: Listing, application: Application, appUrl: string) { const jurisdiction = await this.getListingJurisdiction(listing) void (await this.loadTranslations(jurisdiction, application.language || Language.en)) - let eligibleApplicantsText const listingUrl = `${appUrl}/listing/${listing.id}` const compiledTemplate = this.template("confirmation") @@ -142,22 +141,31 @@ export class EmailService { ) } + let eligibleText + let preferenceText + let contactText = null + if (listing.reviewOrderType === ListingReviewOrder.firstComeFirstServe) { + eligibleText = this.polyglot.t("confirmation.eligible.fcfs") + preferenceText = this.polyglot.t("confirmation.eligible.fcfsPreference") + } if (listing.reviewOrderType === ListingReviewOrder.lottery) { - eligibleApplicantsText = new Handlebars.SafeString( - this.polyglot.t("confirmation.eligibleApplicants.lottery") - ) - } else { - // for when listing.reviewOrderType === ListingReviewOrder.firstComeFirstServe - eligibleApplicantsText = new Handlebars.SafeString( - this.polyglot.t("confirmation.eligibleApplicants.FCFS") - ) + eligibleText = this.polyglot.t("confirmation.eligible.lottery") + preferenceText = this.polyglot.t("confirmation.eligible.lotteryPreference") + } + if (listing.reviewOrderType === ListingReviewOrder.waitlist) { + eligibleText = this.polyglot.t("confirmation.eligible.waitlist") + contactText = this.polyglot.t("confirmation.eligible.waitlistContact") + preferenceText = this.polyglot.t("confirmation.eligible.waitlistPreference") } + const user = { firstName: application.applicant.firstName, middleName: application.applicant.middleName, lastName: application.applicant.lastName, } + const nextStepsUrl = this.polyglot.t("confirmation.nextStepsUrl") + await this.send( application.applicant.emailAddress, jurisdiction.emailFromAddress, @@ -171,7 +179,10 @@ export class EmailService { listing, listingUrl, application, - eligibleApplicantsText, + preferenceText, + interviewText: this.polyglot.t("confirmation.interview"), + eligibleText, + contactText, nextStepsUrl: nextStepsUrl != "confirmation.nextStepsUrl" ? nextStepsUrl : null, user, }) diff --git a/backend/core/src/listings/dto/listing-published-create.dto.ts b/backend/core/src/listings/dto/listing-published-create.dto.ts index 6612e00797..e26e67fe66 100644 --- a/backend/core/src/listings/dto/listing-published-create.dto.ts +++ b/backend/core/src/listings/dto/listing-published-create.dto.ts @@ -20,7 +20,6 @@ import { OmitType } from "@nestjs/swagger" import { UnitCreateDto } from "../../units/dto/unit-create.dto" import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" import { ListingImageUpdateDto } from "./listing-image-update.dto" -import { ListingAvailability } from "../types/listing-availability-enum" export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ "assets", @@ -39,7 +38,6 @@ export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ "rentalAssistance", "reviewOrderType", "units", - "listingAvailability", ] as const) { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @@ -77,10 +75,6 @@ export class ListingPublishedCreateDto extends OmitType(ListingCreateDto, [ @Type(() => ListingImageUpdateDto) images: ListingImageUpdateDto[] - @Expose() - @IsEnum(ListingAvailability, { groups: [ValidationsGroupsEnum.default] }) - listingAvailability: ListingAvailability | null - @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() diff --git a/backend/core/src/listings/dto/listing-published-update.dto.ts b/backend/core/src/listings/dto/listing-published-update.dto.ts index 0f8430f8fa..768413a8c3 100644 --- a/backend/core/src/listings/dto/listing-published-update.dto.ts +++ b/backend/core/src/listings/dto/listing-published-update.dto.ts @@ -20,7 +20,6 @@ import { AssetUpdateDto } from "../../assets/dto/asset.dto" import { UnitUpdateDto } from "../../units/dto/unit-update.dto" import { EnforceLowerCase } from "../../shared/decorators/enforceLowerCase.decorator" import { ListingImageUpdateDto } from "./listing-image-update.dto" -import { ListingAvailability } from "../types/listing-availability-enum" export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ "assets", @@ -39,7 +38,6 @@ export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ "rentalAssistance", "reviewOrderType", "units", - "listingAvailability", ] as const) { @Expose() @ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true }) @@ -77,10 +75,6 @@ export class ListingPublishedUpdateDto extends OmitType(ListingUpdateDto, [ @Type(() => ListingImageUpdateDto) images: ListingImageUpdateDto[] - @Expose() - @IsEnum(ListingAvailability, { groups: [ValidationsGroupsEnum.default] }) - listingAvailability: ListingAvailability | null - @Expose() @IsEmail({}, { groups: [ValidationsGroupsEnum.default] }) @EnforceLowerCase() diff --git a/backend/core/src/listings/entities/listing.entity.ts b/backend/core/src/listings/entities/listing.entity.ts index b707addeff..becf9b3e7d 100644 --- a/backend/core/src/listings/entities/listing.entity.ts +++ b/backend/core/src/listings/entities/listing.entity.ts @@ -43,7 +43,6 @@ import { ApplicationMethod } from "../../application-methods/entities/applicatio import { UnitsSummarized } from "../../units/types/units-summarized" import { UnitsSummary } from "../../units-summary/entities/units-summary.entity" import { ListingReviewOrder } from "../types/listing-review-order-enum" -import { ListingAvailability } from "../types/listing-availability-enum" import { ApplicationMethodDto } from "../../application-methods/dto/application-method.dto" import { ApplicationMethodType } from "../../application-methods/types/application-method-type-enum" import { ListingFeatures } from "./listing-features.entity" @@ -520,16 +519,6 @@ class Listing extends BaseEntity { }) reviewOrderType?: ListingReviewOrder | null - @Column({ type: "enum", enum: ListingAvailability, nullable: true }) - @Expose() - @IsOptional({ groups: [ValidationsGroupsEnum.default] }) - @IsEnum(ListingAvailability, { groups: [ValidationsGroupsEnum.default] }) - @ApiProperty({ - enum: ListingAvailability, - enumName: "ListingAvailability", - }) - listingAvailability?: ListingAvailability | null - @Expose() applicationConfig?: Record diff --git a/backend/core/src/listings/listings.service.ts b/backend/core/src/listings/listings.service.ts index 92824065ea..5a019c8bb2 100644 --- a/backend/core/src/listings/listings.service.ts +++ b/backend/core/src/listings/listings.service.ts @@ -6,7 +6,7 @@ import { Interval } from "@nestjs/schedule" import { Listing } from "./entities/listing.entity" import { getView } from "./views/view" import { summarizeUnits, summarizeUnitsByTypeAndRent } from "../shared/units-transformations" -import { Language, ListingAvailability } from "../../types" +import { Language, ListingReviewOrder } from "../../types" import { AmiChart } from "../ami-charts/entities/ami-chart.entity" import { ListingCreateDto } from "./dto/listing-create.dto" import { ListingUpdateDto } from "./dto/listing-update.dto" @@ -111,9 +111,7 @@ export class ListingsService { await this.authorizeUserActionForListingId(this.req.user, listing.id, authzActions.update) const availableUnits = - listingDto.listingAvailability === ListingAvailability.availableUnits - ? listingDto.units.length - : 0 + listingDto.reviewOrderType !== ListingReviewOrder.waitlist ? listingDto.units.length : 0 listingDto.units.forEach((unit) => { if (!unit.id) { delete unit.id diff --git a/backend/core/src/listings/types/listing-availability-enum.ts b/backend/core/src/listings/types/listing-availability-enum.ts deleted file mode 100644 index 8deb357947..0000000000 --- a/backend/core/src/listings/types/listing-availability-enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ListingAvailability { - availableUnits = "availableUnits", - openWaitlist = "openWaitlist", -} diff --git a/backend/core/src/listings/types/listing-review-order-enum.ts b/backend/core/src/listings/types/listing-review-order-enum.ts index 41c0b46279..b1cbbd181c 100644 --- a/backend/core/src/listings/types/listing-review-order-enum.ts +++ b/backend/core/src/listings/types/listing-review-order-enum.ts @@ -1,4 +1,5 @@ export enum ListingReviewOrder { lottery = "lottery", firstComeFirstServe = "firstComeFirstServe", + waitlist = "waitlist", } diff --git a/backend/core/src/listings/views/config.ts b/backend/core/src/listings/views/config.ts index d7422e28e1..bc9d4a3a7c 100644 --- a/backend/core/src/listings/views/config.ts +++ b/backend/core/src/listings/views/config.ts @@ -52,7 +52,6 @@ const views: Views = { "listingImagesImage.id", "listingImagesImage.fileId", "listingImagesImage.label", - "listings.listingAvailability", "utilities.id", "utilities.water", "utilities.gas", diff --git a/backend/core/src/migration/1663104141106-new-confirmation-email-translations.ts b/backend/core/src/migration/1663104141106-new-confirmation-email-translations.ts new file mode 100644 index 0000000000..74c6031cad --- /dev/null +++ b/backend/core/src/migration/1663104141106-new-confirmation-email-translations.ts @@ -0,0 +1,40 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class newConfirmationEmailTranslations1663104141106 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + let generalTranslation = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id IS NULL AND language = ($1)`, + [Language.en] + ) + + generalTranslation = generalTranslation["0"]["translations"] + + generalTranslation.confirmation = { + ...generalTranslation.confirmation, + eligible: { + fcfs: + "Eligibile applicants will be contacted on a first come first serve basis until vacancies are filled.", + fcfsPreference: + "Housing preferences, if applicable, will affect first come first serve order.", + lottery: + "Once the application period closes, eligible applicants will be placed in order based on lottery rank order.", + lotteryPreference: "Housing preferences, if applicable, will affect lottery rank order.", + waitlist: + "Eligibile applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.", + waitlistPreference: "Housing preferences, if applicable, will affect waitlist order.", + waitlistContact: + "You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.", + }, + interview: + "If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.", + } + + await queryRunner.query( + `UPDATE "translations" SET translations = ($1) where jurisdiction_id IS NULL and language = ($2)`, + [generalTranslation, Language.en] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/migration/1663959354563-new-listing-type-enum.ts b/backend/core/src/migration/1663959354563-new-listing-type-enum.ts new file mode 100644 index 0000000000..33255ec488 --- /dev/null +++ b/backend/core/src/migration/1663959354563-new-listing-type-enum.ts @@ -0,0 +1,61 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class newListingTypeEnum1663959354563 implements MigrationInterface { + name = "newListingTypeEnum1663959354563" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."listings_review_order_type_enum" RENAME TO "listings_review_order_type_enum_old"` + ) + await queryRunner.query( + `CREATE TYPE "public"."listings_review_order_type_enum" AS ENUM('lottery', 'firstComeFirstServe', 'waitlist')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "review_order_type" TYPE "public"."listings_review_order_type_enum" USING "review_order_type"::"text"::"public"."listings_review_order_type_enum"` + ) + await queryRunner.query(`DROP TYPE "public"."listings_review_order_type_enum_old"`) + + const listings = await queryRunner.query(`SELECT id, listing_availability FROM listings`) + + for (const l of listings) { + if (l.listing_availability === "openWaitlist") { + await queryRunner.query(`UPDATE listings SET review_order_type = ($1) WHERE id = ($2)`, [ + "waitlist", + l.id, + ]) + } + } + await queryRunner.query(`ALTER TABLE "listings" DROP COLUMN "listing_availability"`) + await queryRunner.query(`DROP TYPE "public"."listings_listing_availability_enum"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "listings" ADD "listing_availability" "public"."listings_listing_availability_enum"` + ) + await queryRunner.query( + `CREATE TYPE "public"."listings_listing_availability_enum" AS ENUM('availableUnits', 'openWaitlist')` + ) + const listings = await queryRunner.query(`SELECT id, review_order_type FROM listings`) + + for (const l of listings) { + if (l.review_order_type === "waitlist") { + await queryRunner.query(`UPDATE listings SET listing_availability = ($1) WHERE id = ($2)`, [ + "openWaitlist", + l.id, + ]) + } + } + + await queryRunner.query( + `CREATE TYPE "public"."listings_review_order_type_enum_old" AS ENUM('lottery', 'firstComeFirstServe')` + ) + await queryRunner.query( + `ALTER TABLE "listings" ALTER COLUMN "review_order_type" TYPE "public"."listings_review_order_type_enum_old" USING "review_order_type"::"text"::"public"."listings_review_order_type_enum_old"` + ) + await queryRunner.query(`DROP TYPE "public"."listings_review_order_type_enum"`) + await queryRunner.query( + `ALTER TYPE "public"."listings_review_order_type_enum_old" RENAME TO "listings_review_order_type_enum"` + ) + } +} diff --git a/backend/core/src/migration/1665528174645-email-reset-translations.ts b/backend/core/src/migration/1665528174645-email-reset-translations.ts new file mode 100644 index 0000000000..8e204129b2 --- /dev/null +++ b/backend/core/src/migration/1665528174645-email-reset-translations.ts @@ -0,0 +1,40 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { Language } from "../shared/types/language-enum" + +export class emailResetTranslations1665528174645 implements MigrationInterface { + name = "emailResetTranslations1665528174645" + + public async up(queryRunner: QueryRunner): Promise { + let generalTranslation = await queryRunner.query( + `SELECT translations FROM translations WHERE jurisdiction_id IS NULL AND language = ($1)`, + [Language.en] + ) + + generalTranslation = generalTranslation["0"]["translations"] + + generalTranslation.confirmation = { + ...generalTranslation.confirmation, + eligible: { + fcfs: + "Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.", + waitlist: + "Eligible applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.", + lottery: + "Once the application period closes, eligible applicants will be placed in order based on lottery rank order.", + fcfsPreference: + "Housing preferences, if applicable, will affect first come first serve order.", + waitlistContact: + "You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.", + lotteryPreference: "Housing preferences, if applicable, will affect lottery rank order.", + waitlistPreference: "Housing preferences, if applicable, will affect waitlist order.", + }, + } + + await queryRunner.query( + `UPDATE "translations" SET translations = ($1) where jurisdiction_id IS NULL and language = ($2)`, + [generalTranslation, Language.en] + ) + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts index 8ca6ffe556..0b72427e62 100644 --- a/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-coliseum-seed.ts @@ -17,7 +17,6 @@ import { ListingReviewOrder } from "../../../listings/types/listing-review-order import { ListingStatus } from "../../../listings/types/listing-status-enum" import { UnitCreateDto } from "../../../units/dto/unit-create.dto" import { Listing } from "../../../listings/entities/listing.entity" -import { ListingAvailability } from "../../../listings/types/listing-availability-enum" const coliseumListing: ListingSeedType = { jurisdictionName: "Alameda", @@ -106,8 +105,7 @@ const coliseumListing: ListingSeedType = { waitlistMaxSize: 3000, waitlistOpenSpots: 3000, isWaitlistOpen: true, - whatToExpect: "Custom what to expect text", - listingAvailability: ListingAvailability.availableUnits, + whatToExpect: null, utilities: { water: false, gas: false, diff --git a/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts b/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts index a81f023fb4..22daef85b9 100644 --- a/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts +++ b/backend/core/src/seeder/seeds/listings/listing-triton-seed.ts @@ -7,7 +7,6 @@ import { ListingReviewOrder } from "../../../listings/types/listing-review-order import { ListingStatus } from "../../../listings/types/listing-status-enum" import { UnitCreateDto } from "../../../units/dto/unit-create.dto" import { Listing } from "../../../listings/entities/listing.entity" -import { ListingAvailability } from "../../../listings/types/listing-availability-enum" import { classToClass } from "class-transformer" import dayjs from "dayjs" @@ -96,7 +95,6 @@ const tritonListing: ListingSeedType = { waitlistOpenSpots: 200, isWaitlistOpen: true, whatToExpect: null, - listingAvailability: ListingAvailability.availableUnits, utilities: { water: true, gas: true, diff --git a/backend/core/src/seeder/seeds/listings/shared.ts b/backend/core/src/seeder/seeds/listings/shared.ts index bdc2d919bd..fa9f37e85e 100644 --- a/backend/core/src/seeder/seeds/listings/shared.ts +++ b/backend/core/src/seeder/seeds/listings/shared.ts @@ -13,7 +13,6 @@ import { UserCreateDto } from "../../../auth/dto/user-create.dto" import { CountyCode } from "../../../shared/types/county-code" import { ListingReviewOrder } from "../../../listings/types/listing-review-order-enum" import { ListingStatus } from "../../../listings/types/listing-status-enum" -import { ListingAvailability } from "../../../listings/types/listing-availability-enum" import { ApplicationSection } from "../../../multiselect-question/types/multiselect-application-section-enum" export const getDate = (days: number) => { const someDate = new Date() @@ -219,7 +218,7 @@ export const defaultListing: ListingSeedType = { waitlistOpenSpots: null, isWaitlistOpen: false, waitlistMaxSize: null, - listingAvailability: ListingAvailability.availableUnits, + whatToExpect: "Custom what to expect text", } // Preferences diff --git a/backend/core/src/shared/units-transformations.ts b/backend/core/src/shared/units-transformations.ts index 53b088f5fd..b0fdd7a5e8 100644 --- a/backend/core/src/shared/units-transformations.ts +++ b/backend/core/src/shared/units-transformations.ts @@ -10,7 +10,7 @@ import { AmiChart } from "../ami-charts/entities/ami-chart.entity" import { AmiChartItem } from "../ami-charts/entities/ami-chart-item.entity" import { UnitAmiChartOverride } from "../units/entities/unit-ami-chart-override.entity" import { Listing } from "../listings/entities/listing.entity" -import { ListingAvailability } from "../listings/types/listing-availability-enum" +import { ListingReviewOrder } from "../listings/types/listing-review-order-enum" export type AnyDict = { [key: string]: unknown } type Units = Unit[] @@ -335,7 +335,7 @@ export const summarizeUnitsByTypeAndRent = (units: Units, listing: Listing): Uni const finalSummary = unitMap[key].reduce((summary, unit, index) => { return getUnitsSummary(unit, index === 0 ? null : summary) }, {} as UnitSummary) - if (listing.listingAvailability === ListingAvailability.availableUnits) { + if (listing.reviewOrderType !== ListingReviewOrder.waitlist) { finalSummary.totalAvailable = unitMap[key].length } summaries.push(finalSummary) diff --git a/backend/core/src/shared/views/confirmation.hbs b/backend/core/src/shared/views/confirmation.hbs index 7b6ec0bfcf..fb779769de 100644 --- a/backend/core/src/shared/views/confirmation.hbs +++ b/backend/core/src/shared/views/confirmation.hbs @@ -29,9 +29,12 @@

{{t "confirmation.whatHappensNext"}}

    -
  • {{t "confirmation.applicationPeriodCloses"}}
  • -
  • {{eligibleApplicantsText}}
  • -
  • {{t "confirmation.contactedForAnInterview"}}
  • +
  • {{eligibleText}}
  • +
  • {{preferenceText}}
  • +
  • {{interviewText}}
  • + {{#if contactText}} +
  • {{contactText}}
  • + {{/if}}
{{#if nextStepsUrl}} diff --git a/backend/core/src/translations/services/translations.service.ts b/backend/core/src/translations/services/translations.service.ts index 4dd75b3d95..887b2da666 100644 --- a/backend/core/src/translations/services/translations.service.ts +++ b/backend/core/src/translations/services/translations.service.ts @@ -88,6 +88,7 @@ export class TranslationsService extends AbstractServiceFactory< "servicesOffered", "smokingPolicy", "unitAmenities", + "whatToExpect", ] for (let i = 0; i < listing.events.length; i++) { diff --git a/backend/core/types/src/backend-swagger.ts b/backend/core/types/src/backend-swagger.ts index 59e244abe6..fcca925606 100644 --- a/backend/core/types/src/backend-swagger.ts +++ b/backend/core/types/src/backend-swagger.ts @@ -4820,9 +4820,6 @@ export interface Listing { /** */ reviewOrderType?: ListingReviewOrder - /** */ - listingAvailability?: ListingAvailability - /** */ showWaitlist: boolean @@ -5259,9 +5256,6 @@ export interface ListingCreate { /** */ reviewOrderType?: ListingReviewOrder - /** */ - listingAvailability?: ListingAvailability - /** */ applicationMethods: ApplicationMethodCreate[] @@ -5659,9 +5653,6 @@ export interface ListingUpdate { /** */ reviewOrderType?: ListingReviewOrder - /** */ - listingAvailability?: ListingAvailability - /** */ id?: string @@ -6238,11 +6229,7 @@ export enum ListingStatus { export enum ListingReviewOrder { "lottery" = "lottery", "firstComeFirstServe" = "firstComeFirstServe", -} - -export enum ListingAvailability { - "availableUnits" = "availableUnits", - "openWaitlist" = "openWaitlist", + "waitlist" = "waitlist", } export enum ListingEventType { diff --git a/shared-helpers/src/summaryTables.tsx b/shared-helpers/src/summaryTables.tsx index d0086d2986..1af2728576 100644 --- a/shared-helpers/src/summaryTables.tsx +++ b/shared-helpers/src/summaryTables.tsx @@ -7,7 +7,7 @@ import { ContentAccordion, getTranslationWithArguments, } from "@bloom-housing/ui-components" -import { MinMax, UnitSummary, Unit, ListingAvailability } from "@bloom-housing/backend-core/types" +import { MinMax, UnitSummary, Unit, ListingReviewOrder } from "@bloom-housing/backend-core/types" const getTranslationFromCurrencyString = (value: string) => { if (value.startsWith("t.")) return getTranslationWithArguments(value) @@ -16,7 +16,7 @@ const getTranslationFromCurrencyString = (value: string) => { export const unitSummariesTable = ( summaries: UnitSummary[], - listingAvailability: ListingAvailability + listingReviewOrder: ListingReviewOrder ): StandardTableData => { const unitSummaries = summaries?.map((unitSummary) => { const unitPluralization = @@ -63,7 +63,7 @@ export const unitSummariesTable = ( : getRent(unitSummary.rentRange.min, unitSummary.rentRange.max) let availability = null - if (listingAvailability === ListingAvailability.availableUnits) { + if (listingReviewOrder !== ListingReviewOrder.waitlist) { availability = ( {unitSummary.totalAvailable > 0 ? ( @@ -77,7 +77,7 @@ export const unitSummariesTable = ( )} ) - } else if (listingAvailability === ListingAvailability.openWaitlist) { + } else if (listingReviewOrder === ListingReviewOrder.waitlist) { availability = ( {t("listings.waitlist.open")} @@ -104,12 +104,12 @@ export const unitSummariesTable = ( export const getSummariesTable = ( summaries: UnitSummary[], - listingAvailability: ListingAvailability + listingReviewOrder: ListingReviewOrder ): StandardTableData => { let unitSummaries: StandardTableData = [] if (summaries?.length > 0) { - unitSummaries = unitSummariesTable(summaries, listingAvailability) + unitSummaries = unitSummariesTable(summaries, listingReviewOrder) } return unitSummaries } diff --git a/sites/partners/__tests__/PaperListingForm/sections/DetailUnits.test.tsx b/sites/partners/__tests__/PaperListingForm/sections/DetailUnits.test.tsx index f8b85b1a82..8a75605b0d 100644 --- a/sites/partners/__tests__/PaperListingForm/sections/DetailUnits.test.tsx +++ b/sites/partners/__tests__/PaperListingForm/sections/DetailUnits.test.tsx @@ -3,12 +3,14 @@ import { fireEvent, render, within } from "@testing-library/react" import { DetailUnits } from "../../../src/listings/PaperListingDetails/sections/DetailUnits" import { ListingContext } from "../../../src/listings/ListingContext" import { listing, unit } from "../../testHelpers" -import { ListingAvailability } from "@bloom-housing/backend-core" +import { ListingReviewOrder } from "@bloom-housing/backend-core" describe("DetailUnits", () => { it("should render the detail units when no units exist", () => { const results = render( - + ) @@ -32,7 +34,7 @@ describe("DetailUnits", () => { diff --git a/sites/partners/__tests__/testHelpers.ts b/sites/partners/__tests__/testHelpers.ts index 3da65c5184..6bce34e86a 100644 --- a/sites/partners/__tests__/testHelpers.ts +++ b/sites/partners/__tests__/testHelpers.ts @@ -1,7 +1,6 @@ import { ApplicationSection, Listing, - ListingAvailability, ListingReviewOrder, ListingStatus, MultiselectQuestion, @@ -98,7 +97,6 @@ export const unit: Unit = { export const listing: Listing = { id: "Uvbk5qurpB2WI9V6WnNdH", - listingAvailability: ListingAvailability.openWaitlist, applicationConfig: undefined, applicationOpenDate: new Date("2019-12-31T15:22:57.000-07:00"), applicationPickUpAddress: undefined, diff --git a/sites/partners/cypress/integration/03-listing.spec.ts b/sites/partners/cypress/integration/03-listing.spec.ts index 8d3b0be5e9..a1bf418189 100644 --- a/sites/partners/cypress/integration/03-listing.spec.ts +++ b/sites/partners/cypress/integration/03-listing.spec.ts @@ -198,7 +198,6 @@ describe("Listing Management Tests", () => { cy.get("#specialNotes").contains(listing["specialNotes"]) cy.get("#reviewOrderQuestion").contains("First come first serve") cy.get("#dueDateQuestion").contains("No") - cy.getByID("waitlist.openQuestion").contains("No") cy.get("#leasingAgentName").contains(listing["leasingAgentName"]) cy.get("#leasingAgentEmail").contains(listing["leasingAgentEmail"].toLowerCase()) cy.get("#leasingAgentPhone").contains("(520) 245-8811") diff --git a/sites/partners/src/listings/PaperListingDetails/sections/DetailRankingsAndResults.tsx b/sites/partners/src/listings/PaperListingDetails/sections/DetailRankingsAndResults.tsx index 6b8605998b..e7616e4c77 100644 --- a/sites/partners/src/listings/PaperListingDetails/sections/DetailRankingsAndResults.tsx +++ b/sites/partners/src/listings/PaperListingDetails/sections/DetailRankingsAndResults.tsx @@ -2,11 +2,11 @@ import React, { useContext } from "react" import dayjs from "dayjs" import utc from "dayjs/plugin/utc" dayjs.extend(utc) -import { t, GridSection, ViewItem } from "@bloom-housing/ui-components" +import { t, GridSection, ViewItem, GridCell } from "@bloom-housing/ui-components" import { ListingContext } from "../../ListingContext" import { getLotteryEvent } from "@bloom-housing/shared-helpers" import { ListingReviewOrder } from "@bloom-housing/backend-core/types" -import { getDetailFieldNumber, getDetailBoolean } from "./helpers" +import { getDetailFieldNumber, getDetailFieldString, getDetailBoolean } from "./helpers" const DetailRankingsAndResults = () => { const listing = useContext(ListingContext) @@ -26,13 +26,15 @@ const DetailRankingsAndResults = () => { grid={false} inset > - - - {getReviewOrderType() === ListingReviewOrder.firstComeFirstServe - ? t("listings.firstComeFirstServe") - : t("listings.lotteryTitle")} - - + {listing.reviewOrderType !== ListingReviewOrder.waitlist && ( + + + {getReviewOrderType() === ListingReviewOrder.firstComeFirstServe + ? t("listings.firstComeFirstServe") + : t("listings.lotteryTitle")} + + + )} {lotteryEvent && ( <> @@ -60,18 +62,28 @@ const DetailRankingsAndResults = () => { )} - - - {getDetailBoolean(listing.isWaitlistOpen)} - - - {listing.isWaitlistOpen && ( - - - {getDetailFieldNumber(listing.waitlistOpenSpots)} - - + {listing.reviewOrderType === ListingReviewOrder.waitlist && ( + <> + + + {getDetailBoolean(listing.isWaitlistOpen)} + + + + + {getDetailFieldNumber(listing.waitlistOpenSpots)} + + + )} + + + + + {getDetailFieldString(listing.whatToExpect)} + + + ) } diff --git a/sites/partners/src/listings/PaperListingDetails/sections/DetailUnits.tsx b/sites/partners/src/listings/PaperListingDetails/sections/DetailUnits.tsx index b74473da5e..8f019bcae9 100644 --- a/sites/partners/src/listings/PaperListingDetails/sections/DetailUnits.tsx +++ b/sites/partners/src/listings/PaperListingDetails/sections/DetailUnits.tsx @@ -9,7 +9,7 @@ import { } from "@bloom-housing/ui-components" import { ListingContext } from "../../ListingContext" import { UnitDrawer } from "../DetailsUnitDrawer" -import { ListingAvailability } from "@bloom-housing/backend-core" +import { ListingReviewOrder } from "@bloom-housing/backend-core" type DetailUnitsProps = { setUnitDrawer: (unit: UnitDrawer) => void @@ -54,9 +54,9 @@ const DetailUnits = ({ setUnitDrawer }: DetailUnitsProps) => { ) const listingAvailabilityText = useMemo(() => { - if (listing.listingAvailability === ListingAvailability.availableUnits) { + if (listing.reviewOrderType !== ListingReviewOrder.waitlist) { return t("listings.availableUnits") - } else if (listing.listingAvailability === ListingAvailability.openWaitlist) { + } else if (listing.reviewOrderType === ListingReviewOrder.waitlist) { return t("listings.waitlist.open") } return t("t.none") diff --git a/sites/partners/src/listings/PaperListingForm/formTypes.ts b/sites/partners/src/listings/PaperListingForm/formTypes.ts index a109454fda..b586617d3d 100644 --- a/sites/partners/src/listings/PaperListingForm/formTypes.ts +++ b/sites/partners/src/listings/PaperListingForm/formTypes.ts @@ -138,6 +138,8 @@ export const formDefaults: FormListing = { waitlistMaxSize: null, isWaitlistOpen: null, waitlistOpenSpots: null, + whatToExpect: + "Applicants will be contacted by the property agent in rank order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application will be removed from the waitlist if you have made any fraudulent statements. If we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", units: [], accessibility: "", amenities: "", @@ -156,7 +158,6 @@ export const formDefaults: FormListing = { urlSlug: undefined, showWaitlist: false, reviewOrderType: null, - listingAvailability: null, unitsSummary: [], unitsSummarized: { unitTypes: [], diff --git a/sites/partners/src/listings/PaperListingForm/formatters/AdditionalMetadataFormatter.ts b/sites/partners/src/listings/PaperListingForm/formatters/AdditionalMetadataFormatter.ts index c9ca7b8ef7..e8259f6c53 100644 --- a/sites/partners/src/listings/PaperListingForm/formatters/AdditionalMetadataFormatter.ts +++ b/sites/partners/src/listings/PaperListingForm/formatters/AdditionalMetadataFormatter.ts @@ -1,4 +1,4 @@ -import { ListingReviewOrder, ListingAvailability } from "@bloom-housing/backend-core/types" +import { ListingReviewOrder } from "@bloom-housing/backend-core/types" import { listingFeatures, listingUtilities } from "@bloom-housing/shared-helpers" import Formatter from "./Formatter" @@ -50,11 +50,10 @@ export default class AdditionalMetadataFormatter extends Formatter { ? ListingReviewOrder.lottery : ListingReviewOrder.firstComeFirstServe - if (this.data.listingAvailabilityQuestion === "availableUnits") { - this.data.listingAvailability = ListingAvailability.availableUnits - } else if (this.data.listingAvailabilityQuestion === "openWaitlist") { - this.data.listingAvailability = ListingAvailability.openWaitlist + if (this.data.listingAvailabilityQuestion === "openWaitlist") { + this.data.reviewOrderType = ListingReviewOrder.waitlist } + this.data.features = listingFeatures.reduce((acc, current) => { return { ...acc, diff --git a/sites/partners/src/listings/PaperListingForm/sections/RankingsAndResults.tsx b/sites/partners/src/listings/PaperListingForm/sections/RankingsAndResults.tsx index d2bfdf24d1..1a2cf33aa5 100644 --- a/sites/partners/src/listings/PaperListingForm/sections/RankingsAndResults.tsx +++ b/sites/partners/src/listings/PaperListingForm/sections/RankingsAndResults.tsx @@ -72,31 +72,33 @@ const RankingsAndResults = ({ listing }: RankingsAndResultsProps) => { title={t("listings.sections.rankingsResultsTitle")} description={t("listings.sections.rankingsResultsSubtitle")} > - - -

{t("listings.reviewOrderQuestion")}

- -
-
+ {availabilityQuestion !== "openWaitlist" && ( + + +

{t("listings.reviewOrderQuestion")}

+ +
+
+ )} {reviewOrder === "reviewOrderFCFS" && ( diff --git a/sites/partners/src/listings/PaperListingForm/sections/Units.tsx b/sites/partners/src/listings/PaperListingForm/sections/Units.tsx index 59fb17c53f..121cd1a37f 100644 --- a/sites/partners/src/listings/PaperListingForm/sections/Units.tsx +++ b/sites/partners/src/listings/PaperListingForm/sections/Units.tsx @@ -18,7 +18,7 @@ import UnitForm from "../UnitForm" import { useFormContext, useWatch } from "react-hook-form" import { TempUnit } from "../formTypes" import { fieldHasError, fieldMessage } from "../../../../lib/helpers" -import { ListingAvailability } from "@bloom-housing/backend-core/types" +import { ListingReviewOrder } from "@bloom-housing/backend-core" type UnitProps = { units: TempUnit[] @@ -194,15 +194,14 @@ const FormUnits = ({ units, setUnits, disableUnitsAccordion }: UnitProps) => { value: "availableUnits", id: "availableUnits", dataTestId: "listingAvailability.availableUnits", - defaultChecked: - listing?.listingAvailability === ListingAvailability.availableUnits, + defaultChecked: listing?.reviewOrderType !== ListingReviewOrder.waitlist, }, { label: t("listings.waitlist.open"), value: "openWaitlist", id: "openWaitlist", dataTestId: "listingAvailability.openWaitlist", - defaultChecked: listing?.listingAvailability === ListingAvailability.openWaitlist, + defaultChecked: listing?.reviewOrderType === ListingReviewOrder.waitlist, }, ]} /> diff --git a/sites/public/lib/helpers.tsx b/sites/public/lib/helpers.tsx index 7b8228b14a..e8bd831ee1 100644 --- a/sites/public/lib/helpers.tsx +++ b/sites/public/lib/helpers.tsx @@ -5,7 +5,6 @@ import { ListingReviewOrder, UnitsSummarized, ListingStatus, - ListingAvailability, } from "@bloom-housing/backend-core/types" import { t, @@ -52,10 +51,10 @@ const getListingCardSubtitle = (address: Address) => { const getListingTableData = ( unitsSummarized: UnitsSummarized, - listingAvailability: ListingAvailability + listingReviewOrder: ListingReviewOrder ) => { return unitsSummarized !== undefined - ? getSummariesTable(unitsSummarized.byUnitTypeAndRent, listingAvailability) + ? getSummariesTable(unitsSummarized.byUnitTypeAndRent, listingReviewOrder) : [] } @@ -113,13 +112,13 @@ export const getListings = (listings) => { } const generateTableSubHeader = (listing) => { - if (listing.listingAvailability === ListingAvailability.availableUnits) { + if (listing.reviewOrderType !== ListingReviewOrder.waitlist) { return { content: t("listings.availableUnits"), styleType: AppearanceStyleType.success, isPillType: true, } - } else if (listing.listingAvailability === ListingAvailability.openWaitlist) { + } else if (listing.reviewOrderType === ListingReviewOrder.waitlist) { return { content: t("listings.waitlist.open"), styleType: AppearanceStyleType.primary, @@ -147,7 +146,7 @@ export const getListings = (listings) => { }} tableProps={{ headers: unitSummariesHeaders, - data: getListingTableData(listing.unitsSummarized, listing.listingAvailability), + data: getListingTableData(listing.unitsSummarized, listing.reviewOrderType), responsiveCollapse: true, cellClassName: "px-5 py-3", }} diff --git a/sites/public/pages/applications/review/confirmation.tsx b/sites/public/pages/applications/review/confirmation.tsx index 486dc27c2b..cad4fb6cee 100644 --- a/sites/public/pages/applications/review/confirmation.tsx +++ b/sites/public/pages/applications/review/confirmation.tsx @@ -14,7 +14,7 @@ import { FormCard, t, } from "@bloom-housing/ui-components" -import { ListingReviewOrder } from "@bloom-housing/backend-core/types" +import { ListingEvent, ListingReviewOrder } from "@bloom-housing/backend-core/types" import { imageUrlFromListing, PageView, @@ -33,25 +33,22 @@ const ApplicationConfirmation = () => { const imageUrl = imageUrlFromListing(listing, parseInt(process.env.listingPhotoSize)) - const reviewOrder = useMemo(() => { - if (listing) { - if (listing.reviewOrderType == ListingReviewOrder.lottery) { - const lotteryEvent = getLotteryEvent(listing) - const lotteryText = [] - if (lotteryEvent?.startTime) { - lotteryText.push( - t("application.review.confirmation.eligibleApplicants.lotteryDate", { - lotteryDate: dayjs(lotteryEvent?.startTime).format("MMMM D, YYYY"), - }) - ) + const content = useMemo(() => { + switch (listing?.reviewOrderType) { + case ListingReviewOrder.firstComeFirstServe: + return { + text: t("application.review.confirmation.whatHappensNext.fcfs"), } - lotteryText.push(t("application.review.confirmation.eligibleApplicants.lottery")) - return lotteryText.join(" ") - } else { - return t("application.review.confirmation.eligibleApplicants.FCFS") - } - } else { - return "" + case ListingReviewOrder.lottery: + return { + text: t("application.review.confirmation.whatHappensNext.lottery"), + } + case ListingReviewOrder.waitlist: + return { + text: t("application.review.confirmation.whatHappensNext.waitlist"), + } + default: + return { text: "" } } }, [listing, router.locale]) @@ -93,9 +90,7 @@ const ApplicationConfirmation = () => {
- - {t("application.review.confirmation.whatHappensNext", { reviewOrder })} - + {content.text}
diff --git a/sites/public/pages/applications/review/terms.tsx b/sites/public/pages/applications/review/terms.tsx index 2a244a5c91..c1e9fcb389 100644 --- a/sites/public/pages/applications/review/terms.tsx +++ b/sites/public/pages/applications/review/terms.tsx @@ -2,7 +2,7 @@ 5.3 Terms View of application terms with checkbox */ -import React, { useContext, useEffect, useState } from "react" +import React, { useContext, useEffect, useMemo, useState } from "react" import { useRouter } from "next/router" import { AppearanceStyleType, @@ -14,7 +14,7 @@ import { AlertBox, ProgressNav, } from "@bloom-housing/ui-components" -import { ApplicationSection } from "@bloom-housing/backend-core" +import { ApplicationSection, ListingReviewOrder } from "@bloom-housing/backend-core/types" import { useForm } from "react-hook-form" import Markdown from "markdown-to-jsx" import { @@ -87,6 +87,25 @@ const ApplicationTerms = () => { }, ] + const content = useMemo(() => { + switch (listing?.reviewOrderType) { + case ListingReviewOrder.firstComeFirstServe: + return { + text: t("application.review.terms.fcfs.text"), + } + case ListingReviewOrder.lottery: + return { + text: t("application.review.terms.lottery.text"), + } + case ListingReviewOrder.waitlist: + return { + text: t("application.review.terms.waitlist.text"), + } + default: + return { text: "" } + } + }, [listing, router.locale]) + useEffect(() => { pushGtmEvent({ event: "pageView", @@ -122,20 +141,37 @@ const ApplicationTerms = () => { )}
-
+
{listing?.applicationDueDate && ( - - {t("application.review.terms.textSubmissionDate", { - applicationDueDate: applicationDueDate, - })} - + <> + + {t("application.review.terms.textSubmissionDate", { + applicationDueDate: applicationDueDate, + })} + +
+
+ )} - - {t("application.review.terms.text")} + ( +
  • + {children} +
  • + ), + }, + }, + }} + > + {content.text}
    -
    +
    { const { profile } = useContext(AuthContext) const { conductor, application, listing } = useFormConductor("whatToExpect") + const router = useRouter() const currentPageSection = 1 /* Form Handler */ @@ -28,6 +32,28 @@ const ApplicationWhatToExpect = () => { conductor.routeToNextOrReturnUrl() } + const content = useMemo(() => { + switch (listing?.reviewOrderType) { + case ListingReviewOrder.firstComeFirstServe: + return { + steps: t("application.start.whatToExpect.fcfs.steps"), + finePrint: t("application.start.whatToExpect.fcfs.finePrint"), + } + case ListingReviewOrder.lottery: + return { + steps: t("application.start.whatToExpect.lottery.steps"), + finePrint: t("application.start.whatToExpect.lottery.finePrint"), + } + case ListingReviewOrder.waitlist: + return { + steps: t("application.start.whatToExpect.waitlist.steps"), + finePrint: t("application.start.whatToExpect.waitlist.finePrint"), + } + default: + return { steps: "", finePrint: "" } + } + }, [listing, router.locale]) + useEffect(() => { pushGtmEvent({ event: "pageView", @@ -58,9 +84,41 @@ const ApplicationWhatToExpect = () => {
    -

    {t("application.start.whatToExpect.info1")}

    -

    {t("application.start.whatToExpect.info2")}

    -

    {t("application.start.whatToExpect.info3")}

    +
    + ( +
      + {children} +
    + ), + }, + }, + }} + > + {content.steps} +
    + + ( +
  • + {children} +
  • + ), + }, + }, + }} + > + {content.finePrint} +
    +
    diff --git a/sites/public/src/ListingView.tsx b/sites/public/src/ListingView.tsx index 3123448252..10874c9e6e 100644 --- a/sites/public/src/ListingView.tsx +++ b/sites/public/src/ListingView.tsx @@ -9,7 +9,6 @@ import { ApplicationMethod, ApplicationMethodType, ListingStatus, - ListingAvailability, Jurisdiction, ApplicationSection, ListingReviewOrder, @@ -74,7 +73,7 @@ const getWhatToExpectContent = (listing: Listing) => { content: t("whatToExpect.lottery"), expandableContent: t("whatToExpect.lotteryReadMore"), } - if (listing.listingAvailability === ListingAvailability.openWaitlist) + if (listing.reviewOrderType === ListingReviewOrder.waitlist) return { content: t("whatToExpect.waitlist"), expandableContent: t("whatToExpect.waitlistReadMore"), @@ -150,7 +149,7 @@ export const ListingView = (props: ListingProps) => { if (amiValues.length == 1) { groupedUnits = getSummariesTable( listing.unitsSummarized.byUnitTypeAndRent, - listing.listingAvailability + listing.reviewOrderType ) } // else condition is handled inline below @@ -432,15 +431,15 @@ export const ListingView = (props: ListingProps) => { return ( { }) groupedUnits = byAMI - ? getSummariesTable(byAMI.byUnitType, listing.listingAvailability) + ? getSummariesTable(byAMI.byUnitType, listing.reviewOrderType) : [] return ( @@ -788,7 +787,7 @@ export const ListingView = (props: ListingProps) => { )} {lotterySection} li { + border-inline-start: 1px solid var(--bloom-color-gray-450); + font-size: 1.06rem; + margin-bottom: 0; + padding-block-end: var(--bloom-s8); + padding-inline-start: var(--bloom-s8); + + @media (min-width: $screen-md) { + padding-inline-start: var(--bloom-s10); + } + + &::before { + content: counter(list-number, decimal); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border-radius: var(--bloom-rounded-full); + background-color: var(--bloom-color-white); + border: var(--bloom-border-2) solid var(--bloom-color-gray-750); + box-shadow: 0 0 0 1px var(--bloom-color-white); + color: var(--bloom-color-gray-750); + counter-increment: list-number; + font-size: var(--bloom-font-size-base); + font-weight: normal; + line-height: 0.9; + width: var(--bloom-s10); + height: var(--bloom-s10); + margin-block-start: 0; + position: absolute; + left: 0; + } + + &:last-child { + border-inline-start-color: transparent; + } + } + + /* The homepage How it Works section uses a horizontal layout: */ + + &.has-horizontal-layout { + display: flex; + gap: var(--bloom-s8); + padding-inline-start: 0; + margin-block: var(--bloom-s12); + + @media (max-width: 1100px) { + flex-direction: column; + gap: var(--bloom-s12); + } + + & > li { + position: relative; + border-inline-start-width: 0; + text-align: center; + padding-top: var(--bloom-s8); + background-color: var(--bloom-color-white); + box-shadow: 0px 0px 3px var(--bloom-color-gray-450); + padding-inline: var(--bloom-s4); + + &::before { + inset-block-start: calc(0rem - var(--bloom-s7)); + inset-inline-start: 50%; + margin-inline-start: calc(0rem - var(--bloom-s0_5)); + } + } } } } diff --git a/ui-components/src/locales/general.json b/ui-components/src/locales/general.json index 2949ad2f3b..214f15eef1 100644 --- a/ui-components/src/locales/general.json +++ b/ui-components/src/locales/general.json @@ -382,7 +382,9 @@ "application.review.confirmation.whatExpectFirstParagraph.refer": "Please refer to the listing for the lottery results date.", "application.review.confirmation.whatExpectSecondparagraph": "Applicants will be contacted in order until vacancies are filled. Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.", "application.review.confirmation.whatExpectTitle": "What to expect next", - "application.review.confirmation.whatHappensNext": "### What happens next?\n\n* After all applications are submitted, the property manager will begin processing applications.\n\n* %{reviewOrder}\n\n* If you are contacted for an interview, you will need to fill out a more detailed application and provide supporting documents.", + "application.review.confirmation.whatHappensNext.fcfs": "### What happens next?\n\n* Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.\n\n* Housing preferences, if applicable, will affect first come, first serve order.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.", + "application.review.confirmation.whatHappensNext.lottery": "### What happens next?\n\n* Once the application period closes, eligible applicants will be placed in order based on lottery rank order.\n\n* Housing preferences, if applicable, will affect lottery rank order.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.", + "application.review.confirmation.whatHappensNext.waitlist": "### What happens next?\n\n* Eligible applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.\n\n* Housing preferences, if applicable, will affect waitlist order.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.\n\n* You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.", "application.review.demographics.ethnicityLabel": "Which best describes your ethnicity?", "application.review.demographics.ethnicityOptions.hispanicLatino": "Hispanic / Latino", "application.review.demographics.ethnicityOptions.notHispanicLatino": "Not Hispanic / Latino", @@ -442,13 +444,21 @@ "application.review.sameAddressAsApplicant": "Same Address as Applicant", "application.review.takeAMomentToReview": "Take a moment to review your information before submitting your application.", "application.review.terms.confirmCheckboxText": "I agree and understand that I cannot change anything after I submit.", - "application.review.terms.textSubmissionDate": "This application must be submitted by %{applicationDueDate}.

    ", - "application.review.terms.text": "Applicants will be contacted by the leasing agent in lottery and preference order or waitlist order until vacancies are filled. All of the information that you have provided will be verified and your eligibility confirmed. Your application may be removed from the waitlist if you have made any fraudulent statements and duplicate applications from the same household may be removed as only one application per household is permitted. Should your application be chosen for review, be prepared to fill out a more detailed application and provide required supporting documents. For more information, please contact the developer or leasing agent posted in the listing. Please contact the developer/property manager directly if there are any updates to your application.

    If we cannot verify a housing lottery preference that you have claimed, you will not receive the preference but will not be otherwise penalized.

    Completing this housing application does not entitle you to housing or indicate you are eligible for housing; all applicants will be screened as outlined in the property’s Resident Selection Criteria. We offer no guarantees about obtaining housing.

    You cannot change your online application after you submit.

    I declare that the foregoing is true and accurate, and acknowledge that any misstatement fraudulently or negligently made on this application may result in removal from the lottery.

    ", + "application.review.terms.textSubmissionDate": "This application must be submitted by %{applicationDueDate}.", + "application.review.terms.fcfs.text": "* Applicants are applying to currently vacant apartments on a first come, first serve basis.\n\n* Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.\n\n* All of the information that you have provided will be verified and your eligibility confirmed.\n\n* Your application may be removed if you have made any fraudulent statements.\n\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.\n\nFor more information, please contact the housing developer or property manager posted in the listing.\n\nCompleting this application does not entitle you to housing or indicate you are eligible for housing. All applicants will be screened as outlined in the property's Resident Selection Criteria.\n\nYou cannot change your online application after you submit.\n\nI declare that the foregoing is true and accurate, and acknowledge that any misstatement fraudulently or negligently made on this application will result in removal from the lottery.", + "application.review.terms.lottery.text": "* Applicants are applying to enter a lottery for currently vacant apartments.\n\n* Once the application period closes, eligible applicants will be placed in lottery rank order.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.\n\n* All of the information that you have provided will be verified and your eligibility confirmed.\n\n* Your application may be removed if you have made any fraudulent statements.\n\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.\n\nFor more information, please contact the housing developer or property manager posted in the listing.\n\nCompleting this application does not entitle you to housing or indicate you are eligible for housing. All applicants will be screened as outlined in the property's Resident Selection Criteria.\n\nYou cannot change your online application after you submit.\n\nI declare that the foregoing is true and accurate, and acknowledge that any misstatement fraudulently or negligently made on this application will result in removal from the lottery.", + "application.review.terms.waitlist.text": "* Applicants are applying for an open waitlist and not a currently vacant apartment.\n\n* When vacancies become available, eligible applicants will be contacted by the property manager on a first come, first serve basis.\n\n* If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.\n\n* All of the information that you have provided will be verified and your eligibility confirmed.\n\n* Your application may be removed if you have made any fraudulent statements.\n\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.\n\n* You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.\n\nFor more information, please contact the housing developer or property manager posted in the listing.\n\nCompleting this application does not entitle you to housing or indicate you are eligible for housing. All applicants will be screened as outlined in the property's Resident Selection Criteria.\n\nYou cannot change your online application after you submit.\n\nI declare that the foregoing is true and accurate, and acknowledge that any misstatement fraudulently or negligently made on this application will result in removal from the lottery.", "application.review.terms.title": "Terms", "application.review.voucherOrSubsidy": "Housing Voucher or Rental Subsidy", - "application.start.whatToExpect.info1": "First we'll ask about you and the people you plan to live with. Then, we'll ask about your income. Finally, we'll see if you qualify for any affordable housing lottery preference.", - "application.start.whatToExpect.info2": "Please be aware that each household member can only appear on one application for each listing.", - "application.start.whatToExpect.info3": "Any fraudulent statements will cause your application to be removed.", + "application.start.whatToExpect.fcfs.steps": "1. First we'll ask about you and the people you plan to live with.\n2. Then, we'll ask about your income.\n3. Finally, we'll see if you qualify for any affordable housing lottery preferences, if applicable.", + "application.start.whatToExpect.fcfs.finePrint": "* Applicants are applying to currently vacant apartments on a first come, first serve basis.\n* Please be aware that each household member can only appear on one application for each listing.\n* Applicants will be contacted by the property manager on a first come, first serve basis, until vacancies are filled.\n* All of the information that you have provided will be verified and your eligibility confirmed.\n* Your application may be removed if you have made any fraudulent statements.\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.", + + "application.start.whatToExpect.lottery.steps": "1. First we'll ask about you and the people you plan to live with.\n2. Then, we'll ask about your income.\n3. Finally, we'll see if you qualify for any affordable housing lottery preferences, if applicable.", + "application.start.whatToExpect.lottery.finePrint": "* Applicants are applying to enter a lottery for currently vacant apartments.\n* Please be aware that each household member can only appear on one application for each listing.\n* Applicants will be contacted by the property agent in lottery rank order until vacancies are filled.\n* All of the information that you have provided will be verified and your eligibility confirmed.\n* Your application may be removed if you have made any fraudulent statements.\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.", + + "application.start.whatToExpect.waitlist.steps": "1. First we'll ask about you and the people you plan to live with.\n2. Then, we'll ask about your income.\n3. Finally, we'll see if you qualify for any affordable housing preferences, if applicable.", + "application.start.whatToExpect.waitlist.finePrint": "* Applicants are applying for an open waitlist and not a currently available apartment.\n* Please be aware that each household member can only appear on one application for each listing.\n* When vacancies become available, eligible applicants will be contacted by the property manager on a first come, first serve basis.\n* All of the information that you have provided will be verified and your eligibility confirmed.\n* Your application may be removed if you have made any fraudulent statements.\n* For properties with housing preferences, if we cannot verify a housing preference that you have claimed, you will not receive the preference but will not be otherwise penalized.", + "application.start.whatToExpect.title": "Here's what to expect from this application.", "application.status": "Status", "application.statuses.inProgress": "In Progress",