From 081986139d3bb32124d99ae51343ffbc5797a6c1 Mon Sep 17 00:00:00 2001 From: Anders Schneider Date: Thu, 15 Jul 2021 14:51:36 -0400 Subject: [PATCH] Distinguish available unit count and total unit count (in the listing-detail view) (#189) * Updated frontend to include totalCount * Small updates to tests * Revert a few unintended changes * Reverty a small unintended change to backend-swagger * Validate IsNumber (instead of IsString) --- .../core/src/shared/units-transformations.ts | 2 + backend/core/src/units/types/unit-summary.ts | 10 ++++- backend/core/types/src/backend-swagger.ts | 3 ++ sites/public/src/ListingView.tsx | 1 + .../helpers/occupancyFormatting.test.tsx | 3 ++ .../page_components/UnitTables.test.tsx | 45 +++++++++++++++---- ui-components/src/helpers/tableSummaries.tsx | 10 ++++- ui-components/src/locales/es.json | 1 + ui-components/src/locales/general.json | 1 + ui-components/src/locales/general_OLD.json | 1 + ui-components/src/locales/vi.json | 1 + ui-components/src/locales/zh.json | 1 + .../listing/UnitTables.stories.tsx | 22 ++++++--- 13 files changed, 81 insertions(+), 20 deletions(-) diff --git a/backend/core/src/shared/units-transformations.ts b/backend/core/src/shared/units-transformations.ts index bb5c203a5f..1d242366f8 100644 --- a/backend/core/src/shared/units-transformations.ts +++ b/backend/core/src/shared/units-transformations.ts @@ -142,6 +142,7 @@ const getDefaultSummaryRanges = (unit: Unit) => { }, unitType: unit.unitType, totalAvailable: 0, + totalCount: 0, } as UnitSummary } @@ -206,6 +207,7 @@ const summarizeUnitsByTypeAndRent = (units: Units, reservedType?: string): UnitS return getUnitsSummary(unit, index === 0 ? null : summary) }, {} as UnitSummary) finalSummary.totalAvailable = unitMap[key].filter((unit) => unit.status === UnitStatus.available).length + finalSummary.totalCount = unitMap[key].length summaries.push(finalSummary) } diff --git a/backend/core/src/units/types/unit-summary.ts b/backend/core/src/units/types/unit-summary.ts index 8343a89d7c..8836979810 100644 --- a/backend/core/src/units/types/unit-summary.ts +++ b/backend/core/src/units/types/unit-summary.ts @@ -1,5 +1,5 @@ import { Expose, Type } from "class-transformer" -import { IsDefined, IsOptional, IsString, ValidateNested } from "class-validator" +import { IsDefined, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" import { MinMaxCurrency } from "./min-max-currency" import { MinMax } from "./min-max" @@ -42,10 +42,16 @@ export class UnitSummary { @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) - @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) @ApiProperty() totalAvailable: number + @Expose() + @IsDefined({ groups: [ValidationsGroupsEnum.default] }) + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + totalCount: number + @Expose() @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ValidateNested({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend/core/types/src/backend-swagger.ts b/backend/core/types/src/backend-swagger.ts index 14859728fe..d295043b01 100644 --- a/backend/core/types/src/backend-swagger.ts +++ b/backend/core/types/src/backend-swagger.ts @@ -2837,6 +2837,9 @@ export interface UnitSummary { /** */ totalAvailable: number; + /** */ + totalCount: number; + /** */ areaRange: MinMax; diff --git a/sites/public/src/ListingView.tsx b/sites/public/src/ListingView.tsx index 59f2caa19d..51a66ea3e8 100644 --- a/sites/public/src/ListingView.tsx +++ b/sites/public/src/ListingView.tsx @@ -60,6 +60,7 @@ export const ListingView = (props: ListingProps) => { minimumIncome: t("t.minimumIncome"), rent: t("t.rent"), availability: t("t.availability"), + totalCount: t("t.totalCount"), } const amiValues = listing?.unitsSummarized?.amiPercentages diff --git a/ui-components/__tests__/helpers/occupancyFormatting.test.tsx b/ui-components/__tests__/helpers/occupancyFormatting.test.tsx index c17150c56a..d5730c08df 100644 --- a/ui-components/__tests__/helpers/occupancyFormatting.test.tsx +++ b/ui-components/__tests__/helpers/occupancyFormatting.test.tsx @@ -29,6 +29,7 @@ ArcherListing.unitsSummarized = { max: "350", }, totalAvailable: 8, + totalCount: 8, areaRange: { min: 5, max: 60, @@ -55,6 +56,7 @@ ArcherListing.unitsSummarized = { max: "350", }, totalAvailable: 8, + totalCount: 8, areaRange: { min: 5, max: 60, @@ -79,6 +81,7 @@ ArcherListing.unitsSummarized = { max: "350", }, totalAvailable: 8, + totalCount: 8, areaRange: { min: 5, max: 60, diff --git a/ui-components/__tests__/page_components/UnitTables.test.tsx b/ui-components/__tests__/page_components/UnitTables.test.tsx index b221e105b0..6bd904b98a 100644 --- a/ui-components/__tests__/page_components/UnitTables.test.tsx +++ b/ui-components/__tests__/page_components/UnitTables.test.tsx @@ -22,7 +22,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 41, + totalAvailable: 0, + totalCount: 41, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: 10, max: 80 }, @@ -34,7 +35,8 @@ const summaries: { byUnitTypeWithoutFloor: [ { unitType: "studio", - totalAvailable: 41, + totalAvailable: 0, + totalCount: 41, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: 10, max: 80 }, @@ -45,7 +47,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 40, + totalAvailable: 0, + totalCount: 40, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -60,7 +63,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 1, + totalAvailable: 0, + totalCount: 1, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -77,7 +81,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 24, + totalAvailable: 0, + totalCount: 24, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -92,7 +97,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 1, + totalAvailable: 0, + totalCount: 1, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -109,7 +115,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 16, + totalAvailable: 0, + totalCount: 16, minIncomeRange: { min: "$1,438", max: "$1,438" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -135,9 +142,19 @@ describe("", () => { const { getAllByText, getByRole, container } = render( ) + + // Verify that UnitTables shows the total count of units. + const buttonHeader = getByRole("button") + expect(buttonHeader.textContent).toContain(summaries.byUnitType[0].totalCount + " units") + + // All units have the same square-foot area minimum; find all HTML elements that include that + // text (this should be all rows, one per unit) and make sure the total number of rows matches + // the totalCount of units. expect(getAllByText(summaries.byUnitType[0].areaRange.min).length).toBe( - summaries.byUnitType[0].totalAvailable + summaries.byUnitType[0].totalCount ) + + // Expect the table with one row per unit to be hidden until the button is clicked. expect(container.getElementsByClassName("hidden").length).toBe(1) fireEvent.click(getByRole("button")) expect(container.getElementsByClassName("hidden").length).toBe(0) @@ -150,9 +167,19 @@ describe("", () => { disableAccordion={true} /> ) + + // Verify that UnitTables shows the total count of units. + const buttonHeader = getByRole("button") + expect(buttonHeader.textContent).toContain(summaries.byUnitType[0].totalCount + " units") + + // All units have the same square-foot area minimum; find all HTML elements that include that + // text (this should be all rows, one per unit) and make sure the total number of rows matches + // the totalCount of units. expect(getAllByText(summaries.byUnitType[0].areaRange.min).length).toBe( - summaries.byUnitType[0].totalAvailable + summaries.byUnitType[0].totalCount ) + + // Expect the table with one row per unit to be hidden, even if the button is clicked. expect(container.getElementsByClassName("hidden").length).toBe(1) fireEvent.click(getByRole("button")) expect(container.getElementsByClassName("hidden").length).toBe(1) diff --git a/ui-components/src/helpers/tableSummaries.tsx b/ui-components/src/helpers/tableSummaries.tsx index 12babe5a8d..f4a91b42c5 100644 --- a/ui-components/src/helpers/tableSummaries.tsx +++ b/ui-components/src/helpers/tableSummaries.tsx @@ -5,7 +5,6 @@ import { GroupedTableGroup } from "../tables/GroupedTable" export const unitSummariesTable = (summaries: UnitSummary[]) => { const unitSummaries = summaries.map((unitSummary) => { - const unitPluralization = unitSummary.totalAvailable == 1 ? t("t.unit") : t("t.units") const minIncome = unitSummary.minIncomeRange.min == unitSummary.minIncomeRange.max ? ( {unitSummary.minIncomeRange.min} @@ -52,13 +51,20 @@ export const unitSummariesTable = (summaries: UnitSummary[]) => { <> {unitSummary.totalAvailable > 0 ? ( <> - {unitSummary.totalAvailable} {unitPluralization} + {unitSummary.totalAvailable}{" "} + {unitSummary.totalAvailable == 1 ? t("t.unit") : t("t.units")} ) : ( {t("listings.waitlist.label")} )} ), + totalCount: ( + <> + {unitSummary.totalCount}{" "} + {unitSummary.totalCount == 1 ? t("t.unit") : t("t.units")} + + ), } }) diff --git a/ui-components/src/locales/es.json b/ui-components/src/locales/es.json index 779d67e9c7..50f143d37a 100644 --- a/ui-components/src/locales/es.json +++ b/ui-components/src/locales/es.json @@ -722,6 +722,7 @@ "submit": "Enviar", "text": "Texto", "to": "a", + "totalCount": "Cuenta Total", "unit": "vivienda", "units": "viviendas", "unitAmenities": "Servicios en la vivienda", diff --git a/ui-components/src/locales/general.json b/ui-components/src/locales/general.json index a1139c91a8..1b34c4f618 100644 --- a/ui-components/src/locales/general.json +++ b/ui-components/src/locales/general.json @@ -1172,6 +1172,7 @@ "time": "time", "text": "Text", "to": "to", + "totalCount": "Total Count", "unit": "unit", "units": "units", "unitAmenities": "Unit Amenities", diff --git a/ui-components/src/locales/general_OLD.json b/ui-components/src/locales/general_OLD.json index bfbdff4fae..0bf171370a 100644 --- a/ui-components/src/locales/general_OLD.json +++ b/ui-components/src/locales/general_OLD.json @@ -840,6 +840,7 @@ "smokingPolicy": "Smoking Policy", "street": "Street", "sqFeet": "sqft", + "totalCount": "Total Count", "relationship": "Relationship", "unitAmenities": "Unit Amenities", "unitFeatures": "Unit Features", diff --git a/ui-components/src/locales/vi.json b/ui-components/src/locales/vi.json index 42d20c8bd1..08644458f0 100644 --- a/ui-components/src/locales/vi.json +++ b/ui-components/src/locales/vi.json @@ -721,6 +721,7 @@ "squareFeet": "feet vuông", "submit": "Gửi", "text": "Dạng chữ", + "totalCount": "Tổng số", "unit": "căn nhà", "units": "căn nhà", "unitAmenities": "Các tiện ích của Căn nhà", diff --git a/ui-components/src/locales/zh.json b/ui-components/src/locales/zh.json index 11b783386b..11e0a5a1d2 100644 --- a/ui-components/src/locales/zh.json +++ b/ui-components/src/locales/zh.json @@ -721,6 +721,7 @@ "squareFeet": "平方英尺", "submit": "提交", "text": "簡訊", + "totalCount": "总数", "unit": "單位", "units": "單位", "unitAmenities": "單位設施", diff --git a/ui-components/src/page_components/listing/UnitTables.stories.tsx b/ui-components/src/page_components/listing/UnitTables.stories.tsx index ff56c9001f..94306270c6 100644 --- a/ui-components/src/page_components/listing/UnitTables.stories.tsx +++ b/ui-components/src/page_components/listing/UnitTables.stories.tsx @@ -30,7 +30,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 41, + totalAvailable: 0, + totalCount: 41, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: 10, max: 80 }, @@ -42,7 +43,8 @@ const summaries: { byUnitTypeWithoutFloor: [ { unitType: "studio", - totalAvailable: 41, + totalAvailable: 0, + totalCount: 41, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: 10, max: 80 }, @@ -53,7 +55,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 40, + totalAvailable: 0, + totalCount: 40, minIncomeRange: { min: "$1,438", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -68,7 +71,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 1, + totalAvailable: 0, + totalCount: 1, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -85,7 +89,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 24, + totalAvailable: 0, + totalCount: 24, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -100,7 +105,8 @@ const summaries: { byUnitType: [ { unitType: "studio", - totalAvailable: 1, + totalAvailable: 0, + totalCount: 1, minIncomeRange: { min: "$2,208", max: "$2,208" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -117,7 +123,8 @@ const summaries: { byNonReservedUnitType: [ { unitType: "studio", - totalAvailable: 16, + totalAvailable: 0, + totalCount: 16, minIncomeRange: { min: "$1,438", max: "$1,438" }, occupancyRange: { min: 1, max: 2 }, rentAsPercentIncomeRange: { min: null, max: null }, @@ -157,6 +164,7 @@ const unitSummariesHeaders = { minimumIncome: "t.minimumIncome", rent: "t.rent", availability: "t.availability", + totalCount: "t.totalCount", } const amiValues = summaries.amiPercentages