diff --git a/source/backend/api/Areas/Reports/Controllers/LeaseController.cs b/source/backend/api/Areas/Reports/Controllers/LeaseController.cs index ca8468aa47..4fac74ce1e 100644 --- a/source/backend/api/Areas/Reports/Controllers/LeaseController.cs +++ b/source/backend/api/Areas/Reports/Controllers/LeaseController.cs @@ -19,6 +19,7 @@ using Pims.Core.Extensions; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; +using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; using Pims.Dal.Security; using Swashbuckle.AspNetCore.Annotations; @@ -204,9 +205,13 @@ public IActionResult ExportLeasePayments(int fiscalYearStart) public IEnumerable GetCrossJoinLeases(Lease.Models.Search.LeaseFilterModel filter, bool all = false) { var page = _leaseService.GetPage((LeaseFilter)filter, all); - var allLeases = page.Items.SelectMany(l => l.PimsLeasePeriods.DefaultIfEmpty(), (lease, period) => (lease, period)) + var expiryDate = page.Items.Select(l => l.GetExpiryDate()); + + var allLeases = page.Items + .SelectMany(l => l.PimsLeasePeriods.DefaultIfEmpty(), (lease, period) => (lease, period)) .SelectMany(lt => lt.lease.PimsPropertyLeases.DefaultIfEmpty(), (leasePeriod, property) => (leasePeriod.period, leasePeriod.lease, property)) - .SelectMany(ltp => ltp.lease.PimsLeaseTenants.DefaultIfEmpty(), (leasePeriodProperty, tenant) => (leasePeriodProperty.period, leasePeriodProperty.lease, leasePeriodProperty.property, tenant)); + .SelectMany(ltp => ltp.lease.PimsLeaseTenants.DefaultIfEmpty(), (leasePeriodProperty, tenant) => (leasePeriodProperty.period, leasePeriodProperty.lease, leasePeriodProperty.property, tenant)) + .SelectMany(ltpr => expiryDate, (leasePeriodPropertyTenant, expiryDate) => (leasePeriodPropertyTenant.period, leasePeriodPropertyTenant.lease, leasePeriodPropertyTenant.property, leasePeriodPropertyTenant.tenant, expiryDate)); var flatLeases = _mapper.Map>(allLeases); return flatLeases; } diff --git a/source/backend/api/Areas/Reports/Mapping/Lease/LeaseMap.cs b/source/backend/api/Areas/Reports/Mapping/Lease/LeaseMap.cs index fc429f2718..7c3f69122b 100644 --- a/source/backend/api/Areas/Reports/Mapping/Lease/LeaseMap.cs +++ b/source/backend/api/Areas/Reports/Mapping/Lease/LeaseMap.cs @@ -11,19 +11,19 @@ public class LeaseMap : IRegister { public void Register(TypeAdapterConfig config) { - config.NewConfig<(Entity.PimsLeasePeriod period, Entity.PimsLease lease, Entity.PimsPropertyLease property, Entity.PimsLeaseTenant tenant), Model.LeaseModel>() + config.NewConfig<(Entity.PimsLeasePeriod period, Entity.PimsLease lease, Entity.PimsPropertyLease property, Entity.PimsLeaseTenant tenant, DateTime? expiryDate), Model.LeaseModel>() .AfterMapping((src, dest) => { MapLease(src, dest); }); } - private static void MapLease((Entity.PimsLeasePeriod period, Entity.PimsLease lease, Entity.PimsPropertyLease property, Entity.PimsLeaseTenant tenant) src, Model.LeaseModel dest) + private static void MapLease((Entity.PimsLeasePeriod period, Entity.PimsLease lease, Entity.PimsPropertyLease property, Entity.PimsLeaseTenant tenant, DateTime? expiryDate) src, Model.LeaseModel dest) { dest.LFileNo = src.lease.LFileNo; dest.MotiRegion = src.lease.RegionCodeNavigation?.RegionName; dest.StartDate = src.lease.OrigStartDate.FilterSqlMinDate().ToNullableDateOnly(); - dest.EndDate = src.lease.OrigExpiryDate?.FilterSqlMinDate().ToNullableDateOnly(); + dest.EndDate = src.expiryDate.ToNullableDateOnly(); dest.CurrentPeriodStartDate = src.lease.GetCurrentPeriodStartDate()?.FilterSqlMinDate().ToNullableDateOnly(); dest.CurrentTermEndDate = src.lease.GetCurrentPeriodEndDate()?.FilterSqlMinDate().ToNullableDateOnly(); dest.ProgramName = src.lease.LeaseProgramTypeCodeNavigation?.GetTypeDescriptionOther(src.lease.OtherLeaseProgramType); @@ -36,7 +36,7 @@ private static void MapLease((Entity.PimsLeasePeriod period, Entity.PimsLease le dest.InspectionNotes = src.lease.InspectionNotes; dest.InspectionDate = src.lease.InspectionDate?.FilterSqlMinDate(); dest.LeaseNotes = src.lease.LeaseNotes; - dest.IsExpired = (src.lease.GetExpiryDate() < DateTime.Now).BoolToYesNo(); + dest.IsExpired = (src.expiryDate < DateTime.Now).BoolToYesNo(); dest.LeaseAmount = src.period?.PaymentAmount; dest.PeriodStartDate = src.period?.PeriodStartDate.FilterSqlMinDate().ToNullableDateOnly(); dest.PeriodExpiryDate = src.period?.PeriodExpiryDate?.FilterSqlMinDate().ToNullableDateOnly(); diff --git a/source/backend/dal/Repositories/LeaseRepository.cs b/source/backend/dal/Repositories/LeaseRepository.cs index 149b28677d..f8a2c5bb75 100644 --- a/source/backend/dal/Repositories/LeaseRepository.cs +++ b/source/backend/dal/Repositories/LeaseRepository.cs @@ -895,7 +895,7 @@ public IQueryable GenerateLeaseQuery(LeaseFilter filter, HashSet p.Address) .Include(l => l.PimsPropertyLeases) .ThenInclude(p => p.AreaUnitTypeCodeNavigation) - .Include(pl => pl.PimsPropertyLeases) + .Include(l => l.PimsPropertyLeases) .ThenInclude(p => p.Property) .ThenInclude(n => n.PimsHistoricalFileNumbers) .ThenInclude(t => t.HistoricalFileNumberTypeCodeNavigation) @@ -908,8 +908,9 @@ public IQueryable GenerateLeaseQuery(LeaseFilter filter, HashSet t.Person) .Include(l => l.PimsLeaseTenants) .ThenInclude(t => t.Organization) - .Include(p => p.RegionCodeNavigation) + .Include(l => l.RegionCodeNavigation) .Include(l => l.PimsLeasePeriods) + .Include(l => l.PimsLeaseRenewals) .AsNoTracking(); if (loadPayments) @@ -1100,17 +1101,25 @@ private static ExpressionStarter GenerateCommonLeaseQuery(LeaseFilter var expiryStartDate = filter.ExpiryStartDate.ToNullableDateTime(); var expiryEndDate = filter.ExpiryEndDate.ToNullableDateTime(); - if (filter.ExpiryStartDate != null && filter.ExpiryEndDate != null) + if (expiryStartDate != null && expiryEndDate != null) { - predicateBuilder.And(l => l.OrigExpiryDate >= expiryStartDate && l.OrigExpiryDate <= expiryEndDate); + predicateBuilder = predicateBuilder.And(l => l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max().HasValue ? + l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max() >= expiryStartDate && + l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max() <= expiryEndDate : + l.OrigExpiryDate >= expiryStartDate && + l.OrigExpiryDate <= expiryEndDate); } - else if (filter.ExpiryStartDate != null) + else if (expiryStartDate != null) { - predicateBuilder = predicateBuilder.And(l => l.OrigExpiryDate >= expiryStartDate); + predicateBuilder = predicateBuilder.And(l => l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max().HasValue ? + l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max() >= expiryStartDate : + l.OrigExpiryDate >= expiryStartDate); } - else if (filter.ExpiryEndDate != null) + else if (expiryEndDate != null) { - predicateBuilder = predicateBuilder.And(l => l.OrigExpiryDate <= expiryEndDate); + predicateBuilder = predicateBuilder.And(l => l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max().HasValue ? + l.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(rf => rf.ExpiryDt).Max() <= expiryEndDate : + l.OrigExpiryDate <= expiryEndDate); } if (filter.RegionType.HasValue) diff --git a/source/backend/dal/Repositories/PropertyLeaseRepository.cs b/source/backend/dal/Repositories/PropertyLeaseRepository.cs index 9bf7971d11..3b939adb05 100644 --- a/source/backend/dal/Repositories/PropertyLeaseRepository.cs +++ b/source/backend/dal/Repositories/PropertyLeaseRepository.cs @@ -48,6 +48,8 @@ public IEnumerable GetAllByPropertyId(long propertyId) .ThenInclude(l => l.LeaseStatusTypeCodeNavigation) .Include(pl => pl.Lease) .ThenInclude(l => l.PimsLeasePeriods) + .Include(pl => pl.Lease) + .ThenInclude(l => l.PimsLeaseRenewals) .Where(p => p.PropertyId == propertyId); } diff --git a/source/backend/entities/Extensions/LeaseExtensions.cs b/source/backend/entities/Extensions/LeaseExtensions.cs index 214d5d59ff..dad2bb959b 100644 --- a/source/backend/entities/Extensions/LeaseExtensions.cs +++ b/source/backend/entities/Extensions/LeaseExtensions.cs @@ -66,19 +66,8 @@ public static string GetTenantName(this Pims.Dal.Entities.PimsLeaseTenant lease) /// public static DateTime? GetExpiryDate(this Pims.Dal.Entities.PimsLease lease) { - if (lease.PimsLeasePeriods != null && lease.PimsLeasePeriods.Any(p => p.PeriodExpiryDate == null && !p.IsFlexibleDuration)) - { - return null; - } - if (lease.OrigExpiryDate != null) - { - if (lease.PimsLeasePeriods != null && lease.PimsLeasePeriods.Any(p => p.PeriodExpiryDate > lease.OrigExpiryDate && !p.IsFlexibleDuration)) - { - return lease.PimsLeasePeriods.OrderByDescending(p => p.PeriodExpiryDate).FirstOrDefault().PeriodExpiryDate; - } - return lease.OrigExpiryDate; - } - return lease.PimsLeasePeriods?.OrderByDescending(p => p.PeriodExpiryDate).FirstOrDefault(p => !p.IsFlexibleDuration)?.PeriodExpiryDate; + var expiryDate = lease.PimsLeaseRenewals.Where(r => r.IsExercised == true).Select(fr => fr.ExpiryDt).Append(lease.OrigExpiryDate).Max(); + return expiryDate; } } } diff --git a/source/backend/tests/unit/dal/Core/Extensions/LeaseExtensionsTest.cs b/source/backend/tests/unit/dal/Core/Extensions/LeaseExtensionsTest.cs index bbc6211034..3f69db2de8 100644 --- a/source/backend/tests/unit/dal/Core/Extensions/LeaseExtensionsTest.cs +++ b/source/backend/tests/unit/dal/Core/Extensions/LeaseExtensionsTest.cs @@ -30,18 +30,31 @@ public void GetExpiryDate_OrigExpiry() } [Fact] - public void GetExpiryDate_TermExpiry() + public void GetExpiryDate_RenewalExpiry() { DateTime now = DateTime.Now; PimsLease lease = new PimsLease() { OrigExpiryDate = null, - PimsLeasePeriods = new List() { - new PimsLeasePeriod() { PeriodExpiryDate = now }, }, + PimsLeaseRenewals = new List() { + new PimsLeaseRenewal() { IsExercised = true, ExpiryDt = now }, }, }; Assert.Equal(now, lease.GetExpiryDate()); } + [Fact] + public void GetExpiryDate_RenewalExpiry_NotExercised() + { + DateTime now = DateTime.Now; + PimsLease lease = new PimsLease() + { + OrigExpiryDate = null, + PimsLeaseRenewals = new List() { + new PimsLeaseRenewal() { IsExercised = false, ExpiryDt = now }, }, + }; + Assert.Equal(null, lease.GetExpiryDate()); + } + [Fact] public void GetExpiryDate_OrigExpiryLater() { @@ -50,28 +63,28 @@ public void GetExpiryDate_OrigExpiryLater() PimsLease lease = new PimsLease() { OrigExpiryDate = later, - PimsLeasePeriods = new List() { - new PimsLeasePeriod() { PeriodExpiryDate = now }, }, + PimsLeaseRenewals = new List() { + new PimsLeaseRenewal() { IsExercised=true, ExpiryDt = now }, }, }; Assert.Equal(later, lease.GetExpiryDate()); } [Fact] - public void GetExpiryDate_TermExpiryLater() + public void GetExpiryDate_RenewalExpiryLater() { DateTime now = DateTime.Now; DateTime later = now.AddDays(1); PimsLease lease = new PimsLease() { OrigExpiryDate = now, - PimsLeasePeriods = - new List() { new PimsLeasePeriod() { PeriodExpiryDate = later } }, + PimsLeaseRenewals = + new List() { new PimsLeaseRenewal() { IsExercised = true, ExpiryDt = later } }, }; Assert.Equal(later, lease.GetExpiryDate()); } [Fact] - public void GetExpiryDate_MultipleTermExpiryLater() + public void GetExpiryDate_MultipleRenewalExpiryLater() { DateTime now = DateTime.Now; DateTime later = now.AddDays(1); @@ -79,15 +92,15 @@ public void GetExpiryDate_MultipleTermExpiryLater() PimsLease lease = new PimsLease() { OrigExpiryDate = now, - PimsLeasePeriods = - new List() { new PimsLeasePeriod() { PeriodExpiryDate = later }, - new PimsLeasePeriod() { PeriodExpiryDate = before }, }, + PimsLeaseRenewals = + new List() { new PimsLeaseRenewal() { IsExercised=true, ExpiryDt = later }, + new PimsLeaseRenewal() { IsExercised=true, ExpiryDt = before }, }, }; Assert.Equal(later, lease.GetExpiryDate()); } [Fact] - public void GetExpiryDate_TermNoExpiry() + public void GetExpiryDate_RenewalNoExpiry() { DateTime now = DateTime.Now; DateTime later = now.AddDays(1); @@ -95,11 +108,11 @@ public void GetExpiryDate_TermNoExpiry() PimsLease lease = new PimsLease() { OrigExpiryDate = now, - PimsLeasePeriods = - new List() { new PimsLeasePeriod() { PeriodExpiryDate = null }, - new PimsLeasePeriod() { PeriodExpiryDate = before }, }, + PimsLeaseRenewals = + new List() { new PimsLeaseRenewal() { IsExercised=true, ExpiryDt = null }, + new PimsLeaseRenewal() { IsExercised = false, ExpiryDt = before }, }, }; - Assert.Equal(null, lease.GetExpiryDate()); + Assert.Equal(now, lease.GetExpiryDate()); } #endregion } diff --git a/source/frontend/src/features/leases/leaseUtils.ts b/source/frontend/src/features/leases/leaseUtils.ts index 06f0ee4011..a7805e98d1 100644 --- a/source/frontend/src/features/leases/leaseUtils.ts +++ b/source/frontend/src/features/leases/leaseUtils.ts @@ -1,3 +1,7 @@ +import moment from 'moment'; + +import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; +import { ApiGen_Concepts_LeaseRenewal } from '@/models/api/generated/ApiGen_Concepts_LeaseRenewal'; import { ApiGen_Concepts_LeaseTenant } from '@/models/api/generated/ApiGen_Concepts_LeaseTenant'; import { isValidId } from '@/utils'; import { formatNames } from '@/utils/personUtils'; @@ -16,6 +20,21 @@ export const getAllNames = (tenants: ApiGen_Concepts_LeaseTenant[]): string[] => return allNames; }; +export const getCalculatedExpiry = ( + lease: ApiGen_Concepts_Lease, + renewals: ApiGen_Concepts_LeaseRenewal[], +): string => { + const excercisedRenewalDates = renewals.filter(r => r.isExercised).flatMap(fr => fr.expiryDt); + + let calculatedExpiry: string | null = lease?.expiryDate ?? ''; + for (let i = 0; i < excercisedRenewalDates.length; i++) { + if (moment(excercisedRenewalDates[i]).isAfter(calculatedExpiry)) { + calculatedExpiry = excercisedRenewalDates[i]; + } + } + return calculatedExpiry; +}; + export const getSuggestedFee = (isPublicBenefit: boolean, isFinancialGain: boolean): string => { if (isPublicBenefit == null || isFinancialGain == null) return 'Unknown'; else if (isPublicBenefit && isFinancialGain) return 'Licence Administration Fee (LAF) *'; diff --git a/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx b/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx index 787b609dd4..9312286e49 100644 --- a/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx +++ b/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx @@ -9,6 +9,7 @@ import { ResetButton, SearchButton } from '@/components/common/buttons'; import { FastDatePicker, Input } from '@/components/common/form'; import { UserRegionSelectContainer } from '@/components/common/form/UserRegionSelect/UserRegionSelectContainer'; import { SelectInput } from '@/components/common/List/SelectInput'; +import { SectionField } from '@/components/common/Section/SectionField'; import { FilterBoxForm } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; import { LEASE_PROGRAM_TYPES, LEASE_STATUS_TYPES } from '@/constants/API'; @@ -107,9 +108,9 @@ export const LeaseFilter: React.FunctionComponent ( - + - + Search by: @@ -206,48 +207,45 @@ export const LeaseFilter: React.FunctionComponent - - - - Expiry date: - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/frontend/src/features/leases/list/LeaseFilter/__snapshots__/LeaseFilter.test.tsx.snap b/source/frontend/src/features/leases/list/LeaseFilter/__snapshots__/LeaseFilter.test.tsx.snap index 063e447e4c..06004a019f 100644 --- a/source/frontend/src/features/leases/list/LeaseFilter/__snapshots__/LeaseFilter.test.tsx.snap +++ b/source/frontend/src/features/leases/list/LeaseFilter/__snapshots__/LeaseFilter.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Lease Filter > matches snapshot 1`] = ` class="Toastify" />
- .c6.btn { + .c8.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -37,55 +37,55 @@ exports[`Lease Filter > matches snapshot 1`] = ` height: 3.8rem; } -.c6.btn .Button__value { +.c8.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c6.btn:hover { +.c8.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c6.btn:focus { +.c8.btn:focus { outline-width: 0.4rem; outline-style: solid; outline-offset: 1px; box-shadow: none; } -.c6.btn.btn-primary { +.c8.btn.btn-primary { border: none; } -.c6.btn.btn-secondary { +.c8.btn.btn-secondary { background: none; } -.c6.btn.btn-info { +.c8.btn.btn-info { border: none; background: none; padding-left: 0.6rem; padding-right: 0.6rem; } -.c6.btn.btn-info:hover, -.c6.btn.btn-info:active, -.c6.btn.btn-info:focus { +.c8.btn.btn-info:hover, +.c8.btn.btn-info:active, +.c8.btn.btn-info:focus { background: none; } -.c6.btn.btn-light { +.c8.btn.btn-light { border: none; } -.c6.btn.btn-dark { +.c8.btn.btn-dark { border: none; } -.c6.btn.btn-link { +.c8.btn.btn-link { font-size: 1.6rem; font-weight: 400; background: none; @@ -106,9 +106,9 @@ exports[`Lease Filter > matches snapshot 1`] = ` padding: 0; } -.c6.btn.btn-link:hover, -.c6.btn.btn-link:active, -.c6.btn.btn-link:focus { +.c8.btn.btn-link:hover, +.c8.btn.btn-link:active, +.c8.btn.btn-link:focus { -webkit-text-decoration: underline; text-decoration: underline; border: none; @@ -117,14 +117,14 @@ exports[`Lease Filter > matches snapshot 1`] = ` outline: none; } -.c6.btn.btn-link:disabled, -.c6.btn.btn-link.disabled { +.c8.btn.btn-link:disabled, +.c8.btn.btn-link.disabled { background: none; pointer-events: none; } -.c6.btn:disabled, -.c6.btn:disabled:hover { +.c8.btn:disabled, +.c8.btn:disabled:hover { box-shadow: none; -webkit-user-select: none; -moz-user-select: none; @@ -135,19 +135,19 @@ exports[`Lease Filter > matches snapshot 1`] = ` opacity: 0.65; } -.c6.Button .Button__icon { +.c8.Button .Button__icon { margin-right: 1.6rem; } -.c6.Button--icon-only:focus { +.c8.Button--icon-only:focus { outline: none; } -.c6.Button--icon-only .Button__icon { +.c8.Button--icon-only .Button__icon { margin-right: 0; } -.c3 .react-datepicker__calendar-icon { +.c5 .react-datepicker__calendar-icon { width: 2.4rem; height: 3rem; margin-top: 0.5rem; @@ -156,19 +156,19 @@ exports[`Lease Filter > matches snapshot 1`] = ` pointer-events: none; } -.c3 .react-datepicker__view-calendar-icon input { +.c5 .react-datepicker__view-calendar-icon input { padding: 0.8rem 1.2rem 0.8rem 1.2rem; } -.c3 .react-datepicker-wrapper { +.c5 .react-datepicker-wrapper { max-width: 17rem; } -.c4.c4.form-control.is-valid { +.c6.c6.form-control.is-valid { background-image: none; } -.c4.c4.form-control.is-invalid { +.c6.c6.form-control.is-invalid { border-color: #d8292f !important; } @@ -194,7 +194,19 @@ exports[`Lease Filter > matches snapshot 1`] = ` border-radius: 0.5rem; } -.c5 { +.c4.required::before { + content: '*'; + position: absolute; + top: 0.75rem; + left: 0rem; +} + +.c3 { + font-weight: bold; + white-space: nowrap; +} + +.c7 { border-left: 2px solid white; } @@ -206,13 +218,13 @@ exports[`Lease Filter > matches snapshot 1`] = ` class="row" >
Search by: @@ -421,20 +433,22 @@ exports[`Lease Filter > matches snapshot 1`] = `
- + +
matches snapshot 1`] = ` class="col" >
matches snapshot 1`] = ` matches snapshot 1`] = ` class="col" >
matches snapshot 1`] = ` matches snapshot 1`] = `
+
+
+
+
+
@@ -554,10 +579,10 @@ exports[`Lease Filter > matches snapshot 1`] = ` class="col" >
matches snapshot 1`] = `
matches snapshot 1`] = `
matches snapshot 1`] = ` class="pr-0 col-auto" >