From ee095d5200cc145f40a8a32505f05a5c79f33326 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 22 Sep 2023 14:02:35 -0700 Subject: [PATCH] PSP-6865 property management models (#3488) * Add new properties to backend models * Get property leases by property-id * API models and basic edit/view * Fix EF query * Update backend mappings * Model update --- .../Controllers/PropertyController.cs | 22 ++++++++- .../Models/Concepts/Lease/PropertyLeaseMap.cs | 3 +- .../Property/PropertyManagementPurposeMap.cs | 23 ++++++++++ .../PropertyManagementPurposeModel.cs | 24 ++++++++++ .../Models/Concepts/Property/PropertyMap.cs | 10 ++++ .../Models/Concepts/Property/PropertyModel.cs | 23 ++++++++++ .../Repositories/PropertyLeaseRepository.cs | 11 +++-- .../entities/Partials/PropPropPurpose.cs | 15 ++++++ .../entities/Partials/PropertyPurposeType.cs | 32 +++++++++++++ .../detail/ManagementSummaryContainer.tsx | 46 +++++++++++++++++++ .../detail/ManagementSummaryView.tsx | 36 +++++++++++++++ .../detail/PropertyManagementTabView.tsx | 8 +++- .../src/hooks/pims-api/useApiProperties.ts | 3 ++ .../repositories/usePimsPropertyRepository.ts | 21 +++++++-- source/frontend/src/models/api/Property.ts | 11 +++++ 15 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 source/backend/api/Models/Concepts/Property/PropertyManagementPurposeMap.cs create mode 100644 source/backend/api/Models/Concepts/Property/PropertyManagementPurposeModel.cs create mode 100644 source/backend/entities/Partials/PropPropPurpose.cs create mode 100644 source/backend/entities/Partials/PropertyPurposeType.cs create mode 100644 source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryContainer.tsx create mode 100644 source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryView.tsx diff --git a/source/backend/api/Areas/Property/Controllers/PropertyController.cs b/source/backend/api/Areas/Property/Controllers/PropertyController.cs index cf488917ba..1fd70e7806 100644 --- a/source/backend/api/Areas/Property/Controllers/PropertyController.cs +++ b/source/backend/api/Areas/Property/Controllers/PropertyController.cs @@ -25,6 +25,7 @@ public class PropertyController : ControllerBase #region Variables private readonly IPropertyRepository _propertyRepository; private readonly IPropertyService _propertyService; + private readonly IPropertyLeaseRepository _propertyLeaseRepository; private readonly IMapper _mapper; #endregion @@ -35,12 +36,14 @@ public class PropertyController : ControllerBase /// /// /// + /// /// /// - public PropertyController(IPropertyRepository propertyRepository, IPropertyService propertyService, IMapper mapper) + public PropertyController(IPropertyRepository propertyRepository, IPropertyService propertyService, IPropertyLeaseRepository propertyLeaseRepository, IMapper mapper) { _propertyRepository = propertyRepository; _propertyService = propertyService; + _propertyLeaseRepository = propertyLeaseRepository; _mapper = mapper; } #endregion @@ -62,6 +65,23 @@ public IActionResult GetPropertyAssociationsWithId(long id) return new JsonResult(_mapper.Map(property)); } + + /// + /// Get all associated leases for the specified unique property 'id'. + /// + /// + [HttpGet("{id}/leases")] + [HasPermission(Permissions.PropertyView)] + [HasPermission(Permissions.LeaseView)] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [SwaggerOperation(Tags = new[] { "property" })] + public IActionResult GetPropertyLeases(long propertyId) + { + var propertyLeases = _propertyLeaseRepository.GetAllByPropertyId(propertyId); + + return new JsonResult(_mapper.Map>(propertyLeases)); + } #endregion #region Concept Endpoints diff --git a/source/backend/api/Models/Concepts/Lease/PropertyLeaseMap.cs b/source/backend/api/Models/Concepts/Lease/PropertyLeaseMap.cs index ce1c6a5c99..aae79dd042 100644 --- a/source/backend/api/Models/Concepts/Lease/PropertyLeaseMap.cs +++ b/source/backend/api/Models/Concepts/Lease/PropertyLeaseMap.cs @@ -11,7 +11,8 @@ public void Register(TypeAdapterConfig config) config.NewConfig() .Map(dest => dest.Property, src => src.Property) .Map(dest => dest.PropertyId, src => src.PropertyId) - .Map(dest => dest.Lease.Id, src => src.LeaseId) + .Map(dest => dest.Lease, src => src.Lease) + .Map(dest => dest.LeaseId, src => src.LeaseId) .Map(dest => dest.AreaUnitType, src => src.AreaUnitTypeCodeNavigation) .Map(dest => dest.LeaseArea, src => src.LeaseArea) .Map(dest => dest.PropertyName, src => src.Name) diff --git a/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeMap.cs b/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeMap.cs new file mode 100644 index 0000000000..83a85582a3 --- /dev/null +++ b/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeMap.cs @@ -0,0 +1,23 @@ +using Mapster; +using Entity = Pims.Dal.Entities; + +namespace Pims.Api.Models.Concepts +{ + public class PropertyManagementPurposeMap : IRegister + { + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .Map(dest => dest.Id, src => src.Internal_Id) + .Map(dest => dest.PropertyId, src => src.PropertyId) + .Map(dest => dest.PropertyPurposeTypeCode, src => src.PropertyPurposeTypeCodeNavigation) + .Inherits(); + + config.NewConfig() + .Map(dest => dest.Internal_Id, src => src.Id) + .Map(dest => dest.PropertyId, src => src.PropertyId) + .Map(dest => dest.PropertyPurposeTypeCode, src => src.PropertyPurposeTypeCode.Id) + .Inherits(); + } + } +} diff --git a/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeModel.cs b/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeModel.cs new file mode 100644 index 0000000000..8c38a9933d --- /dev/null +++ b/source/backend/api/Models/Concepts/Property/PropertyManagementPurposeModel.cs @@ -0,0 +1,24 @@ +namespace Pims.Api.Models.Concepts +{ + public class PropertyManagementPurposeModel : BaseAppModel + { + #region Properties + + /// + /// get/set - The relationship id. + /// + public long Id { get; set; } + + /// + /// get/set - Parent property id. + /// + public long PropertyId { get; set; } + + /// + /// get/set - The property purpose type code. + /// + public TypeModel PropertyPurposeTypeCode { get; set; } + + #endregion + } +} diff --git a/source/backend/api/Models/Concepts/Property/PropertyMap.cs b/source/backend/api/Models/Concepts/Property/PropertyMap.cs index 173e06ae1d..7a7f1f8810 100644 --- a/source/backend/api/Models/Concepts/Property/PropertyMap.cs +++ b/source/backend/api/Models/Concepts/Property/PropertyMap.cs @@ -63,6 +63,11 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.SurplusDeclarationComment, src => src.SurplusDeclarationComment) .Map(dest => dest.SurplusDeclarationDate, src => src.SurplusDeclarationDate) .Map(dest => dest.SurplusDeclarationType, src => src.SurplusDeclarationTypeCodeNavigation) + + .Map(dest => dest.ManagementPurposes, src => src.PimsPropPropPurposes) + .Map(dest => dest.AdditionalDetails, src => src.AdditionalDetails) + .Map(dest => dest.IsUtilitiesPayable, src => src.IsUtilitiesPayable) + .Map(dest => dest.IsTaxesPayable, src => src.IsTaxesPayable) .Inherits(); config.NewConfig() @@ -109,6 +114,11 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.Zoning, src => src.Zoning) .Map(dest => dest.ZoningPotential, src => src.ZoningPotential) + .Map(dest => dest.PimsPropPropPurposes, src => src.ManagementPurposes) + .Map(dest => dest.AdditionalDetails, src => src.AdditionalDetails) + .Map(dest => dest.IsUtilitiesPayable, src => src.IsUtilitiesPayable) + .Map(dest => dest.IsTaxesPayable, src => src.IsTaxesPayable) + .Inherits(); } } diff --git a/source/backend/api/Models/Concepts/Property/PropertyModel.cs b/source/backend/api/Models/Concepts/Property/PropertyModel.cs index 578381eeb2..de1b364772 100644 --- a/source/backend/api/Models/Concepts/Property/PropertyModel.cs +++ b/source/backend/api/Models/Concepts/Property/PropertyModel.cs @@ -240,6 +240,29 @@ public class PropertyModel : BaseModel public DateTime SurplusDeclarationDate { get; set; } #endregion + + #region Management + /// + /// get/set - The property management purposes. + /// + public IList ManagementPurposes { get; set; } + + /// + /// get/set - Additional details when property management purpose is OTHER. + /// + public string AdditionalDetails { get; set; } + + /// + /// get/set - Whether utilities are payable for this property.. + /// + public bool? IsUtilitiesPayable { get; set; } + + /// + /// get/set - Whether taxes are payable for this property. + /// + public bool? IsTaxesPayable { get; set; } + #endregion + #endregion } } diff --git a/source/backend/dal/Repositories/PropertyLeaseRepository.cs b/source/backend/dal/Repositories/PropertyLeaseRepository.cs index 2ddd2af486..8543176fce 100644 --- a/source/backend/dal/Repositories/PropertyLeaseRepository.cs +++ b/source/backend/dal/Repositories/PropertyLeaseRepository.cs @@ -31,16 +31,19 @@ public PropertyLeaseRepository(PimsContext dbContext, ClaimsPrincipal user, ILog #region Methods /// - /// Get the property lease for the specified property id. + /// Get the associated property leases for the specified property id. /// /// /// public IEnumerable GetAllByPropertyId(long propertyId) { - return this.Context.PimsPropertyLeases.AsNoTracking() - .Include(p => p.Property) + return Context.PimsPropertyLeases.AsNoTracking() + .Include(pl => pl.Property) .ThenInclude(p => p.Address) - .Include(l => l.Lease) + .Include(pl => pl.Lease) + .ThenInclude(l => l.LeaseStatusTypeCodeNavigation) + .Include(pl => pl.Lease) + .ThenInclude(l => l.PimsLeaseTerms) .Where(p => p.PropertyId == propertyId); } diff --git a/source/backend/entities/Partials/PropPropPurpose.cs b/source/backend/entities/Partials/PropPropPurpose.cs new file mode 100644 index 0000000000..7933018b46 --- /dev/null +++ b/source/backend/entities/Partials/PropPropPurpose.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Pims.Dal.Entities +{ + /// + /// PimsPropPropPurpose class, provides an entity for the datamodel to manage property management purpose types. + /// + public partial class PimsPropPropPurpose : StandardIdentityBaseAppEntity, IBaseAppEntity + { + #region Properties + [NotMapped] + public override long Internal_Id { get => this.PropPropPurposeId; set => this.PropPropPurposeId = value; } + #endregion + } +} diff --git a/source/backend/entities/Partials/PropertyPurposeType.cs b/source/backend/entities/Partials/PropertyPurposeType.cs new file mode 100644 index 0000000000..884cd58f8c --- /dev/null +++ b/source/backend/entities/Partials/PropertyPurposeType.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Pims.Dal.Entities +{ + /// + /// PimsPropertyPurposeType class, provides an entity for the datamodel to manage a list of property management purposes. + /// + public partial class PimsPropertyPurposeType : ITypeEntity + { + #region Properties + + /// + /// get/set - Primary key to identify property management purpose type. + /// + [NotMapped] + public string Id { get => PropertyPurposeTypeCode; set => PropertyPurposeTypeCode = value; } + #endregion + + #region Constructors + + /// + /// Create a new instance of a PimsPropertyPurposeType class. + /// + /// + public PimsPropertyPurposeType(string id) + : this() + { + Id = id; + } + #endregion + } +} diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryContainer.tsx new file mode 100644 index 0000000000..3e0cf3ae3d --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryContainer.tsx @@ -0,0 +1,46 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { usePimsPropertyRepository } from '@/hooks/repositories/usePimsPropertyRepository'; +import { Api_Property } from '@/models/api/Property'; +import { Api_PropertyLease } from '@/models/api/PropertyLease'; + +import { IManagementSummaryViewProps } from './ManagementSummaryView'; + +interface IManagementSummaryContainerProps { + property: Api_Property; + setEditMode: (isEditing: boolean) => void; + View: React.FC; +} + +export const ManagementSummaryContainer: React.FunctionComponent< + IManagementSummaryContainerProps +> = ({ property, setEditMode, View }) => { + const [propertyLeases, setPropertyLeases] = useState([]); + + const { + getPropertyLeases: { execute: getPropertyLeases, loading }, + } = usePimsPropertyRepository(); + + const fetchPropertyLeases = useCallback(async () => { + if (!property.id) { + return; + } + const propertyLeasesResponse = await getPropertyLeases(property.id); + if (propertyLeasesResponse) { + setPropertyLeases(propertyLeasesResponse); + } + }, [getPropertyLeases, property]); + + useEffect(() => { + fetchPropertyLeases(); + }, [fetchPropertyLeases]); + + return ( + + ); +}; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryView.tsx new file mode 100644 index 0000000000..d1663545fb --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/ManagementSummaryView.tsx @@ -0,0 +1,36 @@ +import { EditButton } from '@/components/common/EditButton'; +import { Section } from '@/components/common/Section/Section'; +import { StyledEditWrapper } from '@/components/common/Section/SectionStyles'; +import { Claims } from '@/constants/index'; +import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { Api_Property } from '@/models/api/Property'; +import { Api_PropertyLease } from '@/models/api/PropertyLease'; + +export interface IManagementSummaryViewProps { + isLoading: boolean; + property: Api_Property; + propertyLeases: Api_PropertyLease[]; + setEditMode: (isEditing: boolean) => void; +} + +export const ManagementSummaryView: React.FunctionComponent = ({ + isLoading, + property, + propertyLeases, + setEditMode, +}) => { + const { hasClaim } = useKeycloakWrapper(); + return ( +
+ + {/** TODO: Use MANAGEMENT CLAIMS when available */} + {setEditMode !== undefined && hasClaim(Claims.PROPERTY_EDIT) && ( + setEditMode(true)} + /> + )} + +
+ ); +}; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyManagementTabView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyManagementTabView.tsx index 708f3e2dba..4c2f6ed720 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyManagementTabView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyManagementTabView.tsx @@ -4,6 +4,8 @@ import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { StyledSummarySection } from '@/components/common/Section/SectionStyles'; import { Api_Property } from '@/models/api/Property'; +import { ManagementSummaryContainer } from './ManagementSummaryContainer'; +import { ManagementSummaryView } from './ManagementSummaryView'; import { PropertyContactContainer } from './PropertyContactContainer'; import { PropertyContactView } from './PropertyContactView'; @@ -26,7 +28,11 @@ export const PropertyManagementTabView: React.FunctionComponent - + { api.post(`/properties/search/advanced-filter`, params), getPropertyAssociationsApi: (id: number) => api.get(`/properties/${id}/associations`), + getPropertyLeasesApi: (id: number) => + api.get(`/properties/${id}/leases`), exportPropertiesApi: (filter: IPaginateProperties, outputFormat: 'csv' | 'excel' = 'excel') => api.get( `/reports/properties?${filter ? queryString.stringify({ ...filter, all: true }) : ''}`, diff --git a/source/frontend/src/hooks/repositories/usePimsPropertyRepository.ts b/source/frontend/src/hooks/repositories/usePimsPropertyRepository.ts index 0629804733..9270de6805 100644 --- a/source/frontend/src/hooks/repositories/usePimsPropertyRepository.ts +++ b/source/frontend/src/hooks/repositories/usePimsPropertyRepository.ts @@ -13,8 +13,12 @@ import { useAxiosErrorHandler } from '@/utils'; * hook that retrieves a property from the inventory. */ export const usePimsPropertyRepository = () => { - const { getPropertyConceptWithIdApi, putPropertyConceptApi, getMatchingPropertiesApi } = - useApiProperties(); + const { + getPropertyConceptWithIdApi, + putPropertyConceptApi, + getMatchingPropertiesApi, + getPropertyLeasesApi, + } = useApiProperties(); const getPropertyWrapper = useApiRequestWrapper({ requestFunction: useCallback( @@ -53,8 +57,17 @@ export const usePimsPropertyRepository = () => { }, []), }); + const getPropertyLeases = useApiRequestWrapper({ + requestFunction: useCallback( + async (id: number) => await getPropertyLeasesApi(id), + [getPropertyLeasesApi], + ), + requestName: 'getPropertyLeases', + onError: useAxiosErrorHandler('Failed to retrieve property leases.'), + }); + return useMemo( - () => ({ getPropertyWrapper, updatePropertyWrapper, getMatchingProperties }), - [getPropertyWrapper, updatePropertyWrapper, getMatchingProperties], + () => ({ getPropertyWrapper, updatePropertyWrapper, getMatchingProperties, getPropertyLeases }), + [getPropertyWrapper, updatePropertyWrapper, getMatchingProperties, getPropertyLeases], ); }; diff --git a/source/frontend/src/models/api/Property.ts b/source/frontend/src/models/api/Property.ts index ce59ae256c..0e5836d31b 100644 --- a/source/frontend/src/models/api/Property.ts +++ b/source/frontend/src/models/api/Property.ts @@ -82,6 +82,11 @@ export interface Api_Property extends Api_ConcurrentVersion, Api_AuditFields { surplusDeclarationType?: Api_TypeCode; surplusDeclarationComment?: string; surplusDeclarationDate?: string; + + managementPurposes?: Api_PropertyManagementPurpose[]; + additionalDetails?: string; + isUtilitiesPayable?: boolean; + isTaxesPayable?: boolean; } export interface Api_PropertyAnomaly extends Api_ConcurrentVersion, Api_AuditFields { @@ -108,6 +113,12 @@ export interface Api_PropertyTenure extends Api_ConcurrentVersion, Api_AuditFiel propertyTenureTypeCode?: Api_TypeCode; } +export interface Api_PropertyManagementPurpose extends Api_ConcurrentVersion, Api_AuditFields { + id?: number; + propertyId?: number; + propertyPurposeTypeCode?: Api_TypeCode; +} + export interface Api_PropertyAssociations { id?: string; pid?: string;