From de86cc1eba9a4ebf9796476830bf68a2f1822ae9 Mon Sep 17 00:00:00 2001 From: Barrett Falk Date: Wed, 23 Aug 2023 09:18:44 -0700 Subject: [PATCH] COMPENF-648 map marker using location data (#114) --- .github/workflows/unit-tests.yml | 4 +- SECURITY.md | 2 +- backend/openshift.deploy.yml | 5 + backend/package-lock.json | 91 ++++++++++++++++--- backend/package.json | 4 +- backend/src/app.module.ts | 4 +- .../bc_geo_coder.controller.spec.ts | 22 +++++ .../bc_geo_coder/bc_geo_coder.controller.ts | 16 ++++ .../bc_geo_coder/bc_geo_coder.module.ts | 11 +++ .../bc_geo_coder/bc_geo_coder.service.spec.ts | 59 ++++++++++++ .../bc_geo_coder/bc_geo_coder.service.ts | 30 ++++++ .../src/types/bc_geocoder/bcGeocoderType.ts | 75 +++++++++++++++ .../hwcr_complaint/hwcr_complaint.module.ts | 6 +- database/templates/openshift.deploy.yml | 2 +- frontend/package-lock.json | 55 +++++------ .../common/bc-geocoder-autocomplete.tsx | 79 ++++++++++++++++ .../details/complaint-details-edit.tsx | 10 +- .../complaints/details/complaint-location.tsx | 27 ++++-- .../mapping/leaflet-map-with-point.tsx | 24 +---- frontend/src/app/store/reducers/complaints.ts | 59 ++++++++++++ frontend/src/app/types/maps/bcGeocoderType.ts | 75 +++++++++++++++ .../src/app/types/state/complaint-state.ts | 2 + 22 files changed, 583 insertions(+), 79 deletions(-) create mode 100644 backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.spec.ts create mode 100644 backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.ts create mode 100644 backend/src/external_api/bc_geo_coder/bc_geo_coder.module.ts create mode 100644 backend/src/external_api/bc_geo_coder/bc_geo_coder.service.spec.ts create mode 100644 backend/src/external_api/bc_geo_coder/bc_geo_coder.service.ts create mode 100644 backend/src/types/bc_geocoder/bcGeocoderType.ts create mode 100644 frontend/src/app/components/common/bc-geocoder-autocomplete.tsx create mode 100644 frontend/src/app/types/maps/bcGeocoderType.ts diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 9a114f458..903b979f6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -26,10 +26,10 @@ jobs: dir: [backend, frontend] include: - dir: backend - sonar_projectKey: bcgov_nr-quickstart-typescript_backend + sonar_projectKey: bcgov_nr-compliance-enforcement_backend token: SONAR_TOKEN_BACKEND - dir: frontend - sonar_projectKey: bcgov_nr-quickstart-typescript_frontend + sonar_projectKey: bcgov_nr-compliance-enforcement_frontend token: SONAR_TOKEN_FRONTEND steps: - uses: bcgov-nr/action-test-and-analyse@v0.0.1 diff --git a/SECURITY.md b/SECURITY.md index b5e68c492..8a290e6a8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,4 +7,4 @@ This product currently has no support and is experimental. That could change in ## Reporting a Vulnerability -Please report any issues or vulerabilities with an [issue](https://github.com/bcgov/nr-quickstart-typescript/issues). +Please report any issues or vulerabilities with an [issue](https://github.com/bcgov/nr-compliance-enforcement/issues). diff --git a/backend/openshift.deploy.yml b/backend/openshift.deploy.yml index aaa729528..59c044fb6 100644 --- a/backend/openshift.deploy.yml +++ b/backend/openshift.deploy.yml @@ -152,6 +152,11 @@ objects: secretKeyRef: name: ceds-backend-oicd key: jwt-issuer + - name: BC_GEOCODER_API_URL + valueFrom: + secretKeyRef: + name: bc-geo-coder + key: BC_GEOCODER_API_URL ports: - containerPort: 3000 protocol: TCP diff --git a/backend/package-lock.json b/backend/package-lock.json index 77f8598c3..12c00f453 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,14 +1,15 @@ { - "name": "nr-quickstart-typescript", + "name": "nr-compliance-enforcement", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "nr-quickstart-typescript", + "name": "nr-compliance-enforcement", "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "@nestjs/axios": "^3.0.0", "@nestjs/cli": "^9.3.0", "@nestjs/common": "^9.2.1", "@nestjs/config": "^2.2.0", @@ -20,6 +21,7 @@ "@nestjs/swagger": "^6.1.4", "@nestjs/testing": "^9.2.1", "@nestjs/typeorm": "^9.0.1", + "axios": "^1.4.0", "date-fns": "^2.30.0", "dotenv": "^16.0.1", "geojson": "^0.5.0", @@ -1410,6 +1412,17 @@ "node": ">=8" } }, + "node_modules/@nestjs/axios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz", @@ -2890,8 +2903,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -2905,6 +2917,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", @@ -3633,7 +3655,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3862,7 +3883,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5170,6 +5190,25 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5223,7 +5262,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8436,6 +8474,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -11638,6 +11681,12 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.0.1.tgz", "integrity": "sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g==" }, + "@nestjs/axios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "requires": {} + }, "@nestjs/cli": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz", @@ -12735,8 +12784,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "available-typed-arrays": { "version": "1.0.5", @@ -12744,6 +12792,16 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "babel-jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", @@ -13267,7 +13325,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -13441,8 +13498,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "depd": { "version": "2.0.0", @@ -14439,6 +14495,11 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -14483,7 +14544,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -16803,6 +16863,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index e75f1b32b..0db2cf7e7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,5 @@ { - "name": "nr-quickstart-typescript", + "name": "nr-compliance-enforcement", "version": "0.0.1", "description": "BCGov devops quickstart. For reference, testing and new projects.", "main": "index.js", @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/bcgov/nr-compliance-enforcement#readme", "dependencies": { + "@nestjs/axios": "^3.0.0", "@nestjs/cli": "^9.3.0", "@nestjs/common": "^9.2.1", "@nestjs/config": "^2.2.0", @@ -51,6 +52,7 @@ "@nestjs/swagger": "^6.1.4", "@nestjs/testing": "^9.2.1", "@nestjs/typeorm": "^9.0.1", + "axios": "^1.4.0", "date-fns": "^2.30.0", "dotenv": "^16.0.1", "geojson": "^0.5.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f48fc3050..edbb16a4b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -25,6 +25,7 @@ import { CosGeoOrgUnitModule } from "./v1/cos_geo_org_unit/cos_geo_org_unit.modu import { HTTPLoggerMiddleware } from "./middleware/req.res.logger"; import { PersonComplaintXrefModule } from "./v1/person_complaint_xref/person_complaint_xref.module"; import { PersonComplaintXrefCodeModule } from "./v1/person_complaint_xref_code/person_complaint_xref_code.module"; +import { BcGeoCoderModule } from "./external_api/bc_geo_coder/bc_geo_coder.module"; console.log("Var check - POSTGRESQL_HOST", process.env.POSTGRESQL_HOST); @@ -71,7 +72,8 @@ if (process.env.POSTGRESQL_PASSWORD != null ){ AttractantHwcrXrefModule, CosGeoOrgUnitModule, PersonComplaintXrefModule, - PersonComplaintXrefCodeModule + PersonComplaintXrefCodeModule, + BcGeoCoderModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.spec.ts b/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.spec.ts new file mode 100644 index 000000000..4616d4f5e --- /dev/null +++ b/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { BcGeoCoderController } from "./bc_geo_coder.controller"; +import { BcGeoCoderService } from "./bc_geo_coder.service"; +import { HttpModule } from "@nestjs/axios"; + +describe("BcGeoCoderController", () => { + let controller: BcGeoCoderController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [BcGeoCoderController], + providers: [BcGeoCoderService], + imports: [HttpModule], + }).compile(); + + controller = module.get(BcGeoCoderController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.ts b/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.ts new file mode 100644 index 000000000..6637b9df6 --- /dev/null +++ b/backend/src/external_api/bc_geo_coder/bc_geo_coder.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { BcGeoCoderService } from './bc_geo_coder.service'; +import { Roles } from '../../auth/decorators/roles.decorator'; +import { Role } from '../../enum/role.enum'; + +@Controller('bc-geo-coder') +export class BcGeoCoderController { + constructor(private readonly bcGeoCoderService: BcGeoCoderService) {} + + @Get('/address/:query') + @Roles(Role.COS_OFFICER) + findAll(@Param('query') query: string) { + return this.bcGeoCoderService.findAll(query); + } + +} diff --git a/backend/src/external_api/bc_geo_coder/bc_geo_coder.module.ts b/backend/src/external_api/bc_geo_coder/bc_geo_coder.module.ts new file mode 100644 index 000000000..3476e2121 --- /dev/null +++ b/backend/src/external_api/bc_geo_coder/bc_geo_coder.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { BcGeoCoderService } from "./bc_geo_coder.service"; +import { HttpModule } from "@nestjs/axios"; +import { BcGeoCoderController } from "./bc_geo_coder.controller"; + +@Module({ + imports: [HttpModule], + controllers: [BcGeoCoderController], + providers: [BcGeoCoderService], +}) +export class BcGeoCoderModule {} diff --git a/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.spec.ts b/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.spec.ts new file mode 100644 index 000000000..01c31e751 --- /dev/null +++ b/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.spec.ts @@ -0,0 +1,59 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { BcGeoCoderService } from "./bc_geo_coder.service"; +import { HttpService, HttpModule } from "@nestjs/axios"; +import { Feature } from "src/types/bc_geocoder/bcGeocoderType"; +import { AxiosResponse } from 'axios'; +import { of } from 'rxjs'; + +describe("BcGeoCoderService", () => { + let service: BcGeoCoderService; + let httpService: HttpService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BcGeoCoderService], + imports: [HttpModule], + }).compile(); + + service = module.get(BcGeoCoderService); + httpService = module.get(HttpService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("should return coordinates for a given address", async () => { + + const mockCoordinates = [ + -123.3776552, + 48.4406837 + ]; + + const mockResponse: AxiosResponse = { + data: { + features: [ + { + geometry: { + coordinates: mockCoordinates, + }, + }, + ], + }, + status: 200, + statusText: 'OK', + headers: {}, + config: { + headers: undefined + }, + }; + + jest.spyOn(httpService, 'get').mockReturnValue(of(mockResponse)); + const features: Feature = await service.findAll( + "2975 Jutland Road, Victoria, BC" + ); + + expect(features.features[0].geometry.coordinates).toEqual(mockCoordinates); + + }); +}); diff --git a/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.ts b/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.ts new file mode 100644 index 000000000..ea1de2e23 --- /dev/null +++ b/backend/src/external_api/bc_geo_coder/bc_geo_coder.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { catchError, firstValueFrom } from 'rxjs'; +import { AxiosError} from 'axios'; +import { Feature } from 'src/types/bc_geocoder/bcGeocoderType'; + +@Injectable() +export class BcGeoCoderService { + + private readonly logger = new Logger(BcGeoCoderService.name); + + constructor(private readonly httpService: HttpService) {} + + async findAll(query: string): Promise { + const maxResults = 10; + const apiUrl = `${process.env.BC_GEOCODER_API_URL}/addresses.json?addressString=${query}&locationDescriptor=any&maxResults=${maxResults}&interpolation=adaptive&echo=true&brief=false&autoComplete=true&setBack=0&outputSRS=4326&minScore=2&provinceCode=BC`; + + const { data } = await firstValueFrom( + this.httpService.get(apiUrl).pipe( + catchError((error: AxiosError) => { + this.logger.error(error.response.data); + throw 'Error getting BC Geocoder response'; + }), + ), + ); + return data; + + } + +} diff --git a/backend/src/types/bc_geocoder/bcGeocoderType.ts b/backend/src/types/bc_geocoder/bcGeocoderType.ts new file mode 100644 index 000000000..5a5cd5b7d --- /dev/null +++ b/backend/src/types/bc_geocoder/bcGeocoderType.ts @@ -0,0 +1,75 @@ +export interface Feature { + type: string; + queryAddress: string; + searchTimestamp: Date; + executionTime: number; + version: string; + baseDataDate: Date; + crs: CRS; + interpolation: string; + echo: string; + locationDescriptor: string; + setBack: number; + minScore: number; + maxResults: number; + disclaimer: string; + privacyStatement: string; + copyrightNotice: string; + copyrightLicense: string; + features: FeatureElement[]; +} + +export interface CRS { + type: string; + properties: CRSProperties; +} + +export interface CRSProperties { + code: number; +} + +export interface FeatureElement { + type: string; + geometry: Geometry; + properties: FeatureProperties; +} + +export interface Geometry { + type: string; + crs: CRS; + coordinates: number[]; +} + +export interface FeatureProperties { + fullAddress: string; + score: number; + matchPrecision: string; + precisionPoints: number; + faults: any[]; + siteName: string; + unitDesignator: string; + unitNumber: string; + unitNumberSuffix: string; + civicNumber: number; + civicNumberSuffix: string; + streetName: string; + streetType: string; + isStreetTypePrefix: string; + streetDirection: string; + isStreetDirectionPrefix: string; + streetQualifier: string; + localityName: string; + localityType: string; + electoralArea: string; + provinceCode: string; + locationPositionalAccuracy: string; + locationDescriptor: string; + siteID: string; + blockID: number; + fullSiteDescriptor: string; + accessNotes: string; + siteStatus: string; + siteRetireDate: Date; + changeDate: Date; + isOfficial: string; +} diff --git a/backend/src/v1/hwcr_complaint/hwcr_complaint.module.ts b/backend/src/v1/hwcr_complaint/hwcr_complaint.module.ts index d562b3305..a9a7c4411 100644 --- a/backend/src/v1/hwcr_complaint/hwcr_complaint.module.ts +++ b/backend/src/v1/hwcr_complaint/hwcr_complaint.module.ts @@ -27,10 +27,12 @@ import { OfficeService } from '../office/office.service'; import { OfficerService } from '../officer/officer.service'; import { Person } from '../person/entities/person.entity'; import { PersonService } from '../person/person.service'; +import { BcGeoCoderService } from 'src/external_api/bc_geo_coder/bc_geo_coder.service'; +import { HttpModule } from '@nestjs/axios'; @Module({ - imports: [TypeOrmModule.forFeature([HwcrComplaint]), TypeOrmModule.forFeature([Complaint]), TypeOrmModule.forFeature([AgencyCode]), TypeOrmModule.forFeature([ComplaintStatusCode]), TypeOrmModule.forFeature([GeoOrganizationUnitCode]), TypeOrmModule.forFeature([SpeciesCode]), TypeOrmModule.forFeature([HwcrComplaintNatureCode]), TypeOrmModule.forFeature([AttractantHwcrXref]), TypeOrmModule.forFeature([AttractantCode]), TypeOrmModule.forFeature([CosGeoOrgUnit]), TypeOrmModule.forFeature([Office]),TypeOrmModule.forFeature([Officer]), TypeOrmModule.forFeature([Person])], + imports: [TypeOrmModule.forFeature([HwcrComplaint]), TypeOrmModule.forFeature([Complaint]), TypeOrmModule.forFeature([AgencyCode]), TypeOrmModule.forFeature([ComplaintStatusCode]), TypeOrmModule.forFeature([GeoOrganizationUnitCode]), TypeOrmModule.forFeature([SpeciesCode]), TypeOrmModule.forFeature([HwcrComplaintNatureCode]), TypeOrmModule.forFeature([AttractantHwcrXref]), TypeOrmModule.forFeature([AttractantCode]), TypeOrmModule.forFeature([CosGeoOrgUnit]), TypeOrmModule.forFeature([Office]),TypeOrmModule.forFeature([Officer]), TypeOrmModule.forFeature([Person]), HttpModule], controllers: [HwcrComplaintController], - providers: [HwcrComplaintService, ComplaintService, AgencyCodeService, ComplaintStatusCodeService, GeoOrganizationUnitCodeService, SpeciesCodeService, HwcrComplaintNatureCodeService, AttractantHwcrXrefService, AttractantCodeService, CosGeoOrgUnitService, OfficeService, OfficerService, PersonService] + providers: [HwcrComplaintService, ComplaintService, AgencyCodeService, ComplaintStatusCodeService, GeoOrganizationUnitCodeService, SpeciesCodeService, HwcrComplaintNatureCodeService, AttractantHwcrXrefService, AttractantCodeService, CosGeoOrgUnitService, OfficeService, OfficerService, PersonService, BcGeoCoderService] }) export class HwcrComplaintModule {} diff --git a/database/templates/openshift.deploy.yml b/database/templates/openshift.deploy.yml index 15100d0c9..bdd1cef05 100644 --- a/database/templates/openshift.deploy.yml +++ b/database/templates/openshift.deploy.yml @@ -3,7 +3,7 @@ kind: Template parameters: - name: NAME description: Product name - value: nr-quickstart-typescript + value: nr-compliance-enforcement - name: COMPONENT description: Component name value: database diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7e877f0b2..8c897773d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -225,11 +225,11 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -387,11 +387,11 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", "dependencies": { - "@babel/types": "^7.21.4" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -427,9 +427,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "engines": { "node": ">=6.9.0" } @@ -501,17 +501,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "engines": { "node": ">=6.9.0" } @@ -1055,11 +1055,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1983,12 +1983,12 @@ } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", + "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2004,6 +2004,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, "optional": true, "engines": { "node": ">=0.1.90" @@ -6681,6 +6682,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, "dependencies": { "string-width": "^4.2.0" }, @@ -24537,6 +24539,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" diff --git a/frontend/src/app/components/common/bc-geocoder-autocomplete.tsx b/frontend/src/app/components/common/bc-geocoder-autocomplete.tsx new file mode 100644 index 000000000..b8e86e2cb --- /dev/null +++ b/frontend/src/app/components/common/bc-geocoder-autocomplete.tsx @@ -0,0 +1,79 @@ +import { FC, useEffect, useState } from "react"; +import CreatableSelect from "react-select/creatable"; +import { useAppDispatch, useAppSelector } from "../../hooks/hooks"; +import { + getComplaintLocation, + selectComplaintLocation, +} from "../../store/reducers/complaints"; + +interface Props { + value?: string; + id?: string; + maxResults: number; +} + +interface AddressOption { + value: string; + label: string; +} + +/** + * Component that uses the BC Geocoder to autocomplete address input. + */ +export const BCGeocoderAutocomplete: FC = ({ + value, + id, + maxResults, +}) => { + const [addressOptions, setAddressOptions] = useState([]); + const [inputValue, setInputValue] = useState(`${value ?? ""}`); + + const handleInputChange = (inputValue: string) => { + setInputValue(inputValue); + }; + + const dispatch = useAppDispatch(); + const complaintLocation = useAppSelector(selectComplaintLocation); + + useEffect(() => { + const fetchAddresses = async (inputValue: string) => { + dispatch(getComplaintLocation(`${inputValue}`)); + + try { + if (complaintLocation) { + if (complaintLocation.features.length > 0) { + const options = complaintLocation.features.map((feature: any) => ({ + value: feature.properties.fullAddress, + label: feature.properties.fullAddress, + })); + if (options) { + setAddressOptions(options); + } + } else { + console.log("Feature length 0"); + } + } + } catch (error) { + console.error("Error fetching addresses:", error); + } + }; + fetchAddresses(inputValue); + }, [inputValue, maxResults]); + + return ( + undefined} + components={{ + DropdownIndicator: () => null, + IndicatorSeparator: () => null, + }} + /> + ); +}; diff --git a/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx b/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx index 870907fb0..7ee89c618 100644 --- a/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx +++ b/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx @@ -31,6 +31,7 @@ import { DropdownOption } from "../../../../types/code-tables/option"; import COMPLAINT_TYPES from "../../../../types/app/complaint-types"; import { ComplaintSuspectWitness } from "../../../../types/complaints/details/complaint-suspect-witness-details"; import { selectOfficersByZone } from "../../../../store/reducers/officer"; +import { BCGeocoderAutocomplete } from "../../../common/bc-geocoder-autocomplete"; import { ComplaintLocation } from "./complaint-location"; interface ComplaintHeaderProps { @@ -385,12 +386,9 @@ export const ComplaintDetailsEdit: FC = ({ - +
+ +
= ({ complaintType, draggable }) => { const { coordinates } = useAppSelector( selectComplaintDeails(complaintType) ) as ComplaintDetails; + const complaintLocation = useAppSelector(selectComplaintLocation); - const decimalDegreesCoordinates = parseDecimalDegreesCoordinates(coordinates); + // the lat and long of the marker we need to display on the map + // Initialized to 0. This will either be populated using the optionally supplied coordinates + // or they'll be derived using the complaint's location and/or communit. + let lat = 0; + let lng = 0; + if (coordinates && isWithinBC(coordinates)) { + lat = +coordinates[0]; + lng = +coordinates[1]; + } else if (complaintLocation) { + lat = complaintLocation?.features[0].geometry.coordinates[1]; + lng = complaintLocation?.features[0].geometry.coordinates[0]; + } return (
Complaint Location
diff --git a/frontend/src/app/components/mapping/leaflet-map-with-point.tsx b/frontend/src/app/components/mapping/leaflet-map-with-point.tsx index b111a81c7..8c8d047fc 100644 --- a/frontend/src/app/components/mapping/leaflet-map-with-point.tsx +++ b/frontend/src/app/components/mapping/leaflet-map-with-point.tsx @@ -9,7 +9,7 @@ import { faMapMarkerAlt } from "@fortawesome/free-solid-svg-icons"; import Leaflet from "leaflet"; type Props = { - coordinates?: { lat: number; lng: number }; + coordinates: { lat: number; lng: number }; draggable: boolean; }; @@ -18,20 +18,6 @@ type Props = { * */ const LeafletMapWithPoint: FC = ({ coordinates, draggable }) => { - // the derived lat long pair. - // If a coordinate is supplied, then the latLng is set to the supplied coordinates. - // If coordinates aren't supplied, then use the BC Geocoder to determine the latLng based on an address (if supplied), or - // the community. Every complaint will have a community, so theoretically, there will always be a latLng that can be derived. - let latLng: { lat: number; lng: number }; - - if (coordinates) { - latLng = coordinates; - } else { - // handle other methods of determining coordinates - // for now just return 0,0 - latLng = { lat: 0, lng: 0 }; - } - const iconHTML = ReactDOMServer.renderToString( ); @@ -47,8 +33,8 @@ const LeafletMapWithPoint: FC = ({ coordinates, draggable }) => { const map = useMap(); useEffect(() => { - if (latLng) { - map.setView(latLng); + if (coordinates) { + map.setView(coordinates); } }, [map]); @@ -58,7 +44,7 @@ const LeafletMapWithPoint: FC = ({ coordinates, draggable }) => { return ( = ({ coordinates, draggable }) => { diff --git a/frontend/src/app/store/reducers/complaints.ts b/frontend/src/app/store/reducers/complaints.ts index 396314a44..af848a567 100644 --- a/frontend/src/app/store/reducers/complaints.ts +++ b/frontend/src/app/store/reducers/complaints.ts @@ -20,6 +20,7 @@ import { Complaint } from "../../types/complaints/complaint"; import { toggleLoading } from "./app"; import { generateApiParameters, get, patch } from "../../common/api"; import { ComplaintQueryParams } from "../../types/api-params/complaint-query-params"; +import { Feature } from "../../types/maps/bcGeocoderType"; const initialState: ComplaintState = { complaintItems: { @@ -27,6 +28,8 @@ const initialState: ComplaintState = { allegations: [], }, complaint: null, + complaintLocation: null, + zoneAtGlance: { hwcr: { assigned: 0, unassigned: 0, total: 0, offices: [] }, allegation: { assigned: 0, unassigned: 0, total: 0, offices: [] }, @@ -61,6 +64,10 @@ export const complaintSlice = createSlice({ const { payload: complaint } = action; return { ...state, complaint }; }, + setComplaintLocation: (state, action) => { + const { payload: complaintLocation } = action; + return { ...state, complaintLocation }; + }, setZoneAtAGlance: (state, action) => { const { payload: statistics } = action; const { type, ...rest } = statistics; @@ -129,6 +136,7 @@ export const complaintSlice = createSlice({ export const { setComplaints, setComplaint, + setComplaintLocation, setZoneAtAGlance, updateWildlifeComplaintByRow, updateAllegationComplaintByRow, @@ -209,6 +217,19 @@ export const getWildlifeComplaintByComplaintIdentifier = ); const response = await get(dispatch, parameters); + const { complaint_identifier: ceComplaint }: any = response; + + if (ceComplaint) { + const { + location_summary_text, + cos_geo_org_unit: { area_name }, + } = ceComplaint; + + dispatch( + getComplaintLocation(`${location_summary_text ?? ""} ${area_name}`) + ); + } + dispatch(setComplaint({ ...response })); } catch (error) { //-- handle the error @@ -229,6 +250,19 @@ export const getAllegationComplaintByComplaintIdentifier = ); const response = await get(dispatch, parameters); + const { complaint_identifier: ceComplaint }: any = response; + + if (ceComplaint) { + const { + location_summary_text, + cos_geo_org_unit: { area_name }, + } = ceComplaint; + + dispatch( + getComplaintLocation(`${location_summary_text ?? ""} ${area_name}`) + ); + } + dispatch(setComplaint({ ...response })); } catch (error) { //-- handle the error @@ -259,6 +293,21 @@ export const getZoneAtAGlanceStats = } }; +export const getComplaintLocation = + (address: string): AppThunk => + async (dispatch) => { + try { + const parameters = generateApiParameters( + `${config.API_BASE_URL}/bc-geo-coder/address/${address}` + ); + + const response = await get(dispatch, parameters); + dispatch(setComplaintLocation(response)); + } catch (error) { + //-- handle the error message + } + }; + const updateComplaintStatus = async ( dispatch: Dispatch, id: string, @@ -334,6 +383,16 @@ export const selectComplaint = ( return complaint; }; +export const selectComplaintLocation = ( + state: RootState +): Feature | null | undefined => { + const { + complaints: { complaintLocation }, + } = state; + + return complaintLocation; +}; + export const selectComplaintHeader = (complaintType: string) => (state: RootState): any => { diff --git a/frontend/src/app/types/maps/bcGeocoderType.ts b/frontend/src/app/types/maps/bcGeocoderType.ts new file mode 100644 index 000000000..5a5cd5b7d --- /dev/null +++ b/frontend/src/app/types/maps/bcGeocoderType.ts @@ -0,0 +1,75 @@ +export interface Feature { + type: string; + queryAddress: string; + searchTimestamp: Date; + executionTime: number; + version: string; + baseDataDate: Date; + crs: CRS; + interpolation: string; + echo: string; + locationDescriptor: string; + setBack: number; + minScore: number; + maxResults: number; + disclaimer: string; + privacyStatement: string; + copyrightNotice: string; + copyrightLicense: string; + features: FeatureElement[]; +} + +export interface CRS { + type: string; + properties: CRSProperties; +} + +export interface CRSProperties { + code: number; +} + +export interface FeatureElement { + type: string; + geometry: Geometry; + properties: FeatureProperties; +} + +export interface Geometry { + type: string; + crs: CRS; + coordinates: number[]; +} + +export interface FeatureProperties { + fullAddress: string; + score: number; + matchPrecision: string; + precisionPoints: number; + faults: any[]; + siteName: string; + unitDesignator: string; + unitNumber: string; + unitNumberSuffix: string; + civicNumber: number; + civicNumberSuffix: string; + streetName: string; + streetType: string; + isStreetTypePrefix: string; + streetDirection: string; + isStreetDirectionPrefix: string; + streetQualifier: string; + localityName: string; + localityType: string; + electoralArea: string; + provinceCode: string; + locationPositionalAccuracy: string; + locationDescriptor: string; + siteID: string; + blockID: number; + fullSiteDescriptor: string; + accessNotes: string; + siteStatus: string; + siteRetireDate: Date; + changeDate: Date; + isOfficial: string; +} diff --git a/frontend/src/app/types/state/complaint-state.ts b/frontend/src/app/types/state/complaint-state.ts index 37747094a..4e4065ae4 100644 --- a/frontend/src/app/types/state/complaint-state.ts +++ b/frontend/src/app/types/state/complaint-state.ts @@ -1,11 +1,13 @@ import { AllegationComplaint } from "../complaints/allegation-complaint"; import { HwcrComplaint } from "../complaints/hwcr-complaint"; +import { Feature } from "../maps/bcGeocoderType"; import { ZoneAtAGlanceState } from "./zone-at-a-glance-state"; export interface ComplaintState { complaintItems: ComplaintCollection; complaint: HwcrComplaint | AllegationComplaint | undefined | null; zoneAtGlance: ZoneAtAGlanceState; + complaintLocation: Feature | null; } export interface ComplaintCollection {