From 85e851e7174256dd5aec7d22dcb1d98ddda9cdc1 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 10 Jun 2024 22:07:43 +0200 Subject: [PATCH 1/4] passport should record val not text if present, summary stats for dev type --- .../@planx/components/List/Public/Context.tsx | 24 +++ .../@planx/components/List/Public/Fields.tsx | 10 +- .../@planx/components/List/Public/index.tsx | 8 +- .../src/@planx/components/List/model.ts | 2 +- .../List/schemas/ResidentialUnits/GLA/New.ts | 138 ++++++++++++------ .../schemas/ResidentialUnits/GLA/Rebuilt.ts | 138 ++++++++++++------ .../schemas/ResidentialUnits/GLA/Removed.ts | 121 +++++++++------ .../schemas/ResidentialUnits/GLA/Retained.ts | 68 ++++++--- .../src/@planx/components/List/utils.ts | 18 +++ 9 files changed, 364 insertions(+), 163 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx index 7269691949..af4792df0e 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -141,16 +141,40 @@ export const ListProvider: React.FC = (props) => { // basic example of general summary stats we can add onSubmit: // 1. count of items/responses // 2. if the schema includes a field that sets fn = "identicalUnits", sum of total units + // 3. if the schema includes a field that sets fn = "development", sum of total units by development "val" let sumIdenticalUnits = 0; defaultPassportData[`${props.fn}`].map( (item) => (sumIdenticalUnits += parseInt(item?.identicalUnits)), ); + + const sumIdenticalUnitsByDevelopmentType: Record = { + newBuild: 0, + changeOfUseFrom: 0, + changeOfUseTo: 0, + }; + defaultPassportData[`${props.fn}`].map( + (item) => + (sumIdenticalUnitsByDevelopmentType[`${item?.development}`] += + parseInt(item?.identicalUnits)), + ); + const sumIdenticalUnitsByDevelopmentTypeSummary: Record = + {}; + Object.entries(sumIdenticalUnitsByDevelopmentType).forEach(([k, v]) => { + if (v > 0) { + sumIdenticalUnitsByDevelopmentTypeSummary[ + `${props.fn}.total.units.development.${k}` + ] = v; + } + }); + const summaries = { [`${props.fn}.total.listItems`]: defaultPassportData[`${props.fn}`].length, ...(sumIdenticalUnits > 0 && { [`${props.fn}.total.units`]: sumIdenticalUnits, }), + ...(Object.keys(sumIdenticalUnitsByDevelopmentTypeSummary).length > 0 && + sumIdenticalUnitsByDevelopmentTypeSummary), }; handleSubmit?.({ diff --git a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx index de3ee22acd..be39313c42 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Fields.tsx @@ -139,7 +139,7 @@ export const RadioFieldInput: React.FC> = (props) => { {data.options.map(({ id, data }) => ( @@ -159,9 +159,11 @@ export const SelectFieldInput: React.FC> = (props) => { const existingValues = formik.values.userData .map((response) => response[data.fn]) - .filter((value) => value === option.data.text); + .filter( + (value) => value === option.data.val || value === option.data.text, + ); - return existingValues.includes(option.data.text); + return existingValues.includes(option.data.val || option.data.text); }; return ( @@ -185,7 +187,7 @@ export const SelectFieldInput: React.FC> = (props) => { {data.options.map((option) => ( {option.data.text} diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.tsx index d935a367d0..6466cfa70a 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.tsx @@ -17,6 +17,7 @@ import InputRow from "ui/shared/InputRow"; import Card from "../../shared/Preview/Card"; import CardHeader from "../../shared/Preview/CardHeader"; import type { Field, List } from "../model"; +import { formatSchemaDisplayValue } from "../utils"; import { ListProvider, useListContext } from "./Context"; import { NumberFieldInput, @@ -111,7 +112,12 @@ const InactiveListCard: React.FC<{ {field.data.title} - {formik.values.userData[i][field.data.fn]} + + {formatSchemaDisplayValue( + formik.values.userData[i][field.data.fn], + schema, + )} + ))} diff --git a/editor.planx.uk/src/@planx/components/List/model.ts b/editor.planx.uk/src/@planx/components/List/model.ts index 2f1568e2ed..d35532cf9a 100644 --- a/editor.planx.uk/src/@planx/components/List/model.ts +++ b/editor.planx.uk/src/@planx/components/List/model.ts @@ -25,7 +25,7 @@ interface QuestionInput { */ const questionInputValidationSchema = (data: QuestionInput) => string() - .oneOf(data.options.map((option) => option.data.text)) + .oneOf(data.options.map((option) => option.data.val || option.data.text)) .required("Select your answer before continuing"); // TODO: Add summary fields for inactive view? diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts index f72d707a49..8b4e305ab6 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts @@ -10,8 +10,17 @@ export const ResidentialUnitsGLANew: Schema = { fn: "development", options: [ { id: "newBuild", data: { text: "New build", val: "newBuild" } }, - { id: "changeOfUseFrom", data: { text: "Change of use of existing single home", val: "changeOfUseFrom" } }, - { id: "changeOfUseTo", data: { text: "Change of use to a home", val: "changeOfUseTo" } }, + { + id: "changeOfUseFrom", + data: { + text: "Change of use of existing single home", + val: "changeOfUseFrom", + }, + }, + { + id: "changeOfUseTo", + data: { text: "Change of use to a home", val: "changeOfUseTo" }, + }, ], }, }, @@ -42,26 +51,39 @@ export const ResidentialUnitsGLANew: Schema = { id: "AR", data: { text: "Affordable rent (not at LAR benchmark rents)" }, }, - { id: "SR", data: { text: "Social rent" } }, - { id: "LRR", data: { text: "London Living Rent" } }, - { id: "sharedEquity", data: { text: "Shared equity" } }, - { id: "LSO", data: { text: "London Shared Ownership" } }, - { id: "DMS", data: { text: "Discount market sale" } }, - { id: "DMR", data: { text: "Discount market rent" } }, + { id: "SR", data: { text: "Social rent", val: "SR" } }, + { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, + { + id: "sharedEquity", + data: { text: "Shared equity", val: "sharedEquity" }, + }, + { id: "LSO", data: { text: "London Shared Ownership", val: "LSO" } }, + { id: "DMS", data: { text: "Discount market sale", val: "DMS" } }, + { id: "DMR", data: { text: "Discount market rent", val: "DMR" } }, { id: "DMRLLR", data: { text: "Discount market rent (charged at London Living Rents)", + val: "DMRLLR", }, }, - { id: "marketForRent", data: { text: "Market for rent" } }, - { id: "SH", data: { text: "Starter homes" } }, + { + id: "marketForRent", + data: { text: "Market for rent", val: "marketForRent" }, + }, + { id: "SH", data: { text: "Starter homes", val: "SH" } }, { id: "selfCustomBuild", - data: { text: "Self-build and custom build" }, + data: { + text: "Self-build and custom build", + val: "selfCustomBuild", + }, }, - { id: "marketForSale", data: { text: "Market for sale" } }, - { id: "other", data: { text: "Other" } }, + { + id: "marketForSale", + data: { text: "Market for sale", val: "marketForSale" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -94,8 +116,8 @@ export const ResidentialUnitsGLANew: Schema = { "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", fn: "complianceM42", // compliance.m42 options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -106,8 +128,8 @@ export const ResidentialUnitsGLANew: Schema = { "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", fn: "complianceM432a", // compliance.m432a options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -118,8 +140,8 @@ export const ResidentialUnitsGLANew: Schema = { "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", fn: "complianceM432b", // compliance.m432b options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -129,18 +151,27 @@ export const ResidentialUnitsGLANew: Schema = { title: "What best describes the type of this unit?", fn: "type", options: [ - { id: "terraced", data: { text: "Terraced home" } }, - { id: "semiDetached", data: { text: "Semi detached home" } }, - { id: "detached", data: { text: "Detached home" } }, - { id: "flat", data: { text: "Flat/apartment or maisonette" } }, - { id: "LW", data: { text: "Live/work unit" } }, - { id: "cluster", data: { text: "Cluster flat" } }, - { id: "studio", data: { text: "Studio or bedsit" } }, - { id: "coLiving", data: { text: "Co living unit" } }, - { id: "hostel", data: { text: "Hostel room" } }, - { id: "HMO", data: { text: "HMO" } }, - { id: "student", data: { text: "Student accomodation" } }, - { id: "other", data: { text: "Other" } }, + { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, + { + id: "semiDetached", + data: { text: "Semi detached home", val: "semiDetached" }, + }, + { id: "detached", data: { text: "Detached home", val: "detached" } }, + { + id: "flat", + data: { text: "Flat/apartment or maisonette", val: "flat" }, + }, + { id: "LW", data: { text: "Live/work unit", val: "LW" } }, + { id: "cluster", data: { text: "Cluster flat", val: "cluster" } }, + { id: "studio", data: { text: "Studio or bedsit", val: "studio" } }, + { id: "coLiving", data: { text: "Co living unit", val: "coLiving" } }, + { id: "hostel", data: { text: "Hostel room", val: "hostel" } }, + { id: "HMO", data: { text: "HMO", val: "HMO" } }, + { + id: "student", + data: { text: "Student accomodation", val: "student" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -150,21 +181,36 @@ export const ResidentialUnitsGLANew: Schema = { title: "What best describes the provider of this unit?", fn: "provider", options: [ - { id: "private", data: { text: "Private" } }, - { id: "privateRented", data: { text: "Private rented sector" } }, - { id: "HA", data: { text: "Housing association" } }, - { id: "LA", data: { text: "Local authority" } }, - { id: "publicAuthority", data: { text: "Other public authority" } }, - { id: "councilDelivery", data: { text: "Council delivery company" } }, + { id: "private", data: { text: "Private", val: "private" } }, + { + id: "privateRented", + data: { text: "Private rented sector", val: "privateRented" }, + }, + { id: "HA", data: { text: "Housing association", val: "HA" } }, + { id: "LA", data: { text: "Local authority", val: "LA" } }, + { + id: "publicAuthority", + data: { text: "Other public authority", val: "publicAuthority" }, + }, + { + id: "councilDelivery", + data: { text: "Council delivery company", val: "councilDelivery" }, + }, { id: "councilBuildToRent", - data: { text: "Council delivered build to rent" }, + data: { + text: "Council delivered build to rent", + val: "councilBuildToRent", + }, }, { id: "affordableHousing", - data: { text: "Other affordable housing provider" }, + data: { + text: "Other affordable housing provider", + val: "affordableHousing", + }, }, - { id: "selfBuild", data: { text: "Self-build" } }, + { id: "selfBuild", data: { text: "Self-build", val: "selfBuild" } }, ], }, }, @@ -174,8 +220,8 @@ export const ResidentialUnitsGLANew: Schema = { title: "Is this unit built on garden land?", fn: "garden", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -194,8 +240,8 @@ export const ResidentialUnitsGLANew: Schema = { title: "Will this unit provide sheltered accommodation?", fn: "sheltered", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -205,8 +251,8 @@ export const ResidentialUnitsGLANew: Schema = { title: "Is this unit specifically designed for older people?", fn: "olderPersons", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts index 9e6a7ad669..753034fbcb 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts @@ -10,8 +10,17 @@ export const ResidentialUnitsGLARebuilt: Schema = { fn: "development", options: [ { id: "newBuild", data: { text: "New build", val: "newBuild" } }, - { id: "changeOfUseFrom", data: { text: "Change of use of existing single home", val: "changeOfUseFrom" } }, - { id: "changeOfUseTo", data: { text: "Change of use to a home", val: "changeOfUseTo" } }, + { + id: "changeOfUseFrom", + data: { + text: "Change of use of existing single home", + val: "changeOfUseFrom", + }, + }, + { + id: "changeOfUseTo", + data: { text: "Change of use to a home", val: "changeOfUseTo" }, + }, ], }, }, @@ -42,26 +51,39 @@ export const ResidentialUnitsGLARebuilt: Schema = { id: "AR", data: { text: "Affordable rent (not at LAR benchmark rents)" }, }, - { id: "SR", data: { text: "Social rent" } }, - { id: "LRR", data: { text: "London Living Rent" } }, - { id: "sharedEquity", data: { text: "Shared equity" } }, - { id: "LSO", data: { text: "London Shared Ownership" } }, - { id: "DMS", data: { text: "Discount market sale" } }, - { id: "DMR", data: { text: "Discount market rent" } }, + { id: "SR", data: { text: "Social rent", val: "SR" } }, + { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, + { + id: "sharedEquity", + data: { text: "Shared equity", val: "sharedEquity" }, + }, + { id: "LSO", data: { text: "London Shared Ownership", val: "LSO" } }, + { id: "DMS", data: { text: "Discount market sale", val: "DMS" } }, + { id: "DMR", data: { text: "Discount market rent", val: "DMR" } }, { id: "DMRLLR", data: { text: "Discount market rent (charged at London Living Rents)", + val: "DMRLLR", }, }, - { id: "marketForRent", data: { text: "Market for rent" } }, - { id: "SH", data: { text: "Starter homes" } }, + { + id: "marketForRent", + data: { text: "Market for rent", val: "marketForRent" }, + }, + { id: "SH", data: { text: "Starter homes", val: "SH" } }, { id: "selfCustomBuild", - data: { text: "Self-build and custom build" }, + data: { + text: "Self-build and custom build", + val: "selfCustomBuild", + }, }, - { id: "marketForSale", data: { text: "Market for sale" } }, - { id: "other", data: { text: "Other" } }, + { + id: "marketForSale", + data: { text: "Market for sale", val: "marketForSale" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -94,8 +116,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", fn: "complianceM42", // compliance.m42 options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -106,8 +128,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", fn: "complianceM432a", // compliance.m432a options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -118,8 +140,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", fn: "complianceM432b", // compliance.m432b options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -129,18 +151,27 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "What best describes the type of this unit?", fn: "type", options: [ - { id: "terraced", data: { text: "Terraced home" } }, - { id: "semiDetached", data: { text: "Semi detached home" } }, - { id: "detached", data: { text: "Detached home" } }, - { id: "flat", data: { text: "Flat/apartment or maisonette" } }, - { id: "LW", data: { text: "Live/work unit" } }, - { id: "cluster", data: { text: "Cluster flat" } }, - { id: "studio", data: { text: "Studio or bedsit" } }, - { id: "coLiving", data: { text: "Co living unit" } }, - { id: "hostel", data: { text: "Hostel room" } }, - { id: "HMO", data: { text: "HMO" } }, - { id: "student", data: { text: "Student accomodation" } }, - { id: "other", data: { text: "Other" } }, + { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, + { + id: "semiDetached", + data: { text: "Semi detached home", val: "semiDetached" }, + }, + { id: "detached", data: { text: "Detached home", val: "detached" } }, + { + id: "flat", + data: { text: "Flat/apartment or maisonette", val: "flat" }, + }, + { id: "LW", data: { text: "Live/work unit", val: "LW" } }, + { id: "cluster", data: { text: "Cluster flat", val: "cluster" } }, + { id: "studio", data: { text: "Studio or bedsit", val: "studio" } }, + { id: "coLiving", data: { text: "Co living unit", val: "coLiving" } }, + { id: "hostel", data: { text: "Hostel room", val: "hostel" } }, + { id: "HMO", data: { text: "HMO", val: "HMO" } }, + { + id: "student", + data: { text: "Student accomodation", val: "student" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -150,21 +181,36 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "What best describes the provider of this unit?", fn: "provider", options: [ - { id: "private", data: { text: "Private" } }, - { id: "privateRented", data: { text: "Private rented sector" } }, - { id: "HA", data: { text: "Housing association" } }, - { id: "LA", data: { text: "Local authority" } }, - { id: "publicAuthority", data: { text: "Other public authority" } }, - { id: "councilDelivery", data: { text: "Council delivery company" } }, + { id: "private", data: { text: "Private", val: "private" } }, + { + id: "privateRented", + data: { text: "Private rented sector", val: "privateRented" }, + }, + { id: "HA", data: { text: "Housing association", val: "HA" } }, + { id: "LA", data: { text: "Local authority", val: "LA" } }, + { + id: "publicAuthority", + data: { text: "Other public authority", val: "publicAuthority" }, + }, + { + id: "councilDelivery", + data: { text: "Council delivery company", val: "councilDelivery" }, + }, { id: "councilBuildToRent", - data: { text: "Council delivered build to rent" }, + data: { + text: "Council delivered build to rent", + val: "councilBuildToRent", + }, }, { id: "affordableHousing", - data: { text: "Other affordable housing provider" }, + data: { + text: "Other affordable housing provider", + val: "affordableHousing", + }, }, - { id: "selfBuild", data: { text: "Self-build" } }, + { id: "selfBuild", data: { text: "Self-build", val: "selfBuild" } }, ], }, }, @@ -174,8 +220,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "Is this unit built on garden land?", fn: "garden", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -194,8 +240,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "Will this unit provide sheltered accommodation?", fn: "sheltered", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -205,8 +251,8 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "Is this unit specifically designed for older people?", fn: "olderPersons", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts index bd618a4c6d..2c27776df0 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts @@ -30,26 +30,39 @@ export const ResidentialUnitsGLARemoved: Schema = { id: "AR", data: { text: "Affordable rent (not at LAR benchmark rents)" }, }, - { id: "SR", data: { text: "Social rent" } }, - { id: "LRR", data: { text: "London Living Rent" } }, - { id: "sharedEquity", data: { text: "Shared equity" } }, - { id: "LSO", data: { text: "London Shared Ownership" } }, - { id: "DMS", data: { text: "Discount market sale" } }, - { id: "DMR", data: { text: "Discount market rent" } }, + { id: "SR", data: { text: "Social rent", val: "SR" } }, + { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, + { + id: "sharedEquity", + data: { text: "Shared equity", val: "sharedEquity" }, + }, + { id: "LSO", data: { text: "London Shared Ownership", val: "LSO" } }, + { id: "DMS", data: { text: "Discount market sale", val: "DMS" } }, + { id: "DMR", data: { text: "Discount market rent", val: "DMR" } }, { id: "DMRLLR", data: { text: "Discount market rent (charged at London Living Rents)", + val: "DMRLLR", }, }, - { id: "marketForRent", data: { text: "Market for rent" } }, - { id: "SH", data: { text: "Starter homes" } }, + { + id: "marketForRent", + data: { text: "Market for rent", val: "marketForRent" }, + }, + { id: "SH", data: { text: "Starter homes", val: "SH" } }, { id: "selfCustomBuild", - data: { text: "Self-build and custom build" }, + data: { + text: "Self-build and custom build", + val: "selfCustomBuild", + }, }, - { id: "marketForSale", data: { text: "Market for sale" } }, - { id: "other", data: { text: "Other" } }, + { + id: "marketForSale", + data: { text: "Market for sale", val: "marketForSale" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -82,8 +95,8 @@ export const ResidentialUnitsGLARemoved: Schema = { "Is this unit compliant with Part M4(2) of the Building Regulations 2010?", fn: "complianceM42", // compliance.m42 options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -94,8 +107,8 @@ export const ResidentialUnitsGLARemoved: Schema = { "Is this unit compliant with Part M4(3)(2a) of the Building Regulations 2010?", fn: "complianceM432a", // compliance.m432a options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -106,8 +119,8 @@ export const ResidentialUnitsGLARemoved: Schema = { "Is this unit compliant with Part M4(3)(2b) of the Building Regulations 2010?", fn: "complianceM432b", // compliance.m432b options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -117,18 +130,27 @@ export const ResidentialUnitsGLARemoved: Schema = { title: "What best describes the type of this unit?", fn: "type", options: [ - { id: "terraced", data: { text: "Terraced home" } }, - { id: "semiDetached", data: { text: "Semi detached home" } }, - { id: "detached", data: { text: "Detached home" } }, - { id: "flat", data: { text: "Flat/apartment or maisonette" } }, - { id: "LW", data: { text: "Live/work unit" } }, - { id: "cluster", data: { text: "Cluster flat" } }, - { id: "studio", data: { text: "Studio or bedsit" } }, - { id: "coLiving", data: { text: "Co living unit" } }, - { id: "hostel", data: { text: "Hostel room" } }, - { id: "HMO", data: { text: "HMO" } }, - { id: "student", data: { text: "Student accomodation" } }, - { id: "other", data: { text: "Other" } }, + { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, + { + id: "semiDetached", + data: { text: "Semi detached home", val: "semiDetached" }, + }, + { id: "detached", data: { text: "Detached home", val: "detached" } }, + { + id: "flat", + data: { text: "Flat/apartment or maisonette", val: "flat" }, + }, + { id: "LW", data: { text: "Live/work unit", val: "LW" } }, + { id: "cluster", data: { text: "Cluster flat", val: "cluster" } }, + { id: "studio", data: { text: "Studio or bedsit", val: "studio" } }, + { id: "coLiving", data: { text: "Co living unit", val: "coLiving" } }, + { id: "hostel", data: { text: "Hostel room", val: "hostel" } }, + { id: "HMO", data: { text: "HMO", val: "HMO" } }, + { + id: "student", + data: { text: "Student accomodation", val: "student" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -138,21 +160,36 @@ export const ResidentialUnitsGLARemoved: Schema = { title: "What best describes the provider of this unit?", fn: "provider", options: [ - { id: "private", data: { text: "Private" } }, - { id: "privateRented", data: { text: "Private rented sector" } }, - { id: "HA", data: { text: "Housing association" } }, - { id: "LA", data: { text: "Local authority" } }, - { id: "publicAuthority", data: { text: "Other public authority" } }, - { id: "councilDelivery", data: { text: "Council delivery company" } }, + { id: "private", data: { text: "Private", val: "private" } }, + { + id: "privateRented", + data: { text: "Private rented sector", val: "privateRented" }, + }, + { id: "HA", data: { text: "Housing association", val: "HA" } }, + { id: "LA", data: { text: "Local authority", val: "LA" } }, + { + id: "publicAuthority", + data: { text: "Other public authority", val: "publicAuthority" }, + }, + { + id: "councilDelivery", + data: { text: "Council delivery company", val: "councilDelivery" }, + }, { id: "councilBuildToRent", - data: { text: "Council delivered build to rent" }, + data: { + text: "Council delivered build to rent", + val: "councilBuildToRent", + }, }, { id: "affordableHousing", - data: { text: "Other affordable housing provider" }, + data: { + text: "Other affordable housing provider", + val: "affordableHousing", + }, }, - { id: "selfBuild", data: { text: "Self-build" } }, + { id: "selfBuild", data: { text: "Self-build", val: "selfBuild" } }, ], }, }, @@ -171,8 +208,8 @@ export const ResidentialUnitsGLARemoved: Schema = { title: "Will this unit provide sheltered accommodation?", fn: "sheltered", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, @@ -182,8 +219,8 @@ export const ResidentialUnitsGLARemoved: Schema = { title: "Is this unit specifically designed for older people?", fn: "olderPersons", options: [ - { id: "true", data: { text: "Yes" } }, - { id: "false", data: { text: "No" } }, + { id: "true", data: { text: "Yes", val: "true" } }, + { id: "false", data: { text: "No", val: "false" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts index 871aa45029..013ba653c7 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts @@ -22,26 +22,39 @@ export const ResidentialUnitsGLARetained: Schema = { id: "AR", data: { text: "Affordable rent (not at LAR benchmark rents)" }, }, - { id: "SR", data: { text: "Social rent" } }, - { id: "LRR", data: { text: "London Living Rent" } }, - { id: "sharedEquity", data: { text: "Shared equity" } }, - { id: "LSO", data: { text: "London Shared Ownership" } }, - { id: "DMS", data: { text: "Discount market sale" } }, - { id: "DMR", data: { text: "Discount market rent" } }, + { id: "SR", data: { text: "Social rent", val: "SR" } }, + { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, + { + id: "sharedEquity", + data: { text: "Shared equity", val: "sharedEquity" }, + }, + { id: "LSO", data: { text: "London Shared Ownership", val: "LSO" } }, + { id: "DMS", data: { text: "Discount market sale", val: "DMS" } }, + { id: "DMR", data: { text: "Discount market rent", val: "DMR" } }, { id: "DMRLLR", data: { text: "Discount market rent (charged at London Living Rents)", + val: "DMRLLR", }, }, - { id: "marketForRent", data: { text: "Market for rent" } }, - { id: "SH", data: { text: "Starter homes" } }, + { + id: "marketForRent", + data: { text: "Market for rent", val: "marketForRent" }, + }, + { id: "SH", data: { text: "Starter homes", val: "SH" } }, { id: "selfCustomBuild", - data: { text: "Self-build and custom build" }, + data: { + text: "Self-build and custom build", + val: "selfCustomBuild", + }, + }, + { + id: "marketForSale", + data: { text: "Market for sale", val: "marketForSale" }, }, - { id: "marketForSale", data: { text: "Market for sale" } }, - { id: "other", data: { text: "Other" } }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, @@ -51,18 +64,27 @@ export const ResidentialUnitsGLARetained: Schema = { title: "What best describes the type of this unit?", fn: "type", options: [ - { id: "terraced", data: { text: "Terraced home" } }, - { id: "semiDetached", data: { text: "Semi detached home" } }, - { id: "detached", data: { text: "Detached home" } }, - { id: "flat", data: { text: "Flat/apartment or maisonette" } }, - { id: "LW", data: { text: "Live/work unit" } }, - { id: "cluster", data: { text: "Cluster flat" } }, - { id: "studio", data: { text: "Studio or bedsit" } }, - { id: "coLiving", data: { text: "Co living unit" } }, - { id: "hostel", data: { text: "Hostel room" } }, - { id: "HMO", data: { text: "HMO" } }, - { id: "student", data: { text: "Student accomodation" } }, - { id: "other", data: { text: "Other" } }, + { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, + { + id: "semiDetached", + data: { text: "Semi detached home", val: "semiDetached" }, + }, + { id: "detached", data: { text: "Detached home", val: "detached" } }, + { + id: "flat", + data: { text: "Flat/apartment or maisonette", val: "flat" }, + }, + { id: "LW", data: { text: "Live/work unit", val: "LW" } }, + { id: "cluster", data: { text: "Cluster flat", val: "cluster" } }, + { id: "studio", data: { text: "Studio or bedsit", val: "studio" } }, + { id: "coLiving", data: { text: "Co living unit", val: "coLiving" } }, + { id: "hostel", data: { text: "Hostel room", val: "hostel" } }, + { id: "HMO", data: { text: "HMO", val: "HMO" } }, + { + id: "student", + data: { text: "Student accomodation", val: "student" }, + }, + { id: "other", data: { text: "Other", val: "other" } }, ], }, }, diff --git a/editor.planx.uk/src/@planx/components/List/utils.ts b/editor.planx.uk/src/@planx/components/List/utils.ts index 231f676f76..220c5fc1f5 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.ts +++ b/editor.planx.uk/src/@planx/components/List/utils.ts @@ -1,3 +1,5 @@ +import { QuestionField, Schema } from "./model"; + // Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}` // Adapted from https://gist.github.com/penguinboy/762197 export function flatten>( @@ -85,3 +87,19 @@ function convertNumberToText(num: number): string { return convertTens(num); } } + +// In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport +export function formatSchemaDisplayValue(value: string, schema: Schema) { + const questionFields = schema.fields.filter( + (field) => field.type === "question", + ) as QuestionField[]; + const matchingField = questionFields?.find((field) => + field.data.options.some((option) => option.data.val === value), + ); + const matchingOption = matchingField?.data.options.find( + (option) => option.data.val === value, + ); + + // If we found a "val" match, return its text, else just return the value as passed in + return matchingOption?.data?.text || value; +} From a7f59f8fd0b73d249c30b9a6472ec56c066a0117 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 10 Jun 2024 22:39:00 +0200 Subject: [PATCH 2/4] stub out generic units test --- .../components/List/Public/index.test.tsx | 31 ++++++++- .../List/schemas/GenericUnitsTest.ts | 67 +++++++++++++++++++ .../List/schemas/ResidentialUnits/GLA/New.ts | 7 +- .../schemas/ResidentialUnits/GLA/Rebuilt.ts | 7 +- .../schemas/ResidentialUnits/GLA/Removed.ts | 7 +- .../schemas/ResidentialUnits/GLA/Retained.ts | 7 +- 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx index ee007bca9d..248d83dc80 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx @@ -5,6 +5,7 @@ import React from "react"; import { axe, setup } from "testUtils"; import ListComponent, { Props } from "../Public"; +import { GenericUnitsTest } from "../schemas/GenericUnitsTest"; import { Zoo } from "../schemas/Zoo"; const mockProps: Props = { @@ -47,6 +48,19 @@ const mockPayload = { }, }; +const mockPropsUnits: Props = { + fn: "proposal.units.residential", + schema: GenericUnitsTest, + schemaName: "Generic residential units", + title: "Describe residential units", +}; + +const mockPayloadUnits = { + data: { + "proposal.units.residential": [], + }, +}; + jest.setTimeout(20_000); describe("Basic UI", () => { @@ -378,7 +392,7 @@ describe("Form validation and error handling", () => { }); describe("Payload generation", () => { - it("generates a valid payload on submission", async () => { + it("generates a valid payload on submission (Zoo)", async () => { const handleSubmit = jest.fn(); const { getByTestId, user } = setup( , @@ -395,6 +409,21 @@ describe("Payload generation", () => { expect(handleSubmit).toHaveBeenCalled(); expect(handleSubmit.mock.calls[0][0]).toMatchObject(mockPayload); }); + + it.skip("generates a valid payload with summary stats on submission (Units)", async () => { + const handleSubmit = jest.fn(); + const { getByTestId, user } = setup( + , + ); + const addItemButton = getByTestId("list-add-button"); + + // fill in three unique responses + + await user.click(screen.getByTestId("continue-button")); + + expect(handleSubmit).toHaveBeenCalled(); + expect(handleSubmit.mock.calls[0][0]).toMatchObject(mockPayloadUnits); + }); }); describe("Navigating back", () => { diff --git a/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts b/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts new file mode 100644 index 0000000000..e7551a41ac --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts @@ -0,0 +1,67 @@ +import { Schema } from "@planx/components/List/model"; + +export const GenericUnitsTest: Schema = { + type: "Unit", + fields: [ + // fn = "development" triggers summary stat + { + type: "question", + data: { + title: "What development does this unit result from?", + fn: "development", + options: [ + { id: "newBuild", data: { text: "New build", val: "newBuild" } }, + { + id: "changeOfUseFrom", + data: { + text: "Change of use of existing single home", + val: "changeOfUseFrom", + }, + }, + { + id: "changeOfUseTo", + data: { text: "Change of use to a home", val: "changeOfUseTo" }, + }, + ], + }, + }, + // options set "val" + { + type: "question", + data: { + title: "What best describes the type of this unit?", + fn: "type", + options: [ + { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, + { + id: "semiDetached", + data: { text: "Semi detached home", val: "semiDetached" }, + }, + { id: "detached", data: { text: "Detached home", val: "detached" } }, + ], + }, + }, + // options set "text" only + { + type: "question", + data: { + title: "Is this unit built on garden land?", + fn: "garden", + options: [ + { id: "true", data: { text: "Yes" } }, + { id: "false", data: { text: "No" } }, + ], + }, + }, + // fn = "identicalUnits" triggers summary stat + { + type: "number", + data: { + title: "How many identical units does the description above apply to?", + fn: "identicalUnits", + allowNegatives: false, + }, + }, + ], + min: 1, +} as const; diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts index 8b4e305ab6..88f92a1aee 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/New.ts @@ -46,10 +46,13 @@ export const ResidentialUnitsGLANew: Schema = { title: "Which best describes the tenure of this unit?", fn: "tenure", options: [ - { id: "LAR", data: { text: "London Affordable Rent" } }, + { id: "LAR", data: { text: "London Affordable Rent", val: "LAR" } }, { id: "AR", - data: { text: "Affordable rent (not at LAR benchmark rents)" }, + data: { + text: "Affordable rent (not at LAR benchmark rents)", + val: "AR", + }, }, { id: "SR", data: { text: "Social rent", val: "SR" } }, { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts index 753034fbcb..485d0aa04e 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Rebuilt.ts @@ -46,10 +46,13 @@ export const ResidentialUnitsGLARebuilt: Schema = { title: "Which best describes the tenure of this unit?", fn: "tenure", options: [ - { id: "LAR", data: { text: "London Affordable Rent" } }, + { id: "LAR", data: { text: "London Affordable Rent", val: "LAR" } }, { id: "AR", - data: { text: "Affordable rent (not at LAR benchmark rents)" }, + data: { + text: "Affordable rent (not at LAR benchmark rents)", + val: "AR", + }, }, { id: "SR", data: { text: "Social rent", val: "SR" } }, { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts index 2c27776df0..ee7027875c 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Removed.ts @@ -25,10 +25,13 @@ export const ResidentialUnitsGLARemoved: Schema = { title: "Which best describes the tenure of this unit?", fn: "tenure", options: [ - { id: "LAR", data: { text: "London Affordable Rent" } }, + { id: "LAR", data: { text: "London Affordable Rent", val: "LAR" } }, { id: "AR", - data: { text: "Affordable rent (not at LAR benchmark rents)" }, + data: { + text: "Affordable rent (not at LAR benchmark rents)", + val: "AR", + }, }, { id: "SR", data: { text: "Social rent", val: "SR" } }, { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, diff --git a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts index 013ba653c7..70c4bbfbe8 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/ResidentialUnits/GLA/Retained.ts @@ -17,10 +17,13 @@ export const ResidentialUnitsGLARetained: Schema = { title: "Which best describes the tenure of this unit?", fn: "tenure", options: [ - { id: "LAR", data: { text: "London Affordable Rent" } }, + { id: "LAR", data: { text: "London Affordable Rent", val: "LAR" } }, { id: "AR", - data: { text: "Affordable rent (not at LAR benchmark rents)" }, + data: { + text: "Affordable rent (not at LAR benchmark rents)", + val: "AR", + }, }, { id: "SR", data: { text: "Social rent", val: "SR" } }, { id: "LRR", data: { text: "London Living Rent", val: "LRR" } }, From e28156fa7a8611e58a488a81b561d36a3c52d927 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Mon, 10 Jun 2024 22:44:00 +0200 Subject: [PATCH 3/4] better mock --- .../List/schemas/GenericUnitsTest.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts b/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts index e7551a41ac..f8f7d45076 100644 --- a/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts +++ b/editor.planx.uk/src/@planx/components/List/schemas/GenericUnitsTest.ts @@ -3,7 +3,7 @@ import { Schema } from "@planx/components/List/model"; export const GenericUnitsTest: Schema = { type: "Unit", fields: [ - // fn = "development" triggers summary stat + // fn = "development" triggers summary stat and options set "val" { type: "question", data: { @@ -25,22 +25,6 @@ export const GenericUnitsTest: Schema = { ], }, }, - // options set "val" - { - type: "question", - data: { - title: "What best describes the type of this unit?", - fn: "type", - options: [ - { id: "terraced", data: { text: "Terraced home", val: "terraced" } }, - { - id: "semiDetached", - data: { text: "Semi detached home", val: "semiDetached" }, - }, - { id: "detached", data: { text: "Detached home", val: "detached" } }, - ], - }, - }, // options set "text" only { type: "question", From 0ade5c081eaad6561b14fc0b86d2977d9bb8b9b8 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Tue, 11 Jun 2024 10:39:25 +0200 Subject: [PATCH 4/4] add tests, tidy up --- .../@planx/components/List/Public/Context.tsx | 46 +++------ .../components/List/Public/index.test.tsx | 68 ++++++++++++- .../src/@planx/components/List/model.ts | 2 +- .../src/@planx/components/List/utils.test.ts | 77 +++++++++++++++ .../src/@planx/components/List/utils.ts | 98 +++++++++++++++---- 5 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 editor.planx.uk/src/@planx/components/List/utils.test.ts diff --git a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx index af4792df0e..68920e56b2 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/Context.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/Context.tsx @@ -18,7 +18,11 @@ import { Schema, UserData, } from "../model"; -import { flatten } from "../utils"; +import { + flatten, + sumIdenticalUnits, + sumIdenticalUnitsByDevelopmentType, +} from "../utils"; interface ListContextValue { schema: Schema; @@ -132,7 +136,7 @@ export const ListProvider: React.FC = (props) => { userData: getInitialValues(), }, onSubmit: (values) => { - // defaultPassportData is used when coming "back" + // defaultPassportData (array) is used when coming "back" const defaultPassportData = makeData(props, values.userData)?.["data"]; // flattenedPassportData makes individual list items compatible with Calculate components @@ -141,40 +145,22 @@ export const ListProvider: React.FC = (props) => { // basic example of general summary stats we can add onSubmit: // 1. count of items/responses // 2. if the schema includes a field that sets fn = "identicalUnits", sum of total units - // 3. if the schema includes a field that sets fn = "development", sum of total units by development "val" - let sumIdenticalUnits = 0; - defaultPassportData[`${props.fn}`].map( - (item) => (sumIdenticalUnits += parseInt(item?.identicalUnits)), + // 3. if the schema includes a field that sets fn = "development" & fn = "identicalUnits", sum of total units by development "val" + const totalUnits = sumIdenticalUnits(props.fn, defaultPassportData); + const totalUnitsByDevelopmentType = sumIdenticalUnitsByDevelopmentType( + props.fn, + defaultPassportData, ); - const sumIdenticalUnitsByDevelopmentType: Record = { - newBuild: 0, - changeOfUseFrom: 0, - changeOfUseTo: 0, - }; - defaultPassportData[`${props.fn}`].map( - (item) => - (sumIdenticalUnitsByDevelopmentType[`${item?.development}`] += - parseInt(item?.identicalUnits)), - ); - const sumIdenticalUnitsByDevelopmentTypeSummary: Record = - {}; - Object.entries(sumIdenticalUnitsByDevelopmentType).forEach(([k, v]) => { - if (v > 0) { - sumIdenticalUnitsByDevelopmentTypeSummary[ - `${props.fn}.total.units.development.${k}` - ] = v; - } - }); - const summaries = { [`${props.fn}.total.listItems`]: defaultPassportData[`${props.fn}`].length, - ...(sumIdenticalUnits > 0 && { - [`${props.fn}.total.units`]: sumIdenticalUnits, + ...(totalUnits > 0 && { + [`${props.fn}.total.units`]: totalUnits, }), - ...(Object.keys(sumIdenticalUnitsByDevelopmentTypeSummary).length > 0 && - sumIdenticalUnitsByDevelopmentTypeSummary), + ...(totalUnits > 0 && + Object.keys(totalUnitsByDevelopmentType).length > 0 && + totalUnitsByDevelopmentType), }; handleSubmit?.({ diff --git a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx index 248d83dc80..2f72a1e8f9 100644 --- a/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx +++ b/editor.planx.uk/src/@planx/components/List/Public/index.test.tsx @@ -4,9 +4,15 @@ import { cloneDeep, merge } from "lodash"; import React from "react"; import { axe, setup } from "testUtils"; +import { UserResponse } from "../model"; import ListComponent, { Props } from "../Public"; import { GenericUnitsTest } from "../schemas/GenericUnitsTest"; import { Zoo } from "../schemas/Zoo"; +import { + flatten, + sumIdenticalUnits, + sumIdenticalUnitsByDevelopmentType, +} from "../utils"; const mockProps: Props = { fn: "mockFn", @@ -57,7 +63,36 @@ const mockPropsUnits: Props = { const mockPayloadUnits = { data: { - "proposal.units.residential": [], + "proposal.units.residential": [ + { + development: "newBuild", + garden: "Yes", + identicalUnits: 1, + }, + { + development: "newBuild", + garden: "No", + identicalUnits: 2, + }, + { + development: "changeOfUseTo", + garden: "No", + identicalUnits: 2, + }, + ], + "proposal.units.residential.one.development": "newBuild", + "proposal.units.residential.one.garden": "Yes", + "proposal.units.residential.one.identicalUnits": 1, + "proposal.units.residential.two.development": "newBuild", + "proposal.units.residential.two.garden": "No", + "proposal.units.residential.two.identicalUnits": 2, + "proposal.units.residential.three.development": "changeOfUseTo", + "proposal.units.residential.three.garden": "No", + "proposal.units.residential.three.identicalUnits": 2, + "proposal.units.residential.total.listItems": 3, + "proposal.units.residential.total.units": 5, + "proposal.units.residential.total.units.newBuid": 3, + "proposal.units.residential.total.units.changeOfUseTo": 2, }, }; @@ -415,9 +450,38 @@ describe("Payload generation", () => { const { getByTestId, user } = setup( , ); + + const saveButton = screen.getByRole("button", { name: /Save/ }); const addItemButton = getByTestId("list-add-button"); + const developmentSelect = screen.getByRole("combobox"); + const gardenYesRadio = screen.getAllByRole("radio")[0]; + const gardenNoRadio = screen.getAllByRole("radio")[1]; + const unitsNumberInput = screen.getByLabelText(/identical units/); + + // Response 1 + await user.click(developmentSelect); + await user.click(screen.getByRole("option", { name: /New build/ })); + await user.click(gardenYesRadio); + await user.type(unitsNumberInput, "1"); + await user.click(saveButton); + + // Response 2 + await user.click(addItemButton); + await user.click(developmentSelect); + await user.click(screen.getByRole("option", { name: /New build/ })); + await user.click(gardenNoRadio); + await user.type(unitsNumberInput, "2"); + await user.click(saveButton); - // fill in three unique responses + // Response 3 + await user.click(addItemButton); + await user.click(developmentSelect); + await user.click( + screen.getByRole("option", { name: /Change of use to a home/ }), + ); + await user.click(gardenNoRadio); + await user.type(unitsNumberInput, "2"); + await user.click(saveButton); await user.click(screen.getByTestId("continue-button")); diff --git a/editor.planx.uk/src/@planx/components/List/model.ts b/editor.planx.uk/src/@planx/components/List/model.ts index d35532cf9a..89ad950cfc 100644 --- a/editor.planx.uk/src/@planx/components/List/model.ts +++ b/editor.planx.uk/src/@planx/components/List/model.ts @@ -62,7 +62,7 @@ export interface Schema { max?: number; } -type UserResponse = Record; +export type UserResponse = Record; export type UserData = { userData: UserResponse[] }; diff --git a/editor.planx.uk/src/@planx/components/List/utils.test.ts b/editor.planx.uk/src/@planx/components/List/utils.test.ts new file mode 100644 index 0000000000..5fdb8abc68 --- /dev/null +++ b/editor.planx.uk/src/@planx/components/List/utils.test.ts @@ -0,0 +1,77 @@ +import { UserResponse } from "./model"; +import { + flatten, + sumIdenticalUnits, + sumIdenticalUnitsByDevelopmentType, +} from "./utils"; + +describe("passport data shape", () => { + it("flattens list items", async () => { + const defaultPassportData = { + mockFn: [ + { + age: 10, + cuteness: "Very", + email: "richard.parker@pi.com", + name: "Richard Parker", + size: "Medium", + }, + { + age: 10, + cuteness: "Very", + email: "richard.parker@pi.com", + name: "Richard Parker", + size: "Medium", + }, + ], + }; + + expect(flatten(defaultPassportData)).toEqual({ + "mockFn.one.age": 10, + "mockFn.one.cuteness": "Very", + "mockFn.one.email": "richard.parker@pi.com", + "mockFn.one.name": "Richard Parker", + "mockFn.one.size": "Medium", + "mockFn.two.age": 10, + "mockFn.two.cuteness": "Very", + "mockFn.two.email": "richard.parker@pi.com", + "mockFn.two.name": "Richard Parker", + "mockFn.two.size": "Medium", + }); + }); + + it("adds summary stats when applicable fields are set", async () => { + const defaultPassportData = { + "proposal.units.residential": [ + { + development: "newBuild", + garden: "Yes", + identicalUnits: 1, + }, + { + development: "newBuild", + garden: "No", + identicalUnits: 2, + }, + { + development: "changeOfUseTo", + garden: "No", + identicalUnits: 2, + }, + ], + } as unknown as Record; + + expect( + sumIdenticalUnits("proposal.units.residential", defaultPassportData), + ).toEqual(5); + expect( + sumIdenticalUnitsByDevelopmentType( + "proposal.units.residential", + defaultPassportData, + ), + ).toEqual({ + "proposal.units.residential.total.units.development.newBuild": 3, + "proposal.units.residential.total.units.development.changeOfUseTo": 2, + }); + }); +}); diff --git a/editor.planx.uk/src/@planx/components/List/utils.ts b/editor.planx.uk/src/@planx/components/List/utils.ts index 220c5fc1f5..b5c8226afa 100644 --- a/editor.planx.uk/src/@planx/components/List/utils.ts +++ b/editor.planx.uk/src/@planx/components/List/utils.ts @@ -1,7 +1,77 @@ -import { QuestionField, Schema } from "./model"; +import { QuestionField, Schema, UserResponse } from "./model"; -// Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}` -// Adapted from https://gist.github.com/penguinboy/762197 +/** + * In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport + * @param value - the `val` or `text` of an Option defined in the schema's fields + * @param schema - the Schema object + * @returns string - the `text` for the given value `val`, or the original value + */ +export function formatSchemaDisplayValue(value: string, schema: Schema) { + const questionFields = schema.fields.filter( + (field) => field.type === "question", + ) as QuestionField[]; + const matchingField = questionFields?.find((field) => + field.data.options.some((option) => option.data.val === value), + ); + const matchingOption = matchingField?.data.options.find( + (option) => option.data.val === value, + ); + + // If we found a "val" match, return its text, else just return the value as passed in + return matchingOption?.data?.text || value; +} + +/** + * If the schema includes a field that sets fn = "identicalUnits", sum of total units + * @param fn - passport key of current List + * @param passportData - default passport data format for the List + * @returns - sum of all units, or 0 if field not set + */ +export function sumIdenticalUnits( + fn: string, + passportData: Record, +): number { + let sum = 0; + passportData[`${fn}`].map((item) => (sum += parseInt(item?.identicalUnits))); + return sum; +} + +/** + * If the schema includes fields that set fn = "development" and fn = "identicalUnits", sum of total units by development option "val" + * @param fn - passport key of current List + * @param passportData - default passport data format for the List + * @returns - sum of all units by development type, or empty object if fields not set + */ +export function sumIdenticalUnitsByDevelopmentType( + fn: string, + passportData: Record, +): Record { + // Sum identical units by development type (read option `val` from Schema in future?) + const baseSums: Record = { + newBuild: 0, + changeOfUseFrom: 0, + changeOfUseTo: 0, + }; + passportData[`${fn}`].map( + (item) => + (baseSums[`${item?.development}`] += parseInt(item?.identicalUnits)), + ); + + // Format property names for passport, and filter out any entries with default sum = 0 + const formattedSums: Record = {}; + Object.entries(baseSums).forEach(([k, v]) => { + if (v > 0) { + formattedSums[`${fn}.total.units.development.${k}`] = v; + } + }); + + return formattedSums; +} + +/** + * Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}` + * Adapted from https://gist.github.com/penguinboy/762197 + */ export function flatten>( object: T, path: string | null = null, @@ -30,8 +100,6 @@ export function flatten>( }, {} as T); } -// Convert a whole number up to 99 to a spelled-out word (eg 34 => 'thirtyfour') -// Adapted from https://stackoverflow.com/questions/5529934/javascript-numbers-to-words const ones = [ "", "one", @@ -80,6 +148,10 @@ function convertTens(num: number): string { } } +/** + * Convert a whole number up to 99 to a spelled-out word (eg 34 => 'thirtyfour') + * Adapted from https://stackoverflow.com/questions/5529934/javascript-numbers-to-words + */ function convertNumberToText(num: number): string { if (num == 0) { return "zero"; @@ -87,19 +159,3 @@ function convertNumberToText(num: number): string { return convertTens(num); } } - -// In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport -export function formatSchemaDisplayValue(value: string, schema: Schema) { - const questionFields = schema.fields.filter( - (field) => field.type === "question", - ) as QuestionField[]; - const matchingField = questionFields?.find((field) => - field.data.options.some((option) => option.data.val === value), - ); - const matchingOption = matchingField?.data.options.find( - (option) => option.data.val === value, - ); - - // If we found a "val" match, return its text, else just return the value as passed in - return matchingOption?.data?.text || value; -}