diff --git a/source/backend/api/Areas/HistoricalNumber/HistoricalNumberController.cs b/source/backend/api/Areas/HistoricalNumber/HistoricalNumberController.cs index d22af5f6ac..80ee157f35 100644 --- a/source/backend/api/Areas/HistoricalNumber/HistoricalNumberController.cs +++ b/source/backend/api/Areas/HistoricalNumber/HistoricalNumberController.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; using MapsterMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Pims.Api.Models.Concepts.Property; using Pims.Api.Policies; using Pims.Api.Services; +using Pims.Core.Extensions; using Pims.Core.Json; using Pims.Dal.Security; using Swashbuckle.AspNetCore.Annotations; @@ -25,6 +28,7 @@ public class HistoricalNumberController : ControllerBase #region Variables private readonly IPropertyService _propertyService; private readonly IMapper _mapper; + private readonly ILogger _logger; #endregion #region Constructors @@ -35,10 +39,11 @@ public class HistoricalNumberController : ControllerBase /// /// /// - public HistoricalNumberController(IPropertyService propertyService, IMapper mapper) + public HistoricalNumberController(IPropertyService propertyService, IMapper mapper, ILogger logger) { _propertyService = propertyService; _mapper = mapper; + _logger = logger; } /// @@ -47,14 +52,45 @@ public HistoricalNumberController(IPropertyService propertyService, IMapper mapp [HttpGet("{propertyId}/historicalNumbers")] [HasPermission(Permissions.PropertyView)] [Produces("application/json")] - [ProducesResponseType(typeof(HistoricalFileNumberModel), 200)] + [ProducesResponseType(typeof(IEnumerable), 200)] [SwaggerOperation(Tags = new[] { "property" })] [TypeFilter(typeof(NullJsonResultFilter))] public IActionResult GetHistoricalNumbersForPropertyId(long propertyId) { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(HistoricalNumberController), + nameof(GetHistoricalNumbersForPropertyId), + User.GetUsername(), + DateTime.Now); + var historicalNumbers = _propertyService.GetHistoricalNumbersForPropertyId(propertyId); return new JsonResult(_mapper.Map>(historicalNumbers)); } + + /// + /// Updates the list of historic numbers for a given property id. + /// + [HttpPut("{propertyId}/historicalNumbers")] + [HasPermission(Permissions.PropertyEdit)] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [SwaggerOperation(Tags = new[] { "property" })] + [TypeFilter(typeof(NullJsonResultFilter))] + public IActionResult UpdateHistoricalNumbers(long propertyId, IEnumerable historicalNumbers) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(HistoricalNumberController), + nameof(UpdateHistoricalNumbers), + User.GetUsername(), + DateTime.Now); + + var historicalEntities = _mapper.Map>(historicalNumbers); + var updatedEntities = _propertyService.UpdateHistoricalFileNumbers(propertyId, historicalEntities); + + return new JsonResult(_mapper.Map>(updatedEntities)); + } #endregion } } diff --git a/source/backend/api/Controllers/LookupController.cs b/source/backend/api/Controllers/LookupController.cs index c55d592765..62b12abbe5 100644 --- a/source/backend/api/Controllers/LookupController.cs +++ b/source/backend/api/Controllers/LookupController.cs @@ -146,6 +146,7 @@ public IActionResult GetAll() var dispositionChecklistItemStatusTypes = _mapper.Map(_lookupRepository.GetAllDispositionChecklistItemStatusTypes()); var dispositionChecklistItemTypes = _mapper.Map(_lookupRepository.GetAllDispositionChecklistItemTypes()); var dispositionChecklistSectionTypes = _mapper.Map(_lookupRepository.GetAllDispositionChecklistSectionTypes()); + var historicalNumberTypes = _mapper.Map(_lookupRepository.GetAllHistoricalNumberTypes()); var codes = new List(); codes.AddRange(areaUnitTypes); @@ -220,6 +221,7 @@ public IActionResult GetAll() codes.AddRange(dispositionChecklistItemStatusTypes); codes.AddRange(dispositionChecklistItemTypes); codes.AddRange(dispositionChecklistSectionTypes); + codes.AddRange(historicalNumberTypes); var response = new JsonResult(codes); diff --git a/source/backend/api/Services/IPropertyService.cs b/source/backend/api/Services/IPropertyService.cs index f76a8fbe88..1df12315d6 100644 --- a/source/backend/api/Services/IPropertyService.cs +++ b/source/backend/api/Services/IPropertyService.cs @@ -48,5 +48,7 @@ public interface IPropertyService void UpdateLocation(PimsProperty acquisitionProperty, ref PimsProperty propertyToUpdate, IEnumerable overrideCodes); IList GetHistoricalNumbersForPropertyId(long propertyId); + + IList UpdateHistoricalFileNumbers(long propertyId, IEnumerable pimsHistoricalNumbers); } } diff --git a/source/backend/api/Services/PropertyService.cs b/source/backend/api/Services/PropertyService.cs index 46c5a1048a..c48c1d0e10 100644 --- a/source/backend/api/Services/PropertyService.cs +++ b/source/backend/api/Services/PropertyService.cs @@ -397,6 +397,18 @@ public IList GetHistoricalNumbersForPropertyId(long pr return _historicalNumberRepository.GetAllByPropertyId(propertyId); } + public IList UpdateHistoricalFileNumbers(long propertyId, IEnumerable pimsHistoricalNumbers) + { + + _logger.LogInformation("Updating historical numbers for property with id {id}", propertyId); + _user.ThrowIfNotAuthorized(Permissions.PropertyEdit); + + _historicalNumberRepository.UpdateHistoricalFileNumbers(propertyId, pimsHistoricalNumbers); + _historicalNumberRepository.CommitTransaction(); + + return GetHistoricalNumbersForPropertyId(propertyId); + } + private Point TransformCoordinates(Geometry location) { // return property spatial location in lat/long (4326) diff --git a/source/backend/dal/Repositories/HistoricNumberRepository.cs b/source/backend/dal/Repositories/HistoricNumberRepository.cs index 10729a3e36..dd9e5eb20e 100644 --- a/source/backend/dal/Repositories/HistoricNumberRepository.cs +++ b/source/backend/dal/Repositories/HistoricNumberRepository.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Pims.Dal.Entities; +using Pims.Dal.Helpers.Extensions; namespace Pims.Dal.Repositories { @@ -42,6 +43,13 @@ public IList GetAllByPropertyId(long propertyId) .ToList(); return historicalFileNumbers; } + + public IList UpdateHistoricalFileNumbers(long propertyId, IEnumerable pimsHistoricalNumbers) + { + using var scope = Logger.QueryScope(); + Context.UpdateChild(l => l.PimsHistoricalFileNumbers, propertyId, pimsHistoricalNumbers.ToArray()); + return GetAllByPropertyId(propertyId); + } #endregion } } diff --git a/source/backend/dal/Repositories/Interfaces/IHistoricalNumberRepository.cs b/source/backend/dal/Repositories/Interfaces/IHistoricalNumberRepository.cs index 27b745319b..3fb0d1f1f6 100644 --- a/source/backend/dal/Repositories/Interfaces/IHistoricalNumberRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IHistoricalNumberRepository.cs @@ -10,5 +10,7 @@ namespace Pims.Dal.Repositories public interface IHistoricalNumberRepository : IRepository { IList GetAllByPropertyId(long propertyId); + + IList UpdateHistoricalFileNumbers(long propertyId, IEnumerable pimsHistoricalNumbers); } } diff --git a/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs b/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs index fd2e2cbef1..68df74b791 100644 --- a/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs @@ -155,5 +155,7 @@ public interface ILookupRepository : IRepository IEnumerable GetAllDispositionChecklistItemTypes(); IEnumerable GetAllDispositionChecklistSectionTypes(); + + IEnumerable GetAllHistoricalNumberTypes(); } } diff --git a/source/backend/dal/Repositories/LookupRepository.cs b/source/backend/dal/Repositories/LookupRepository.cs index f9e53751a4..8dc6e40383 100644 --- a/source/backend/dal/Repositories/LookupRepository.cs +++ b/source/backend/dal/Repositories/LookupRepository.cs @@ -438,6 +438,11 @@ public IEnumerable GetAllDispositionChecklistSectionTy return Context.PimsDspChklstSectionTypes.AsNoTracking().ToArray(); } + public IEnumerable GetAllHistoricalNumberTypes() + { + return Context.PimsHistoricalFileNumberTypes.AsNoTracking().ToArray(); + } + #endregion } } diff --git a/source/frontend/src/constants/API.ts b/source/frontend/src/constants/API.ts index 84f715f714..119b5b2f09 100644 --- a/source/frontend/src/constants/API.ts +++ b/source/frontend/src/constants/API.ts @@ -128,6 +128,7 @@ export const DISPOSITION_INITIATING_BRANCH_TYPES = 'PimsDspInitiatingBranchType' export const DISPOSITION_TEAM_PROFILE_TYPES = 'PimsDspFlTeamProfileType'; export const DISPOSITION_FUNDING_TYPES = 'PimsDispositionFundingType'; export const DISPOSITION_OFFER_STATUS_TYPES = 'PimsDispositionOfferStatusType'; +export const HISTORICAL_NUMBER_TYPES = 'PimsHistoricalFileNumberType'; // TODO: PSP-4395 This should all be removed from this and moved to the useApi* hooks. // Auth Service diff --git a/source/frontend/src/features/mapSideBar/disposition/DispositionView.test.tsx b/source/frontend/src/features/mapSideBar/disposition/DispositionView.test.tsx index 43b02e4551..052e84e6d3 100644 --- a/source/frontend/src/features/mapSideBar/disposition/DispositionView.test.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/DispositionView.test.tsx @@ -13,18 +13,18 @@ import { server } from '@/mocks/msw/server'; import { getUserMock } from '@/mocks/user.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { prettyFormatUTCDate } from '@/utils'; -import { act, cleanup, render, RenderOptions, userEvent, screen } from '@/utils/test-utils'; +import { RenderOptions, act, cleanup, render, userEvent } from '@/utils/test-utils'; -import DispositionView, { IDispositionViewProps } from './DispositionView'; import { useApiProperties } from '@/hooks/pims-api/useApiProperties'; +import { useHistoricalNumberRepository } from '@/hooks/repositories/useHistoricalNumberRepository'; +import { useProjectProvider } from '@/hooks/repositories/useProjectProvider'; +import { useLtsa } from '@/hooks/useLtsa'; import { ApiGen_Base_Page } from '@/models/api/generated/ApiGen_Base_Page'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { vi } from 'vitest'; -import { useLtsa } from '@/hooks/useLtsa'; -import { useProjectProvider } from '@/hooks/repositories/useProjectProvider'; -import { createRef } from 'react'; import { HttpResponse, http } from 'msw'; -import { useHistoricalNumberRepository } from '@/hooks/repositories/useHistoricalNumberRepository'; +import { createRef } from 'react'; +import { vi } from 'vitest'; +import DispositionView, { IDispositionViewProps } from './DispositionView'; // mock auth library @@ -176,6 +176,13 @@ describe('DispositionView component', () => { loading: false, status: 200, }, + updatePropertyHistoricalNumbers: { + error: null, + response: [], + execute: vi.fn().mockResolvedValue([]), + loading: false, + status: 200, + }, }); history.replace(`/mapview/sidebar/disposition/1`); diff --git a/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.test.tsx b/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.test.tsx index 80d3a632f0..a09f09e3af 100644 --- a/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.test.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.test.tsx @@ -5,9 +5,9 @@ import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_C import { prettyFormatUTCDate } from '@/utils/dateUtils'; import { act, render, RenderOptions } from '@/utils/test-utils'; -import DispositionHeader, { IDispositionHeaderProps } from './DispositionHeader'; -import { http, HttpResponse } from 'msw'; import { useHistoricalNumberRepository } from '@/hooks/repositories/useHistoricalNumberRepository'; +import { http, HttpResponse } from 'msw'; +import DispositionHeader, { IDispositionHeaderProps } from './DispositionHeader'; vi.mock('@/hooks/repositories/useHistoricalNumberRepository'); vi.mocked(useHistoricalNumberRepository).mockReturnValue({ @@ -18,6 +18,13 @@ vi.mocked(useHistoricalNumberRepository).mockReturnValue({ loading: false, status: 200, }, + updatePropertyHistoricalNumbers: { + error: null, + response: [], + execute: vi.fn().mockResolvedValue([]), + loading: false, + status: 200, + }, }); describe('DispositionHeader component', () => { @@ -74,7 +81,10 @@ describe('DispositionHeader component', () => { it('renders the file number and name concatenated', async () => { const testDispositionFile = mockDispositionFileResponse(); - const { getByText } = await setup({ dispositionFile: testDispositionFile, lastUpdatedBy: null }); + const { getByText } = await setup({ + dispositionFile: testDispositionFile, + lastUpdatedBy: null, + }); expect(getByText('File:')).toBeVisible(); expect(getByText(/FILE_NUMBER 3A8F46B/)).toBeVisible(); @@ -108,7 +118,10 @@ describe('DispositionHeader component', () => { isDisabled: false, }, }; - const { getByText } = await setup({ dispositionFile: testDispositionFile, lastUpdatedBy: null }); + const { getByText } = await setup({ + dispositionFile: testDispositionFile, + lastUpdatedBy: null, + }); expect(getByText('Status:')).toBeVisible(); expect(getByText(/mock file status/i)).toBeVisible(); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdateHistoricalNumbersSubForm.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdateHistoricalNumbersSubForm.tsx new file mode 100644 index 0000000000..f8d8e8a949 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdateHistoricalNumbersSubForm.tsx @@ -0,0 +1,87 @@ +import { FieldArray, useFormikContext } from 'formik'; +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; + +import { LinkButton, RemoveButton } from '@/components/common/buttons'; +import { Input, Select } from '@/components/common/form'; +import { SectionField } from '@/components/common/Section/SectionField'; +import * as API from '@/constants/API'; +import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; +import { exists } from '@/utils'; + +import { HistoricalNumberForm, UpdatePropertyDetailsFormModel } from './models'; + +export interface IUpdateHistoricalNumbersSubFormProps { + propertyId: number; +} + +export const UpdateHistoricalNumbersSubForm: React.FC = ({ + propertyId, +}) => { + const { values, setFieldValue } = useFormikContext(); + const { getOptionsByType } = useLookupCodeHelpers(); + + // (sort alpha; exceptions: 1. Other at end, and 2. PS second in list) + // The order is set via displayOrder in the DB + const historicalNumberTypes = getOptionsByType(API.HISTORICAL_NUMBER_TYPES); + + return ( + ( + <> + {values.historicalNumbers?.map((hn, index) => ( + + + + + + + + + )} + + ))} + { + const hn = new HistoricalNumberForm(); + hn.propertyId = propertyId; + arrayHelpers.push(hn); + }} + > + + Add historical file # + + + )} + /> + ); +}; + +export default UpdateHistoricalNumbersSubForm; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.test.tsx index 365b8d9796..5438691a58 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.test.tsx @@ -1,5 +1,4 @@ -import axios, { AxiosResponse } from 'axios'; -import MockAdapter from 'axios-mock-adapter'; +import { AxiosResponse } from 'axios'; import { FormikProps } from 'formik'; import { createMemoryHistory } from 'history'; import { createRef } from 'react'; @@ -16,15 +15,18 @@ import { ApiGen_Concepts_CodeType } from '@/models/api/generated/ApiGen_Concepts import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { getEmptyBaseAudit, getEmptyProperty } from '@/models/defaultInitializers'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; -import { act, render, RenderOptions, userEvent, waitFor } from '@/utils/test-utils'; +import { RenderOptions, act, render, userEvent } from '@/utils/test-utils'; -import { UpdatePropertyDetailsFormModel } from './models'; +import { IResponseWrapper } from '@/hooks/util/useApiRequestWrapper'; +import { server } from '@/mocks/msw/server'; +import { getUserMock } from '@/mocks/user.mock'; +import { LatLngLiteral } from 'leaflet'; +import { HttpResponse, http } from 'msw'; import { IUpdatePropertyDetailsContainerProps, UpdatePropertyDetailsContainer, } from './UpdatePropertyDetailsContainer'; -import { IResponseWrapper } from '@/hooks/util/useApiRequestWrapper'; -import { LatLngLiteral } from 'leaflet'; +import { UpdatePropertyDetailsFormModel } from './models'; const history = createMemoryHistory(); const storeState = { @@ -38,7 +40,6 @@ const DEFAULT_PROPS: IUpdatePropertyDetailsContainerProps = { onSuccess, }; -const mockAxios = new MockAdapter(axios); const fakeProperty: ApiGen_Concepts_Property = { ...getEmptyProperty(), id: 205, @@ -261,7 +262,11 @@ describe('UpdatePropertyDetailsContainer component', () => { }; beforeEach(() => { - mockAxios.onGet(new RegExp('users/info/*')).reply(200, {}); + server.use( + http.get('/api/users/info/*', () => HttpResponse.json(getUserMock())), + http.get('/api/properties/:id/historicalNumbers', () => HttpResponse.json([])), + http.put('/api/properties/:id/historicalNumbers', () => HttpResponse.json([])), + ); }); afterEach(() => { @@ -270,12 +275,14 @@ describe('UpdatePropertyDetailsContainer component', () => { it('renders as expected', async () => { const { asFragment, findByTitle } = setup(); + await act(async () => {}); expect(await findByTitle('Down by the River')).toBeInTheDocument(); expect(asFragment()).toMatchSnapshot(); }); it('saves the form with minimal data', async () => { const { findByTitle, formikRef } = setup(); + await act(async () => {}); expect(await findByTitle('Down by the River')).toBeInTheDocument(); await act(async () => formikRef.current?.submitForm() as Promise); @@ -295,46 +302,44 @@ describe('UpdatePropertyDetailsContainer component', () => { }), }); - expect(updateProperty).toBeCalledWith(expectedValues); - expect(onSuccess).toBeCalled(); + expect(updateProperty).toHaveBeenCalledWith(expectedValues); + expect(onSuccess).toHaveBeenCalled(); }); it('saves the form with updated values', async () => { const { findByTitle, formikRef } = setup(); + await act(async () => {}); expect(await findByTitle('Down by the River')).toBeInTheDocument(); const addressLine1 = document.querySelector( `input[name='address.streetAddress1']`, ) as HTMLElement; - await act(async () => { - await act(async () => userEvent.clear(addressLine1)); - await act(async () => userEvent.paste(addressLine1, '123 Mock St')); - formikRef.current?.submitForm(); - }); + await act(async () => userEvent.clear(addressLine1)); + await act(async () => userEvent.paste(addressLine1, '123 Mock St')); + await act(async () => formikRef.current?.submitForm()); - await waitFor(() => { - const expectedValues = expect.objectContaining>({ - address: expect.objectContaining>({ - streetAddress1: '123 Mock St', - streetAddress2: fakeProperty.address?.streetAddress2, - streetAddress3: fakeProperty.address?.streetAddress3, - municipality: fakeProperty.address?.municipality, - postal: fakeProperty.address?.postal, - country: expect.objectContaining>({ - id: fakeProperty.address!.country!.id, - }), - province: expect.objectContaining>({ - id: fakeProperty.address!.province!.id, - }), + const expectedValues = expect.objectContaining>({ + address: expect.objectContaining>({ + streetAddress1: '123 Mock St', + streetAddress2: fakeProperty.address?.streetAddress2, + streetAddress3: fakeProperty.address?.streetAddress3, + municipality: fakeProperty.address?.municipality, + postal: fakeProperty.address?.postal, + country: expect.objectContaining>({ + id: fakeProperty.address!.country!.id, + }), + province: expect.objectContaining>({ + id: fakeProperty.address!.province!.id, }), - }); - expect(updateProperty).toBeCalledWith(expectedValues); - expect(onSuccess).toBeCalled(); + }), }); + expect(updateProperty).toHaveBeenCalledWith(expectedValues); + expect(onSuccess).toHaveBeenCalled(); }); it('sends no address when all fields are cleared', async () => { const { findByTitle, formikRef } = setup(); + await act(async () => {}); expect(await findByTitle('Down by the River')).toBeInTheDocument(); const addressLine1 = document.querySelector( @@ -351,22 +356,18 @@ describe('UpdatePropertyDetailsContainer component', () => { ) as HTMLElement; const postal = document.querySelector(`input[name='address.postal']`) as HTMLElement; - await act(async () => { - await act(async () => userEvent.clear(addressLine1)); - await act(async () => userEvent.clear(addressLine2)); - await act(async () => userEvent.clear(addressLine3)); - await act(async () => userEvent.clear(municipality)); - await act(async () => userEvent.clear(postal)); - formikRef.current?.submitForm() as Promise; - }); - - await waitFor(() => { - const expectedValues = expect.objectContaining>({ - address: null, - }); + await act(async () => userEvent.clear(addressLine1)); + await act(async () => userEvent.clear(addressLine2)); + await act(async () => userEvent.clear(addressLine3)); + await act(async () => userEvent.clear(municipality)); + await act(async () => userEvent.clear(postal)); + await act(async () => formikRef.current?.submitForm()); - expect(updateProperty).toBeCalledWith(expectedValues); - expect(onSuccess).toBeCalled(); + const expectedValues = expect.objectContaining>({ + address: null, }); + + expect(updateProperty).toHaveBeenCalledWith(expectedValues); + expect(onSuccess).toHaveBeenCalled(); }); }); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.tsx index 1998244b7c..d8057638f4 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer.tsx @@ -1,18 +1,22 @@ +import axios, { AxiosError } from 'axios'; import { Formik, FormikHelpers, FormikProps } from 'formik'; import isNumber from 'lodash/isNumber'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; import styled from 'styled-components'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import * as API from '@/constants/API'; +import { useHistoricalNumberRepository } from '@/hooks/repositories/useHistoricalNumberRepository'; import { usePimsPropertyRepository } from '@/hooks/repositories/usePimsPropertyRepository'; import { useQueryMapLayersByLocation } from '@/hooks/repositories/useQueryMapLayersByLocation'; import { useLookupCodeHelpers } from '@/hooks/useLookupCodeHelpers'; import useIsMounted from '@/hooks/util/useIsMounted'; +import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { isValidId } from '@/utils'; +import { exists, isValidId } from '@/utils'; -import { UpdatePropertyDetailsFormModel } from './models'; +import { HistoricalNumberForm, UpdatePropertyDetailsFormModel } from './models'; import { UpdatePropertyDetailsForm } from './UpdatePropertyDetailsForm'; import { UpdatePropertyDetailsYupSchema } from './validation'; @@ -25,10 +29,21 @@ export const UpdatePropertyDetailsContainer = React.forwardRef< FormikProps, IUpdatePropertyDetailsContainerProps >((props, ref) => { + const { id, onSuccess } = props; const isMounted = useIsMounted(); - const { getPropertyWrapper, updatePropertyWrapper } = usePimsPropertyRepository(); - const executeGetProperty = getPropertyWrapper.execute; + const { + getPropertyWrapper: { execute: executeGetProperty, loading: loadingGetProperty }, + updatePropertyWrapper: { execute: executeUpdateProperty }, + } = usePimsPropertyRepository(); + + const { + getPropertyHistoricalNumbers: { + execute: executeGetHistoricalNumbers, + loading: loadingGetHistoricalNumbers, + }, + updatePropertyHistoricalNumbers: { execute: executeUpdateHistoricalNumbers }, + } = useHistoricalNumberRepository(); const { queryAll } = useQueryMapLayersByLocation(); @@ -45,63 +60,101 @@ export const UpdatePropertyDetailsContainer = React.forwardRef< [getByType], ); - useEffect(() => { - async function fetchProperty() { - if (isValidId(props.id)) { - const retrieved = await executeGetProperty(props.id); - if (retrieved !== undefined && isMounted()) { - const formValues = UpdatePropertyDetailsFormModel.fromApi(retrieved); - - // This triggers API calls to DataBC map layers - if (isNumber(retrieved.latitude) && isNumber(retrieved.longitude)) { - const layers = await queryAll({ lat: retrieved.latitude, lng: retrieved.longitude }); - formValues.isALR = !!layers.isALR; - formValues.motiRegion = layers.motiRegion; - formValues.highwaysDistrict = layers.highwaysDistrict; - formValues.electoralDistrict = layers.electoralDistrict; - formValues.firstNations = layers.firstNations; - } - - setForm(formValues); + const fetchProperty = useCallback( + async (id: number) => { + const retrieved = await executeGetProperty(id); + if (exists(retrieved) && isMounted()) { + // fetch historical file numbers for the retrieved property + const apiHistoricalNumbers = await executeGetHistoricalNumbers(id); + // create formik form model + const formValues = UpdatePropertyDetailsFormModel.fromApi(retrieved); + formValues.historicalNumbers = + apiHistoricalNumbers?.map(hn => HistoricalNumberForm.fromApi(hn)) ?? []; + + // This triggers API calls to DataBC map layers + if (isNumber(retrieved.latitude) && isNumber(retrieved.longitude)) { + const layers = await queryAll({ lat: retrieved.latitude, lng: retrieved.longitude }); + formValues.isALR = !!layers.isALR; + formValues.motiRegion = layers.motiRegion; + formValues.highwaysDistrict = layers.highwaysDistrict; + formValues.electoralDistrict = layers.electoralDistrict; + formValues.firstNations = layers.firstNations; } + + setForm(formValues); } + }, + [executeGetHistoricalNumbers, executeGetProperty, isMounted, queryAll], + ); + + useEffect(() => { + if (isValidId(id)) { + fetchProperty(id); } - fetchProperty(); - }, [isMounted, props.id, queryAll, executeGetProperty]); + }, [fetchProperty, id]); // save handler - sends updated property information to backend - const savePropertyInformation = async ( - values: UpdatePropertyDetailsFormModel, - formikHelpers: FormikHelpers, - ) => { - // default province and country to BC, Canada - if (values.address !== undefined) { - values.address.province = { - id: Number(provinceBC?.id), - code: null, - description: null, - displayOrder: null, - }; - values.address.country = { - id: Number(countryCA?.id), - code: null, - description: null, - displayOrder: null, - }; - } + const savePropertyInformation = useCallback( + async ( + values: UpdatePropertyDetailsFormModel, + formikHelpers: FormikHelpers, + ) => { + try { + // default province and country to BC, Canada + if (values.address !== undefined) { + values.address.province = { + id: Number(provinceBC?.id), + code: null, + description: null, + displayOrder: null, + }; + values.address.country = { + id: Number(countryCA?.id), + code: null, + description: null, + displayOrder: null, + }; + } - const apiProperty: ApiGen_Concepts_Property = values.toApi(); - const response = await updatePropertyWrapper.execute(apiProperty); + // save changes to the concept property + const apiProperty: ApiGen_Concepts_Property = values.toApi(); + const response = await executeUpdateProperty(apiProperty); - formikHelpers.setSubmitting(false); + // update list of historical numbers for this property + if (isValidId(response?.id)) { + const apiHistoricalNumbers = (values.historicalNumbers ?? []) + .filter(hn => !hn.isEmpty()) + .map(hn => hn.toApi()); + await executeUpdateHistoricalNumbers(apiProperty.id, apiHistoricalNumbers); - if (isValidId(response?.id)) { - formikHelpers.resetForm(); - props.onSuccess(); - } - }; + formikHelpers.resetForm(); + if (typeof onSuccess === 'function') { + onSuccess(); + } + } + } catch (e) { + if (axios.isAxiosError(e)) { + const axiosError = e as AxiosError; + if (axiosError?.response?.status === 400) { + toast.error(axiosError?.response?.data?.error); + } else { + toast.error('Unable to save. Please try again.'); + } + } + } finally { + formikHelpers.setSubmitting(false); + } + }, + [ + countryCA?.id, + executeUpdateHistoricalNumbers, + executeUpdateProperty, + onSuccess, + provinceBC?.id, + ], + ); - if (getPropertyWrapper?.loading || !initialForm) { + if (loadingGetProperty || loadingGetHistoricalNumbers || !initialForm) { return ; } diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsForm.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsForm.tsx index d002b7cea7..33ce1f1e3d 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsForm.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetails/update/UpdatePropertyDetailsForm.tsx @@ -27,6 +27,7 @@ import { PropertyTenureFormModel, UpdatePropertyDetailsFormModel, } from './models'; +import UpdateHistoricalNumbersSubForm from './UpdateHistoricalNumbersSubForm'; export interface IUpdatePropertyDetailsFormProps { formikProps: FormikProps; @@ -176,6 +177,9 @@ export const UpdatePropertyDetailsForm: React.FunctionComponent<
+ + +