diff --git a/source/backend/dal/Models/PropertyFilterCriteria.cs b/source/backend/dal/Models/PropertyFilterCriteria.cs index aa1692b895..3adc064f15 100644 --- a/source/backend/dal/Models/PropertyFilterCriteria.cs +++ b/source/backend/dal/Models/PropertyFilterCriteria.cs @@ -34,6 +34,11 @@ public class PropertyFilterCriteria /// public string LeaseStatus { get; set; } + /// + /// get/set - The lease receivable/payable type to filter by. + /// + public string LeasePayRcvblType { get; set; } + /// /// get/set - The multiple lease types to filter by. /// diff --git a/source/backend/dal/Repositories/PropertyRepository.cs b/source/backend/dal/Repositories/PropertyRepository.cs index 8f378ff0f5..53b2849a72 100644 --- a/source/backend/dal/Repositories/PropertyRepository.cs +++ b/source/backend/dal/Repositories/PropertyRepository.cs @@ -448,6 +448,12 @@ public HashSet GetMatchingIds(PropertyFilterCriteria filter) p.PimsPropertyLeases.Any(pl => filter.LeasePurposes.Contains(pl.Lease.LeasePurposeTypeCode))); } + if (!string.IsNullOrEmpty(filter.LeasePayRcvblType)) + { + query = query.Where(p => + p.PimsPropertyLeases.Any(pl => pl.Lease.LeasePayRvblTypeCode == filter.LeasePayRcvblType || filter.LeasePayRcvblType == "all")); + } + // Anomalies if (filter.AnomalyIds != null && filter.AnomalyIds.Count > 0) { diff --git a/source/backend/tests/unit/dal/Repositories/PropertyRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/PropertyRepositoryTest.cs index 3471d2955a..a3bfb687e1 100644 --- a/source/backend/tests/unit/dal/Repositories/PropertyRepositoryTest.cs +++ b/source/backend/tests/unit/dal/Repositories/PropertyRepositoryTest.cs @@ -132,6 +132,165 @@ public void GetById_Success() } #endregion + #region GetMatchingIds + [Fact] + public void GetMatchingIds_LeaseRcbvl_All_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + var lease = EntityHelper.CreateLease(1, addProperty:false); + property.PimsPropertyLeases.Add(new PimsPropertyLease() { PropertyId = property.Internal_Id, LeaseId = lease.Internal_Id, Lease = lease }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { LeasePayRcvblType = "all" }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_LeaseStatus_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + var lease = EntityHelper.CreateLease(1, pimsLeaseStatusType: new PimsLeaseStatusType() { Id = "test2" }, addProperty: false); + property.PimsPropertyLeases.Add(new PimsPropertyLease() { PropertyId = property.Internal_Id, LeaseId = lease.Internal_Id, Lease = lease }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { LeaseStatus = "test2" }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_LeaseType_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + var lease = EntityHelper.CreateLease(1, pimsLeaseLicenseType: new PimsLeaseLicenseType() { Id = "test" }, addProperty: false); + property.PimsPropertyLeases.Add(new PimsPropertyLease() { PropertyId = property.Internal_Id, LeaseId = lease.Internal_Id, Lease = lease }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { LeaseTypes = new List() { "test" } }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_LeasePurpose_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + var lease = EntityHelper.CreateLease(1, pimsLeasePurposeType: new PimsLeasePurposeType() { Id = "test" }, addProperty: false); + property.PimsPropertyLeases.Add(new PimsPropertyLease() { PropertyId = property.Internal_Id, LeaseId = lease.Internal_Id, Lease = lease }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { LeasePurposes = new List() { "test" } }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_Anomaly_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + property.PimsPropPropAnomalyTypes.Add(new PimsPropPropAnomalyType() { PropertyAnomalyTypeCode = "test" }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { AnomalyIds = new List() { "test" } }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_Project_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + property.PimsPropertyAcquisitionFiles.Add(new PimsPropertyAcquisitionFile() { AcquisitionFile = new PimsAcquisitionFile() { ProjectId = 1 } }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { ProjectId = 1 }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_Tenure_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + property.PimsPropPropTenureTypes.Add(new PimsPropPropTenureType() { PropertyTenureTypeCode = "test" }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { TenureStatuses = new List() { "test" } }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_TenureRoad_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + property.PimsPropPropRoadTypes.Add(new PimsPropPropRoadType() { PropertyRoadTypeCode = "test" }); + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { TenureRoadTypes = new List() { "test" } }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + + [Fact] + public void GetMatchingIds_TenurePph_Success() + { + // Arrange + var repository = CreateRepositoryWithPermissions(Permissions.PropertyView); + var property = EntityHelper.CreateProperty(100); + property.PphStatusTypeCode = "test"; + _helper.AddAndSaveChanges(property); + + // Act + var result = repository.GetMatchingIds(new PropertyFilterCriteria() { TenurePPH = "test" }); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(1); + } + #endregion + #region GetByPid [Fact] public void GetByPid_Success() diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx new file mode 100644 index 0000000000..65d7e51e11 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.test.tsx @@ -0,0 +1,87 @@ +import { FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { forwardRef } from 'react'; + +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { mockLookups } from '@/mocks/lookups.mock'; +import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; +import { getMockApiPropertyManagement } from '@/mocks/propertyManagement.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { render, RenderOptions, waitFor } from '@/utils/test-utils'; + +import { FilterContentContainer, IFilterContentContainerProps } from './FilterContentContainer'; +import { IFilterContentFormProps } from './FilterContentForm'; +import { PropertyFilterFormModel } from './models'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockGetApi = { + error: undefined, + response: [1] as number[] | undefined, + execute: jest.fn().mockResolvedValue([1]), + loading: false, +}; +jest.mock('@/components/common/mapFSM/MapStateMachineContext'); +jest.mock('@/hooks/repositories/usePimsPropertyRepository', () => ({ + usePimsPropertyRepository: () => { + return { + getMatchingProperties: mockGetApi, + }; + }, +})); + +describe('FilterContentContainer component', () => { + let viewProps: IFilterContentFormProps; + + const View = forwardRef, IFilterContentFormProps>((props, ref) => { + viewProps = props; + return <>; + }); + + const setup = ( + renderOptions?: RenderOptions & { props?: Partial }, + ) => { + renderOptions = renderOptions ?? {}; + const utils = render(, { + ...renderOptions, + store: storeState, + history, + }); + + return { + ...utils, + }; + }; + + beforeEach(() => { + jest.resetAllMocks(); + (useMapStateMachine as jest.Mock).mockImplementation(() => mapMachineBaseMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('fetches filter data from the api', async () => { + mockGetApi.execute.mockResolvedValue(getMockApiPropertyManagement(1)); + setup({}); + viewProps.onChange(new PropertyFilterFormModel()); + expect(mockGetApi.execute).toBeCalledWith(new PropertyFilterFormModel().toApi()); + await waitFor(() => + expect(mapMachineBaseMock.setVisiblePimsProperties).toBeCalledWith({ + additionalDetails: 'test', + id: 1, + isLeaseActive: false, + isLeaseExpired: false, + isTaxesPayable: null, + isUtilitiesPayable: null, + leaseExpiryDate: null, + managementPurposes: [], + rowVersion: 1, + }), + ); + }); +}); diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx index 345b6588d8..0f5f4f6a2b 100644 --- a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentContainer.tsx @@ -9,7 +9,7 @@ import { Api_PropertyFilterCriteria } from '@/models/api/ProjectFilterCriteria'; import { IFilterContentFormProps } from './FilterContentForm'; import { PropertyFilterFormModel } from './models'; -interface IFilterContentContainerProps { +export interface IFilterContentContainerProps { View: React.FunctionComponent; } diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.test.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.test.tsx new file mode 100644 index 0000000000..acce1d6491 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.test.tsx @@ -0,0 +1,74 @@ +import { createMemoryHistory } from 'history'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { getMockApiPropertyManagement } from '@/mocks/propertyManagement.mock'; +import { Api_PropertyManagement } from '@/models/api/Property'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { act, render, RenderOptions, userEvent } from '@/utils/test-utils'; + +import { FilterContentForm, IFilterContentFormProps } from './FilterContentForm'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockGetApi = { + error: undefined, + response: undefined as Api_PropertyManagement | undefined, + execute: jest.fn(), + loading: false, +}; + +jest.mock('@/hooks/repositories/usePropertyManagementRepository', () => ({ + usePropertyManagementRepository: () => { + return { + getPropertyManagement: mockGetApi, + }; + }, +})); + +describe('FilterContentForm component', () => { + const onChange = jest.fn(); + + const setup = (renderOptions: RenderOptions & { props: IFilterContentFormProps }) => { + renderOptions = renderOptions ?? ({} as any); + const utils = render(, { + ...renderOptions, + store: storeState, + history, + }); + + return { + ...utils, + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('shows loading spinner when loading', () => { + mockGetApi.execute.mockResolvedValue(getMockApiPropertyManagement(1)); + const { getByTestId } = setup({ props: { onChange, isLoading: true } }); + expect(getByTestId('filter-backdrop-loading')).toBeVisible(); + }); + + it('displays filters when not loading', async () => { + const apiManagement = getMockApiPropertyManagement(1); + mockGetApi.response = apiManagement; + const { getByDisplayValue } = setup({ props: { onChange, isLoading: false } }); + expect(getByDisplayValue('Select a highway')).toBeVisible(); + expect(getByDisplayValue('Select Lease Transaction')).toBeVisible(); + }); + + it('calls onChange when a filter is changed', async () => { + const apiManagement = getMockApiPropertyManagement(1); + mockGetApi.response = apiManagement; + const { getByTestId } = setup({ props: { onChange, isLoading: false } }); + await act(async () => { + userEvent.selectOptions(getByTestId('leasePayRcvblType'), ['all']); + expect(onChange).toBeCalled(); + }); + }); +}); diff --git a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.tsx b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.tsx index 3df8450006..312adf501d 100644 --- a/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/AdvancedFilter/FilterContentForm.tsx @@ -83,6 +83,13 @@ export const FilterContentForm: React.FC( + x => { + return { value: x.id.toString(), label: x.name }; + }, + ); + leasePaymentRcvblOptions.push({ value: 'all', label: 'Payable and Receivable' }); + return ( initialValues={initialFilter} onSubmit={noop}>
@@ -122,6 +129,18 @@ export const FilterContentForm: React.FC
+ + lp.codeType), leaseStatus: this.leaseStatus !== '' ? this.leaseStatus : null, + leasePayRcvblType: this.leasePayRcvblType !== '' ? this.leasePayRcvblType : null, leaseTypes: this.leaseTypes.map(lt => lt.codeType), leasePurposes: this.leasePurposes.map(lp => lp.codeType), diff --git a/source/frontend/src/models/api/ProjectFilterCriteria.ts b/source/frontend/src/models/api/ProjectFilterCriteria.ts index 3ebe1a81e6..033f6578d2 100644 --- a/source/frontend/src/models/api/ProjectFilterCriteria.ts +++ b/source/frontend/src/models/api/ProjectFilterCriteria.ts @@ -6,6 +6,7 @@ export interface Api_PropertyFilterCriteria { tenureRoadTypes: string[]; leaseStatus: string | null; + leasePayRcvblType: string | null; leaseTypes: string[]; leasePurposes: string[];