Skip to content

Commit

Permalink
fix: Paper Application not saving lat long (bloom-housing#3828)
Browse files Browse the repository at this point in the history
* fix: add map ui to include lat and long in paper application preference options

* refactor: remove redundant class

* fix: trigger geocoding validation on listing update
  • Loading branch information
KrissDrawing authored and ludtkemorgan committed Jan 26, 2024
1 parent 3c1b008 commit 0c6eea6
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 3 deletions.
11 changes: 11 additions & 0 deletions backend/core/src/applications/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ export class ApplicationsService {
return await applicationsRepository.findOne({ where: { id: newApplication.id } })
}
)

const listing = await this.listingsService.findOne(application.listingId)

// Calculate geocoding preferences after save
if (listing.jurisdiction?.enableGeocodingPreferences) {
try {
void this.geocodingService.validateGeocodingPreferences(application, listing)
} catch (e) {
console.warn("error while validating geocoding preferences")
}
}
return app
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Expose, Type } from "class-transformer"
import { ArrayMaxSize, IsBoolean, IsString, ValidateNested } from "class-validator"
import { ArrayMaxSize, IsBoolean, IsOptional, IsString, ValidateNested } from "class-validator"
import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum"
import { ApiProperty, getSchemaPath } from "@nestjs/swagger"
import { BooleanInput } from "./form-metadata/boolean-input"
Expand All @@ -19,6 +19,12 @@ export class ApplicationMultiselectQuestionOption {
@ApiProperty()
checked: boolean

@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty({ required: false })
mapPinPosition?: string

@Expose()
@ApiProperty({
type: "array",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export class MultiselectOption {
@ApiProperty({ required: false })
collectRelationship?: boolean

@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsString({ groups: [ValidationsGroupsEnum.default] })
@ApiProperty({ required: false })
mapPinPosition?: string

@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsBoolean({ groups: [ValidationsGroupsEnum.default] })
Expand Down
6 changes: 6 additions & 0 deletions backend/core/types/src/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2674,6 +2674,9 @@ export interface ApplicationMultiselectQuestionOption {
/** */
checked: boolean

/** */
mapPinPosition?: string

/** */
extraData?: AllExtraDataTypes[]
}
Expand Down Expand Up @@ -4565,6 +4568,9 @@ export interface MultiselectOption {
/** */
collectRelationship?: boolean

/** */
mapPinPosition?: string

/** */
exclusive?: boolean
}
Expand Down
6 changes: 5 additions & 1 deletion shared-helpers/src/views/multiselectQuestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ export const mapCheckboxesToApi = (
const addressHolderRelationshipData = addressFields.filter(
(addressField) => addressField === `${key}-${AddressHolder.Relationship}`
)
if (addressData.length) {
if (data[key] === true && addressData.length) {
extraData.push({ type: InputType.address, key: "address", value: data[addressData[0]] })

if (addressHolderNameData.length) {
Expand All @@ -380,6 +380,7 @@ export const mapCheckboxesToApi = (

return {
key,
mapPinPosition: data?.[`${key}-mapPinPosition`],
checked: data[key] === true,
extraData: extraData,
}
Expand Down Expand Up @@ -450,6 +451,9 @@ export const mapApiToMultiselectForm = (
if (addressHolderRelationship) {
acc[`${curr.key}-${AddressHolder.Relationship}`] = addressHolderRelationship.value
}
if (curr?.mapPinPosition) {
acc[`${curr.key}-mapPinPosition`] = curr.mapPinPosition
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import React, { useEffect, useState } from "react"
import { FieldGroup, LatitudeLongitude, ListingMap, t } from "@bloom-housing/ui-components"
import { FieldValue, Grid } from "@bloom-housing/ui-seeds"
import { useFormContext, useWatch } from "react-hook-form"
import { GeocodeService as GeocodeServiceType } from "@mapbox/mapbox-sdk/services/geocoding"

interface MapBoxFeature {
center: number[] // Index 0: longitude, Index 1: latitude
}

interface MapboxApiResponseBody {
features: MapBoxFeature[]
}

interface MapboxApiResponse {
body: MapboxApiResponseBody
}

interface BuildingAddress {
city: string
state: string
street: string
zipCode: string
longitude?: number
latitude?: number
}

type MultiselectQuestionsMapProps = {
geocodingClient: GeocodeServiceType
dataKey: string
}

const MultiselectQuestionsMap = ({ geocodingClient, dataKey }: MultiselectQuestionsMapProps) => {
const [customMapPositionChosen, setCustomMapPositionChosen] = useState(true)
const formMethods = useFormContext()

// eslint-disable-next-line @typescript-eslint/unbound-method
const { register, control, getValues, setValue, watch } = formMethods

const buildingAddress: BuildingAddress = useWatch({
control,
name: `${dataKey}-address`,
})
const mapPinPosition = useWatch({
control,
name: `${dataKey}-mapPinPosition`,
})

const [latLong, setLatLong] = useState<LatitudeLongitude>({
latitude: buildingAddress?.latitude ?? null,
longitude: buildingAddress?.longitude ?? null,
})

const displayMapPreview = () => {
return (
buildingAddress?.city &&
buildingAddress?.state &&
buildingAddress?.street &&
buildingAddress?.zipCode &&
buildingAddress?.zipCode.length >= 5
)
}

const getNewLatLong = () => {
if (
buildingAddress?.city &&
buildingAddress?.state &&
buildingAddress?.street &&
buildingAddress?.zipCode &&
geocodingClient
) {
geocodingClient
.forwardGeocode({
query: `${buildingAddress.street}, ${buildingAddress.city}, ${buildingAddress.state}, ${buildingAddress.zipCode}`,
limit: 1,
})
.send()
.then((response: MapboxApiResponse) => {
setLatLong({
latitude: response.body.features[0].center[1],
longitude: response.body.features[0].center[0],
})
})
.catch((err) => console.error(`Error calling Mapbox API: ${err}`))
}
}

if (
getValues(`${dataKey}-address.latitude`) !== latLong.latitude ||
getValues(`${dataKey}-address.longitude`) !== latLong.longitude
) {
setValue(`${dataKey}-address.latitude`, latLong.latitude)
setValue(`${dataKey}-address.longitude`, latLong.longitude)
}

useEffect(() => {
if (watch(dataKey)) {
register(`${dataKey}-address.longitude`)
register(`${dataKey}-address.latitude`)
}
}, [dataKey, register, setValue, watch])

useEffect(() => {
let timeout
if (!customMapPositionChosen || mapPinPosition === "automatic") {
timeout = setTimeout(() => {
getNewLatLong()
}, 1000)
}
return () => {
clearTimeout(timeout)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
buildingAddress?.city,
buildingAddress?.state,
buildingAddress?.street,
buildingAddress?.zipCode,
])

useEffect(() => {
if (mapPinPosition === "automatic") {
getNewLatLong()
setCustomMapPositionChosen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mapPinPosition])

return (
<>
<Grid.Row>
<FieldValue label={t("listings.mapPreview")}>
{displayMapPreview() ? (
<ListingMap
address={{
city: buildingAddress.city,
state: buildingAddress.state,
street: buildingAddress.street,
zipCode: buildingAddress.zipCode,
latitude: buildingAddress.latitude,
longitude: buildingAddress.longitude,
}}
enableCustomPinPositioning={mapPinPosition === "custom"}
setCustomMapPositionChosen={setCustomMapPositionChosen}
setLatLong={setLatLong}
/>
) : (
<div
className={"w-full bg-gray-400 p-3 flex items-center justify-center"}
style={{ height: "400px" }}
>
{t("listings.mapPreviewNoAddress")}
</div>
)}
</FieldValue>
</Grid.Row>
<Grid.Row>
<p className="field-label m-4 ml-0">{t("listings.mapPinPosition")}</p>
</Grid.Row>
<Grid.Row>
<FieldGroup
name={`${dataKey}-mapPinPosition`}
type="radio"
fieldGroupClassName={"flex-col"}
fieldClassName={"ml-0"}
register={register}
fields={[
{
label: t("t.automatic"),
value: "automatic",
id: `${dataKey}-mapPinPosition-automatic`,
note: t("listings.mapPinAutomaticDescription"),
defaultChecked: mapPinPosition === "automatic" || mapPinPosition === undefined,
},
{
label: t("t.custom"),
value: "custom",
id: `${dataKey}-mapPinPosition-custom`,
note: t("listings.mapPinCustomDescription"),
defaultChecked: mapPinPosition === "custom",
},
]}
/>
</Grid.Row>
</>
)
}

export default MultiselectQuestionsMap
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from "react"
import React, { useEffect, useMemo, useState } from "react"
import { Field, t, FieldGroup, resolveObject } from "@bloom-housing/ui-components"
import { FieldValue, Grid } from "@bloom-housing/ui-seeds"
import { useFormContext } from "react-hook-form"
Expand All @@ -11,6 +11,10 @@ import {
} from "@bloom-housing/backend-core/types"
import SectionWithGrid from "../../../shared/SectionWithGrid"
import { FormAddressAlternate } from "@bloom-housing/shared-helpers/src/views/address/FormAddressAlternate"
import GeocodeService, {
GeocodeService as GeocodeServiceType,
} from "@mapbox/mapbox-sdk/services/geocoding"
import MultiselectQuestionsMap from "../MultiselectQuestionsMap"

type FormMultiselectQuestionsProps = {
questions: ListingMultiselectQuestion[]
Expand Down Expand Up @@ -45,6 +49,18 @@ const FormMultiselectQuestions = ({
return keys
}, [questions, applicationSection])

const [geocodingClient, setGeocodingClient] = useState<GeocodeServiceType>()

useEffect(() => {
if (process.env.mapBoxToken || process.env.MAPBOX_TOKEN) {
setGeocodingClient(
GeocodeService({
accessToken: process.env.mapBoxToken || process.env.MAPBOX_TOKEN,
})
)
}
}, [])

if (questions?.length === 0) {
return null
}
Expand Down Expand Up @@ -106,6 +122,10 @@ const FormMultiselectQuestions = ({
stateKeys={stateKeys}
data-testid={"app-question-extra-field"}
/>
<MultiselectQuestionsMap
dataKey={fieldName(question.text, applicationSection, `${option.text}`)}
geocodingClient={geocodingClient}
/>
</div>
)}
</React.Fragment>
Expand Down

0 comments on commit 0c6eea6

Please sign in to comment.