From 65d949dd12b368c68360a964a9b111ab1427d8e3 Mon Sep 17 00:00:00 2001 From: Morgan Ludtke <42942267+ludtkemorgan@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:07:43 -0600 Subject: [PATCH] Release: 2024-01-23 (#658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: seeds button uptake (#3677) * fix: validate radius geocoding preferences (#3718) * fix: validate radius geocoding preferences * fix: move to more generic * fix: add test * fix: one more test * fix: change to true/false * feat: add address verification for preferences step (#3715) * feat: add collectAddress checkbox with subfields * test: update preference tests * fix: add minimum value validation for radius field * fix: make collect address not required * fix: expand collect address fields * fix: change fields order in PreferenceDrawer * feat: add address holder fields to application * fix: make added field optional * fix: display errors properly * feat: add address holder fields to application summary * feat: add address verification for preferences step * feat: adjust padding for application summary * fix: move address holder name and relationship fields to extraData * fix: remove redundant backend address holder fields * fix: use enum for address holder fields * fix: add alternate address form component * fix: remove redundant fields from new address form * fix: verify preferences address when collectAddress true * fix: block going back on address verification add string to translation * fix: use onClick to block address Verification back button * fix: update dependencies for backend (#3694) * feat: core changes for application style alignment (#3662) * test: fix cypress lat long error (#3746) * fix: padding issue on address verify (#3745) * feat: add show mandated accounts flag (#3773) * feat: mA-3752 feat: add SHOW_MANDATED_ACCOUNTS flag * fix: add feature flag to bloom public next config * fix: remove flag from partners * feat: add address holder fields to paper application (#3716) * feat: add collectAddress checkbox with subfields * test: update preference tests * fix: add minimum value validation for radius field * fix: make collect address not required * fix: expand collect address fields * fix: change fields order in PreferenceDrawer * feat: add address holder fields to application * fix: make added field optional * fix: display errors properly * feat: add address holder fields to application summary * feat: adjust padding for application summary * feat: add address holder fields to paper application * fix: use enum for address holder paper application fields * fix: remove redundant backend fields * fix: add correct labels, and values for extraData summary * fix: export geocoding values enum from backend * fix: display address as last extraData field * fix: remove placeholders from form * fix: set default values after listingDto fetched * style: adjust details multiselect question * feat: add geocoding preference label (#3765) * feat: add additional fields info to add listing preferences * feat: add additional fields column to table * refactor: reuse additional field tag * refactor: change base tag style * fix: revert onClose drawer function * fix: add geocoding fields to csv (#3778) * fix: update seeds button (#3793) * fix: add translations for geocoding (#3784) * fix: add translations for geocoding * fix: capitalize translation for qualifying address in vi.json * fix: provides additional fields info text overflows (#3809) * fix: provides additional fields info text overflows * fix: check if length of links greater than 0 * refactor: remove console.log * fix: add links to preference description (#3813) * fix: seeds button layout fixes for Partners Application sidebar (#3789) * feat: geocoding preference set up fixed layers (#3808) * feat: add geocoding map layers to multiselect option * fix: remove link from select description * fix: default value for mapLayerId * fix: add decorators for mapLayers dto's and entities * fix: use smaller font for mapLayer select helper text * fix: test fixes * fix: add map layer geocoding checks (#3825) * fix: add map layer geocoding checks * fix: review comment fix --------- Co-authored-by: Krzysztof Zięcina Co-authored-by: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com> Co-authored-by: cade-exygy <131277283+cade-exygy@users.noreply.github.com> Co-authored-by: ColinBuyck <53269332+ColinBuyck@users.noreply.github.com> Co-authored-by: Jared White --- backend/core/package.json | 16 +- backend/core/src/app.module.ts | 2 + .../src/applications/applications.module.ts | 6 +- backend/core/src/applications/helpers.ts | 14 +- .../application-csv-exporter.service.ts | 53 +- .../services/applications.service.ts | 11 + .../services/geocoding.service.spec.ts | 308 +++ .../services/geocoding.service.ts | 203 ++ .../core/src/map-layers/dto/map-layer.dto.ts | 18 + .../map-layers/dto/map-layers-query-params.ts | 16 + .../map-layers/entities/map-layer.entity.ts | 28 + .../src/map-layers/map-layers.controller.ts | 26 + .../core/src/map-layers/map-layers.module.ts | 13 + .../core/src/map-layers/map-layers.service.ts | 20 + .../migration/1704908499461-addMapLayers.ts | 15 + .../1705637577495-add-feature-collection.ts | 13 + .../types/multiselect-option.ts | 6 + backend/core/src/seeder/seed.ts | 4 + backend/core/src/seeder/seeder.module.ts | 4 + backend/core/src/seeder/seeds/map-layers.ts | 338 +++ .../core/src/shared/types/geocoding-values.ts | 5 + backend/core/types/index.ts | 1 + backend/core/types/src/backend-swagger.ts | 38 + backend/core/types/src/geocoding-keys.ts | 1 + shared-helpers/package.json | 3 +- shared-helpers/src/auth/AuthContext.ts | 5 +- shared-helpers/src/auth/Timeout.tsx | 8 +- shared-helpers/src/locales/es.json | 25 +- shared-helpers/src/locales/general.json | 31 +- shared-helpers/src/locales/tl.json | 29 +- shared-helpers/src/locales/vi.json | 29 +- shared-helpers/src/locales/zh.json | 29 +- .../views/address/FormAddressAlternate.tsx | 98 + .../forgot-password/FormForgotPassword.tsx | 7 +- .../src/views/multiselectQuestions.tsx | 6 +- .../src/views/sign-in/FormSignIn.tsx | 19 +- .../views/sign-in/ResendConfirmationModal.tsx | 26 +- .../__tests__/pages/settings/index.test.tsx | 4 +- .../cypress/e2e/default/03-listing.spec.ts | 170 +- .../e2e/default/05-paperApplication.spec.ts | 2 +- .../default/06-admin-user-mangement.spec.ts | 16 +- ...risdictional-admin-user-management.spec.ts | 8 +- .../default/08-preference-management.spec.ts | 24 +- .../listings-approval.spec.ts | 22 +- sites/partners/cypress/support/commands.js | 20 +- sites/partners/package.json | 4 +- .../src/components/applications/Aside.tsx | 86 +- .../DetailsAddressColumns.tsx | 17 +- .../DetailsMemberDrawer.tsx | 6 +- .../sections/DetailsHouseholdMembers.tsx | 8 +- .../sections/DetailsMultiselectQuestions.tsx | 129 +- .../PaperApplicationForm/FormMember.tsx | 10 +- .../PaperApplicationForm.tsx | 8 + .../sections/FormHouseholdMembers.tsx | 35 +- .../sections/FormMultiselectQuestions.tsx | 49 +- .../listings/ListingFormActions.tsx | 111 +- .../PaperListingDetails/DetailsUnitDrawer.tsx | 6 +- .../sections/DetailApplicationDates.tsx | 29 +- .../sections/DetailUnits.tsx | 8 +- .../PaperListingForm/OpenHouseForm.tsx | 19 +- .../PaperListingForm/RequestChangesModal.tsx | 19 +- .../listings/PaperListingForm/UnitForm.tsx | 41 +- .../listings/PaperListingForm/index.tsx | 41 +- .../sections/ApplicationDates.tsx | 43 +- .../sections/ApplicationTypes.tsx | 21 +- .../sections/BuildingSelectionCriteria.tsx | 35 +- .../sections/ListingPhotos.tsx | 22 +- .../sections/LotteryResults.tsx | 16 +- .../sections/SelectAndOrder.tsx | 67 +- .../PaperListingForm/sections/Units.tsx | 28 +- .../src/components/listings/helpers.tsx | 30 +- .../settings/PreferenceDeleteModal.tsx | 35 +- .../settings/PreferenceDrawer.module.scss | 4 + .../components/settings/PreferenceDrawer.tsx | 417 ++-- .../components/users/FormSignInAddPhone.tsx | 8 +- .../components/users/FormSignInMFACode.tsx | 11 +- .../components/users/FormSignInMFAType.tsx | 14 +- .../src/components/users/FormTerms.tsx | 14 +- .../src/components/users/FormUserConfirm.tsx | 7 +- .../src/components/users/FormUserManage.tsx | 44 +- .../users/ReRequestConfirmation.tsx | 10 +- sites/partners/src/lib/hooks.ts | 14 + .../locale_overrides/general.json | 8 + .../src/pages/application/[id]/index.tsx | 6 +- .../src/pages/application/[id]/review.tsx | 28 +- sites/partners/src/pages/index.tsx | 39 +- .../listings/[id]/applications/index.tsx | 28 +- sites/partners/src/pages/reset-password.tsx | 5 +- sites/partners/src/pages/settings/index.tsx | 21 +- sites/partners/src/pages/sign-in.tsx | 2 +- sites/partners/src/pages/users/index.tsx | 19 +- sites/partners/styles/overrides.scss | 22 +- sites/public/.env.template | 3 + sites/public/cypress/e2e/navigation.spec.ts | 2 +- .../pages/application/contact/address.spec.ts | 2 +- .../application/household/add-members.spec.ts | 4 +- .../application/household/member.spec.ts | 4 +- .../application/review/confirmation.spec.ts | 2 +- .../pages/application/review/terms.spec.ts | 2 +- sites/public/cypress/support/commands.js | 54 +- sites/public/cypress/support/index.d.ts | 1 + sites/public/next.config.js | 80 +- sites/public/package.json | 5 +- .../locale_overrides/general.json | 3 + sites/public/postcss.config.js | 9 +- .../components/account/ConfirmationModal.tsx | 20 +- .../components/account/StatusItem.module.scss | 8 +- .../src/components/account/StatusItem.tsx | 15 +- .../ApplicationMultiselectQuestionStep.tsx | 270 ++- .../components/applications/FormBackLink.tsx | 19 +- .../applications/HouseholdMemberForm.tsx | 12 +- .../applications/ValidateAddress.tsx | 26 +- .../src/components/listing/GetApplication.tsx | 38 +- .../src/components/page/Hero.module.scss | 80 + sites/public/src/components/page/Hero.tsx | 24 + .../components/shared/FormSummaryDetails.tsx | 15 +- .../src/layouts/application-form.module.scss | 37 + sites/public/src/layouts/application-form.tsx | 113 + sites/public/src/layouts/forms.tsx | 3 +- sites/public/src/pages/404.tsx | 7 +- sites/public/src/pages/_app.tsx | 51 +- sites/public/src/pages/_error.tsx | 7 +- .../public/src/pages/account/applications.tsx | 14 +- sites/public/src/pages/account/edit.tsx | 18 +- .../pages/applications/contact/address.tsx | 397 ++-- .../contact/alternate-contact-contact.tsx | 139 +- .../contact/alternate-contact-name.tsx | 138 +- .../contact/alternate-contact-type.tsx | 104 +- .../src/pages/applications/contact/name.tsx | 225 +- .../pages/applications/financial/income.tsx | 178 +- .../pages/applications/financial/vouchers.tsx | 134 +- .../src/pages/applications/household/ada.tsx | 169 +- .../applications/household/add-members.tsx | 156 +- .../pages/applications/household/changes.tsx | 106 +- .../applications/household/live-alone.tsx | 138 +- .../pages/applications/household/member.tsx | 705 +++--- .../applications/household/members-info.tsx | 97 +- .../household/preferred-units.tsx | 95 +- .../pages/applications/household/student.tsx | 105 +- .../pages/applications/preferences/all.tsx | 2 +- .../applications/preferences/general.tsx | 25 +- .../applications/review/confirmation.tsx | 138 +- .../applications/review/demographics.tsx | 86 +- .../src/pages/applications/review/summary.tsx | 133 +- .../src/pages/applications/review/terms.tsx | 173 +- .../src/pages/applications/start/autofill.tsx | 107 +- .../applications/start/choose-language.tsx | 187 +- .../applications/start/what-to-expect.tsx | 89 +- sites/public/src/pages/applications/view.tsx | 67 +- sites/public/src/pages/create-account.tsx | 18 +- sites/public/src/pages/index.tsx | 55 +- sites/public/src/pages/reset-password.tsx | 5 +- sites/public/src/pages/sign-in.tsx | 2 +- sites/public/styles/overrides.scss | 11 + yarn.lock | 2066 +++++++++++++++-- 155 files changed, 6562 insertions(+), 3820 deletions(-) create mode 100644 backend/core/src/applications/services/geocoding.service.spec.ts create mode 100644 backend/core/src/applications/services/geocoding.service.ts create mode 100644 backend/core/src/map-layers/dto/map-layer.dto.ts create mode 100644 backend/core/src/map-layers/dto/map-layers-query-params.ts create mode 100644 backend/core/src/map-layers/entities/map-layer.entity.ts create mode 100644 backend/core/src/map-layers/map-layers.controller.ts create mode 100644 backend/core/src/map-layers/map-layers.module.ts create mode 100644 backend/core/src/map-layers/map-layers.service.ts create mode 100644 backend/core/src/migration/1704908499461-addMapLayers.ts create mode 100644 backend/core/src/migration/1705637577495-add-feature-collection.ts create mode 100644 backend/core/src/seeder/seeds/map-layers.ts create mode 100644 backend/core/src/shared/types/geocoding-values.ts create mode 100644 backend/core/types/src/geocoding-keys.ts create mode 100644 shared-helpers/src/views/address/FormAddressAlternate.tsx create mode 100644 sites/partners/src/components/settings/PreferenceDrawer.module.scss create mode 100644 sites/public/page_content/locale_overrides/general.json create mode 100644 sites/public/src/components/page/Hero.module.scss create mode 100644 sites/public/src/components/page/Hero.tsx create mode 100644 sites/public/src/layouts/application-form.module.scss create mode 100644 sites/public/src/layouts/application-form.tsx create mode 100644 sites/public/styles/overrides.scss diff --git a/backend/core/package.json b/backend/core/package.json index ebbbcd1554..b95abcf017 100644 --- a/backend/core/package.json +++ b/backend/core/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@anchan828/nest-sendgrid": "^0.3.25", - "@google-cloud/translate": "^6.2.6", + "@google-cloud/translate": "^7.2.1", "@nestjs/axios": "1.0.1", "@nestjs/cli": "^9.5.0", "@nestjs/common": "^9.4.2", @@ -52,9 +52,13 @@ "@nestjs/swagger": "^6.3.0", "@nestjs/throttler": "^4.0.0", "@nestjs/typeorm": "^9.0.1", + "@turf/buffer": "6.5.0", + "@turf/helpers": "6.5.0", + "@turf/boolean-point-in-polygon": "6.5.0", + "@turf/points-within-polygon": "6.5.0", "@types/cache-manager": "^3.4.0", "async-retry": "^1.3.1", - "axios": "0.21.2", + "axios": "0.21.3", "cache-manager": "^3.4.0", "casbin": "5.13.0", "class-transformer": "0.3.1", @@ -72,17 +76,17 @@ "nanoid": "^3.1.12", "nestjs-twilio": "^4.1.1", "nestjs-typeorm-paginate": "^4.0.3", - "newrelic": "7.5.1", + "newrelic": "11.4.0", "node-polyglot": "^2.4.0", "passport": "^0.6.0", "passport-custom": "^1.1.1", - "passport-jwt": "^4.0.0", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", - "pg": "^8.4.1", + "pg": "^8.11.3", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.5.4", - "swagger-ui-express": "^4.1.4", + "swagger-ui-express": "^4.2.0", "ts-node": "10.8.0", "twilio": "^3.71.3", "typeorm": "0.3.12", diff --git a/backend/core/src/app.module.ts b/backend/core/src/app.module.ts index 69cd46db0a..0d8a758e6c 100644 --- a/backend/core/src/app.module.ts +++ b/backend/core/src/app.module.ts @@ -37,6 +37,7 @@ import { PaperApplicationsModule } from "./paper-applications/paper-applications import { ActivityLogModule } from "./activity-log/activity-log.module" import { logger } from "./shared/middlewares/logger.middleware" import { CatchAllFilter } from "./shared/filters/catch-all-filter" +import { MapLayersModule } from "./map-layers/map-layers.module" export function applicationSetup(app: INestApplication) { const { httpAdapter } = app.get(HttpAdapterHost) @@ -106,6 +107,7 @@ export class AppModule { UnitTypesModule, UnitRentTypesModule, UnitAccessibilityPriorityTypesModule, + MapLayersModule, ], } } diff --git a/backend/core/src/applications/applications.module.ts b/backend/core/src/applications/applications.module.ts index 4d22d0c6e1..310c485af0 100644 --- a/backend/core/src/applications/applications.module.ts +++ b/backend/core/src/applications/applications.module.ts @@ -7,6 +7,7 @@ import { SharedModule } from "../shared/shared.module" import { ListingsModule } from "../listings/listings.module" import { Address } from "../shared/entities/address.entity" import { Applicant } from "./entities/applicant.entity" +import { MapLayer } from "../map-layers/entities/map-layer.entity" import { ApplicationsSubmissionController } from "./applications-submission.controller" import { TranslationsModule } from "../translations/translations.module" import { Listing } from "../listings/entities/listing.entity" @@ -16,10 +17,11 @@ import { CsvBuilder } from "./services/csv-builder.service" import { ApplicationCsvExporterService } from "./services/application-csv-exporter.service" import { EmailModule } from "../email/email.module" import { ActivityLogModule } from "../activity-log/activity-log.module" +import { GeocodingService } from "./services/geocoding.service" @Module({ imports: [ - TypeOrmModule.forFeature([Application, Applicant, Address, Listing]), + TypeOrmModule.forFeature([Application, Applicant, Address, Listing, MapLayer]), AuthModule, ActivityLogModule, SharedModule, @@ -28,7 +30,7 @@ import { ActivityLogModule } from "../activity-log/activity-log.module" EmailModule, ScheduleModule.forRoot(), ], - providers: [ApplicationsService, CsvBuilder, ApplicationCsvExporterService], + providers: [ApplicationsService, CsvBuilder, ApplicationCsvExporterService, GeocodingService], exports: [ApplicationsService], controllers: [ApplicationsController, ApplicationsSubmissionController], }) diff --git a/backend/core/src/applications/helpers.ts b/backend/core/src/applications/helpers.ts index 97e2445852..fc62339bf6 100644 --- a/backend/core/src/applications/helpers.ts +++ b/backend/core/src/applications/helpers.ts @@ -1,6 +1,6 @@ import dayjs from "dayjs" import { formatLocalDate } from "../shared/utils/format-local-date" -import { ApplicationSubmissionType } from "../../types" +import { ApplicationSubmissionType, GeocodingValues } from "../../types" import { isEmpty } from "class-validator" export const formatApplicationDate = ( @@ -14,3 +14,15 @@ export const formatApplicationDate = ( } return dayjs(dateString).format("MM-DD-YYYY hh:mm:ssA") } + +export const formatGeocodingValues = (key: GeocodingValues) => { + switch (key) { + case GeocodingValues.true: + return "Yes" + case GeocodingValues.false: + return "No" + case GeocodingValues.unknown: + default: + return "Needs Manual Verification" + } +} diff --git a/backend/core/src/applications/services/application-csv-exporter.service.ts b/backend/core/src/applications/services/application-csv-exporter.service.ts index b15a0726c6..15612bd05d 100644 --- a/backend/core/src/applications/services/application-csv-exporter.service.ts +++ b/backend/core/src/applications/services/application-csv-exporter.service.ts @@ -5,7 +5,8 @@ import { formatBoolean } from "../../shared/utils/format-boolean" import { ApplicationMultiselectQuestion } from "../entities/application-multiselect-question.entity" import { AddressCreateDto } from "../../shared/dto/address.dto" import { ApplicationReviewStatus } from "../types/application-review-status-enum" -import { formatApplicationDate } from "../helpers" +import { formatApplicationDate, formatGeocodingValues } from "../helpers" +import { GeocodingValues } from "../../shared/types/geocoding-values" @Injectable({ scope: Scope.REQUEST }) export class ApplicationCsvExporterService { @@ -88,20 +89,44 @@ export class ApplicationCsvExporterService { claimedString = claimedString.concat(`${option.key}, `) } if (option.extraData?.length) { - const extraKey = `${root}: ${option.key} - Address` + let extraKey let extraString = "" - option.extraData.forEach((extra) => { - if (extra.type === "address") { - extraString += `${(extra.value as AddressCreateDto).street}, ${ - (extra.value as AddressCreateDto).street2 - ? `${(extra.value as AddressCreateDto).street2},` - : "" - } ${(extra.value as AddressCreateDto).city}, ${ - (extra.value as AddressCreateDto).state - }, ${(extra.value as AddressCreateDto).zipCode}` - } - }) - extraData[extraKey] = extraString + const order = [ + "address", + "geocodingVerified", + "addressHolderName", + "addressHolderRelationship", + ] + + option.extraData + .sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key)) + .forEach((extra) => { + if (extra.type === "address") { + extraKey = `${root}: ${option.key} - Provided Address` + extraString += `${(extra.value as AddressCreateDto).street}, ${ + (extra.value as AddressCreateDto).street2 + ? `${(extra.value as AddressCreateDto).street2},` + : "" + } ${(extra.value as AddressCreateDto).city}, ${ + (extra.value as AddressCreateDto).state + }, ${(extra.value as AddressCreateDto).zipCode}` + } + if (extra.type === "text") { + if (extra.key === "geocodingVerified") { + extraKey = `${root}: ${option.key} - Passed Address Check` + extraString = formatGeocodingValues(extra.value as GeocodingValues) + } + if (extra.key === "addressHolderName") { + extraKey = `${root}: ${option.key} - Name of Address Holder` + extraString = extra.value as string + } + if (extra.key === "addressHolderRelationship") { + extraKey = `${root}: ${option.key} - Relationship to Address Holder` + extraString = extra.value as string + } + } + extraData[extraKey] = extraString + }) } }) preferenceKeys[root] = 1 diff --git a/backend/core/src/applications/services/applications.service.ts b/backend/core/src/applications/services/applications.service.ts index 0813fa17b2..1e7490ca1e 100644 --- a/backend/core/src/applications/services/applications.service.ts +++ b/backend/core/src/applications/services/applications.service.ts @@ -29,6 +29,7 @@ import { Listing } from "../../listings/entities/listing.entity" import { ApplicationCsvExporterService } from "./application-csv-exporter.service" import { User } from "../../auth/entities/user.entity" import { StatusDto } from "../../shared/dto/status.dto" +import { GeocodingService } from "./geocoding.service" @Injectable({ scope: Scope.REQUEST }) export class ApplicationsService { @@ -38,6 +39,7 @@ export class ApplicationsService { private readonly listingsService: ListingsService, private readonly emailService: EmailService, private readonly applicationCsvExporter: ApplicationCsvExporterService, + private readonly geocodingService: GeocodingService, @InjectRepository(Application) private readonly repository: Repository, @InjectRepository(Listing) private readonly listingsRepository: Repository ) {} @@ -423,6 +425,15 @@ export class ApplicationsService { if (application.applicant.emailAddress && shouldSendConfirmation) { await this.emailService.confirmation(listing, application, applicationCreateDto.appUrl) } + + // Calculate geocoding preferences after save and email sent + if (listing.jurisdiction?.enableGeocodingPreferences) { + try { + void this.geocodingService.validateGeocodingPreferences(application, listing) + } catch (e) { + console.warn("error while validating geocoding preferences") + } + } return application } diff --git a/backend/core/src/applications/services/geocoding.service.spec.ts b/backend/core/src/applications/services/geocoding.service.spec.ts new file mode 100644 index 0000000000..5a21a65d8d --- /dev/null +++ b/backend/core/src/applications/services/geocoding.service.spec.ts @@ -0,0 +1,308 @@ +import { Test, TestingModule } from "@nestjs/testing" +import { GeocodingService } from "./geocoding.service" +import { Address } from "../../shared/entities/address.entity" +import { getRepositoryToken } from "@nestjs/typeorm" +import { Application } from "../entities/application.entity" +import { ValidationMethod } from "../../multiselect-question/types/validation-method-enum" +import { Listing } from "../../listings/entities/listing.entity" +import { InputType } from "../../shared/types/input-type" +import { MapLayer } from "../../map-layers/entities/map-layer.entity" +import { FeatureCollection } from "@turf/helpers" + +describe("GeocodingService", () => { + let service: GeocodingService + const applicationRepoUpdate = jest.fn() + const mockApplicationRepo = { + createQueryBuilder: jest.fn(), + update: applicationRepoUpdate, + } + const date = new Date() + const address: Address = { + id: "id", + createdAt: date, + updatedAt: date, + city: "Washington", + county: null, + state: "DC", + street: "1600 Pennsylvania Avenue", + street2: null, + zipCode: "20500", + latitude: 38.8977, + longitude: -77.0365, + } + const featureCollection: FeatureCollection = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + properties: {}, + geometry: { + coordinates: [ + [ + [ + [-77.0392589333301, 38.79186072967565], + [-76.90981025809415, 38.89293952026222], + [-77.04122027689426, 38.996161202682146], + [-77.12000091005532, 38.93465307055658], + [-77.10561772391833, 38.91990351952725], + [-77.09123453778136, 38.90565966392609], + [-77.06802530560486, 38.9015894658674], + [-77.06181438431805, 38.889377471720564], + [-77.03697069917165, 38.870801038935525], + [-77.03043288729134, 38.850437727576235], + [-77.03435557441966, 38.80816525459605], + [-77.0392589333301, 38.79186072967565], + ], + ], + ], + type: "Polygon", + }, + }, + ], + } + const mockMapLayerRepo = { + createQueryBuilder: jest.fn(), + findBy: jest.fn().mockResolvedValue([ + { + id: "mapLayerId", + name: "map layer", + jurisdictionId: "1", + featureCollection: featureCollection, + }, + ]), + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GeocodingService, + { + provide: getRepositoryToken(Application), + useValue: mockApplicationRepo, + }, + { + provide: getRepositoryToken(MapLayer), + useValue: mockMapLayerRepo, + }, + ], + }).compile() + + service = await module.resolve(GeocodingService) + }) + + describe("verifyRadius", () => { + it("should return 'unknown' if lat and long not there", () => { + expect( + service.verifyRadius( + { + ...address, + latitude: null, + longitude: null, + }, + 5, + address + ) + ).toBe("unknown") + }) + it("should return 'true' if within radius", () => { + expect( + service.verifyRadius( + { + ...address, + latitude: 38.89485, + longitude: -77.04251, + }, + 5, + address + ) + ).toBe("true") + }) + it("should return 'false' if not within radius", () => { + expect( + service.verifyRadius( + { + ...address, + latitude: 39.284205, + longitude: -76.621698, + }, + 5, + address + ) + ).toBe("false") + }) + it("should return 'true' if same lat long", () => { + expect( + service.verifyRadius( + { + ...address, + }, + 5, + address + ) + ).toBe("true") + }) + }) + describe("verifyLayers", () => { + it("should return 'unknown' if no lat/long", () => { + expect( + service.verifyLayers( + { + ...address, + latitude: null, + longitude: null, + }, + featureCollection + ) + ).toBe("unknown") + }) + it("should return 'true' if address is within layer", () => { + expect(service.verifyLayers(address, featureCollection)).toBe("true") + }) + it("should return 'false' if address is within layer", () => { + expect( + service.verifyLayers( + { ...address, latitude: 39.284205, longitude: -76.621698 }, + featureCollection + ) + ).toBe("false") + }) + }) + describe("validateRadiusPreferences", () => { + const listing = { + buildingAddress: address, + listingMultiselectQuestions: [ + { + multiselectQuestion: { + options: [ + { + text: "Geocoding option by radius", + collectAddress: true, + radiusSize: 5, + validationMethod: ValidationMethod.radius, + }, + ], + }, + }, + ], + } + const preferenceAddress = { ...address, latitude: 38.89485, longitude: -77.04251 } + const application = { + id: "applicationId", + preferences: [ + { + key: "Geocoding preference", + options: [ + { + key: "Geocoding option by radius", + checked: true, + extraData: [ + { + type: InputType.address, + value: preferenceAddress, + }, + ], + }, + ], + }, + ], + } + it("should save the validated value as extraData", async () => { + await service.validateRadiusPreferences( + (application as unknown) as Application, + listing as Listing + ) + expect(applicationRepoUpdate).toBeCalledWith( + { id: "applicationId" }, + { + preferences: expect.arrayContaining([ + expect.objectContaining({ + key: "Geocoding preference", + options: [ + { + checked: true, + extraData: [ + { + type: "address", + value: preferenceAddress, + }, + { key: "geocodingVerified", type: "text", value: "true" }, + ], + key: "Geocoding option by radius", + }, + ], + }), + ]), + } + ) + }) + }) + describe("validateGeoLayerPreferences", () => { + const listing = { + buildingAddress: address, + listingMultiselectQuestions: [ + { + multiselectQuestion: { + options: [ + { + text: "Geocoding option by map", + collectAddress: true, + mapLayerId: "mapLayerId", + validationMethod: ValidationMethod.map, + }, + ], + }, + }, + ], + } + const preferenceAddress = { ...address, latitude: 38.89485, longitude: -77.04251 } + const application = { + id: "applicationId", + preferences: [ + { + key: "Geocoding preference", + options: [ + { + key: "Geocoding option by map", + checked: true, + extraData: [ + { + type: InputType.address, + value: preferenceAddress, + }, + ], + }, + ], + }, + ], + } + it("should save the validated value as extraData for map layer", async () => { + await service.validateGeoLayerPreferences( + (application as unknown) as Application, + listing as Listing + ) + expect(applicationRepoUpdate).toBeCalledWith( + { id: "applicationId" }, + { + preferences: expect.arrayContaining([ + expect.objectContaining({ + key: "Geocoding preference", + options: [ + { + checked: true, + extraData: [ + { + type: "address", + value: preferenceAddress, + }, + { key: "geocodingVerified", type: "text", value: "true" }, + ], + key: "Geocoding option by map", + }, + ], + }), + ]), + } + ) + }) + }) +}) diff --git a/backend/core/src/applications/services/geocoding.service.ts b/backend/core/src/applications/services/geocoding.service.ts new file mode 100644 index 0000000000..651827fa35 --- /dev/null +++ b/backend/core/src/applications/services/geocoding.service.ts @@ -0,0 +1,203 @@ +import { FeatureCollection, point, polygons } from "@turf/helpers" +import buffer from "@turf/buffer" +import booleanPointInPolygon from "@turf/boolean-point-in-polygon" +import pointsWithinPolygon from "@turf/points-within-polygon" +import { InjectRepository } from "@nestjs/typeorm" +import { In, Repository } from "typeorm" +import { Address } from "../../shared/entities/address.entity" +import { Application } from "../entities/application.entity" +import { MapLayer } from "../../map-layers/entities/map-layer.entity" +import { Listing } from "../../listings/entities/listing.entity" +import { ValidationMethod } from "../../multiselect-question/types/validation-method-enum" +import { MultiselectOption } from "../../multiselect-question/types/multiselect-option" +import { ApplicationMultiselectQuestion } from "../entities/application-multiselect-question.entity" +import { ApplicationMultiselectQuestionOption } from "../types/application-multiselect-question-option" +import { InputType } from "../../shared/types/input-type" +import { GeocodingValues } from "../../shared/types/geocoding-values" + +export class GeocodingService { + constructor( + @InjectRepository(Application) private readonly applicationRepository: Repository, + @InjectRepository(MapLayer) private readonly mapLayerRepository: Repository + ) {} + + public async validateGeocodingPreferences(application: Application, listing: Listing) { + await this.validateRadiusPreferences(application, listing) + await this.validateGeoLayerPreferences(application, listing) + } + + verifyRadius( + preferenceAddress: Address, + radius: number, + listingAddress: Address + ): GeocodingValues { + try { + if (preferenceAddress.latitude && preferenceAddress.longitude) { + const preferencePoint = point([ + Number.parseFloat(preferenceAddress.longitude.toString()), + Number.parseFloat(preferenceAddress.latitude.toString()), + ]) + const listingPoint = point([ + Number.parseFloat(listingAddress.longitude.toString()), + Number.parseFloat(listingAddress.latitude.toString()), + ]) + const calculatedBuffer = buffer(listingPoint.geometry, radius, { units: "miles" }) + return booleanPointInPolygon(preferencePoint, calculatedBuffer) + ? GeocodingValues.true + : GeocodingValues.false + } + } catch (e) { + console.log("error happened while calculating radius") + } + return GeocodingValues.unknown + } + + verifyLayers( + preferenceAddress: Address, + featureCollectionLayers: FeatureCollection + ): GeocodingValues { + try { + if (preferenceAddress.latitude && preferenceAddress.longitude) { + const preferencePoint = point([ + Number.parseFloat(preferenceAddress.longitude.toString()), + Number.parseFloat(preferenceAddress.latitude.toString()), + ]) + + // Convert the features to the format that turfjs wants + const polygonsFromFeature = [] + featureCollectionLayers.features.forEach((feature) => { + if (feature.geometry.type === "MultiPolygon" || feature.geometry.type === "Polygon") { + feature.geometry.coordinates.forEach((coordinate) => { + polygonsFromFeature.push(coordinate) + }) + } + }) + const layer = polygons(polygonsFromFeature) + + const points = pointsWithinPolygon(preferencePoint, layer) + if (points && points.features?.length) { + return GeocodingValues.true + } + + return GeocodingValues.false + } + } catch (e) { + console.log("e", e) + } + // If the geocoding value was not able to be verified we need to set it as "unknown" + // in order to signify we are unable to automatically verify and manually checking will need to be done + return GeocodingValues.unknown + } + + public async validateRadiusPreferences(application: Application, listing: Listing) { + // Get all radius preferences from the listing + const radiusPreferenceOptions: MultiselectOption[] = listing.listingMultiselectQuestions.reduce( + (options, multiselectQuestion) => { + const newOptions = multiselectQuestion.multiselectQuestion.options?.filter( + (option) => option.validationMethod === ValidationMethod.radius + ) + return [...options, ...newOptions] + }, + [] + ) + // If there are any radius preferences do the calculation and save the new preferences + if (radiusPreferenceOptions.length) { + const preferences: ApplicationMultiselectQuestion[] = application.preferences.map( + (preference) => { + const newPreferenceOptions: ApplicationMultiselectQuestionOption[] = preference.options.map( + (option) => { + const addressData = option.extraData.find((data) => data.type === InputType.address) + if (option.checked && addressData) { + const foundOption = radiusPreferenceOptions.find( + (preferenceOption) => preferenceOption.text === option.key + ) + if (foundOption) { + const geocodingVerified = this.verifyRadius( + addressData.value as Address, + foundOption.radiusSize, + listing.buildingAddress + ) + return { + ...option, + extraData: [ + ...option.extraData, + { + key: "geocodingVerified", + type: InputType.text, + value: geocodingVerified, + }, + ], + } + } + } + return option + } + ) + return { ...preference, options: newPreferenceOptions } + } + ) + await this.applicationRepository.update({ id: application.id }, { preferences: preferences }) + } + } + + public async validateGeoLayerPreferences(application: Application, listing: Listing) { + // Get all map layer preferences from the listing + const mapPreferenceOptions: MultiselectOption[] = listing.listingMultiselectQuestions?.reduce( + (options, multiselectQuestion) => { + const newOptions = multiselectQuestion.multiselectQuestion?.options?.filter( + (option) => option.validationMethod === ValidationMethod.map + ) + return [...options, ...newOptions] + }, + [] + ) + + const preferencesOptions = ( + preference: ApplicationMultiselectQuestion, + mapLayers: MapLayer[] + ): ApplicationMultiselectQuestionOption[] => { + const preferenceOptions = [] + preference.options.forEach((option) => { + const addressData = option.extraData.find((data) => data.type === InputType.address) + if (option.checked && addressData) { + const foundOption = mapPreferenceOptions.find( + (preferenceOption) => preferenceOption.text === option.key + ) + if (foundOption && foundOption.mapLayerId) { + const layer = mapLayers.find((layer) => layer.id === foundOption.mapLayerId) + const geocodingVerified = this.verifyLayers( + addressData.value as Address, + layer?.featureCollection + ) + preferenceOptions.push({ + ...option, + extraData: [ + ...option.extraData, + { + key: "geocodingVerified", + type: InputType.text, + value: geocodingVerified, + }, + ], + }) + return + } + } + preferenceOptions.push(option) + }) + return preferenceOptions + } + if (mapPreferenceOptions?.length) { + const preferences = [] + const mapLayers = await this.mapLayerRepository.findBy({ + id: In(mapPreferenceOptions.map((option) => option.mapLayerId)), + }) + application.preferences.forEach((preference) => { + const newPreferenceOptions = preferencesOptions(preference, mapLayers) + preferences.push({ ...preference, options: newPreferenceOptions }) + }) + + await this.applicationRepository.update({ id: application.id }, { preferences: preferences }) + } + } +} diff --git a/backend/core/src/map-layers/dto/map-layer.dto.ts b/backend/core/src/map-layers/dto/map-layer.dto.ts new file mode 100644 index 0000000000..a397aff273 --- /dev/null +++ b/backend/core/src/map-layers/dto/map-layer.dto.ts @@ -0,0 +1,18 @@ +import { Expose } from "class-transformer" +import { IsString, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class MapLayerDto { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + id: string + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + name: string + + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId: string +} diff --git a/backend/core/src/map-layers/dto/map-layers-query-params.ts b/backend/core/src/map-layers/dto/map-layers-query-params.ts new file mode 100644 index 0000000000..a817a2dbc3 --- /dev/null +++ b/backend/core/src/map-layers/dto/map-layers-query-params.ts @@ -0,0 +1,16 @@ +import { Expose } from "class-transformer" +import { ApiProperty } from "@nestjs/swagger" +import { IsOptional, IsString } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +export class MapLayersQueryParams { + @Expose() + @ApiProperty({ + name: "jurisdictionId", + required: false, + type: String, + }) + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId?: string +} diff --git a/backend/core/src/map-layers/entities/map-layer.entity.ts b/backend/core/src/map-layers/entities/map-layer.entity.ts new file mode 100644 index 0000000000..e7edb5eac0 --- /dev/null +++ b/backend/core/src/map-layers/entities/map-layer.entity.ts @@ -0,0 +1,28 @@ +import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" +import { Expose } from "class-transformer" +import { IsString, IsUUID } from "class-validator" +import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum" + +@Entity({ name: "map_layers" }) +export class MapLayer { + @PrimaryGeneratedColumn("uuid") + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + id: string + + @Expose() + @Column() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + name: string + + @Expose() + @Column() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + jurisdictionId: string + + @Column("jsonb") + @Expose() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + featureCollection: any +} diff --git a/backend/core/src/map-layers/map-layers.controller.ts b/backend/core/src/map-layers/map-layers.controller.ts new file mode 100644 index 0000000000..cc25386ebb --- /dev/null +++ b/backend/core/src/map-layers/map-layers.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common" +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger" +import { MapLayersService } from "./map-layers.service" +import { ResourceType } from "../auth/decorators/resource-type.decorator" +import { OptionalAuthGuard } from "../auth/guards/optional-auth.guard" +import { AuthzGuard } from "../auth/guards/authz.guard" +import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options" +import { mapTo } from "../shared/mapTo" +import { MapLayerDto } from "./dto/map-layer.dto" +import { MapLayersQueryParams } from "./dto/map-layers-query-params" + +@Controller("/mapLayers") +@ApiTags("mapLayers") +@ApiBearerAuth() +@ResourceType("mapLayer") +@UseGuards(OptionalAuthGuard, AuthzGuard) +@UsePipes(new ValidationPipe(defaultValidationPipeOptions)) +export class MapLayersController { + constructor(private readonly mapLayerService: MapLayersService) {} + + @Get() + @ApiOperation({ summary: "List map layers", operationId: "list" }) + async list(@Query() queryParams: MapLayersQueryParams): Promise { + return mapTo(MapLayerDto, await this.mapLayerService.list(queryParams)) + } +} diff --git a/backend/core/src/map-layers/map-layers.module.ts b/backend/core/src/map-layers/map-layers.module.ts new file mode 100644 index 0000000000..3bb84ed481 --- /dev/null +++ b/backend/core/src/map-layers/map-layers.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common" +import { MapLayersService } from "./map-layers.service" +import { MapLayersController } from "./map-layers.controller" +import { TypeOrmModule } from "@nestjs/typeorm" +import { MapLayer } from "./entities/map-layer.entity" +import { AuthModule } from "../auth/auth.module" + +@Module({ + imports: [TypeOrmModule.forFeature([MapLayer]), AuthModule], + providers: [MapLayersService], + controllers: [MapLayersController], +}) +export class MapLayersModule {} diff --git a/backend/core/src/map-layers/map-layers.service.ts b/backend/core/src/map-layers/map-layers.service.ts new file mode 100644 index 0000000000..961ff29ec2 --- /dev/null +++ b/backend/core/src/map-layers/map-layers.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from "@nestjs/common" +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { MapLayer } from "./entities/map-layer.entity" +import { MapLayersQueryParams } from "./dto/map-layers-query-params" + +@Injectable() +export class MapLayersService { + constructor( + @InjectRepository(MapLayer) + private readonly mapLayerRepository: Repository + ) {} + + list(queryParams: MapLayersQueryParams): Promise { + if (queryParams.jurisdictionId) { + return this.mapLayerRepository.find({ where: { jurisdictionId: queryParams.jurisdictionId } }) + } + return this.mapLayerRepository.find() + } +} diff --git a/backend/core/src/migration/1704908499461-addMapLayers.ts b/backend/core/src/migration/1704908499461-addMapLayers.ts new file mode 100644 index 0000000000..5945636621 --- /dev/null +++ b/backend/core/src/migration/1704908499461-addMapLayers.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addMapLayers1704908499461 implements MigrationInterface { + name = "addMapLayers1704908499461" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "map_layers" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "jurisdiction_id" character varying NOT NULL, CONSTRAINT "PK_d1bcb10041ba88ffea330dc10d9" PRIMARY KEY ("id"))` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "map_layers"`) + } +} diff --git a/backend/core/src/migration/1705637577495-add-feature-collection.ts b/backend/core/src/migration/1705637577495-add-feature-collection.ts new file mode 100644 index 0000000000..04b1e08d67 --- /dev/null +++ b/backend/core/src/migration/1705637577495-add-feature-collection.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class addFeatureCollection1705637577495 implements MigrationInterface { + name = "addFeatureCollection1705637577495" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "map_layers" ADD "feature_collection" jsonb NOT NULL`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "map_layers" DROP COLUMN "feature_collection"`) + } +} diff --git a/backend/core/src/multiselect-question/types/multiselect-option.ts b/backend/core/src/multiselect-question/types/multiselect-option.ts index c4da532868..16296fba79 100644 --- a/backend/core/src/multiselect-question/types/multiselect-option.ts +++ b/backend/core/src/multiselect-question/types/multiselect-option.ts @@ -56,6 +56,12 @@ export class MultiselectOption { @ApiProperty({ required: false }) radiusSize?: number + @Expose() + @IsOptional({ groups: [ValidationsGroupsEnum.default] }) + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @ApiProperty({ required: false }) + mapLayerId?: string + @Expose() @IsOptional({ groups: [ValidationsGroupsEnum.default] }) @IsBoolean({ groups: [ValidationsGroupsEnum.default] }) diff --git a/backend/core/src/seeder/seed.ts b/backend/core/src/seeder/seed.ts index 719945dc38..ab86127752 100644 --- a/backend/core/src/seeder/seed.ts +++ b/backend/core/src/seeder/seed.ts @@ -54,6 +54,7 @@ import { UnitTypesService } from "../unit-types/unit-types.service" import dayjs from "dayjs" import { CountyCode } from "../shared/types/county-code" import { ApplicationFlaggedSetsCronjobService } from "../application-flagged-sets/application-flagged-sets-cronjob.service" +import { MapLayerSeeder } from "./seeds/map-layers" const argv = yargs.scriptName("seed").options({ test: { type: "boolean", default: false }, @@ -224,6 +225,9 @@ async function seed() { await seedAmiCharts(app) const listings = await seedListings(app, rolesRepo, jurisdictions) + const mapLayerSeeder = app.get(MapLayerSeeder) + await mapLayerSeeder.seed(jurisdictions) + const user1 = await userService.createPublicUser( plainToClass(UserCreateDto, { email: "test@example.com", diff --git a/backend/core/src/seeder/seeder.module.ts b/backend/core/src/seeder/seeder.module.ts index 83009bdaac..6f5944b17b 100644 --- a/backend/core/src/seeder/seeder.module.ts +++ b/backend/core/src/seeder/seeder.module.ts @@ -52,6 +52,8 @@ import { AmiDefaultTritonDetroit } from "../seeder/seeds/ami-charts/triton-ami-c import { AmiDefaultSanJose } from "../seeder/seeds/ami-charts/default-ami-chart-san-jose" import { AmiDefaultSanMateo } from "../seeder/seeds/ami-charts/default-ami-chart-san-mateo" import { Asset } from "../assets/entities/asset.entity" +import { MapLayer } from "../map-layers/entities/map-layer.entity" +import { MapLayerSeeder } from "./seeds/map-layers" @Module({}) export class SeederModule { @@ -80,6 +82,7 @@ export class SeederModule { ApplicationMethod, PaperApplication, Jurisdiction, + MapLayer, ]), ThrottlerModule.forRoot({ ttl: 60, @@ -119,6 +122,7 @@ export class SeederModule { AmiDefaultTritonDetroit, AmiDefaultSanJose, AmiDefaultSanMateo, + MapLayerSeeder, ], } } diff --git a/backend/core/src/seeder/seeds/map-layers.ts b/backend/core/src/seeder/seeds/map-layers.ts new file mode 100644 index 0000000000..d9c65a3d96 --- /dev/null +++ b/backend/core/src/seeder/seeds/map-layers.ts @@ -0,0 +1,338 @@ +import { InjectRepository } from "@nestjs/typeorm" +import { Repository } from "typeorm" +import { MapLayer } from "../../map-layers/entities/map-layer.entity" +import { Jurisdiction } from "../../../types" + +export class MapLayerSeeder { + constructor( + @InjectRepository(MapLayer) + protected readonly mapLayerRepository: Repository + ) {} + + async seed(jurisdictions: Jurisdiction[]) { + const mapLayers = [ + { + name: "Map Layer 1", + jurisdictionId: jurisdictions?.[0]?.id ?? "1", + featureCollection: { + type: "FeatureCollection", + features: [ + { + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-122.26591, 37.89001], + [-122.26565, 37.88796], + [-122.26533, 37.88531], + [-122.26311, 37.88555], + [-122.26276, 37.88617], + [-122.2626, 37.88673], + [-122.2626, 37.88705], + [-122.26255, 37.88735], + [-122.26236, 37.8875], + [-122.26211, 37.88761], + [-122.26177, 37.88773], + [-122.26153, 37.88782], + [-122.26144, 37.88802], + [-122.26145, 37.88821], + [-122.2616, 37.88848], + [-122.26208, 37.88886], + [-122.2623, 37.8891], + [-122.26241, 37.88967], + [-122.26188, 37.88994], + [-122.2609, 37.89018], + [-122.26052, 37.89016], + [-122.2602, 37.89014], + [-122.25989, 37.89016], + [-122.25931, 37.89032], + [-122.25876, 37.89063], + [-122.25887, 37.89067], + [-122.25919, 37.89067], + [-122.25943, 37.8907], + [-122.25976, 37.89081], + [-122.25983, 37.89091], + [-122.25991, 37.89104], + [-122.25969, 37.8914], + [-122.25976, 37.89166], + [-122.26018, 37.89202], + [-122.26051, 37.89218], + [-122.26087, 37.89218], + [-122.26223, 37.89188], + [-122.26268, 37.8917], + [-122.26314, 37.89137], + [-122.26353, 37.89106], + [-122.26407, 37.89062], + [-122.2649, 37.89022], + [-122.26535, 37.89002], + [-122.26591, 37.89001], + ], + ], + ], + }, + properties: { + area_id: 6664, + city_id: 17, + grade: "D", + fill: "#d9838d", + label: "D1", + name: " ", + category_id: 4, + sheets: 1, + area: 0.0000230966496717784, + bounds: [ + [37.88531, -122.26591], + [37.89218, -122.25876], + ], + label_coords: [37.888, -122.264], + residential: true, + commercial: false, + industrial: false, + }, + }, + { + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-122.27239, 37.86322], + [-122.27236, 37.86524], + [-122.27271, 37.8653], + [-122.27275, 37.86581], + [-122.27328, 37.86584], + [-122.27332, 37.86662], + [-122.27303, 37.86696], + [-122.27253, 37.86696], + [-122.27268, 37.86879], + [-122.27371, 37.86876], + [-122.27392, 37.87025], + [-122.27296, 37.87062], + [-122.27303, 37.87119], + [-122.29152, 37.86896], + [-122.29132, 37.86812], + [-122.28897, 37.86111], + [-122.27239, 37.86322], + ], + [ + [-122.28572, 37.86679], + [-122.28455, 37.86695], + [-122.28449, 37.86636], + [-122.2857, 37.86622], + [-122.28572, 37.86679], + ], + ], + [ + [ + [-122.2732, 37.87251], + [-122.27479, 37.87235], + [-122.27526, 37.8765], + [-122.28269, 37.87569], + [-122.28235, 37.87301], + [-122.29281, 37.87163], + [-122.29201, 37.86973], + [-122.27314, 37.87202], + [-122.2732, 37.87251], + ], + ], + ], + }, + properties: { + area_id: 6774, + city_id: 17, + grade: "D", + fill: "#d9838d", + label: "D2", + name: " ", + category_id: 4, + sheets: 1, + area: 0.000197049068477924, + bounds: [ + [37.86111, -122.29281], + [37.8765, -122.27236], + ], + label_coords: [37.866, -122.279], + residential: true, + commercial: false, + industrial: false, + }, + id: 10, + }, + { + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-122.28911, 37.84974], + [-122.28665, 37.85025], + [-122.28947, 37.85919], + [-122.28987, 37.86059], + [-122.29112, 37.86446], + [-122.29249, 37.86893], + [-122.29947, 37.86731], + [-122.30288, 37.86649], + [-122.30249, 37.86552], + [-122.3012, 37.86364], + [-122.30055, 37.86264], + [-122.30056, 37.86192], + [-122.30041, 37.86166], + [-122.29993, 37.86082], + [-122.29948, 37.85998], + [-122.29893, 37.85862], + [-122.29834, 37.85694], + [-122.2977, 37.85497], + [-122.29654, 37.85182], + [-122.29601, 37.85002], + [-122.29601, 37.8496], + [-122.29559, 37.84842], + [-122.28911, 37.84974], + ], + ], + [ + [ + [-122.29704, 37.88312], + [-122.29804, 37.88301], + [-122.29894, 37.88281], + [-122.29949, 37.88271], + [-122.30042, 37.88277], + [-122.30115, 37.88271], + [-122.30215, 37.88249], + [-122.30306, 37.88239], + [-122.30348, 37.8822], + [-122.3055, 37.88209], + [-122.30612, 37.882], + [-122.30691, 37.88181], + [-122.30741, 37.88175], + [-122.30783, 37.88187], + [-122.30812, 37.88211], + [-122.30831, 37.8822], + [-122.30959, 37.88196], + [-122.30698, 37.87299], + [-122.30479, 37.86635], + [-122.30298, 37.86678], + [-122.30306, 37.86731], + [-122.30244, 37.86759], + [-122.29284, 37.86967], + [-122.29704, 37.88312], + ], + [ + [-122.29787, 37.8724], + [-122.29692, 37.87264], + [-122.29651, 37.87144], + [-122.29752, 37.87126], + [-122.29787, 37.8724], + ], + ], + ], + }, + properties: { + area_id: 6672, + city_id: 17, + grade: "D", + fill: "#d9838d", + label: "D3", + name: " ", + category_id: 4, + sheets: 1, + area: 0.000366050178227747, + bounds: [ + [37.84842, -122.30959], + [37.88312, -122.28665], + ], + label_coords: [37.863, -122.296], + residential: true, + commercial: false, + industrial: false, + }, + id: 12, + }, + { + type: "Feature", + geometry: { + type: "MultiPolygon", + coordinates: [ + [ + [ + [-122.27165, 37.85605], + [-122.2719, 37.85796], + [-122.27262, 37.85796], + [-122.27276, 37.85959], + [-122.27201, 37.85973], + [-122.27215, 37.86142], + [-122.26838, 37.86198], + [-122.26869, 37.8637], + [-122.27239, 37.86322], + [-122.28897, 37.86111], + [-122.28559, 37.85014], + [-122.28381, 37.8505], + [-122.28313, 37.84867], + [-122.28271, 37.84876], + [-122.28185, 37.84681], + [-122.27675, 37.84746], + [-122.27625, 37.84566], + [-122.27314, 37.846], + [-122.27265, 37.84605], + [-122.27232, 37.84679], + [-122.27511, 37.84639], + [-122.27545, 37.8482], + [-122.27146, 37.84869], + [-122.27037, 37.85112], + [-122.2708, 37.85382], + [-122.27169, 37.85388], + [-122.27212, 37.85582], + [-122.27165, 37.85605], + ], + [ + [-122.28512, 37.85717], + [-122.28352, 37.85733], + [-122.283, 37.85442], + [-122.28464, 37.85417], + [-122.28512, 37.85717], + ], + ], + [ + [ + [-122.26623, 37.8623], + [-122.26474, 37.86252], + [-122.26498, 37.86432], + [-122.26545, 37.86711], + [-122.26688, 37.86696], + [-122.26623, 37.8623], + ], + ], + ], + }, + properties: { + area_id: 6675, + city_id: 17, + grade: "D", + fill: "#d9838d", + label: "D4", + name: " ", + category_id: 4, + sheets: 1, + area: 0.000231566437344764, + bounds: [ + [37.84566, -122.28897], + [37.86711, -122.26474], + ], + label_coords: [37.853, -122.277], + residential: true, + commercial: false, + industrial: false, + }, + id: 13, + }, + ], + }, + }, + ] + + await this.mapLayerRepository.save(mapLayers) + } +} diff --git a/backend/core/src/shared/types/geocoding-values.ts b/backend/core/src/shared/types/geocoding-values.ts new file mode 100644 index 0000000000..fa3f90d5f5 --- /dev/null +++ b/backend/core/src/shared/types/geocoding-values.ts @@ -0,0 +1,5 @@ +export enum GeocodingValues { + true = "true", + false = "false", + unknown = "unknown", +} diff --git a/backend/core/types/index.ts b/backend/core/types/index.ts index dda58a886b..ecfef8ad0f 100644 --- a/backend/core/types/index.ts +++ b/backend/core/types/index.ts @@ -3,3 +3,4 @@ export * from "./src/backend-swagger" export * from "./src/archer-listing" export * from "./src/Member" export * from "./src/filter-keys" +export * from "./src/geocoding-keys" diff --git a/backend/core/types/src/backend-swagger.ts b/backend/core/types/src/backend-swagger.ts index 05d4f7e64a..0703790b18 100644 --- a/backend/core/types/src/backend-swagger.ts +++ b/backend/core/types/src/backend-swagger.ts @@ -2329,6 +2329,30 @@ export class UnitAccessibilityPriorityTypesService { } } +export class MapLayersService { + /** + * List map layers + */ + list( + params: { + /** */ + jurisdictionId?: string + } = {} as any, + options: IRequestOptions = {} + ): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/mapLayers" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + configs.params = { jurisdictionId: params["jurisdictionId"] } + let data = null + + configs.data = data + axios(configs, resolve, reject) + }) + } +} + export interface AmiChartItem { /** */ percentOfAmi: number @@ -4532,6 +4556,9 @@ export interface MultiselectOption { /** */ radiusSize?: number + /** */ + mapLayerId?: string + /** */ collectName?: boolean @@ -6186,6 +6213,17 @@ export interface UnitAccessibilityPriorityTypeUpdate { id: string } +export interface MapLayer { + /** */ + id: string + + /** */ + name: string + + /** */ + jurisdictionId: string +} + export enum IncomePeriod { "perMonth" = "perMonth", "perYear" = "perYear", diff --git a/backend/core/types/src/geocoding-keys.ts b/backend/core/types/src/geocoding-keys.ts new file mode 100644 index 0000000000..e2b0f1bd59 --- /dev/null +++ b/backend/core/types/src/geocoding-keys.ts @@ -0,0 +1 @@ +export * from "../../src/shared/types/geocoding-values" diff --git a/shared-helpers/package.json b/shared-helpers/package.json index 439a29f16d..cb58b39566 100644 --- a/shared-helpers/package.json +++ b/shared-helpers/package.json @@ -17,7 +17,8 @@ }, "dependencies": { "@bloom-housing/backend-core": "^7.13.0", - "@bloom-housing/ui-components": "12.0.21", + "@bloom-housing/ui-components": "12.1.0", + "@bloom-housing/ui-seeds": "1.12.1", "axios-cookiejar-support": "4.0.6", "tough-cookie": "4.1.3" }, diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts index 9594e411c7..f3bff26f8f 100644 --- a/shared-helpers/src/auth/AuthContext.ts +++ b/shared-helpers/src/auth/AuthContext.ts @@ -19,6 +19,7 @@ import { RequestMfaCodeResponse, EnumRequestMfaCodeMfaType, EnumLoginMfaType, + MapLayersService, } from "@bloom-housing/backend-core/types" import { GenericRouter, NavigationContext } from "@bloom-housing/ui-components" import { @@ -46,9 +47,10 @@ type ContextProps = { userProfileService: UserProfileService authService: AuthService multiselectQuestionsService: MultiselectQuestionsService + unitTypesService: UnitTypesService reservedCommunityTypeService: ReservedCommunityTypesService unitPriorityService: UnitAccessibilityPriorityTypesService - unitTypesService: UnitTypesService + mapLayersService: MapLayersService loadProfile: (redirect?: string) => void login: ( email: string, @@ -213,6 +215,7 @@ export const AuthProvider: FunctionComponent = ({ child userProfileService: new UserProfileService(), authService: new AuthService(), multiselectQuestionsService: new MultiselectQuestionsService(), + mapLayersService: new MapLayersService(), reservedCommunityTypeService: new ReservedCommunityTypesService(), unitPriorityService: new UnitAccessibilityPriorityTypesService(), unitTypesService: new UnitTypesService(), diff --git a/shared-helpers/src/auth/Timeout.tsx b/shared-helpers/src/auth/Timeout.tsx index 7e6e98a531..0fc0194ad5 100644 --- a/shared-helpers/src/auth/Timeout.tsx +++ b/shared-helpers/src/auth/Timeout.tsx @@ -1,15 +1,13 @@ import React, { createElement, FunctionComponent, useContext, useEffect, useState } from "react" import { AuthContext } from "./AuthContext" import { ConfigContext } from "./ConfigContext" +import { Button } from "@bloom-housing/ui-seeds" import { NavigationContext, - Button, Modal, setSiteAlertMessage, AlertTypes, t, - AppearanceStyleType, - AppearanceSizeType, } from "@bloom-housing/ui-components" const PROMPT_TIMEOUT = 60000 @@ -85,12 +83,12 @@ export const IdleTimeout: FunctionComponent = ({ const modalActions = [ , diff --git a/shared-helpers/src/locales/es.json b/shared-helpers/src/locales/es.json index 90da735246..4ddcbe631f 100644 --- a/shared-helpers/src/locales/es.json +++ b/shared-helpers/src/locales/es.json @@ -39,7 +39,7 @@ "application.alternateContact.contact.description": "Solo utilizaremos esta información para ponernos en contacto con él o ella en relación con su solicitud.", "application.alternateContact.contact.emailAddressFormLabel": "Dirección de email del contacto", "application.alternateContact.contact.phoneNumberFormLabel": "Número telefónico del contacto", - "application.alternateContact.contact.title": "Díganos cómo comunicarnos con su contacto alternativo", + "application.alternateContact.contact.title": "Díganos cómo comunicarnos con su contacto alternativo.", "application.alternateContact.name.alternateContactFormLabel": "Nombre del contacto alternativo", "application.alternateContact.name.caseManagerAgencyFormLabel": "¿En dónde trabaja su administrador(a) de casos o asesor(a) sobre vivienda?", "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Agencia", @@ -78,8 +78,11 @@ "application.contact.couldntLocateAddress": "No hemos podido ubicar la dirección que ingresó. Por favor_ confirme que sea la dirección correcta.", "application.contact.doYouWorkInDescription": "Por decidirse", "application.contact.doYouWorkIn": "¿Trabaja usted en ?", + "application.contact.familyName": "Apellido", + "application.contact.givenName": "Nombre de pila", "application.contact.mailingAddress": "Dirección postal", "application.contact.noPhoneNumber": "No tengo un número telefónico", + "application.contact.number.subNote": "10 dígitos, por ejemplo 999-999-9999", "application.contact.phoneNumberTypes.cell": "Celular", "application.contact.phoneNumberTypes.home": "Casa", "application.contact.phoneNumberTypes.prompt": "¿Qué tipo de número es este?", @@ -90,11 +93,13 @@ "application.contact.state": "Estado", "application.contact.streetAddress": "Domicilio", "application.contact.suggestedAddress": "Dirección sugerida:", - "application.contact.title": "Gracias %{firstName}. Ahora, necesitamos saber cómo comunicarnos con usted.", + "application.contact.title": "Gracias, %{firstName}. Ahora necesitamos saber cómo comunicarnos con usted acerca de su solicitud.", "application.contact.verifyAddressTitle": "Hemos localizado la siguiente dirección. Por favor_ confirme que sea la dirección correcta.", + "application.contact.verifyMultipleAddresses": "Dado que existen varias opciones para esta preferencia, deberá verificar varias direcciones.", "application.contact.workAddress": "Dirección del trabajo", "application.contact.youEntered": "Ha ingresado los siguientes datos:", "application.contact.yourAdditionalPhoneNumber": "Su segundo número de teléfono", + "application.contact.yourAddress": "Su dirección", "application.contact.yourPhoneNumber": "Su número telefónico", "application.contact.zipCode": "Código Postal", "application.contact.zip": "Código Postal", @@ -150,7 +155,7 @@ "application.household.householdStudent.question": "¿Alguien de su grupo familiar es estudiante a tiempo completo o cumplirá 18 años en los próximos 60 días?", "application.household.householdStudent.title": "El grupo familiar incluye un estudiante o miembro que está por cumplir 18 años", "application.household.liveAlone.liveWithOtherPeople": "Otras personas vivirán conmigo", - "application.household.liveAlone.title": "Ahora nos gustaría obtener información acerca de las otras personas que residirán con usted en la vivienda", + "application.household.liveAlone.title": "Ahora nos gustaría obtener información acerca de las otras personas que residirán con usted en la vivienda.", "application.household.liveAlone.willLiveAlone": "Viviré solo(a)", "application.household.member.cancelAddingThisPerson": "Cancelar añadir a esta persona", "application.household.member.dateOfBirth": "Fecha de nacimiento", @@ -158,8 +163,8 @@ "application.household.member.haveSameAddress": "¿Tiene la misma dirección que usted?", "application.household.member.name": "Nombre del miembro del hogar", "application.household.member.saveHouseholdMember": "Guardar el miembro del hogar", - "application.household.member.subTitle": "Tendrá la oportunidad de añadir más miembros del hogar en la próxima pantalla", - "application.household.member.title": "Háblenos acerca de esta persona", + "application.household.member.subTitle": "Tendrá la oportunidad de añadir más miembros del hogar en la próxima pantalla.", + "application.household.member.title": "Háblenos acerca de esta persona.", "application.household.member.updateHouseholdMember": "Actualizar al miembro del hogar", "application.household.member.whatIsTheirRelationship": "¿Cuál es su parentesco o relación con usted?", "application.household.member.whatReletionship": "¿Cuál es su parentesco o relación con usted?", @@ -174,11 +179,12 @@ "application.household.preferredUnit.options.studio": "Estudio", "application.household.preferredUnit.options.threeBdrm": "3 dormitorios", "application.household.preferredUnit.options.twoBdrm": "2 dormitorios", - "application.household.preferredUnit.optionsLabel": "Marque todas las opciones que correspondan:", + "application.household.preferredUnit.optionsLabel": "Marque todas las opciones que correspondan", "application.household.preferredUnit.preferredUnitType": "Tipo de vivienda preferida", "application.household.preferredUnit.subTitle": "Aunque los tamaños de las unidades en general se basen en la ocupación, indique el tamaño de unidad que desee para determinar su preferencia en esta oportunidad o establecer una lista de espera (solo por esta oportunidad).", "application.household.preferredUnit.title": "¿Cuáles son los tamaños de vivienda que le interesan?", "application.household.primaryApplicant": "Solicitante primario", + "application.name.dobHelper": "Por ejemplo: 01 19 2000", "application.name.emailPrivacy": "Solo utilizaremos su dirección de email para comunicarnos con usted en relación con su solicitud.", "application.name.firstName": "Nombre", "application.name.lastName": "Apellido", @@ -223,6 +229,9 @@ "application.preferences.liveWorkFosterCity.work.label": "Al menos un miembro de mi hogar trabaja en Foster City", "application.preferences.liveWorkSanMateo.live.label": "Al menos un miembro de mi hogar vive en la Ciudad de San Mateo", "application.preferences.liveWorkSanMateo.work.label": "Al menos un miembro de mi hogar trabaja en la Ciudad de San Mateo", + "application.preferences.options.qualifyingAddress": "Dirección calificada", + "application.preferences.options.addressHolderName": "Nombre completo del titular de la dirección", + "application.preferences.options.addressHolderRelationship": "Relación con el titular de la dirección", "application.preferences.preamble": "Si reúne los requisitos de esta preferencia, recibirá una clasificación más alta.", "application.preferences.rosefieldAUSD.title": "Empleado(a) del Distrito Escolar Unificado Alameda (AUSD)", "application.preferences.rosefieldAUSD.yes.description": "Al menos un miembro de mi hogar es empleado del Distrito Escolar Unificado de Alameda", @@ -425,7 +434,9 @@ "errors.dateOfBirthError": "Por favor ingrese una fecha de nacimiento válida", "errors.emailAddressError": "Por favor ingrese una dirección de email", "errors.errorsToResolve": "Hay errores que tendrá que corregir antes de poder seguir adelante.", + "errors.familyNameError": "Por favor ingrese un apellido", "errors.firstNameError": "Por favor ingrese un nombre", + "errors.givenNameError": "Por favor ingrese un nombre de pila", "errors.householdTooBig": "El número de miembros de su hogar es demasiado alto.", "errors.householdTooSmall": "El número de miembros de su hogar es demasiado bajo.", "errors.lastNameError": "Por favor ingrese un apellido", @@ -745,7 +756,7 @@ "t.petsPolicy": "Política de mascotas", "t.phoneNumberPlaceholder": "(555) 555-5555", "t.phone": "Teléfono", - "t.pleaseSelectOne": "Por favor seleccione una opción.", + "t.pleaseSelectOne": "Por favor seleccione una opción", "t.pleaseSelectYesNo": "Elija “sí” o “no”.", "t.pm": "PM", "t.preferNotToSay": "Prefiero no decirlo", diff --git a/shared-helpers/src/locales/general.json b/shared-helpers/src/locales/general.json index 3289b27af7..45c59c6255 100644 --- a/shared-helpers/src/locales/general.json +++ b/shared-helpers/src/locales/general.json @@ -39,7 +39,7 @@ "application.alternateContact.contact.description": "We'll only use this information to contact them about your application.", "application.alternateContact.contact.emailAddressFormLabel": "Contact email address", "application.alternateContact.contact.phoneNumberFormLabel": "Contact phone number", - "application.alternateContact.contact.title": "Let us know how to reach your alternate contact", + "application.alternateContact.contact.title": "Let us know how to reach your alternate contact.", "application.alternateContact.name.alternateContactFormLabel": "Name of alternate contact", "application.alternateContact.name.caseManagerAgencyFormLabel": "Where does your case manager or housing counselor work?", "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Agency", @@ -78,25 +78,32 @@ "application.contact.couldntLocateAddress": "We couldn't locate the address you entered. Please confirm it's correct.", "application.contact.doYouWorkIn": "Do you work in %{county} County?", "application.contact.doYouWorkInDescription": "TBD", + "application.contact.familyName": "Family Name", + "application.contact.givenName": "Given Name", "application.contact.mailingAddress": "Mailing Address", "application.contact.noPhoneNumber": "I don't have a telephone number", + "application.contact.number": "Number", + "application.contact.number.subNote": "10-digit, for example 999-999-9999", "application.contact.phoneNumberTypes.cell": "Cell", "application.contact.phoneNumberTypes.home": "Home", "application.contact.phoneNumberTypes.prompt": "What type of number is this?", "application.contact.phoneNumberTypes.work": "Work", "application.contact.preferredContactType": "Preferred Contact Type", "application.contact.provideAMailingAddress": "Provide an address where you can receive updates and materials about your application.", + "application.contact.secondNumber": "Second Number", "application.contact.sendMailToMailingAddress": "Send my mail to a different address", "application.contact.state": "State", "application.contact.streetAddress": "Street Address", "application.contact.suggestedAddress": "Suggested Address:", - "application.contact.title": "Thanks %{firstName}. Now we need to know how to contact you.", + "application.contact.title": "Thanks, %{firstName}. Now we need to know how to contact you about your application.", "application.contact.verifyAddressTitle": "We have located the following address. Please confirm it's correct.", + "application.contact.verifyMultipleAddresses": "Since there are multiple options for this preference, you’ll need to verify multiple addresses.", "application.contact.workAddress": "Work Address", "application.contact.youEntered": "You Entered:", "application.contact.yourAdditionalPhoneNumber": "Your Second Phone Number", + "application.contact.yourAddress": "Your Address", "application.contact.yourPhoneNumber": "Your Phone Number", - "application.contact.zip": "Zip", + "application.contact.zip": "Zip Code", "application.contact.zipCode": "Zipcode", "application.details.adaPriorities": "ADA Priorities Selected", "application.edited": "Edited", @@ -150,7 +157,7 @@ "application.household.householdStudent.question": "Is someone in your household a full time student or going to turn 18 years old within 60 days?", "application.household.householdStudent.title": "Household Includes Student or Member Nearing 18", "application.household.liveAlone.liveWithOtherPeople": "Other people will live with me", - "application.household.liveAlone.title": "Next we would like to know about the others who will live with you in the unit", + "application.household.liveAlone.title": "Next we would like to know about the others who will live with you in the unit.", "application.household.liveAlone.willLiveAlone": "I will live alone", "application.household.member.cancelAddingThisPerson": "Cancel adding this person", "application.household.member.dateOfBirth": "Date of Birth", @@ -158,8 +165,8 @@ "application.household.member.haveSameAddress": "Do they have same address as you?", "application.household.member.name": "Household member's name", "application.household.member.saveHouseholdMember": "Save household member", - "application.household.member.subTitle": "You will have an opportunity to add more household members on the next screen", - "application.household.member.title": "Tell us about this person", + "application.household.member.subTitle": "You will have an opportunity to add more household members on the next screen.", + "application.household.member.title": "Tell us about this person.", "application.household.member.updateHouseholdMember": "Update Household Member", "application.household.member.whatIsTheirRelationship": "What is their relationship to you?", "application.household.member.whatReletionship": "What is their relationship to you", @@ -174,11 +181,12 @@ "application.household.preferredUnit.options.studio": "Studio", "application.household.preferredUnit.options.threeBdrm": "3 Bedroom", "application.household.preferredUnit.options.twoBdrm": "2 Bedroom", - "application.household.preferredUnit.optionsLabel": "Check all that apply:", + "application.household.preferredUnit.optionsLabel": "Check all that apply", "application.household.preferredUnit.preferredUnitType": "Preferred Unit Type", - "application.household.preferredUnit.subTitle": "Although unit sizes will typically be based on occupancy, please provide your preferred unit size for determining your preference in this opportunity or establishing a waitlist (for this opportunity only)", + "application.household.preferredUnit.subTitle": "Although unit sizes will typically be based on occupancy, please provide your preferred unit size for determining your preference in this opportunity or establishing a waitlist (for this opportunity only).", "application.household.preferredUnit.title": "What unit sizes are you interested in?", "application.household.primaryApplicant": "Primary Applicant", + "application.name.dobHelper": "For example: 01 19 2000", "application.name.emailPrivacy": "We will only use your email address to contact you about your application.", "application.name.firstName": "First Name", "application.name.lastName": "Last Name", @@ -452,6 +460,7 @@ "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 may result in removal from the application process.", "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 may result in removal from the application process.", + "application.review.terms.submittingApplication": "Submitting application", "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 may result in removal from the application process.", "application.review.terms.title": "Terms", "application.review.voucherOrSubsidy": "Housing Voucher or Rental Subsidy", @@ -564,7 +573,9 @@ "errors.dateOfBirthErrorAge": "Please enter a valid Date of Birth, must be 18 or older", "errors.emailAddressError": "Please enter an email address", "errors.errorsToResolve": "There are errors you'll need to resolve before moving on.", + "errors.familyNameError": "Please enter a Family Name", "errors.firstNameError": "Please enter a First Name", + "errors.givenNameError": "Please enter a Given Name", "errors.householdTooBig": "Your household size is too big.", "errors.householdTooSmall": "Your household size is too small.", "errors.lastNameError": "Please enter a Last Name", @@ -586,7 +597,7 @@ "errors.stateError": "Please enter a state", "errors.streetError": "Please enter an address", "errors.timeError": "Please enter a valid time", - "errors.zipCodeError": "Please enter a zipcode", + "errors.zipCodeError": "Please enter a zip code", "footer.contact": "Contact", "footer.copyright": "Demonstration Jurisdiction © 2021 • All Rights Reserved", "footer.disclaimer": "Disclaimer", @@ -937,7 +948,7 @@ "t.petsPolicy": "Pets Policy", "t.phone": "Phone", "t.phoneNumberPlaceholder": "(555) 555-5555", - "t.pleaseSelectOne": "Please select one.", + "t.pleaseSelectOne": "Please select one", "t.pleaseSelectYesNo": "Please select yes or no.", "t.pm": "PM", "t.preferNotToSay": "Prefer not to say", diff --git a/shared-helpers/src/locales/tl.json b/shared-helpers/src/locales/tl.json index 9e360f3a94..875c484346 100644 --- a/shared-helpers/src/locales/tl.json +++ b/shared-helpers/src/locales/tl.json @@ -39,7 +39,7 @@ "application.alternateContact.contact.description": "Gagamitin lamang namin ang impormasyong ito para kontakin sila tungkol sa iyong application.", "application.alternateContact.contact.emailAddressFormLabel": "Email address ng Contact", "application.alternateContact.contact.phoneNumberFormLabel": "Numero ng telepono ng contact", - "application.alternateContact.contact.title": "Ipaalam sa ami kung paano matatawagan ang iyong kahaliling contact", + "application.alternateContact.contact.title": "Ipaalam sa ami kung paano matatawagan ang iyong kahaliling contact.", "application.alternateContact.name.alternateContactFormLabel": "Pangalan ng kahaliling contact", "application.alternateContact.name.caseManagerAgencyFormLabel": "Saan nagtatrabaho ang iyong case manager o tagapayo sa pabahay?", "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Ahensiya", @@ -78,8 +78,11 @@ "application.contact.couldntLocateAddress": "Hindi namin mahanap ang lugar na iyong inilagay. Pakikumpirmahin kung tama ito.", "application.contact.doYouWorkInDescription": "TBD", "application.contact.doYouWorkIn": "Nagtatrabaho ka ba sa %{county} County?", + "application.contact.familyName": "Apelyido", + "application.contact.givenName": "Ibinigay na pangalan", "application.contact.mailingAddress": "Mailing Address", "application.contact.noPhoneNumber": "Wala akong numero ng telepono", + "application.contact.number.subNote": "10-digit, halimbawa 999-999-9999", "application.contact.phoneNumberTypes.cell": "Cell", "application.contact.phoneNumberTypes.home": "Home", "application.contact.phoneNumberTypes.prompt": "Anong uri ng numero ito?", @@ -90,14 +93,16 @@ "application.contact.state": "State", "application.contact.streetAddress": "Street Address", "application.contact.suggestedAddress": "Iminungkahing Address:", - "application.contact.title": "Salamat %{firstName}. Ngayon ay kailangang malaman namin kung paano ka makokontak.", + "application.contact.title": "Salamat, %{firstName}. Ngayon ay kailangan naming malaman kung paano makipag-ugnayan sa iyo tungkol sa iyong aplikasyon.", "application.contact.verifyAddressTitle": "Nahanap namin ang sumusunod na address. Pakikumpirma kung tama ito.", + "application.contact.verifyMultipleAddresses": "Dahil maraming opsyon para sa kagustuhang ito, kakailanganin mong i-verify ang maraming address.", "application.contact.workAddress": "Address ng Trabaho", "application.contact.youEntered": "Inilagay Mo ay:", "application.contact.yourAdditionalPhoneNumber": "Iyong Pangalawang Numero ng Telepono", + "application.contact.yourAddress": "Ang iyong Address", "application.contact.yourPhoneNumber": "Ang Iyong Numero ng Telepono", "application.contact.zipCode": "Zipcode", - "application.contact.zip": "Zip", + "application.contact.zip": "Zip Code", "application.details.adaPriorities": "Napiling ADA Priorities", "application.edited": "Binago", "application.financial.income.instruction1": "Idagdag ang iyong kabuuang (bago ang buwis) na kita ng sambahayan mula sa sahod_ benepisyo at iba pang pinagkukunan mula sa lahat ng miyembro ng sambahayan.", @@ -150,7 +155,7 @@ "application.household.householdStudent.question": "Mayroon ba sa iyong sambahayan na isang full time na estudyante o magiging 18 taong gulang sa loob ng 60 araw?", "application.household.householdStudent.title": "Kasama sa Sambahayan ang Mag-aaral o Miyembro na Malapit nang Mag-18", "application.household.liveAlone.liveWithOtherPeople": "Iba pang mga tao na kasama kong nakatira", - "application.household.liveAlone.title": "Susunod ay gusto naming malaman ang tungkol sa iba pang titira na kasama mo sa unit", + "application.household.liveAlone.title": "Susunod ay gusto naming malaman ang tungkol sa iba pang titira na kasama mo sa unit.", "application.household.liveAlone.willLiveAlone": "Mag-isa akong titira", "application.household.member.cancelAddingThisPerson": "Kanselahin ang pagdagdag sa taong ito", "application.household.member.dateOfBirth": "Petsa ng Kapanganakan", @@ -158,8 +163,8 @@ "application.household.member.haveSameAddress": "Kapareho ba nila ang address mo?", "application.household.member.name": "Pangalan ng miyembro ng sambahayan", "application.household.member.saveHouseholdMember": "I-save ang miyembro ng sambahayan", - "application.household.member.subTitle": "Magkakaroon ka ng oportunidad na magdagdag ng mga miyembro ng sambahayan sa susunod na screen", - "application.household.member.title": "Sabihin sa amin ang tungkol sa taong ito", + "application.household.member.subTitle": "Magkakaroon ka ng oportunidad na magdagdag ng mga miyembro ng sambahayan sa susunod na screen.", + "application.household.member.title": "Sabihin sa amin ang tungkol sa taong ito.", "application.household.member.updateHouseholdMember": "I-update ang Miyembro ng Household", "application.household.member.whatIsTheirRelationship": "Ano ang kanilang relasyon sa iyo?", "application.household.member.whatReletionship": "Ano ang kanilang relasyon sa iyo", @@ -174,11 +179,12 @@ "application.household.preferredUnit.options.studio": "Studio", "application.household.preferredUnit.options.threeBdrm": "3 Kwarto", "application.household.preferredUnit.options.twoBdrm": "2 Kwarto", - "application.household.preferredUnit.optionsLabel": "Tingnan ang lahat ng naaangkop:", + "application.household.preferredUnit.optionsLabel": "Tingnan ang lahat ng naaangkop", "application.household.preferredUnit.preferredUnitType": "Napiling Uri ng Unit", - "application.household.preferredUnit.subTitle": "Bagama't ang mga sukat ng unit ay karaniwang ibabatay sa nakatira_ mangyaring ibigay ang iyong gustong laki ng unit para sa pagtukoy ng iyong kagustuhan sa pagkakataong ito o pagbuo ng waitlist (para sa pagkakataong ito lamang)", + "application.household.preferredUnit.subTitle": "Bagama't ang mga sukat ng unit ay karaniwang ibabatay sa nakatira_ mangyaring ibigay ang iyong gustong laki ng unit para sa pagtukoy ng iyong kagustuhan sa pagkakataong ito o pagbuo ng waitlist (para sa pagkakataong ito lamang).", "application.household.preferredUnit.title": "Sa anong mga sukat ng unit ka interesado?", "application.household.primaryApplicant": "Pangunahing Aplikante", + "application.name.dobHelper": "Halimbawa: 01 19 2000", "application.name.emailPrivacy": "Gagamitin lang namin ang iyong email address para makipag-ugnayan sa iyo tungkol sa iyong application.", "application.name.firstName": "Pangalan", "application.name.lastName": "Apelyido", @@ -192,6 +198,9 @@ "application.referralApplication.furtherInformation": "Para sa karagdagang impormasyon", "application.referralApplication.instructions": "Ang permanenteng sumusuporta sa mga yunit ng pabahay ay direktang tinutukoy sa pamamagitan ng Coordinated Entry System. Maaaring tumawag sa ang mga sambahayang nakararanas ng kawalan ng tirahan upang makakonekta sa isang Access Point upang matuto pa tungkol sa coordinated entry system at ma-access ang mga resource at impormasyong nauugnay sa pabahay.", "application.referralApplication.phoneNumber": "211", + "application.preferences.options.qualifyingAddress": "Kwalipikadong Address", + "application.preferences.options.addressHolderName": "Buong Pangalan ng May-ari ng Address", + "application.preferences.options.addressHolderRelationship": "Relasyon sa May-ari ng Address", "application.review.confirmation.applicationReceived": "Natanggap \na ang application", "application.review.confirmation.applicationsClosed": "Sarado na ang \nmga application", "application.review.confirmation.applicationsRanked": "Nakaranggo na ang \nmga application", @@ -360,7 +369,9 @@ "errors.dateOfBirthError": "Pakilagay ang tamang Petsa ng Kapanganakan", "errors.emailAddressError": "Pakilagay ang email address", "errors.errorsToResolve": "May mga problema na gusto mong resolbahin bago magpatuloy.", + "errors.familyNameError": "Mangyaring maglagay ng Pangalan ng Pamilya", "errors.firstNameError": "Pakilagay ang pangalan", + "errors.givenNameError": "Mangyaring maglagay ng Ibinigay na Pangalan", "errors.householdTooBig": "Napakalaki ng iyong sambahayan.", "errors.householdTooSmall": "Napakaliit ng iyong sambahayan.", "errors.lastNameError": "Pakilagay ang Apelyido", @@ -615,7 +626,7 @@ "t.petsPolicy": "Patakaran sa mga Alagang Hayop", "t.phoneNumberPlaceholder": "(555) 555-5555", "t.phone": "Telepono", - "t.pleaseSelectOne": "Pumili ng isa.", + "t.pleaseSelectOne": "Pumili ng isa", "t.pleaseSelectYesNo": "Pakipiliin ang oo o hindi.", "t.pm": "PM", "t.preferNotToSay": "Piniling hindi sabihin", diff --git a/shared-helpers/src/locales/vi.json b/shared-helpers/src/locales/vi.json index 6243b1f657..ee7d057a5b 100644 --- a/shared-helpers/src/locales/vi.json +++ b/shared-helpers/src/locales/vi.json @@ -39,7 +39,7 @@ "application.alternateContact.contact.description": "Chúng tôi sẽ chỉ sử dụng thông tin này để liên lạc với họ về đơn ghi danh của quý vị.", "application.alternateContact.contact.emailAddressFormLabel": "Địa chỉ email của người liên lạc", "application.alternateContact.contact.phoneNumberFormLabel": "Số điện thoại của người liên lạc", - "application.alternateContact.contact.title": "Hãy cho chúng tôi biết cách để liên lạc với người liên lạc thay thế của quý vị", + "application.alternateContact.contact.title": "Hãy cho chúng tôi biết cách để liên lạc với người liên lạc thay thế của quý vị.", "application.alternateContact.name.alternateContactFormLabel": "Tên người liên lạc thay thế", "application.alternateContact.name.caseManagerAgencyFormLabel": "Quản lý hồ sơ hoặc cố vấn nhà ở của quý vị công tác tại đâu?", "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "Cơ quan", @@ -78,8 +78,11 @@ "application.contact.couldntLocateAddress": "Chúng tôi không thể tìm thấy địa chỉ quý vị đã nhập. Vui lòng xác nhận địa chỉ chính xác.", "application.contact.doYouWorkInDescription": "Chưa xác định", "application.contact.doYouWorkIn": "Quý vị có làm việc tại không?", + "application.contact.familyName": "Tên gia đình", + "application.contact.givenName": "Tên", "application.contact.mailingAddress": "Địa chỉ Nhận thư", "application.contact.noPhoneNumber": "Tôi không có số điện thoạii", + "application.contact.number.subNote": "10 chữ số, ví dụ 999-999-9999", "application.contact.phoneNumberTypes.cell": "Di động", "application.contact.phoneNumberTypes.home": "Nhà riêng", "application.contact.phoneNumberTypes.prompt": "Kiểu số điện thoại này là gì?", @@ -90,14 +93,16 @@ "application.contact.state": "Tiểu bang", "application.contact.streetAddress": "Địa chỉ Đường phố", "application.contact.suggestedAddress": "Địa chỉ đề xuất:", - "application.contact.title": "Cảm ơn %{firstName}. Bây giờ chúng tôi cần biết cách để liên lạc với quý vị.", + "application.contact.title": "Cảm ơn, %{firstName}. Bây giờ chúng tôi cần biết cách liên hệ với bạn về đơn đăng ký của bạn.", "application.contact.verifyAddressTitle": "Chúng tôi đã xác định được địa chỉ sau. Vui lòng xác nhận địa chỉ chính xác.", + "application.contact.verifyMultipleAddresses": "Vì có nhiều tùy chọn cho tùy chọn này nên bạn sẽ cần xác minh nhiều địa chỉ.", "application.contact.workAddress": "Địa chỉ Sở Làm", "application.contact.youEntered": "Quý vị đã nhập:", "application.contact.yourAdditionalPhoneNumber": "Số điện thoại thứ hai của quý vị", + "application.contact.yourAddress": "Địa chỉ của bạn", "application.contact.yourPhoneNumber": "Số Điện thoại của Quý vị", "application.contact.zipCode": "Số zipcode", - "application.contact.zip": "Mã Zip", + "application.contact.zip": "Mã Bưu Chính", "application.details.adaPriorities": "Ưu tiên ADA đã chọn", "application.edited": "Đã chỉnh sửa", "application.financial.income.instruction1": "Tính tổng thu nhập hộ gia đình (trước thuế) từ tiền lương, tiền trợ cấp và các nguồn khác từ tất cả các thành viên trong hộ gia đình.", @@ -150,7 +155,7 @@ "application.household.householdStudent.question": "Có ai đó trong hộ gia đình quý vị là sinh viên chính quy hoặc sắp đủ 18 tuổi trong vòng 60 ngày không?", "application.household.householdStudent.title": "Hộ Gia Đình Có Sinh Viên hoặc Thành Viên Sắp 18 Tuổi", "application.household.liveAlone.liveWithOtherPeople": "Những người khác sẽ sống cùng tôi", - "application.household.liveAlone.title": "Tiếp theo, chúng tôi muốn biết về những người khác sẽ sống trong căn nhà cùng quý vị", + "application.household.liveAlone.title": "Tiếp theo, chúng tôi muốn biết về những người khác sẽ sống trong căn nhà cùng quý vị.", "application.household.liveAlone.willLiveAlone": "Tôi sẽ sống một mình", "application.household.member.cancelAddingThisPerson": "Hủy bỏ việc thêm người này", "application.household.member.dateOfBirth": "Ngày sinh", @@ -158,8 +163,8 @@ "application.household.member.haveSameAddress": "Họ có cùng địa chỉ với quý vị không?", "application.household.member.name": "Tên của thành viên hộ gia đình", "application.household.member.saveHouseholdMember": "Lưu thành viên hộ gia đình", - "application.household.member.subTitle": "Quý vị sẽ có cơ hội thêm nhiều thành viên hộ gia đình hơn trong màn hình tiếp theo", - "application.household.member.title": "Hãy cho chúng tôi biết về người này", + "application.household.member.subTitle": "Quý vị sẽ có cơ hội thêm nhiều thành viên hộ gia đình hơn trong màn hình tiếp theo.", + "application.household.member.title": "Hãy cho chúng tôi biết về người này.", "application.household.member.updateHouseholdMember": "Cập nhật Thành viên Hộ Gia đình", "application.household.member.whatIsTheirRelationship": "Quan hệ của người đó với quý vị là gì?", "application.household.member.whatReletionship": "Quan hệ của người đó với quý vị là gì", @@ -174,11 +179,12 @@ "application.household.preferredUnit.options.studio": "Một phòng kiểu studio", "application.household.preferredUnit.options.threeBdrm": "3 Phòng ngủ", "application.household.preferredUnit.options.twoBdrm": "2 Phòng ngủ", - "application.household.preferredUnit.optionsLabel": "Đánh dấu tất cả các lựa chọn phù hợp:", + "application.household.preferredUnit.optionsLabel": "Đánh dấu tất cả các lựa chọn phù hợp", "application.household.preferredUnit.preferredUnitType": "Loại Căn nhà Ưa thích", - "application.household.preferredUnit.subTitle": "Mặc dù các kích thước nhà ở thường dựa vào mức độ sử dụng nhưng vui lòng cho biết kích thước nhà ở mà quý vị yêu thích để xác định ưu tiên của quý vị trong cơ hội này hoặc lập danh sách chờ (chỉ dành cho cơ hội này)", + "application.household.preferredUnit.subTitle": "Mặc dù các kích thước nhà ở thường dựa vào mức độ sử dụng nhưng vui lòng cho biết kích thước nhà ở mà quý vị yêu thích để xác định ưu tiên của quý vị trong cơ hội này hoặc lập danh sách chờ (chỉ dành cho cơ hội này).", "application.household.preferredUnit.title": "Kích thước căn nhà mà quý vị quan tâm là gì?", "application.household.primaryApplicant": "Người nộp đơn Chính", + "application.name.dobHelper": "Ví dụ: 01 19 2000", "application.name.emailPrivacy": "Chúng tôi sẽ chỉ sử dụng địa chỉ email của quý vị để liên lạc với quý vị về việc ghi danh của quý vị.", "application.name.firstName": "Tên", "application.name.lastName": "Họ", @@ -223,6 +229,9 @@ "application.preferences.liveWorkFosterCity.work.label": "Ít nhất một thành viên trong gia đình tôi làm việc ở Foster City", "application.preferences.liveWorkSanMateo.live.label": "Ít nhất một thành viên trong gia đình tôi sống ở Thành phố San Mateo", "application.preferences.liveWorkSanMateo.work.label": "Ít nhất một thành viên trong gia đình tôi làm việc ở Thành phố San Mateo", + "application.preferences.options.qualifyingAddress": "Địa chỉ đủ điều kiện", + "application.preferences.options.addressHolderName": "Tên đầy đủ của người giữ địa chỉ", + "application.preferences.options.addressHolderRelationship": "Mối quan hệ với người giữ địa chỉ", "application.preferences.preamble": "Nếu quý vị hội đủ điều kiện cho lựa chọn ưu tiên này, quý vị sẽ được xếp ở thứ hạng cao hơn.", "application.preferences.rosefieldAUSD.title": "Nhân viên của Học khu Thống nhất Alameda (AUSD)", "application.preferences.rosefieldAUSD.yes.description": "Ít nhất một thành viên trong gia đình tôi là nhân viên của Học khu Thống nhất Alameda", @@ -407,7 +416,9 @@ "errors.dateOfBirthError": "Vui lòng nhập Ngày sinh hợp lệ", "errors.emailAddressError": "Vui lòng nhập địa chỉ email", "errors.errorsToResolve": "Quý vị cần giải quyết những lỗi này trước khi chuyển sang bước tiếp.", + "errors.familyNameError": "Vui lòng nhập Họ Tên", "errors.firstNameError": "Vui lòng nhập Tên", + "errors.givenNameError": "Vui lòng nhập Tên riêng", "errors.householdTooBig": "Quy mô hộ gia đình của quý vị quá lớn.", "errors.householdTooSmall": "Quy mô hộ gia đình của quý vị quá nhỏ.", "errors.lastNameError": "Vui lòng nhập Họ", @@ -722,7 +733,7 @@ "t.petsPolicy": "Chính sách đối với Vật nuôi", "t.phoneNumberPlaceholder": "(555) 555-5555", "t.phone": "Điện thoại", - "t.pleaseSelectOne": "Vui lòng chọn một.", + "t.pleaseSelectOne": "Vui lòng chọn một", "t.pleaseSelectYesNo": "Vui lòng chọn có hoặc không.", "t.pm": "Chiều", "t.preferNotToSay": "Không muốn nói", diff --git a/shared-helpers/src/locales/zh.json b/shared-helpers/src/locales/zh.json index 4b4649a6d7..e80991ee8b 100644 --- a/shared-helpers/src/locales/zh.json +++ b/shared-helpers/src/locales/zh.json @@ -39,7 +39,7 @@ "application.alternateContact.contact.description": "我們只會為您的申請事宜使用此資料聯絡他們。", "application.alternateContact.contact.emailAddressFormLabel": "聯絡人電郵地址", "application.alternateContact.contact.phoneNumberFormLabel": "聯絡人電話號碼", - "application.alternateContact.contact.title": "請告知我們如何接觸您的其他聯絡人", + "application.alternateContact.contact.title": "請告知我們如何接觸您的其他聯絡人。", "application.alternateContact.name.alternateContactFormLabel": "其他聯絡人姓名", "application.alternateContact.name.caseManagerAgencyFormLabel": "您的個案經理或房屋顧問在哪裡工作?", "application.alternateContact.name.caseManagerAgencyFormPlaceHolder": "機構", @@ -78,8 +78,11 @@ "application.contact.couldntLocateAddress": "我們找不到您輸入的地址。 請確認這是正確地址。", "application.contact.doYouWorkInDescription": "待定", "application.contact.doYouWorkIn": "您是否在 工作?", + "application.contact.familyName": "姓", + "application.contact.givenName": "給定的名稱", "application.contact.mailingAddress": "郵寄地址", "application.contact.noPhoneNumber": "我沒有電話號碼", + "application.contact.number.subNote": "10 位數字,例如 999-999-9999", "application.contact.phoneNumberTypes.cell": "手機", "application.contact.phoneNumberTypes.home": "住家", "application.contact.phoneNumberTypes.prompt": "這是哪類電話號碼?", @@ -90,11 +93,13 @@ "application.contact.state": "州", "application.contact.streetAddress": "街道地址", "application.contact.suggestedAddress": "建議的地址:", - "application.contact.title": "%{firstName},謝謝您。現在我們需要知道如何聯絡您。", + "application.contact.title": "謝謝,%{firstName}。現在我們需要知道如何就您的申請與您聯繫。", "application.contact.verifyAddressTitle": "我們已找到下列地址。請確認這是正確地址。", + "application.contact.verifyMultipleAddresses": "由於此首選項有多個選項,因此您需要驗證多個位址。", "application.contact.workAddress": "工作地址", "application.contact.youEntered": "您已輸入:", "application.contact.yourAdditionalPhoneNumber": "您的第二電話號碼", + "application.contact.yourAddress": "你的地址", "application.contact.yourPhoneNumber": "您的電話號碼", "application.contact.zipCode": "郵遞區號", "application.contact.zip": "郵遞區號", @@ -144,13 +149,13 @@ "application.household.dontQualifyInfo": "如果您認為自己可能填寫錯誤,請更改資料。請注意,如果您偽造任何申請資料,您將會被取消資格。如果您填寫的資料正確無誤,我們建議您日後再回來查看,因為會有更多可供申請的物業。", "application.household.expectingChanges.question": "在未來12個月中,您預期您的住家會發生什麼變化嗎,例如家庭人數?", "application.household.expectingChanges.title": "預計家庭變化", - "application.household.genericSubtitle": "如果您的申請被選中,請準備提供必要文件。 ", + "application.household.genericSubtitle": "如果您的申請被選中,請準備提供必要文件。", "application.household.householdMember": "家庭成員(一人)", "application.household.householdMembers": "家庭成員(多人)", "application.household.householdStudent.question": "您的家人是否有全日制學生或在 60 天內年滿 18 歲?", "application.household.householdStudent.title": "家人包括接近 18 歲的學生或成員", "application.household.liveAlone.liveWithOtherPeople": "其他將會與我同住的人", - "application.household.liveAlone.title": "接着,我們想知道將會與您同住一個單位的其他人", + "application.household.liveAlone.title": "接着,我們想知道將會與您同住一個單位的其他人。", "application.household.liveAlone.willLiveAlone": "我將會獨居", "application.household.member.cancelAddingThisPerson": "取消加入此人", "application.household.member.dateOfBirth": "出生日期", @@ -158,8 +163,8 @@ "application.household.member.haveSameAddress": "他們的地址是否和您一樣?", "application.household.member.name": "家庭成員姓名", "application.household.member.saveHouseholdMember": "儲存家庭成員資料", - "application.household.member.subTitle": "在下一個頁面,您將有機會加入更多家庭成員", - "application.household.member.title": "請提供此人的資料", + "application.household.member.subTitle": "在下一個頁面,您將有機會加入更多家庭成員。", + "application.household.member.title": "請提供此人的資料。", "application.household.member.updateHouseholdMember": "更新家庭成員資料", "application.household.member.whatIsTheirRelationship": "他們與您是什麼關係?", "application.household.member.whatReletionship": "他們與您是什麼關係?", @@ -174,11 +179,12 @@ "application.household.preferredUnit.options.studio": "套房", "application.household.preferredUnit.options.threeBdrm": "3 臥室", "application.household.preferredUnit.options.twoBdrm": "2 間臥室", - "application.household.preferredUnit.optionsLabel": "請勾選所有適用的單位:", + "application.household.preferredUnit.optionsLabel": "請勾選所有適用的單位", "application.household.preferredUnit.preferredUnitType": "首選單位類型", - "application.household.preferredUnit.subTitle": "雖然單位尺寸通常根據入住率分配,但請提供您的首選單位尺寸,以確定您在此機會的偏好或建立候補名單(僅針對此機會)", + "application.household.preferredUnit.subTitle": "雖然單位尺寸通常根據入住率分配,但請提供您的首選單位尺寸,以確定您在此機會的偏好或建立候補名單(僅針對此機會)。", "application.household.preferredUnit.title": "您對哪類單位面積感興趣?", "application.household.primaryApplicant": "主要申請人", + "application.name.dobHelper": "例如:2000 年 1 月 19 日", "application.name.emailPrivacy": "我們只會為申請事宜使用您的電郵地址與您聯絡。", "application.name.firstName": "名字", "application.name.lastName": "姓氏", @@ -223,6 +229,9 @@ "application.preferences.liveWorkFosterCity.work.label": "我的至少一名家庭成员在福斯特城工作", "application.preferences.liveWorkSanMateo.live.label": "我的至少一名家庭成员住在圣马特奥市", "application.preferences.liveWorkSanMateo.work.label": "我的至少一名家庭成员在圣马特奥市工作", + "application.preferences.options.qualifyingAddress": "合格地址", + "application.preferences.options.addressHolderName": "地址持有人的全名", + "application.preferences.options.addressHolderRelationship": "與地址持有人的關係", "application.preferences.preamble": "如果您符合這項優先權資格,您將會獲得更高排名。", "application.preferences.rosefieldAUSD.title": "阿拉米达联合学区 (AUSD) 员工", "application.preferences.rosefieldAUSD.yes.description": "我的至少一名家庭成员是阿拉米达联合学区的雇员", @@ -407,7 +416,9 @@ "errors.dateOfBirthError": "請輸入有效的出生日期", "errors.emailAddressError": "請輸入電郵地址", "errors.errorsToResolve": "出現一些錯誤;您需要解決問題才能繼續操作。", + "errors.familyNameError": "請輸入姓氏", "errors.firstNameError": "請輸入名字", + "errors.givenNameError": "請輸入名字", "errors.householdTooBig": "您的家庭人數過多。", "errors.householdTooSmall": "您的家庭人數過少。", "errors.lastNameError": "請輸入姓氏", @@ -727,7 +738,7 @@ "t.petsPolicy": "寵物政策", "t.phoneNumberPlaceholder": "(555) 555-5555", "t.phone": "電話號碼", - "t.pleaseSelectOne": "請選取一項。", + "t.pleaseSelectOne": "請選取一項", "t.pleaseSelectYesNo": "請選擇是或否。", "t.pm": "下午", "t.preferNotToSay": "保密", diff --git a/shared-helpers/src/views/address/FormAddressAlternate.tsx b/shared-helpers/src/views/address/FormAddressAlternate.tsx new file mode 100644 index 0000000000..6b79ea9466 --- /dev/null +++ b/shared-helpers/src/views/address/FormAddressAlternate.tsx @@ -0,0 +1,98 @@ +import { UseFormMethods } from "react-hook-form" +import { Field, resolveObject, Select, t } from "@bloom-housing/ui-components" +import React from "react" + +type FormAddressProps = { + subtitle?: string + dataKey: string + register: UseFormMethods["register"] + errors?: UseFormMethods["errors"] + required?: boolean + stateKeys: string[] +} + +export const FormAddressAlternate = ({ + subtitle, + dataKey, + register, + errors, + required, + stateKeys, +}: FormAddressProps) => { + return ( + <> + + {!subtitle ? t("application.preferences.options.address") : subtitle} + + + + + +
+ + +