From ccef4f2a23747c78d39651ef578bca6f3fbc21b5 Mon Sep 17 00:00:00 2001 From: Herrera Date: Wed, 15 Apr 2026 22:33:28 -0700 Subject: [PATCH 1/3] PSP-11373 : Correct PIMS file/project access --- .../Models/AcquisitionFilterModel.cs | 7 + .../Models/DispositionFilterModel.cs | 26 +- .../Areas/Leases/Models/LeaseFilterModel.cs | 47 ++-- .../Models/ManagementFilterModel.cs | 7 + .../Projects/Models/ProjectFilterModel.cs | 8 +- .../api/Services/AcquisitionFileService.cs | 9 +- .../api/Services/LeaseReportsService.cs | 15 +- source/backend/api/Services/LeaseService.cs | 7 +- .../api/Services/ManagementFileService.cs | 10 +- source/backend/api/Services/ProjectService.cs | 9 +- .../core/Extensions/DictionaryExtensions.cs | 12 + .../Repositories/AcquisitionFileRepository.cs | 28 +- .../Repositories/DispositionFileRepository.cs | 6 + .../Interfaces/IAcquisitionFileRepository.cs | 4 +- .../Interfaces/ILeaseRepository.cs | 4 +- .../Interfaces/IManagementFileRepository.cs | 2 +- .../Interfaces/IProjectRepository.cs | 2 +- .../dal/Repositories/LeaseRepository.cs | 30 +-- .../Repositories/ManagementFileRepository.cs | 16 +- .../dal/Repositories/ProjectRepository.cs | 18 +- .../entities/Models/AcquisitionFilter.cs | 7 + .../entities/Models/DispositionFilter.cs | 7 + source/backend/entities/Models/LeaseFilter.cs | 31 +-- .../entities/Models/ManagementFilter.cs | 8 + .../backend/entities/Models/ProjectFilter.cs | 16 +- .../entities/Partials/LeaseStatusType.cs | 10 - .../AcquisitionFilter/AcquisitionFilter.tsx | 86 +++--- .../acquisition/list/AcquisitionListView.tsx | 77 +++++- .../features/acquisition/list/interfaces.ts | 35 +-- .../DispositionFilter/DispositionFilter.tsx | 55 ++-- .../disposition/list/DispositionListView.tsx | 79 ++++-- .../src/features/disposition/list/models.ts | 10 + .../src/features/leases/interfaces.ts | 2 +- .../leases/list/LeaseFilter/LeaseFilter.tsx | 246 ++++++------------ .../LeaseFilter/models/LeaseFilterModel.ts | 93 +++++++ .../features/leases/list/LeaseListView.tsx | 109 +++++++- .../ManagementFilter/ManagementFilter.tsx | 74 +++--- .../management/list/ManagementListView.tsx | 79 ++++-- .../src/features/management/list/models.ts | 10 + .../src/features/projects/interfaces.ts | 1 + .../list/ProjectFilter/ProjectFilter.tsx | 49 ++-- .../models/ProjectFilterModel.ts | 35 +++ .../projects/list/ProjectListView.tsx | 47 +++- .../src/interfaces/MultiSelectOption.ts | 4 + .../src/models/api/DispositionFilter.ts | 1 + .../src/models/api/ManagementFilter.ts | 1 + source/frontend/src/utils/personUtils.ts | 19 ++ 47 files changed, 921 insertions(+), 537 deletions(-) create mode 100644 source/frontend/src/features/leases/list/LeaseFilter/models/LeaseFilterModel.ts create mode 100644 source/frontend/src/features/projects/list/ProjectFilter/models/ProjectFilterModel.ts create mode 100644 source/frontend/src/interfaces/MultiSelectOption.ts diff --git a/source/backend/api/Areas/Acquisition/Models/AcquisitionFilterModel.cs b/source/backend/api/Areas/Acquisition/Models/AcquisitionFilterModel.cs index 1d7427cd1d..12523295b8 100644 --- a/source/backend/api/Areas/Acquisition/Models/AcquisitionFilterModel.cs +++ b/source/backend/api/Areas/Acquisition/Models/AcquisitionFilterModel.cs @@ -59,6 +59,11 @@ public class AcquisitionFilterModel : PageFilter /// public bool HasNoticeOfClaim { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors @@ -90,6 +95,7 @@ public AcquisitionFilterModel(Dictionary public long? TeamMemberOrganizationId { get; set; } + + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors @@ -74,15 +80,16 @@ public DispositionFilterModel(Dictionary(query, StringComparer.OrdinalIgnoreCase); - this.Pid = filter.GetStringValue(nameof(this.Pid)); - this.Pin = filter.GetStringValue(nameof(this.Pin)); - this.Address = filter.GetStringValue(nameof(this.Address)); - this.FileNameOrNumberOrReference = filter.GetStringValue(nameof(this.FileNameOrNumberOrReference)); - this.DispositionFileStatusCode = filter.GetStringValue(nameof(this.DispositionFileStatusCode)); - this.DispositionStatusCode = filter.GetStringValue(nameof(this.DispositionStatusCode)); - this.DispositionTypeCode = filter.GetStringValue(nameof(this.DispositionTypeCode)); - this.TeamMemberPersonId = filter.GetLongNullValue(nameof(this.TeamMemberPersonId)); - this.TeamMemberOrganizationId = filter.GetLongNullValue(nameof(this.TeamMemberOrganizationId)); + Pid = filter.GetStringValue(nameof(this.Pid)); + Pin = filter.GetStringValue(nameof(this.Pin)); + Address = filter.GetStringValue(nameof(this.Address)); + FileNameOrNumberOrReference = filter.GetStringValue(nameof(this.FileNameOrNumberOrReference)); + DispositionFileStatusCode = filter.GetStringValue(nameof(this.DispositionFileStatusCode)); + DispositionStatusCode = filter.GetStringValue(nameof(this.DispositionStatusCode)); + DispositionTypeCode = filter.GetStringValue(nameof(this.DispositionTypeCode)); + TeamMemberPersonId = filter.GetLongNullValue(nameof(this.TeamMemberPersonId)); + TeamMemberOrganizationId = filter.GetLongNullValue(nameof(this.TeamMemberOrganizationId)); + Regions = filter.GetIntArrayValue(nameof(Regions)); this.Sort = filter.GetStringArrayValue(nameof(this.Sort)); } @@ -110,6 +117,7 @@ public static explicit operator DispositionFilter(DispositionFilterModel model) DispositionTypeCode = model.DispositionTypeCode, TeamMemberPersonId = model.TeamMemberPersonId, TeamMemberOrganizationId = model.TeamMemberOrganizationId, + Regions = model.Regions, Sort = model.Sort, }; diff --git a/source/backend/api/Areas/Leases/Models/LeaseFilterModel.cs b/source/backend/api/Areas/Leases/Models/LeaseFilterModel.cs index ea59d1545c..1e84f85d36 100644 --- a/source/backend/api/Areas/Leases/Models/LeaseFilterModel.cs +++ b/source/backend/api/Areas/Leases/Models/LeaseFilterModel.cs @@ -59,11 +59,6 @@ public class LeaseFilterModel : PageFilter /// public DateOnly? ExpiryEndDate { get; set; } - /// - /// get/set - The region type. - /// - public int? RegionType { get; set; } - /// /// get/set - Filter for additional lease details. /// @@ -83,6 +78,12 @@ public class LeaseFilterModel : PageFilter /// get/set - Filter to return only receivable leases. /// public bool? IsReceivable { get; set; } + + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors @@ -104,22 +105,23 @@ public LeaseFilterModel(Dictionary(query, StringComparer.OrdinalIgnoreCase); - this.Pid = filter.GetStringValue(nameof(this.Pid)); - this.Pin = filter.GetStringValue(nameof(this.Pin)); - this.LFileNo = filter.GetStringValue(nameof(this.LFileNo)); - this.Address = filter.GetStringValue(nameof(this.Address)); - this.Historical = filter.GetStringValue(nameof(this.Historical)); - this.LeaseStatusTypes = filter.GetStringArrayValue(nameof(this.LeaseStatusTypes)); - this.TenantName = filter.GetStringValue(nameof(this.TenantName)); - this.Programs = filter.GetStringArrayValue(nameof(this.Programs)); - this.ExpiryStartDate = filter.GetDateOnlyNullValue(nameof(this.ExpiryStartDate)); - this.ExpiryEndDate = filter.GetDateOnlyNullValue(nameof(this.ExpiryEndDate)); - this.RegionType = filter.GetIntNullValue(nameof(this.RegionType)); - this.Details = filter.GetStringValue(nameof(this.Details)); - this.LeaseTeamPersonId = filter.GetIntNullValue(nameof(this.LeaseTeamPersonId)); - this.LeaseTeamOrganizationId = filter.GetIntNullValue(nameof(this.LeaseTeamOrganizationId)); - this.IsReceivable = filter.GetValue(nameof(this.IsReceivable)); - this.Sort = filter.GetStringArrayValue(nameof(this.Sort)); + Pid = filter.GetStringValue(nameof(this.Pid)); + Pin = filter.GetStringValue(nameof(this.Pin)); + LFileNo = filter.GetStringValue(nameof(this.LFileNo)); + Address = filter.GetStringValue(nameof(this.Address)); + Historical = filter.GetStringValue(nameof(this.Historical)); + LeaseStatusTypes = filter.GetStringArrayValue(nameof(this.LeaseStatusTypes)); + TenantName = filter.GetStringValue(nameof(this.TenantName)); + Programs = filter.GetStringArrayValue(nameof(this.Programs)); + ExpiryStartDate = filter.GetDateOnlyNullValue(nameof(this.ExpiryStartDate)); + ExpiryEndDate = filter.GetDateOnlyNullValue(nameof(this.ExpiryEndDate)); + Details = filter.GetStringValue(nameof(this.Details)); + LeaseTeamPersonId = filter.GetIntNullValue(nameof(this.LeaseTeamPersonId)); + LeaseTeamOrganizationId = filter.GetIntNullValue(nameof(this.LeaseTeamOrganizationId)); + IsReceivable = filter.GetValue(nameof(this.IsReceivable)); + Regions = filter.GetShortArrayValue(nameof(Regions)); + + Sort = filter.GetStringArrayValue(nameof(this.Sort)); } #endregion @@ -146,11 +148,11 @@ public static explicit operator LeaseFilter(LeaseFilterModel model) Programs = model.Programs, ExpiryStartDate = model.ExpiryStartDate, ExpiryEndDate = model.ExpiryEndDate, - RegionType = model.RegionType, Details = model.Details?.Trim(), LeaseTeamOrganizationId = model.LeaseTeamOrganizationId, LeaseTeamPersonId = model.LeaseTeamPersonId, IsReceivable = model.IsReceivable, + Regions = model.Regions, Sort = model.Sort, }; @@ -180,7 +182,6 @@ public override bool IsValid() || (Programs.Count != 0) || ExpiryStartDate.HasValue || ExpiryEndDate.HasValue - || RegionType.HasValue || LeaseTeamPersonId.HasValue || LeaseTeamOrganizationId.HasValue || IsReceivable.HasValue diff --git a/source/backend/api/Areas/Management/Models/ManagementFilterModel.cs b/source/backend/api/Areas/Management/Models/ManagementFilterModel.cs index dc13226533..7a4904874e 100644 --- a/source/backend/api/Areas/Management/Models/ManagementFilterModel.cs +++ b/source/backend/api/Areas/Management/Models/ManagementFilterModel.cs @@ -69,6 +69,11 @@ public class ManagementFilterModel : PageFilter /// public bool HasNoticeOfClaim { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors @@ -101,6 +106,7 @@ public ManagementFilterModel(Dictionary + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); public static explicit operator ProjectFilter(ProjectFilterModel model) { @@ -26,7 +30,7 @@ public static explicit operator ProjectFilter(ProjectFilterModel model) ProjectNumber = model.ProjectNumber?.Trim(), ProjectName = model.ProjectName?.Trim(), ProjectStatusCode = model.ProjectStatusCode, - ProjectRegionCode = model.ProjectRegionCode, + Regions = model.Regions, Sort = model.Sort, }; diff --git a/source/backend/api/Services/AcquisitionFileService.cs b/source/backend/api/Services/AcquisitionFileService.cs index 58223d3821..2628bb1108 100644 --- a/source/backend/api/Services/AcquisitionFileService.cs +++ b/source/backend/api/Services/AcquisitionFileService.cs @@ -93,12 +93,10 @@ public Paged GetPage(AcquisitionFilter filter) _user.ThrowIfNotAuthorized(Permissions.AcquisitionFileView); - // Limit search results to user's assigned region(s) var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); - var userRegions = pimsUser.PimsRegionUsers.Select(r => r.RegionCode).ToHashSet(); long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; - return _acqFileRepository.GetPageDeep(filter, userRegions, contractorPersonId); + return _acqFileRepository.GetPageDeep(filter, contractorPersonId); } public List GetAcquisitionFileExport(AcquisitionFilter filter) @@ -106,7 +104,6 @@ public List GetAcquisitionFileExport(AcquisitionFilt _logger.LogInformation("Searching all Acquisition Files matching the filter: {filter}", filter); _user.ThrowIfNotAuthorized(Permissions.AcquisitionFileView); - // Limit search results to user's assigned region(s) var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); var userRegions = pimsUser.PimsRegionUsers.Select(r => r.RegionCode).ToHashSet(); long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; @@ -670,12 +667,10 @@ public List GetAcquisitionSubFiles(long id) throw new BadRequestException("Acquisition file should not be a sub-file."); } - // Limit search results to user's assigned region(s) var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); - var userRegions = pimsUser.PimsRegionUsers.Select(r => r.RegionCode).ToHashSet(); long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; - return _acqFileRepository.GetAcquisitionSubFiles(id, userRegions, contractorPersonId); + return _acqFileRepository.GetAcquisitionSubFiles(id, contractorPersonId); } private void CheckFileNumberDuplicate(PimsAcquisitionFile acquisitionFile) diff --git a/source/backend/api/Services/LeaseReportsService.cs b/source/backend/api/Services/LeaseReportsService.cs index f41af9765e..d24c5c8ada 100644 --- a/source/backend/api/Services/LeaseReportsService.cs +++ b/source/backend/api/Services/LeaseReportsService.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; +using Pims.Api.Models.CodeTypes; using Pims.Core.Extensions; using Pims.Core.Security; using Pims.Dal.Entities; using Pims.Dal.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; using static Pims.Dal.Entities.PimsLeaseStatusType; namespace Pims.Api.Services @@ -40,9 +41,9 @@ public IEnumerable GetAggregatedLeaseReport(int fiscalYearStart) { ExpiryAfterDate = fiscalYearStartDate, StartBeforeDate = fiscalYearStartDate.AddYears(1).AddDays(-1), - NotInStatus = new List() { PimsLeaseStatusTypes.DRAFT, PimsLeaseStatusTypes.DISCARD, PimsLeaseStatusTypes.DUPLICATE }, + NotInStatus = new List() { LeaseStatusTypes.DRAFT.ToString(), LeaseStatusTypes.DISCARD.ToString(), LeaseStatusTypes.DUPLICATE.ToString() }, IsReceivable = true, - }, pimsUser.PimsRegionUsers.Select(u => u.RegionCode).ToHashSet(), + }, true, contractorPersonId); } @@ -60,7 +61,7 @@ public IEnumerable GetLeasePaymentsReport(int fiscalYearStart) var allPayments = _leasePaymentRepository.GetAllByDateRange(fiscalYearStartDate, fiscalYearEndDate, contractorPersonId).ToList(); var leaseIds = allPayments.Select(payment => payment.LeasePeriod.LeaseId); - var activeLeases = _leaseService.GetAllByIds(leaseIds).Where(l => l.LeaseStatusTypeCode != PimsLeaseStatusTypes.DUPLICATE && l.LeaseStatusTypeCode != PimsLeaseStatusTypes.DRAFT && l.LeaseStatusTypeCode != PimsLeaseStatusTypes.DISCARD).ToList(); + var activeLeases = _leaseService.GetAllByIds(leaseIds).Where(l => l.LeaseStatusTypeCode != LeaseStatusTypes.DUPLICATE.ToString() && l.LeaseStatusTypeCode != LeaseStatusTypes.DRAFT.ToString() && l.LeaseStatusTypeCode != LeaseStatusTypes.DISCARD.ToString()).ToList(); var activePayments = allPayments.Where(payment => activeLeases.Any(lease => lease.LeaseId == payment.LeasePeriod.LeaseId)).ToList(); // Required to display the latest payment on the lease, which may not be part of the current date range filter of payments. This ensures that all payments for a lease associated to one of the payments in the date range are included. diff --git a/source/backend/api/Services/LeaseService.cs b/source/backend/api/Services/LeaseService.cs index bdf590996d..333d736393 100644 --- a/source/backend/api/Services/LeaseService.cs +++ b/source/backend/api/Services/LeaseService.cs @@ -134,13 +134,14 @@ public Paged GetPage(LeaseFilter filter, bool? all = false) { _logger.LogInformation("Getting lease page {filter}", filter); _user.ThrowIfNotAuthorized(Permissions.LeaseView); + filter.Page = all.HasValue && all.Value ? 1 : filter.Page; filter.Quantity = all.HasValue && all.Value ? _leaseRepository.Count() : filter.Quantity; + var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; - var leases = _leaseRepository.GetPage(filter, pimsUser.PimsRegionUsers.Select(u => u.RegionCode).ToHashSet(), contractorPersonId); - return leases; + return _leaseRepository.GetPage(filter, contractorPersonId); } public IEnumerable GetInsuranceByLeaseId(long leaseId) @@ -556,7 +557,7 @@ private static void ValidateLeaseAccountTypeChange(PimsLease currentLease, PimsL private static void ValidateRenewalDates(PimsLease lease, PimsLease currentLease, IEnumerable userOverrides) { - if (lease.LeaseStatusTypeCode != PimsLeaseStatusTypes.ACTIVE) + if (lease.LeaseStatusTypeCode != LeaseStatusTypes.ACTIVE.ToString()) { return; } diff --git a/source/backend/api/Services/ManagementFileService.cs b/source/backend/api/Services/ManagementFileService.cs index 56515de9dc..080fd1b983 100644 --- a/source/backend/api/Services/ManagementFileService.cs +++ b/source/backend/api/Services/ManagementFileService.cs @@ -21,6 +21,7 @@ public class ManagementFileService : IManagementFileService { private readonly ClaimsPrincipal _user; private readonly ILogger _logger; + private readonly IUserRepository _userRepository; private readonly IManagementFileRepository _managementFileRepository; private readonly IManagementFilePropertyRepository _managementFilePropertyRepository; private readonly IPropertyRepository _propertyRepository; @@ -44,7 +45,8 @@ public ManagementFileService( IManagementFileStatusSolver managementStatusSolver, IPropertyOperationService propertyOperationService, IManagementActivityRepository managementActivityRepository, - IFilePropertyLocationUpdateSolver propertyLocationSolver) + IFilePropertyLocationUpdateSolver propertyLocationSolver, + IUserRepository userRepository) { _user = user; _logger = logger; @@ -58,6 +60,7 @@ public ManagementFileService( _propertyOperationService = propertyOperationService; _managementActivityRepository = managementActivityRepository; _propertyLocationSolver = propertyLocationSolver; + _userRepository = userRepository; } public PimsManagementFile Add(PimsManagementFile managementFile, IEnumerable userOverrides) @@ -288,7 +291,10 @@ public Paged GetPage(ManagementFilter filter) _logger.LogDebug("Management file search with filter: {filter}", filter); _user.ThrowIfNotAuthorized(Permissions.ManagementView); - return _managementFileRepository.GetPageDeep(filter); + var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); + long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; + + return _managementFileRepository.GetPageDeep(filter, contractorPersonId); } public IEnumerable GetContacts(long id) diff --git a/source/backend/api/Services/ProjectService.cs b/source/backend/api/Services/ProjectService.cs index afb8e90338..de6f3ef61d 100644 --- a/source/backend/api/Services/ProjectService.cs +++ b/source/backend/api/Services/ProjectService.cs @@ -90,9 +90,8 @@ public Task> GetPage(ProjectFilter filter) _logger.LogInformation("Searching for projects ..."); _logger.LogDebug("Project search with filter", filter); - // Limit search results to user's assigned region(s), but always include "Cannot determine" region var pimsUser = _userRepository.GetUserInfoByKeycloakUserId(_user.GetUserKey()); - var userRegions = pimsUser.PimsRegionUsers.Select(r => r.RegionCode).ToHashSet(); + long? contractorPersonId = pimsUser.IsContractor ? pimsUser.PersonId : null; filter.ThrowIfNull(nameof(filter)); if (!filter.IsValid()) @@ -100,7 +99,7 @@ public Task> GetPage(ProjectFilter filter) throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - return GetPageAsync(filter, userRegions); + return GetPageAsync(filter, contractorPersonId); } public PimsProject GetById(long projectId) @@ -258,9 +257,9 @@ private List MatchProducts(PimsProject project) return externalProducts; } - private async Task> GetPageAsync(ProjectFilter filter, IEnumerable userRegions) + private async Task> GetPageAsync(ProjectFilter filter, long? contractorPersonId = null) { - return await _projectRepository.GetPageAsync(filter, userRegions); + return await _projectRepository.GetPageAsync(filter, contractorPersonId); } private void AddNoteIfStatusChanged(PimsProject updatedProject) diff --git a/source/backend/core/Extensions/DictionaryExtensions.cs b/source/backend/core/Extensions/DictionaryExtensions.cs index 2690f44820..cb0e768722 100644 --- a/source/backend/core/Extensions/DictionaryExtensions.cs +++ b/source/backend/core/Extensions/DictionaryExtensions.cs @@ -34,6 +34,18 @@ public static short GetShortValue(this IDictionary + /// Get the value from the dictionary for the specified 'key' and return it as an array of short. + /// + /// + /// + /// + /// + public static short[] GetShortArrayValue(this IDictionary dict, string key, string separator = ",") + { + return dict.TryGetValue(key, out Microsoft.Extensions.Primitives.StringValues value) ? value.ToString().Split(separator).Select(v => { return short.TryParse(v, out short iv) ? (short?)iv : null; }).Where(v => v != null).Select(v => (short)v).ToArray() : Array.Empty(); + } + /// /// Get the value from the dictionary for the specified 'key' and return it as an int. /// diff --git a/source/backend/dal/Repositories/AcquisitionFileRepository.cs b/source/backend/dal/Repositories/AcquisitionFileRepository.cs index 66b14d52d5..a789a29277 100644 --- a/source/backend/dal/Repositories/AcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/AcquisitionFileRepository.cs @@ -47,7 +47,7 @@ public AcquisitionFileRepository(PimsContext dbContext, ClaimsPrincipal user, IL /// /// /// - public Paged GetPageDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null) + public Paged GetPageDeep(AcquisitionFilter filter, long? contractorPersonId = null) { // RECOMMENDED - use a log scope to group all potential SQL statements generated by EF for this method call using var scope = Logger.QueryScope(); @@ -58,7 +58,7 @@ public Paged GetPageDeep(AcquisitionFilter filter, HashSet< throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - IQueryable query = GetCommonAcquisitionFileQueryDeep(filter, regions, contractorPersonId); + IQueryable query = GetCommonAcquisitionFileQueryDeep(filter, contractorPersonId); var skip = (filter.Page - 1) * filter.Quantity; var pageItems = query.Skip(skip).Take(filter.Quantity).ToList(); @@ -84,7 +84,7 @@ public List GetAcquisitionFileExportDeep(AcquisitionFilter throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - return GetCommonAcquisitionFileQueryDeep(filter, regions, contractorPersonId).ToList(); + return GetCommonAcquisitionFileQueryDeep(filter, contractorPersonId).ToList(); } /// @@ -737,8 +737,7 @@ public PimsAcquisitionFile Update(PimsAcquisitionFile acquisitionFile) // PSP-9268 Changes to Project/Product on the main file need to be propagated to all sub-files if (existingAcqFile.ProjectId != acquisitionFile.ProjectId || existingAcqFile.ProductId != acquisitionFile.ProductId) { - var allRegions = Context.PimsRegions.AsNoTracking().Select(r => r.RegionCode).ToHashSet(); - var subFiles = GetAcquisitionSubFiles(existingAcqFile.Internal_Id, allRegions); + var subFiles = GetAcquisitionSubFiles(existingAcqFile.Internal_Id); foreach (var subFile in subFiles) { subFile.ProjectId = acquisitionFile.ProjectId; @@ -811,14 +810,12 @@ public PimsProperty GetProperty(long acquisitionFilePropertyId) .FirstOrDefault(); } - public List GetAcquisitionSubFiles(long acquisitionFileId, HashSet regions, long? contractorPersonId = null) + public List GetAcquisitionSubFiles(long acquisitionFileId, long? contractorPersonId = null) { var predicate = PredicateBuilder.New(acq => true); predicate.And(acq => acq.PrntAcquisitionFileId == acquisitionFileId); - predicate = predicate.And(acq => regions.Contains(acq.RegionCode)); - if (contractorPersonId is not null) { predicate = predicate.And(acq => acq.PimsAcquisitionFileTeams.Any(x => x.PersonId == contractorPersonId)); @@ -876,11 +873,8 @@ private int GetNextAcquisitionFileNumberSequenceValue() private short GetNextSubFileSuffixValue(long parentAcquisitionFileId) { - // To determine the next suffix number we need to grab all sub-files (regardless of any region restriction) - var allRegions = Context.PimsRegions.AsNoTracking().Select(r => r.RegionCode).ToHashSet(); - // The suffix numbers for sub-interest files start from "02", and will increment by 1 for sub-sequent file in the order of creation. - var existingSubFiles = GetAcquisitionSubFiles(parentAcquisitionFileId, allRegions); + var existingSubFiles = GetAcquisitionSubFiles(parentAcquisitionFileId); if (existingSubFiles.Count == 0) { return 2; @@ -897,10 +891,9 @@ private short GetNextSubFileSuffixValue(long parentAcquisitionFileId) /// Generate a common IQueryable for Acquisition Files. /// /// - /// /// /// - private IQueryable GetCommonAcquisitionFileQueryDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null) + private IQueryable GetCommonAcquisitionFileQueryDeep(AcquisitionFilter filter, long? contractorPersonId = null) { var predicate = PredicateBuilder.New(acq => true); @@ -966,8 +959,6 @@ private IQueryable GetCommonAcquisitionFileQueryDeep(Acquis predicate = predicate.And(ownerBuilder); } - predicate = predicate.And(acq => regions.Contains(acq.RegionCode) || acq.RegionCode == 4); - if (contractorPersonId is not null) { predicate = predicate.And(acq => acq.PimsAcquisitionFileTeams.Any(x => x.PersonId == contractorPersonId) || (acq.Project != null && acq.Project.PimsProjectPeople.Any(x => x.PersonId == contractorPersonId))); @@ -988,6 +979,11 @@ private IQueryable GetCommonAcquisitionFileQueryDeep(Acquis predicate = predicate.And(acq => acq.PimsNoticeOfClaims.Any(x => x.ReceivedDt != null || x.Comment != null)); } + if (filter.Regions.Any()) + { + predicate = predicate.And(acq => filter.Regions.Any(r => r == acq.RegionCode)); + } + var query = Context.PimsAcquisitionFiles.AsNoTracking() .Include(r => r.RegionCodeNavigation) .Include(p => p.Project) diff --git a/source/backend/dal/Repositories/DispositionFileRepository.cs b/source/backend/dal/Repositories/DispositionFileRepository.cs index 0fec45c061..12700fa0bb 100644 --- a/source/backend/dal/Repositories/DispositionFileRepository.cs +++ b/source/backend/dal/Repositories/DispositionFileRepository.cs @@ -780,6 +780,7 @@ public bool TryDeleteAgreement(long dispositionFileId, long agreementId) private IQueryable GetCommonDispositionFileQueryDeep(DispositionFilter filter, long? contractorPersonId = null) { filter.FileNameOrNumberOrReference = Regex.Replace(filter.FileNameOrNumberOrReference ?? string.Empty, @"^[d,D]-", string.Empty); + var predicate = PredicateBuilder.New(disp => true); if (!string.IsNullOrWhiteSpace(filter.Pid)) { @@ -841,6 +842,11 @@ private IQueryable GetCommonDispositionFileQueryDeep(Dispos predicate = predicate.And(disp => disp.PimsDispositionFileTeams.Any(x => x.OrganizationId == filter.TeamMemberOrganizationId.Value)); } + if (filter.Regions.Any()) + { + predicate = predicate.And(x => filter.Regions.Any(r => r == x.RegionCode)); + } + var query = Context.PimsDispositionFiles.AsNoTracking() .Include(d => d.RegionCodeNavigation) .Include(d => d.DspPhysFileStatusTypeCodeNavigation) diff --git a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs index f57f788418..14f766b01a 100644 --- a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs @@ -7,7 +7,7 @@ namespace Pims.Dal.Repositories { public interface IAcquisitionFileRepository : IRepository { - Paged GetPageDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null); + Paged GetPageDeep(AcquisitionFilter filter, long? contractorPersonId = null); PimsAcquisitionFile GetById(long id); @@ -37,7 +37,7 @@ public interface IAcquisitionFileRepository : IRepository List GetAcquisitionFileExportDeep(AcquisitionFilter filter, HashSet regions, long? contractorPersonId = null); - List GetAcquisitionSubFiles(long acquisitionFileId, HashSet regions, long? contractorPersonId = null); + List GetAcquisitionSubFiles(long acquisitionFileId, long? contractorPersonId = null); PimsAcquisitionFile GetAcquisitionAtTime(long acquisitionFileId, DateTime time); } diff --git a/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs b/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs index 25f5ae4c14..6f9ea4a649 100644 --- a/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs @@ -12,7 +12,7 @@ public interface ILeaseRepository : IRepository { int Count(); - IEnumerable GetAllByFilter(LeaseFilter filter, HashSet regionCodes, bool loadPayments = false, long? contractorPersonId = null); + IEnumerable GetAllByFilter(LeaseFilter filter, bool loadPayments = false, long? contractorPersonId = null); long GetRowVersion(long id); @@ -24,7 +24,7 @@ public interface ILeaseRepository : IRepository LastUpdatedByModel GetLastUpdateBy(long leaseId); - Paged GetPage(LeaseFilter filter, HashSet regions, long? contractorPersonId = null); + Paged GetPage(LeaseFilter filter, long? contractorPersonId = null); PimsLease Add(PimsLease lease); diff --git a/source/backend/dal/Repositories/Interfaces/IManagementFileRepository.cs b/source/backend/dal/Repositories/Interfaces/IManagementFileRepository.cs index 049516625d..e7eaaa1f26 100644 --- a/source/backend/dal/Repositories/Interfaces/IManagementFileRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IManagementFileRepository.cs @@ -20,7 +20,7 @@ public interface IManagementFileRepository : IRepository long GetRowVersion(long id); - Paged GetPageDeep(ManagementFilter filter); + Paged GetPageDeep(ManagementFilter filter, long? contractorPersonId = null); List GetContacts(long managementFileId); diff --git a/source/backend/dal/Repositories/Interfaces/IProjectRepository.cs b/source/backend/dal/Repositories/Interfaces/IProjectRepository.cs index 6d77e5d789..3e21042bec 100644 --- a/source/backend/dal/Repositories/Interfaces/IProjectRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IProjectRepository.cs @@ -11,7 +11,7 @@ namespace Pims.Dal.Repositories /// public interface IProjectRepository : IRepository { - Task> GetPageAsync(ProjectFilter filter, IEnumerable userRegions); + Task> GetPageAsync(ProjectFilter filter, long? contractorPersonId = null); IList SearchProjects(string filter, HashSet regions, int maxResults); diff --git a/source/backend/dal/Repositories/LeaseRepository.cs b/source/backend/dal/Repositories/LeaseRepository.cs index f6734fb62a..925a0030c2 100644 --- a/source/backend/dal/Repositories/LeaseRepository.cs +++ b/source/backend/dal/Repositories/LeaseRepository.cs @@ -61,7 +61,7 @@ public int Count() /// /// /// - public IEnumerable GetAllByFilter(LeaseFilter filter, HashSet regionCodes, bool loadPayments = false, long? contractorPersonId = null) + public IEnumerable GetAllByFilter(LeaseFilter filter, bool loadPayments = false, long? contractorPersonId = null) { this.User.ThrowIfNotAuthorized(Permissions.LeaseView); filter.ThrowIfNull(nameof(filter)); @@ -70,7 +70,7 @@ public IEnumerable GetAllByFilter(LeaseFilter filter, HashSet throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - var query = GenerateLeaseQuery(filter, regionCodes, loadPayments, contractorPersonId); + var query = GenerateLeaseQuery(filter, loadPayments, contractorPersonId); // Getting all by the filter will ignore the order by passed and instead use the lease id. var leases = query.OrderBy(l => l.LeaseId).ToArray(); @@ -785,20 +785,21 @@ public PimsLease GetNoTracking(long id) /// Note that the 'leaseFilter' will control the 'page' and 'quantity'. /// /// - /// /// The contractor person id to filter by. Only applies if calling user is a Contractor. /// - public Paged GetPage(LeaseFilter filter, HashSet regions, long? contractorPersonId = null) + public Paged GetPage(LeaseFilter filter, long? contractorPersonId = null) { - this.User.ThrowIfNotAuthorized(Permissions.LeaseView); + User.ThrowIfNotAuthorized(Permissions.LeaseView); filter.ThrowIfNull(nameof(filter)); + if (!filter.IsValid()) { throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } var skip = (filter.Page - 1) * filter.Quantity; - var query = GenerateLeaseQuery(filter, regions, contractorPersonId: contractorPersonId); + var query = GenerateLeaseQuery(filter, contractorPersonId: contractorPersonId); + var items = query .Skip(skip) .Take(filter.Quantity) @@ -890,7 +891,7 @@ public PimsLease UpdateLeaseRenewals(long leaseId, long? rowVersion, ICollection /// /// The contractor person id to filter by. Only applies if calling user is a Contractor. /// - public IQueryable GenerateLeaseQuery(LeaseFilter filter, HashSet regionCodes, bool loadPayments = false, long? contractorPersonId = null) + public IQueryable GenerateLeaseQuery(LeaseFilter filter, bool loadPayments = false, long? contractorPersonId = null) { filter.ThrowIfNull(nameof(filter)); @@ -924,7 +925,7 @@ public IQueryable GenerateLeaseQuery(LeaseFilter filter, HashSet l.PimsLeasePayments); } - var predicate = GenerateCommonLeaseQuery(filter, regionCodes, contractorPersonId); + var predicate = GenerateCommonLeaseQuery(filter, contractorPersonId); query = query.Where(predicate); if (filter.Sort?.Length > 0) @@ -1094,17 +1095,14 @@ private static string NormalizeLFileNo(string input) /// Generate an SQL statement for the specified 'region' and 'filter'. /// /// - /// /// The contractor person id to filter by. Only applies if calling user is a Contractor. /// - private static ExpressionStarter GenerateCommonLeaseQuery(LeaseFilter filter, HashSet regions, long? contractorPersonId = null) + private static ExpressionStarter GenerateCommonLeaseQuery(LeaseFilter filter, long? contractorPersonId = null) { filter.ThrowIfNull(nameof(filter)); var predicateBuilder = PredicateBuilder.New(l => true); - predicateBuilder = predicateBuilder.And(l => !l.RegionCode.HasValue || regions.Contains(l.RegionCode.Value)); - // Enforce contractor access to only their leases if (contractorPersonId is not null) { @@ -1222,14 +1220,14 @@ private static ExpressionStarter GenerateCommonLeaseQuery(LeaseFilter l.OrigExpiryDate <= expiryEndDate); } - if (filter.RegionType.HasValue) + if (!string.IsNullOrWhiteSpace(filter.Details)) { - predicateBuilder = predicateBuilder.And(l => l.RegionCode == filter.RegionType); + predicateBuilder = predicateBuilder.And(l => EF.Functions.Like(l.LeaseDescription, $"%{filter.Details}%") || EF.Functions.Like(l.LeaseNotes, $"%{filter.Details}%")); } - if (!string.IsNullOrWhiteSpace(filter.Details)) + if (filter.Regions.Any()) { - predicateBuilder = predicateBuilder.And(l => EF.Functions.Like(l.LeaseDescription, $"%{filter.Details}%") || EF.Functions.Like(l.LeaseNotes, $"%{filter.Details}%")); + predicateBuilder = predicateBuilder.And(x => x.RegionCode != null && filter.Regions.Any(r => r == x.RegionCode)); } return predicateBuilder; diff --git a/source/backend/dal/Repositories/ManagementFileRepository.cs b/source/backend/dal/Repositories/ManagementFileRepository.cs index 0a8d7704e8..dde4f1f5b7 100644 --- a/source/backend/dal/Repositories/ManagementFileRepository.cs +++ b/source/backend/dal/Repositories/ManagementFileRepository.cs @@ -388,7 +388,7 @@ public void DeleteContact(long managementFileId, long contactId) /// /// /// - public Paged GetPageDeep(ManagementFilter filter) + public Paged GetPageDeep(ManagementFilter filter, long? contractorPersonId = null) { using var scope = Logger.QueryScope(); @@ -398,7 +398,7 @@ public Paged GetPageDeep(ManagementFilter filter) throw new ArgumentException("Argument must have a valid filter", nameof(filter)); } - var query = GetCommonManagementFileQueryDeep(filter); + var query = GetCommonManagementFileQueryDeep(filter, contractorPersonId); var skip = (filter.Page - 1) * filter.Quantity; var pageItems = query.Skip(skip).Take(filter.Quantity).ToList(); @@ -411,7 +411,7 @@ public Paged GetPageDeep(ManagementFilter filter) /// /// The filter to apply. /// - private IQueryable GetCommonManagementFileQueryDeep(ManagementFilter filter) + private IQueryable GetCommonManagementFileQueryDeep(ManagementFilter filter, long? contractorPersonId = null) { filter.FileNameOrNumberOrReference = Regex.Replace(filter.FileNameOrNumberOrReference ?? string.Empty, @"^[m,M]-", string.Empty); var predicate = PredicateBuilder.New(disp => true); @@ -480,6 +480,16 @@ private IQueryable GetCommonManagementFileQueryDeep(Manageme predicate = predicate.And(x => x.PimsNoticeOfClaims.Any(y => y.ReceivedDt != null || y.Comment != null)); } + if (filter.Regions.Any()) + { + predicate = predicate.And(x => x.RegionCode != null && filter.Regions.Any(r => r == x.RegionCode)); + } + + if (contractorPersonId is not null) + { + predicate = predicate.And(mgmt => mgmt.PimsManagementFileTeams.Any(x => x.PersonId == contractorPersonId) || (mgmt.Project != null && mgmt.Project.PimsProjectPeople.Any(x => x.PersonId == contractorPersonId))); + } + var query = this.Context.PimsManagementFiles.AsNoTracking() .Include(d => d.ManagementFileStatusTypeCodeNavigation) .Include(d => d.Project) diff --git a/source/backend/dal/Repositories/ProjectRepository.cs b/source/backend/dal/Repositories/ProjectRepository.cs index 12c995d39b..b0b540470a 100644 --- a/source/backend/dal/Repositories/ProjectRepository.cs +++ b/source/backend/dal/Repositories/ProjectRepository.cs @@ -56,7 +56,7 @@ public IList SearchProjects(string filter, HashSet regions, /// Returns a Paged Result of Projects based on ProjectFilter params. /// /// - public Task> GetPageAsync(ProjectFilter filter, IEnumerable userRegions) + public Task> GetPageAsync(ProjectFilter filter, long? contractorPersonId = null) { User.ThrowIfNotAuthorized(Permissions.ProjectView); filter.ThrowIfNull(nameof(filter)); @@ -65,7 +65,7 @@ public Task> GetPageAsync(ProjectFilter filter, IEnumerable @@ -205,10 +205,9 @@ public PimsProject GetProjectAtTime(long projectId, DateTime time) return project; } - private async Task> GetPage(ProjectFilter filter, IEnumerable userRegions) + private async Task> GetPage(ProjectFilter filter, long? contractorPersonId = null) { - var query = Context.PimsProjects.AsNoTracking() - .Where(p => userRegions.Contains(p.RegionCode)); + var query = Context.PimsProjects.AsNoTracking(); if (!string.IsNullOrWhiteSpace(filter.ProjectNumber)) { @@ -225,9 +224,14 @@ private async Task> GetPage(ProjectFilter filter, IEnumerable query = query.Where(x => x.ProjectStatusTypeCodeNavigation.ProjectStatusTypeCode == filter.ProjectStatusCode); } - if (!string.IsNullOrWhiteSpace(filter.ProjectRegionCode)) + if (contractorPersonId is not null) { - query = query.Where(x => x.RegionCode == short.Parse(filter.ProjectRegionCode)); + query = query.Where(x => x.PimsProjectPeople.Any(x => x.PersonId == contractorPersonId)); + } + + if (filter.Regions.Any()) + { + query = query.Where(x => filter.Regions.Any(r => r == x.RegionCode)); } if (filter.Sort?.Any() == true) diff --git a/source/backend/entities/Models/AcquisitionFilter.cs b/source/backend/entities/Models/AcquisitionFilter.cs index c44a55d7ee..3f7022e987 100644 --- a/source/backend/entities/Models/AcquisitionFilter.cs +++ b/source/backend/entities/Models/AcquisitionFilter.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Pims.Dal.Entities.Models { public class AcquisitionFilter : PageFilter @@ -54,6 +56,11 @@ public class AcquisitionFilter : PageFilter /// public bool HasNoticeOfClaim { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors diff --git a/source/backend/entities/Models/DispositionFilter.cs b/source/backend/entities/Models/DispositionFilter.cs index f4191280c3..59bdc758c4 100644 --- a/source/backend/entities/Models/DispositionFilter.cs +++ b/source/backend/entities/Models/DispositionFilter.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Pims.Dal.Entities.Models { public class DispositionFilter : PageFilter @@ -49,6 +51,11 @@ public class DispositionFilter : PageFilter /// public long? TeamMemberOrganizationId { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors diff --git a/source/backend/entities/Models/LeaseFilter.cs b/source/backend/entities/Models/LeaseFilter.cs index fbb85914e9..d0cb023b70 100644 --- a/source/backend/entities/Models/LeaseFilter.cs +++ b/source/backend/entities/Models/LeaseFilter.cs @@ -90,22 +90,24 @@ public class LeaseFilter : PageFilter /// public int? LeaseTeamOrganizationId { get; set; } - public LeaseFilter(string lFileNo, string tenantName, string pid, string pin, string historical, int? leaseTeamPersonId, int? leaseTeamOrganizationId, string[] sort) - { - this.LFileNo = lFileNo; - this.TenantName = tenantName; - this.Pid = pid; - this.Pin = pin; - this.Historical = historical; - this.LeaseTeamPersonId = leaseTeamPersonId; - this.LeaseTeamOrganizationId = leaseTeamOrganizationId; - this.Sort = sort; - } - /// - /// get/set - The region type. + /// get/set - The region types. /// - public int? RegionType { get; set; } + public IList Regions { get; set; } = new List(); + + public LeaseFilter(string lFileNo, string tenantName, string pid, string pin, string historical, int? leaseTeamPersonId, int? leaseTeamOrganizationId, short[] regions, string[] sort) + { + LFileNo = lFileNo; + TenantName = tenantName; + Pid = pid; + Pin = pin; + Historical = historical; + LeaseTeamPersonId = leaseTeamPersonId; + LeaseTeamOrganizationId = leaseTeamOrganizationId; + Regions = regions; + + Sort = sort; + } /// /// get/set - Filter for additional lease details. @@ -147,7 +149,6 @@ public override bool IsValid() || (Programs.Count != 0) || ExpiryStartDate.HasValue || ExpiryEndDate.HasValue - || RegionType.HasValue || !string.IsNullOrWhiteSpace(Details); } #endregion diff --git a/source/backend/entities/Models/ManagementFilter.cs b/source/backend/entities/Models/ManagementFilter.cs index 2d4a2d6819..cfb219d949 100644 --- a/source/backend/entities/Models/ManagementFilter.cs +++ b/source/backend/entities/Models/ManagementFilter.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace Pims.Dal.Entities.Models { public class ManagementFilter : PageFilter @@ -59,6 +62,11 @@ public class ManagementFilter : PageFilter /// public bool HasNoticeOfClaim { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + #endregion #region Constructors diff --git a/source/backend/entities/Models/ProjectFilter.cs b/source/backend/entities/Models/ProjectFilter.cs index c8571d5fa7..71916dc52f 100644 --- a/source/backend/entities/Models/ProjectFilter.cs +++ b/source/backend/entities/Models/ProjectFilter.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Pims.Dal.Entities.Models { public class ProjectFilter : PageFilter @@ -6,20 +8,16 @@ public ProjectFilter() { } - public ProjectFilter(string projectNumber, string projectName, string projectStatus, string projectRegion) - { - ProjectNumber = projectNumber; - ProjectName = projectName; - ProjectStatusCode = projectStatus; - ProjectRegionCode = projectRegion; - } - public string ProjectNumber { get; set; } public string ProjectName { get; set; } public string ProjectStatusCode { get; set; } - public string ProjectRegionCode { get; set; } + /// + /// get/set - The region types. + /// + public IList Regions { get; set; } = new List(); + } } diff --git a/source/backend/entities/Partials/LeaseStatusType.cs b/source/backend/entities/Partials/LeaseStatusType.cs index 04104801fc..a0ce9db748 100644 --- a/source/backend/entities/Partials/LeaseStatusType.cs +++ b/source/backend/entities/Partials/LeaseStatusType.cs @@ -32,15 +32,5 @@ public PimsLeaseStatusType() { } #endregion - - public static class PimsLeaseStatusTypes - { - public const string ACTIVE = "ACTIVE"; - public const string DISCARD = "DISCARD"; - public const string DRAFT = "DRAFT"; - public const string INACTIVE = "INACTIVE"; - public const string TERMINATED = "TERMINATED"; - public const string DUPLICATE = "DUPLICATE"; - } } } diff --git a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx index 30d14f546b..d31cbe0102 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.tsx @@ -1,28 +1,23 @@ -import { Formik, FormikHelpers, FormikProps } from 'formik'; -import React, { useMemo } from 'react'; +import { Formik, FormikHelpers } from 'formik'; +import React from 'react'; import { Col, Row } from 'react-bootstrap'; import styled from 'styled-components'; import { ResetButton, SearchButton } from '@/components/common/buttons'; -import { Check, Form, Input, Multiselect, Select } from '@/components/common/form'; +import { Check, Form, Input, Multiselect, Select, SelectOption } from '@/components/common/form'; import { SelectInput } from '@/components/common/List/SelectInput'; import { ColButtons } from '@/components/common/styles'; -import { ACQUISITION_FILE_STATUS_TYPES } from '@/constants/API'; -import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; -import { ApiGen_Concepts_AcquisitionFileTeam } from '@/models/api/generated/ApiGen_Concepts_AcquisitionFileTeam'; -import { exists, mapLookupCode } from '@/utils'; -import { formatApiPersonNames } from '@/utils/personUtils'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; -import { - AcquisitionFilterModel, - ApiGen_Concepts_AcquisitionFilter, - MultiSelectOption, -} from '../interfaces'; +import { AcquisitionFilterModel, ApiGen_Concepts_AcquisitionFilter } from '../interfaces'; export interface IAcquisitionFilterProps { - filter?: ApiGen_Concepts_AcquisitionFilter; + initialValues: AcquisitionFilterModel; + pimsRegionsOptions: MultiSelectOption[]; + acquisitionTeamOptions: MultiSelectOption[]; + acquisitionStatusOptions: SelectOption[]; setFilter: (filter: ApiGen_Concepts_AcquisitionFilter) => void; - acquisitionTeam: ApiGen_Concepts_AcquisitionFileTeam[]; + onResetFilter: () => void; } /** @@ -30,9 +25,12 @@ export interface IAcquisitionFilterProps { * @param {IAcquisitionFilterProps} props */ export const AcquisitionFilter: React.FC> = ({ - filter, + initialValues, + pimsRegionsOptions, + acquisitionTeamOptions, + acquisitionStatusOptions, setFilter, - acquisitionTeam, + onResetFilter, }) => { const onSearchSubmit = ( values: AcquisitionFilterModel, @@ -42,40 +40,10 @@ export const AcquisitionFilter: React.FC { - setFilter(new AcquisitionFilterModel().toApi()); - }; - - const onResetClick = (formikProps: FormikProps) => { - resetFilter(); - formikProps.resetForm(); - }; - - const lookupCodes = useLookupCodeHelpers(); - - const acquisitionStatusOptions = lookupCodes - .getByType(ACQUISITION_FILE_STATUS_TYPES) - .map(c => mapLookupCode(c)); - - const acquisitionTeamOptions = useMemo(() => { - if (exists(acquisitionTeam)) { - return acquisitionTeam?.map(x => ({ - id: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, - text: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', - })); - } else { - return []; - } - }, [acquisitionTeam]); - return ( enableReinitialize - initialValues={ - filter - ? AcquisitionFilterModel.fromApi(filter, acquisitionTeam || []) - : new AcquisitionFilterModel() - } + initialValues={initialValues} onSubmit={onSearchSubmit} > {formikProps => ( @@ -139,20 +107,29 @@ export const AcquisitionFilter: React.FC - + - - - + + {' '} + + + + + onResetClick(formikProps)} + onClick={() => { + formikProps.resetForm(); + onResetFilter(); + }} /> diff --git a/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx b/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx index 475c3b7589..51757891d1 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionListView.tsx @@ -1,5 +1,5 @@ import { isEmpty } from 'lodash'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; import { FaFileExcel, FaPlus } from 'react-icons/fa'; import { useHistory } from 'react-router'; @@ -11,14 +11,21 @@ import { StyledIconButton } from '@/components/common/buttons/IconButton'; import { PaddedScrollable, StyledAddButton } from '@/components/common/styles'; import * as CommonStyled from '@/components/common/styles'; import TooltipWrapper from '@/components/common/TooltipWrapper'; +import * as API from '@/constants/API'; +import { ACQUISITION_FILE_STATUS_TYPES } from '@/constants/API'; import Claims from '@/constants/claims'; import { useApiAcquisitionFile } from '@/hooks/pims-api/useApiAcquisitionFile'; import { useAcquisitionProvider } from '@/hooks/repositories/useAcquisitionProvider'; -import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { useUserInfoRepository } from '@/hooks/repositories/useUserInfoRepository'; +import useKeycloakWrapper, { IUserInfo } from '@/hooks/useKeycloakWrapper'; +import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { useSearch } from '@/hooks/useSearch'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_AcquisitionFile } from '@/models/api/generated/ApiGen_Concepts_AcquisitionFile'; import { toFilteredApiPaginateParams } from '@/utils/CommonFunctions'; -import { generateMultiSortCriteria } from '@/utils/utils'; +import { mapLookupCode } from '@/utils/mapLookupCode'; +import { formatApiPersonNames } from '@/utils/personUtils'; +import { exists, formatGuid, generateMultiSortCriteria } from '@/utils/utils'; import { useAcquisitionFileExport } from '../hooks/useAcquisitionFileExport'; import { AcquisitionFilter } from './AcquisitionFilter/AcquisitionFilter'; @@ -33,8 +40,34 @@ export const AcquisitionListView: React.FunctionComponent< React.PropsWithChildren > = () => { const history = useHistory(); + const { hasClaim, obj } = useKeycloakWrapper(); + const { sub } = obj.userInfo as IUserInfo; + const formattedGuid = formatGuid(sub); + + const lookupCodes = useLookupCodeHelpers(); + const { retrieveUserInfo, retrieveUserInfoResponse } = useUserInfoRepository(); const { getAcquisitionFiles } = useApiAcquisitionFile(); - const { hasClaim } = useKeycloakWrapper(); + const { + getAllAcquisitionFileTeamMembers: { response: team, execute: loadAcquisitionTeam }, + } = useAcquisitionProvider(); + + const pimsRegionsTypes = lookupCodes.getOptionsByType(API.REGION_TYPES); + const pimsRegionOptions: MultiSelectOption[] = pimsRegionsTypes.map(x => { + return { id: x.code as string, text: x.label }; + }); + + const acquisitionStatusOptions = lookupCodes + .getByType(ACQUISITION_FILE_STATUS_TYPES) + .map(c => mapLookupCode(c)); + + const userRegionsIds: string[] = + retrieveUserInfoResponse?.userRegions.map(x => x.regionCode.toString()) ?? []; + const userRegionsOptions: MultiSelectOption[] = pimsRegionsTypes + .filter(opt => userRegionsIds.includes(opt.code)) + .map(x => { + return { id: x.code as string, text: x.label }; + }); + const { results, filter, @@ -50,7 +83,7 @@ export const AcquisitionListView: React.FunctionComponent< setPageSize, loading, } = useSearch( - new AcquisitionFilterModel().toApi(), + new AcquisitionFilterModel(userRegionsOptions).toApi(), getAcquisitionFiles, 'No matching results can be found. Try widening your search criteria.', ); @@ -80,20 +113,35 @@ export const AcquisitionListView: React.FunctionComponent< [setFilter], ); + const handleResetFilter = useCallback(() => { + setFilter(new AcquisitionFilterModel(userRegionsOptions).toApi()); + }, [setFilter, userRegionsOptions]); + + const acquisitionTeamOptions = useMemo(() => { + if (exists(team)) { + return team?.map(x => ({ + id: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, + text: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', + })); + } else { + return []; + } + }, [team]); + useEffect(() => { if (error) { toast.error(error?.message); } }, [error]); - const { - getAllAcquisitionFileTeamMembers: { response: team, execute: loadAcquisitionTeam }, - } = useAcquisitionProvider(); - useEffect(() => { loadAcquisitionTeam(); }, [loadAcquisitionTeam]); + useEffect(() => { + formattedGuid && retrieveUserInfo(formattedGuid); + }, [formattedGuid, retrieveUserInfo]); + return ( @@ -115,9 +163,16 @@ export const AcquisitionListView: React.FunctionComponent< diff --git a/source/frontend/src/features/acquisition/list/interfaces.ts b/source/frontend/src/features/acquisition/list/interfaces.ts index 8d4e672427..650b71c6a6 100644 --- a/source/frontend/src/features/acquisition/list/interfaces.ts +++ b/source/frontend/src/features/acquisition/list/interfaces.ts @@ -1,7 +1,6 @@ +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_AcquisitionFileTeam } from '@/models/api/generated/ApiGen_Concepts_AcquisitionFileTeam'; -import { formatApiPersonNames } from '@/utils/personUtils'; - -type IdSelector = 'O' | 'P'; +import { formatApiPersonNames, getParameterIdFromOptions } from '@/utils/personUtils'; export interface ApiGen_Concepts_AcquisitionFilter { acquisitionFileStatusTypeCode: string; @@ -15,6 +14,7 @@ export interface ApiGen_Concepts_AcquisitionFilter { pin: string; pid: string; address: string; + regions: string[]; } export class AcquisitionFilterModel { @@ -28,6 +28,11 @@ export class AcquisitionFilterModel { pin = ''; pid = ''; address = ''; + regions: MultiSelectOption[] = []; + + constructor(initialRegions: MultiSelectOption[] = []) { + this.regions = initialRegions; + } toApi(): ApiGen_Concepts_AcquisitionFilter { return { @@ -45,12 +50,14 @@ export class AcquisitionFilterModel { pin: this.pin, pid: this.pid, address: this.address, + regions: this.regions.map(x => x.id), }; } static fromApi( model: ApiGen_Concepts_AcquisitionFilter, teamMembers: ApiGen_Concepts_AcquisitionFileTeam[], + userRegions: MultiSelectOption[], ): AcquisitionFilterModel { const newModel = new AcquisitionFilterModel(); newModel.acquisitionFileStatusTypeCode = model.acquisitionFileStatusTypeCode; @@ -62,6 +69,7 @@ export class AcquisitionFilterModel { newModel.pid = model.pid; newModel.hasNoticeOfClaim = model.hasNoticeOfClaim; newModel.address = model.address; + newModel.regions = userRegions ?? []; if (model.acquisitionTeamMemberPersonId) { const memberPerson = teamMembers.find( @@ -92,24 +100,3 @@ export class AcquisitionFilterModel { return newModel; } } - -export interface MultiSelectOption { - id: string; - text: string; -} - -export const getParameterIdFromOptions = ( - options: MultiSelectOption[], - selector: IdSelector = 'P', -): string => { - if (options.length === 0) { - return ''; - } - - const filterOrgItems = options.filter(option => String(option.id).startsWith(selector)); - if (filterOrgItems.length === 0) { - return ''; - } - - return filterOrgItems[0].id.split('-').pop() ?? ''; -}; diff --git a/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.tsx b/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.tsx index 1efb56e23c..95c25bb95a 100644 --- a/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.tsx +++ b/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.tsx @@ -3,31 +3,42 @@ import React from 'react'; import { Col, Row } from 'react-bootstrap'; import { ResetButton, SearchButton } from '@/components/common/buttons'; -import { Input, Select, SelectOption, TypeaheadSelect } from '@/components/common/form'; +import { + Input, + Multiselect, + Select, + SelectOption, + TypeaheadSelect, +} from '@/components/common/form'; import { SelectInput } from '@/components/common/List/SelectInput'; import { ColButtons, FilterBoxForm } from '@/components/common/styles'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { Api_DispositionFilter } from '@/models/api/DispositionFilter'; import { ApiGen_Concepts_DispositionFileTeam } from '@/models/api/generated/ApiGen_Concepts_DispositionFileTeam'; -import { formatApiPersonNames } from '@/utils/personUtils'; import { DispositionFilterModel } from '../models'; export interface IDispositionFilterProps { - filter?: Api_DispositionFilter; - setFilter: (filter: Api_DispositionFilter) => void; + initialValues: DispositionFilterModel; dispositionTeam: ApiGen_Concepts_DispositionFileTeam[]; fileStatusOptions: SelectOption[]; dispositionStatusOptions: SelectOption[]; dispositionTypeOptions: SelectOption[]; + pimsRegionsOptions: MultiSelectOption[]; + dispositionTeamOptions: SelectOption[]; + onResetFilter: () => void; + setFilter: (filter: Api_DispositionFilter) => void; } export const DispositionFilter: React.FC = ({ - filter, - setFilter, - dispositionTeam, + initialValues, fileStatusOptions, dispositionStatusOptions, dispositionTypeOptions, + pimsRegionsOptions, + dispositionTeamOptions, + setFilter, + onResetFilter, }) => { const onSearchSubmit = async ( values: DispositionFilterModel, @@ -37,26 +48,10 @@ export const DispositionFilter: React.FC = ({ formikHelpers.setSubmitting(false); }; - const resetFilter = () => { - setFilter(new DispositionFilterModel().toApi()); - }; - - const dispositionTeamOptions = React.useMemo(() => { - const arr = dispositionTeam || []; - return arr.map(t => ({ - value: t.personId ? `P-${t.personId}` : `O-${t.organizationId}`, - label: t.personId && t.person ? formatApiPersonNames(t.person) : t.organization?.name ?? '', - })); - }, [dispositionTeam]); - return ( enableReinitialize - initialValues={ - filter - ? DispositionFilterModel.fromApi(filter, dispositionTeamOptions || []) - : new DispositionFilterModel() - } + initialValues={initialValues} onSubmit={onSearchSubmit} > {formikProps => ( @@ -110,6 +105,16 @@ export const DispositionFilter: React.FC = ({ /> + + + + + @@ -147,7 +152,7 @@ export const DispositionFilter: React.FC = ({ disabled={formikProps.isSubmitting} onClick={() => { formikProps.resetForm(); - resetFilter(); + onResetFilter(); }} /> diff --git a/source/frontend/src/features/disposition/list/DispositionListView.tsx b/source/frontend/src/features/disposition/list/DispositionListView.tsx index b75f23ba8f..a07515e0d0 100644 --- a/source/frontend/src/features/disposition/list/DispositionListView.tsx +++ b/source/frontend/src/features/disposition/list/DispositionListView.tsx @@ -1,5 +1,5 @@ import isEmpty from 'lodash/isEmpty'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Col, Row } from 'react-bootstrap'; import { FaFileExcel, FaPlus } from 'react-icons/fa'; import { useHistory } from 'react-router-dom'; @@ -8,10 +8,12 @@ import styled from 'styled-components'; import DispositionFileIcon from '@/assets/images/disposition-icon.svg?react'; import { StyledIconButton } from '@/components/common/buttons'; +import { SelectOption } from '@/components/common/form/Select'; import * as CommonStyled from '@/components/common/styles'; import { PaddedScrollable, StyledAddButton } from '@/components/common/styles'; import TooltipWrapper from '@/components/common/TooltipWrapper'; import { Claims } from '@/constants'; +import * as API from '@/constants/API'; import { DISPOSITION_FILE_STATUS_TYPES, DISPOSITION_STATUS_TYPES, @@ -19,13 +21,16 @@ import { } from '@/constants/API'; import { useApiDispositionFile } from '@/hooks/pims-api/useApiDispositionFile'; import { useDispositionProvider } from '@/hooks/repositories/useDispositionProvider'; -import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; +import { useUserInfoRepository } from '@/hooks/repositories/useUserInfoRepository'; +import { IUserInfo, useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { useSearch } from '@/hooks/useSearch'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { Api_DispositionFilter } from '@/models/api/DispositionFilter'; import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_Concepts_DispositionFile'; -import { generateMultiSortCriteria, mapLookupCode } from '@/utils'; +import { formatGuid, generateMultiSortCriteria, mapLookupCode } from '@/utils'; import { toFilteredApiPaginateParams } from '@/utils/CommonFunctions'; +import { formatApiPersonNames } from '@/utils/personUtils'; import { useDispositionFileExport } from '../hooks/useDispositionFileExport'; import DispositionFilter from './DispositionFilter/DispositionFilter'; @@ -37,8 +42,13 @@ import { DispositionFilterModel, DispositionSearchResultModel } from './models'; */ export const DispositionListView: React.FC = () => { const history = useHistory(); - const { hasClaim } = useKeycloakWrapper(); + const { hasClaim, obj } = useKeycloakWrapper(); + const { sub } = obj.userInfo as IUserInfo; + const formattedGuid = formatGuid(sub); + const { getDispositionFilesPagedApi } = useApiDispositionFile(); + const { retrieveUserInfo, retrieveUserInfoResponse } = useUserInfoRepository(); + const { getAllDispositionTeamMembers: { response: team, execute: loadDispositionTeam }, } = useDispositionProvider(); @@ -58,6 +68,20 @@ export const DispositionListView: React.FC = () => { .getByType(DISPOSITION_TYPES) .map(c => mapLookupCode(c)); + const pimsRegionsTypes = lookupCodes.getOptionsByType(API.REGION_TYPES); + const pimsRegionOptions: MultiSelectOption[] = pimsRegionsTypes.map(x => { + return { id: x.code as string, text: x.label }; + }); + + const userRegionsIds: string[] = + retrieveUserInfoResponse?.userRegions.map(x => x.regionCode.toString()) ?? []; + + const userRegionsOptions: MultiSelectOption[] = pimsRegionsTypes + .filter(opt => userRegionsIds.includes(opt.code)) + .map(x => { + return { id: x.code as string, text: x.label }; + }); + const { results, filter, @@ -73,7 +97,7 @@ export const DispositionListView: React.FC = () => { setPageSize, loading, } = useSearch( - new DispositionFilterModel().toApi(), + new DispositionFilterModel(userRegionsOptions).toApi(), getDispositionFilesPagedApi, 'No matching results can be found. Try widening your search criteria.', ); @@ -95,16 +119,6 @@ export const DispositionListView: React.FC = () => { exportDispositionFiles(query, accept); }; - React.useEffect(() => { - if (error) { - toast.error(error?.message); - } - }, [error]); - - React.useEffect(() => { - loadDispositionTeam(); - }, [loadDispositionTeam]); - // update internal state whenever the filter bar changes const changeFilter = React.useCallback( (filter: Api_DispositionFilter) => { @@ -114,6 +128,32 @@ export const DispositionListView: React.FC = () => { [setFilter, setCurrentPage], ); + const handleResetFilter = useCallback(() => { + setFilter(new DispositionFilterModel(userRegionsOptions).toApi()); + }, [setFilter, userRegionsOptions]); + + const dispositionTeamOptions = React.useMemo(() => { + const arr = team || []; + return arr.map(t => ({ + value: t.personId ? `P-${t.personId}` : `O-${t.organizationId}`, + label: t.personId && t.person ? formatApiPersonNames(t.person) : t.organization?.name ?? '', + })); + }, [team]); + + useEffect(() => { + if (error) { + toast.error(error?.message); + } + }, [error]); + + useEffect(() => { + loadDispositionTeam(); + }, [loadDispositionTeam]); + + useEffect(() => { + formattedGuid && retrieveUserInfo(formattedGuid); + }, [formattedGuid, retrieveUserInfo]); + return ( @@ -135,12 +175,19 @@ export const DispositionListView: React.FC = () => { diff --git a/source/frontend/src/features/disposition/list/models.ts b/source/frontend/src/features/disposition/list/models.ts index 68f027ee85..dd0a6e74dd 100644 --- a/source/frontend/src/features/disposition/list/models.ts +++ b/source/frontend/src/features/disposition/list/models.ts @@ -1,6 +1,7 @@ import isNumber from 'lodash/isNumber'; import { SelectOption } from '@/components/common/form'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { Api_DispositionFilter } from '@/models/api/DispositionFilter'; import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_Concepts_DispositionFile'; import { ApiGen_Concepts_DispositionFileProperty } from '@/models/api/generated/ApiGen_Concepts_DispositionFileProperty'; @@ -16,6 +17,11 @@ export class DispositionFilterModel { dispositionFileStatusCode = ''; dispositionStatusCode = ''; dispositionTypeCode = ''; + regions: MultiSelectOption[] = []; + + constructor(initialRegions: MultiSelectOption[] = []) { + this.regions = initialRegions; + } toApi(): Api_DispositionFilter { const personMemberId = @@ -36,6 +42,7 @@ export class DispositionFilterModel { dispositionFileStatusCode: this.dispositionFileStatusCode, dispositionStatusCode: this.dispositionStatusCode, dispositionTypeCode: this.dispositionTypeCode, + regions: this.regions.map(x => x.id), // disposition team members teamMemberPersonId: personMemberId && isNumber(+personMemberId) ? Number(personMemberId) : null, @@ -46,6 +53,7 @@ export class DispositionFilterModel { static fromApi( base: Api_DispositionFilter, teamMemberOptions: SelectOption[] = [], + userRegions: MultiSelectOption[], ): DispositionFilterModel { const newModel = new DispositionFilterModel(); newModel.searchBy = base.searchBy ?? 'address'; @@ -56,6 +64,8 @@ export class DispositionFilterModel { newModel.dispositionFileStatusCode = base.dispositionFileStatusCode ?? ''; newModel.dispositionStatusCode = base.dispositionStatusCode ?? ''; newModel.dispositionTypeCode = base.dispositionTypeCode ?? ''; + newModel.regions = userRegions ?? []; + // disposition team members newModel.dispositionTeamMember = base.teamMemberPersonId ? teamMemberOptions.find(c => c.value === `P-${base.teamMemberPersonId}`) ?? null diff --git a/source/frontend/src/features/leases/interfaces.ts b/source/frontend/src/features/leases/interfaces.ts index 311440ce32..d7751eb2e5 100644 --- a/source/frontend/src/features/leases/interfaces.ts +++ b/source/frontend/src/features/leases/interfaces.ts @@ -8,11 +8,11 @@ export interface ILeaseFilter { programs: string[]; expiryStartDate: string; expiryEndDate: string; - regionType: string; details: string; leaseTeamPersonId: number | null; leaseTeamOrganizationId: number | null; isReceivable: string | null; + regions: string[]; } export interface ILeaseSearchBy { diff --git a/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx b/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx index 4b0333cbc7..0211a94ec0 100644 --- a/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx +++ b/source/frontend/src/features/leases/list/LeaseFilter/LeaseFilter.tsx @@ -1,35 +1,29 @@ -import { Formik } from 'formik'; -import Multiselect from 'multiselect-react-dropdown'; -import React, { useEffect, useMemo, useState } from 'react'; +import { Formik, FormikHelpers } from 'formik'; +import React from 'react'; import { Col, Row } from 'react-bootstrap'; -import { FaTimes } from 'react-icons/fa'; import styled from 'styled-components'; import { ResetButton, SearchButton } from '@/components/common/buttons'; -import { FastDatePicker, Form, Input, Select } from '@/components/common/form'; -import { UserRegionSelectContainer } from '@/components/common/form/UserRegionSelect/UserRegionSelectContainer'; +import { FastDatePicker, Form, Input, Multiselect, Select } from '@/components/common/form'; import { SelectInput } from '@/components/common/List/SelectInput'; import { SectionField } from '@/components/common/Section/SectionField'; import { ColButtons } from '@/components/common/styles'; import TooltipIcon from '@/components/common/TooltipIcon'; -import { LEASE_PROGRAM_TYPES, LEASE_STATUS_TYPES } from '@/constants/API'; -import { getParameterIdFromOptions } from '@/features/acquisition/list/interfaces'; -import { useLeaseRepository } from '@/hooks/repositories/useLeaseRepository'; -import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; -import { exists, isValidId } from '@/utils'; -import { formatApiPersonNames } from '@/utils/personUtils'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ILeaseFilter, ILeaseSearchBy } from '../../interfaces'; import { LeaseFilterSchema } from './LeaseFilterYupSchema'; +import { LeaseFilterModel } from './models/LeaseFilterModel'; export interface ILeaseFilterProps { - filter?: ILeaseFilter; - setFilter: (filter: ILeaseFilter) => void; -} + initialValues: LeaseFilterModel; + pimsRegionsOptions: MultiSelectOption[]; + leaseTeamOptions: MultiSelectOption[]; + leaseStatusOptions: MultiSelectOption[]; + leaseProgramOptions: MultiSelectOption[]; -interface MultiSelectOption { - id: string; - text: string; + setFilter: (filter: ILeaseFilter) => void; + onResetFilter: () => void; } export const defaultFilter: ILeaseFilter = { @@ -51,11 +45,11 @@ export const defaultFilter: ILeaseFilter = { tenantName: '', expiryStartDate: '', expiryEndDate: '', - regionType: '', details: '', leaseTeamOrganizationId: null, leaseTeamPersonId: null, isReceivable: null, + regions: [], }; /** @@ -63,108 +57,59 @@ export const defaultFilter: ILeaseFilter = { * @param {ILeaseFilterProps} props */ export const LeaseFilter: React.FunctionComponent> = ({ - filter, + initialValues, + pimsRegionsOptions, + leaseTeamOptions, + leaseStatusOptions, + leaseProgramOptions, setFilter, + onResetFilter, }) => { - const onSearchSubmit = (values: ILeaseFilter, { setSubmitting }: any) => { - const selectedPrograms: MultiSelectOption[] = multiselectProgramRef.current?.getSelectedItems(); - const selectedStatuses: MultiSelectOption[] = multiselectStatusRef.current?.getSelectedItems(); - const selectedTeam: MultiSelectOption[] = multiselectTeamRef.current?.getSelectedItems(); - const programIds = selectedPrograms.map(x => x.id); - const statuses = selectedStatuses.map(x => x.id); - const leaseTeamPersonId = exists(selectedTeam) - ? +getParameterIdFromOptions(selectedTeam, 'P') - : undefined; - const leaseTeamOrganizationId = exists(selectedTeam) - ? +getParameterIdFromOptions(selectedTeam, 'O') - : undefined; - values = { - ...values, - programs: programIds, - leaseStatusTypes: statuses, - leaseTeamPersonId: isValidId(leaseTeamPersonId) ? leaseTeamPersonId : null, - leaseTeamOrganizationId: - exists(selectedTeam) && isValidId(leaseTeamOrganizationId) - ? leaseTeamOrganizationId - : undefined, - }; - setFilter(values); - setSubmitting(false); - }; - const resetFilter = () => { - multiselectProgramRef.current?.resetSelectedValues(); - multiselectTeamRef.current?.resetSelectedValues(); - setSelectedStatus( - statusFilterOptions.filter(x => defaultFilter.leaseStatusTypes.includes(x.id)), - ); - - setFilter(defaultFilter); - }; - - const { - getAllLeaseTeamMembers: { response: leaseTeam, execute: loadLeaseTeam }, - } = useLeaseRepository(); - - useEffect(() => { - loadLeaseTeam(); - }, [loadLeaseTeam]); - - const multiselectProgramRef = React.createRef(); - const multiselectStatusRef = React.createRef(); - const multiselectTeamRef = React.createRef(); - - const lookupCodes = useLookupCodeHelpers(); + // const onSearchSubmit = (values: ILeaseFilter, { setSubmitting }: any) => { - const leaseStatusOptions = lookupCodes.getByType(LEASE_STATUS_TYPES); + // const programIds = selectedPrograms.map(x => x.id); + // const statuses = selectedStatuses.map(x => x.id); - const leaseProgramTypes = lookupCodes.getByType(LEASE_PROGRAM_TYPES); - const programFilterOptions: MultiSelectOption[] = leaseProgramTypes.map(x => { - return { id: x.id as string, text: x.name }; - }); - - const statusFilterOptions: MultiSelectOption[] = leaseStatusOptions.map(x => { - return { id: x.id as string, text: x.name }; - }); - - const leaseTeamOptions = useMemo(() => { - if (exists(leaseTeam)) { - return leaseTeam?.map(x => ({ - id: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, - text: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', - })); - } else { - return []; - } - }, [leaseTeam]); - - const initialLeaseStatusList = statusFilterOptions.filter(x => - defaultFilter.leaseStatusTypes.includes(x.id), - ); - - const [selectedStatus, setSelectedStatus] = useState(initialLeaseStatusList); + // values = { + // ...values, + // programs: programIds, + // leaseStatusTypes: statuses, + // leaseTeamPersonId: isValidId(leaseTeamPersonId) ? leaseTeamPersonId : null, + // leaseTeamOrganizationId: + // exists(selectedTeam) && isValidId(leaseTeamOrganizationId) + // ? leaseTeamOrganizationId + // : undefined, + // }; + // setFilter(values); + // setSubmitting(false); + // }; // Necessary since the lookup codes might have not been loaded before the first render - useEffect(() => { - setSelectedStatus([ - { id: 'ACTIVE', text: 'Active' }, - { id: 'ARCHIVED', text: 'Archived' }, - { id: 'DISCARD', text: 'Cancelled' }, - { id: 'DRAFT', text: 'Draft' }, - { id: 'DUPLICATE', text: 'Duplicate' }, - { id: 'EXPIRED', text: 'Expired' }, - { id: 'INACTIVE', text: 'Hold' }, - { id: 'TERMINATED', text: 'Terminated' }, - ]); - }, []); + // useEffect(() => { + // setSelectedStatus([ + // { id: 'ACTIVE', text: 'Active' }, + // { id: 'ARCHIVED', text: 'Archived' }, + // { id: 'DISCARD', text: 'Cancelled' }, + // { id: 'DRAFT', text: 'Draft' }, + // { id: 'DUPLICATE', text: 'Duplicate' }, + // { id: 'EXPIRED', text: 'Expired' }, + // { id: 'INACTIVE', text: 'Hold' }, + // { id: 'TERMINATED', text: 'Terminated' }, + // ]); + // }, []); - function onSelectedStatusChange(selectedList: MultiSelectOption[]) { - setSelectedStatus(selectedList); - } + const onSearchSubmit = ( + values: LeaseFilterModel, + formikHelpers: FormikHelpers, + ) => { + setFilter(values.toApi()); + formikHelpers.setSubmitting(false); + }; return ( - enableReinitialize - initialValues={filter ?? defaultFilter} + initialValues={initialValues} onSubmit={onSearchSubmit} validationSchema={LeaseFilterSchema} > @@ -203,65 +148,21 @@ export const LeaseFilter: React.FunctionComponent } hidePlaceholder={true} - style={{ - chips: { - background: '#F2F2F2', - borderRadius: '4px', - color: 'black', - fontSize: '16px', - marginRight: '1em', - }, - multiselectContainer: { - width: 'auto', - color: 'black', - paddingBottom: '12px', - }, - searchBox: { - background: 'white', - border: '1px solid #606060', - }, - }} /> } - hidePlaceholder={true} - style={{ - chips: { - background: '#F2F2F2', - borderRadius: '4px', - color: 'black', - fontSize: '16px', - marginRight: '1em', - }, - multiselectContainer: { - width: 'auto', - color: 'black', - paddingBottom: '12px', - }, - searchBox: { - background: 'white', - border: '1px solid #606060', - }, - }} /> @@ -271,8 +172,7 @@ export const LeaseFilter: React.FunctionComponent - + - @@ -349,7 +253,7 @@ export const LeaseFilter: React.FunctionComponent { formikProps.resetForm(); - resetFilter(); + onResetFilter(); }} /> diff --git a/source/frontend/src/features/leases/list/LeaseFilter/models/LeaseFilterModel.ts b/source/frontend/src/features/leases/list/LeaseFilter/models/LeaseFilterModel.ts new file mode 100644 index 0000000000..fc22a02e04 --- /dev/null +++ b/source/frontend/src/features/leases/list/LeaseFilter/models/LeaseFilterModel.ts @@ -0,0 +1,93 @@ +import { ILeaseFilter } from '@/features/leases/interfaces'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; +import { ApiGen_Concepts_LeaseFileTeam } from '@/models/api/generated/ApiGen_Concepts_LeaseFileTeam'; +import { formatApiPersonNames, getParameterIdFromOptions } from '@/utils/personUtils'; + +export class LeaseFilterModel { + pin: string; + pid: string; + lFileNo: string; + searchBy: string; + leaseTeamMembers: MultiSelectOption[] = []; + programs: MultiSelectOption[] = []; + expiryStartDate: string; + leaseStatusTypes: MultiSelectOption[] = []; + tenantName: string; + expiryEndDate: string; + regionType: string; + details: string; + isReceivable: string | null; + regions: MultiSelectOption[] = []; + + constructor(initialRegions: MultiSelectOption[] = [], initialStatuses: MultiSelectOption[] = []) { + this.regions = initialRegions; + this.leaseStatusTypes = initialStatuses; + this.searchBy = 'lFileNo'; + } + + toApi(): ILeaseFilter { + const leaseTeamPersonId = getParameterIdFromOptions(this.leaseTeamMembers, 'P'); + const leaseTeamOrganizationId = getParameterIdFromOptions(this.leaseTeamMembers, 'O'); + + return { + pid: this.pid, + pin: this.pin, + lFileNo: this.lFileNo, + searchBy: this.searchBy, + leaseStatusTypes: this.leaseStatusTypes.map(x => x.id), + tenantName: this.tenantName, + programs: this.programs.map(x => x.id), + expiryStartDate: this.expiryStartDate, + expiryEndDate: this.expiryEndDate, + details: this.details, + leaseTeamPersonId: leaseTeamPersonId ? +leaseTeamPersonId : null, + leaseTeamOrganizationId: leaseTeamOrganizationId ? +leaseTeamOrganizationId : null, + isReceivable: this.isReceivable, + regions: this.regions.map(x => x.id), + }; + } + + static fromApi( + base: ILeaseFilter, + teamMembers: ApiGen_Concepts_LeaseFileTeam[], + userRegions: MultiSelectOption[], + initialStatuses: MultiSelectOption[], + ): LeaseFilterModel { + const newModel = new LeaseFilterModel(); + + newModel.pin = base.pin; + newModel.pid = base.pid; + newModel.lFileNo = base.lFileNo; + newModel.searchBy = base.searchBy; + newModel.tenantName = base.tenantName; + + newModel.regions = userRegions ?? []; + newModel.leaseStatusTypes = initialStatuses ?? []; + + if (base.leaseTeamPersonId) { + const memberPerson = teamMembers.find(p => p.personId === Number(base.leaseTeamPersonId)); + + newModel.leaseTeamMembers = [ + { + id: `P-${memberPerson?.personId}`, + text: formatApiPersonNames(memberPerson?.person), + }, + ]; + } + + if (base.leaseTeamOrganizationId) { + const memberOrganization = teamMembers.find( + p => p.organizationId === Number(base.leaseTeamOrganizationId), + ); + + newModel.leaseTeamMembers = [ + { + id: `O-${memberOrganization?.organizationId}`, + text: `${memberOrganization?.organization?.name}`, + }, + ]; + } + + return newModel; + } +} diff --git a/source/frontend/src/features/leases/list/LeaseListView.tsx b/source/frontend/src/features/leases/list/LeaseListView.tsx index 1ad900ed64..ded85dbbe6 100644 --- a/source/frontend/src/features/leases/list/LeaseListView.tsx +++ b/source/frontend/src/features/leases/list/LeaseListView.tsx @@ -1,5 +1,5 @@ import { isEmpty } from 'lodash'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; import { FaFileAlt, FaFileExcel, FaPlus } from 'react-icons/fa'; import { useHistory } from 'react-router'; @@ -11,26 +11,91 @@ import { StyledIconButton } from '@/components/common/buttons'; import * as CommonStyled from '@/components/common/styles'; import { PaddedScrollable, StyledAddButton } from '@/components/common/styles'; import TooltipWrapper from '@/components/common/TooltipWrapper'; +import * as API from '@/constants/API'; import Claims from '@/constants/claims'; import { useApiLeases } from '@/hooks/pims-api/useApiLeases'; -import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import { useLeaseRepository } from '@/hooks/repositories/useLeaseRepository'; +import { useUserInfoRepository } from '@/hooks/repositories/useUserInfoRepository'; +import useKeycloakWrapper, { IUserInfo } from '@/hooks/useKeycloakWrapper'; +import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { useSearch } from '@/hooks/useSearch'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; -import { generateMultiSortCriteria } from '@/utils'; +import { exists, formatGuid, generateMultiSortCriteria } from '@/utils'; import { toFilteredApiPaginateParams } from '@/utils/CommonFunctions'; +import { formatApiPersonNames } from '@/utils/personUtils'; import { useLeaseExport } from '../hooks/useLeaseExport'; import { ILeaseFilter } from '../interfaces'; -import { defaultFilter, LeaseFilter } from './LeaseFilter/LeaseFilter'; +import { LeaseFilter } from './LeaseFilter/LeaseFilter'; +import { LeaseFilterModel } from './LeaseFilter/models/LeaseFilterModel'; import { LeaseSearchResults } from './LeaseSearchResults/LeaseSearchResults'; +const initialLeaseStatusTypes: string[] = [ + 'ACTIVE', + 'ARCHIVED', + 'DISCARD', + 'DRAFT', + 'DUPLICATE', + 'EXPIRED', + 'INACTIVE', + 'TERMINATED', +]; + /** * Page that displays leases information. */ export const LeaseListView: React.FunctionComponent> = () => { const history = useHistory(); + const { hasClaim, obj } = useKeycloakWrapper(); + const { sub } = obj.userInfo as IUserInfo; + const formattedGuid = formatGuid(sub); + const lookupCodes = useLookupCodeHelpers(); + const { getLeases } = useApiLeases(); - const { hasClaim } = useKeycloakWrapper(); + const { exportLeases } = useLeaseExport(); + const { retrieveUserInfo, retrieveUserInfoResponse } = useUserInfoRepository(); + const { + getAllLeaseTeamMembers: { response: leaseTeam, execute: loadLeaseTeam }, + } = useLeaseRepository(); + + const leaseProgramTypes = lookupCodes.getOptionsByType(API.LEASE_PROGRAM_TYPES); + const leaseProgramOptions: MultiSelectOption[] = leaseProgramTypes.map(x => { + return { id: x.value as string, text: x.label }; + }); + + const leaseStatusTypes = lookupCodes.getOptionsByType(API.LEASE_STATUS_TYPES); + const leaseStatusOptions: MultiSelectOption[] = leaseStatusTypes.map(x => { + return { id: x.value as string, text: x.label }; + }); + const initialStatusOptions = leaseStatusOptions.filter(x => + initialLeaseStatusTypes.includes(x.id), + ); + + const pimsRegionsTypes = lookupCodes.getOptionsByType(API.REGION_TYPES); + const pimsRegionOptions: MultiSelectOption[] = pimsRegionsTypes.map(x => { + return { id: x.code as string, text: x.label }; + }); + + const userRegionsIds: string[] = + retrieveUserInfoResponse?.userRegions.map(x => x.regionCode.toString()) ?? []; + const userRegionsOptions: MultiSelectOption[] = pimsRegionsTypes + .filter(opt => userRegionsIds.includes(opt.code)) + .map(x => { + return { id: x.code as string, text: x.label }; + }); + + const leaseTeamOptions = useMemo(() => { + if (exists(leaseTeam)) { + return leaseTeam?.map(x => ({ + id: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, + text: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', + })); + } else { + return []; + } + }, [leaseTeam]); + const { results, filter, @@ -45,9 +110,10 @@ export const LeaseListView: React.FunctionComponent(defaultFilter, getLeases); - - const { exportLeases } = useLeaseExport(); + } = useSearch( + new LeaseFilterModel(userRegionsOptions, initialStatusOptions).toApi(), + getLeases, + ); /** * @param {'csv' | 'excel'} accept Whether the fetch is for type of CSV or EXCEL @@ -73,12 +139,24 @@ export const LeaseListView: React.FunctionComponent { + setFilter(new LeaseFilterModel(userRegionsOptions, initialStatusOptions).toApi()); + }, [initialStatusOptions, setFilter, userRegionsOptions]); + + useEffect(() => { + loadLeaseTeam(); + }, [loadLeaseTeam]); + useEffect(() => { if (error) { toast.error(error?.message); } }, [error]); + useEffect(() => { + formattedGuid && retrieveUserInfo(formattedGuid); + }, [formattedGuid, retrieveUserInfo]); + return ( @@ -99,7 +177,20 @@ export const LeaseListView: React.FunctionComponent - + diff --git a/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.tsx b/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.tsx index 20095c39b0..49b7d899df 100644 --- a/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.tsx +++ b/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.tsx @@ -5,30 +5,41 @@ import styled from 'styled-components'; import { ResetButton } from '@/components/common/buttons'; import { SearchButton } from '@/components/common/buttons/SearchButton'; -import { Check, Input, Select, SelectOption, TypeaheadSelect } from '@/components/common/form'; -import { UserRegionSelectContainer } from '@/components/common/form/UserRegionSelect/UserRegionSelectContainer'; +import { + Check, + Input, + Multiselect, + Select, + SelectOption, + TypeaheadSelect, +} from '@/components/common/form'; import { SelectInput } from '@/components/common/List/SelectInput'; import { ColButtons, FilterBoxForm } from '@/components/common/styles'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_ManagementFileTeam } from '@/models/api/generated/ApiGen_Concepts_ManagementFileTeam'; import { Api_ManagementFilter } from '@/models/api/ManagementFilter'; -import { formatApiPersonNames } from '@/utils/personUtils'; import { ManagementFilterModel } from '../models'; export interface IManagementFilterProps { - filter?: Api_ManagementFilter; - setFilter: (filter: Api_ManagementFilter) => void; + initialValues: ManagementFilterModel; managementTeam: ApiGen_Concepts_ManagementFileTeam[]; fileStatusOptions: SelectOption[]; managementPurposeOptions: SelectOption[]; + pimsRegionsOptions: MultiSelectOption[]; + managementTeamOptions: SelectOption[]; + setFilter: (filter: Api_ManagementFilter) => void; + onResetFilter: () => void; } export const ManagementFilter: React.FC = ({ - filter, - setFilter, - managementTeam, + initialValues, fileStatusOptions, managementPurposeOptions, + pimsRegionsOptions, + managementTeamOptions, + setFilter, + onResetFilter, }) => { const onSearchSubmit = async ( values: ManagementFilterModel, @@ -38,26 +49,10 @@ export const ManagementFilter: React.FC = ({ formikHelpers.setSubmitting(false); }; - const resetFilter = () => { - setFilter(new ManagementFilterModel().toApi()); - }; - - const managementTeamOptions = React.useMemo(() => { - const arr = managementTeam ?? []; - return arr.map(t => ({ - value: t.personId ? `P-${t.personId}` : `O-${t.organizationId}`, - label: t.personId && t.person ? formatApiPersonNames(t.person) : t.organization?.name ?? '', - })); - }, [managementTeam]); - return ( enableReinitialize - initialValues={ - filter - ? ManagementFilterModel.fromApi(filter, managementTeamOptions ?? []) - : new ManagementFilterModel() - } + initialValues={initialValues} onSubmit={onSearchSubmit} > {formikProps => ( @@ -112,20 +107,14 @@ export const ManagementFilter: React.FC = ({ - - + - - - @@ -152,6 +141,15 @@ export const ManagementFilter: React.FC = ({ /> + + + + + @@ -163,7 +161,7 @@ export const ManagementFilter: React.FC = ({ disabled={formikProps.isSubmitting} onClick={() => { formikProps.resetForm(); - resetFilter(); + onResetFilter(); }} /> diff --git a/source/frontend/src/features/management/list/ManagementListView.tsx b/source/frontend/src/features/management/list/ManagementListView.tsx index 869c20ce07..a01064b487 100644 --- a/source/frontend/src/features/management/list/ManagementListView.tsx +++ b/source/frontend/src/features/management/list/ManagementListView.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; import { FaPlus } from 'react-icons/fa'; import { useHistory } from 'react-router-dom'; @@ -6,18 +6,23 @@ import { toast } from 'react-toastify'; import styled from 'styled-components'; import ManagementFileIcon from '@/assets/images/management-icon.svg?react'; +import { SelectOption } from '@/components/common/form/Select'; import * as CommonStyled from '@/components/common/styles'; import { PaddedScrollable, StyledAddButton } from '@/components/common/styles'; import { Claims } from '@/constants'; +import * as API from '@/constants/API'; import { MANAGEMENT_FILE_PURPOSE_TYPES, MANAGEMENT_FILE_STATUS_TYPES } from '@/constants/API'; import { useApiManagementFile } from '@/hooks/pims-api/useApiManagementFile'; import { useManagementFileRepository } from '@/hooks/repositories/useManagementFileRepository'; -import { useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; +import { useUserInfoRepository } from '@/hooks/repositories/useUserInfoRepository'; +import { IUserInfo, useKeycloakWrapper } from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { useSearch } from '@/hooks/useSearch'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { Api_ManagementFilter } from '@/models/api/ManagementFilter'; -import { mapLookupCode } from '@/utils'; +import { formatGuid, mapLookupCode } from '@/utils'; +import { formatApiPersonNames } from '@/utils/personUtils'; import ManagementFilter from './ManagementFilter/ManagementFilter'; import { ManagementSearchResults } from './ManagementSearchResults/ManagementSearchResults'; @@ -28,7 +33,11 @@ import { ManagementFilterModel, ManagementSearchResultModel } from './models'; */ export const ManagementListView: React.FC = () => { const history = useHistory(); - const { hasClaim } = useKeycloakWrapper(); + const { hasClaim, obj } = useKeycloakWrapper(); + const { sub } = obj.userInfo as IUserInfo; + const formattedGuid = formatGuid(sub); + + const { retrieveUserInfo, retrieveUserInfoResponse } = useUserInfoRepository(); const { getManagementFilesPagedApi } = useApiManagementFile(); const { getAllManagementTeamMembers: { response: team, execute: loadManagementTeam }, @@ -37,6 +46,11 @@ export const ManagementListView: React.FC = () => { // lookup codes to filter management list const lookupCodes = useLookupCodeHelpers(); + const pimsRegionsTypes = lookupCodes.getOptionsByType(API.REGION_TYPES); + const pimsRegionOptions: MultiSelectOption[] = pimsRegionsTypes.map(x => { + return { id: x.code as string, text: x.label }; + }); + const managementFileStatusOptions = lookupCodes .getByType(MANAGEMENT_FILE_STATUS_TYPES) .map(c => mapLookupCode(c)); @@ -45,6 +59,14 @@ export const ManagementListView: React.FC = () => { .getByType(MANAGEMENT_FILE_PURPOSE_TYPES) .map(c => mapLookupCode(c)); + const userRegionsIds: string[] = + retrieveUserInfoResponse?.userRegions.map(x => x.regionCode.toString()) ?? []; + const userRegionsOptions: MultiSelectOption[] = pimsRegionsTypes + .filter(opt => userRegionsIds.includes(opt.code)) + .map(x => { + return { id: x.code as string, text: x.label }; + }); + const { results, filter, @@ -60,23 +82,21 @@ export const ManagementListView: React.FC = () => { setPageSize, loading, } = useSearch( - new ManagementFilterModel().toApi(), + new ManagementFilterModel(userRegionsOptions).toApi(), getManagementFilesPagedApi, 'No matching results can be found. Try widening your search criteria.', ); - React.useEffect(() => { - if (error) { - toast.error(error?.message); - } - }, [error]); - - React.useEffect(() => { - loadManagementTeam(); - }, [loadManagementTeam]); + const managementTeamOptions = useMemo(() => { + const arr = team ?? []; + return arr.map(t => ({ + value: t.personId ? `P-${t.personId}` : `O-${t.organizationId}`, + label: t.personId && t.person ? formatApiPersonNames(t.person) : t.organization?.name ?? '', + })); + }, [team]); // update internal state whenever the filter bar changes - const changeFilter = React.useCallback( + const changeFilter = useCallback( (filter: Api_ManagementFilter) => { setFilter(filter); setCurrentPage(0); @@ -84,6 +104,24 @@ export const ManagementListView: React.FC = () => { [setFilter, setCurrentPage], ); + const handleResetFilter = useCallback(() => { + setFilter(new ManagementFilterModel(userRegionsOptions).toApi()); + }, [setFilter, userRegionsOptions]); + + useEffect(() => { + if (error) { + toast.error(error?.message); + } + }, [error]); + + useEffect(() => { + loadManagementTeam(); + }, [loadManagementTeam]); + + useEffect(() => { + formattedGuid && retrieveUserInfo(formattedGuid); + }, [formattedGuid, retrieveUserInfo]); + return ( @@ -105,11 +143,18 @@ export const ManagementListView: React.FC = () => { diff --git a/source/frontend/src/features/management/list/models.ts b/source/frontend/src/features/management/list/models.ts index e92a297dbb..39df37cfe7 100644 --- a/source/frontend/src/features/management/list/models.ts +++ b/source/frontend/src/features/management/list/models.ts @@ -1,6 +1,7 @@ import isNumber from 'lodash/isNumber'; import { SelectOption } from '@/components/common/form'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; import { ApiGen_Concepts_ManagementFileProperty } from '@/models/api/generated/ApiGen_Concepts_ManagementFileProperty'; import { ApiGen_Concepts_ManagementFileTeam } from '@/models/api/generated/ApiGen_Concepts_ManagementFileTeam'; @@ -19,6 +20,11 @@ export class ManagementFilterModel { managementFilePurposeCode = ''; projectNameOrNumber = ''; hasNoticeOfClaim = false; + regions: MultiSelectOption[] = []; + + constructor(initialRegions: MultiSelectOption[] = []) { + this.regions = initialRegions; + } toApi(): Api_ManagementFilter { const personMemberId = @@ -41,6 +47,7 @@ export class ManagementFilterModel { managementFilePurposeCode: this.managementFilePurposeCode, projectNameOrNumber: this.projectNameOrNumber, hasNoticeOfClaim: this.hasNoticeOfClaim, + regions: this.regions.map(x => x.id), // management team members teamMemberPersonId: personMemberId && isNumber(+personMemberId) ? Number(personMemberId) : null, @@ -51,6 +58,7 @@ export class ManagementFilterModel { static fromApi( base: Api_ManagementFilter, teamMemberOptions: SelectOption[] = [], + userRegions: MultiSelectOption[], ): ManagementFilterModel { const newModel = new ManagementFilterModel(); newModel.searchBy = base.searchBy ?? 'address'; @@ -62,6 +70,8 @@ export class ManagementFilterModel { newModel.managementFilePurposeCode = base.managementFilePurposeCode ?? ''; newModel.projectNameOrNumber = base.projectNameOrNumber ?? ''; newModel.hasNoticeOfClaim = base.hasNoticeOfClaim; + newModel.regions = userRegions ?? []; + // management team members if (base.teamMemberPersonId) { newModel.managementTeamMember = diff --git a/source/frontend/src/features/projects/interfaces.ts b/source/frontend/src/features/projects/interfaces.ts index abb81e62ab..23cb1929ca 100644 --- a/source/frontend/src/features/projects/interfaces.ts +++ b/source/frontend/src/features/projects/interfaces.ts @@ -3,6 +3,7 @@ export interface IProjectFilter { projectStatusCode: string; projectName: string; projectNumber: string; + regions: string[]; } export interface IProjectSearchBy { diff --git a/source/frontend/src/features/projects/list/ProjectFilter/ProjectFilter.tsx b/source/frontend/src/features/projects/list/ProjectFilter/ProjectFilter.tsx index b6b1647e03..d8b2de7ddf 100644 --- a/source/frontend/src/features/projects/list/ProjectFilter/ProjectFilter.tsx +++ b/source/frontend/src/features/projects/list/ProjectFilter/ProjectFilter.tsx @@ -1,43 +1,38 @@ -import { Formik } from 'formik'; +import { Formik, FormikHelpers } from 'formik'; import React from 'react'; import { Col, Row } from 'react-bootstrap'; import { ResetButton, SearchButton } from '@/components/common/buttons'; -import { Input, Select } from '@/components/common/form'; -import { UserRegionSelectContainer } from '@/components/common/form/UserRegionSelect/UserRegionSelectContainer'; +import { Input, Multiselect, Select } from '@/components/common/form'; import { ColButtons, FilterBoxForm } from '@/components/common/styles'; import { PROJECT_STATUS_TYPES } from '@/constants/API'; import { IProjectFilter } from '@/features/projects/interfaces'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; +import { MultiSelectOption } from '@/interfaces/MultiSelectOption'; import { mapLookupCode } from '@/utils'; +import { ProjectFilterModel } from './models/ProjectFilterModel'; + export interface IProjectFilterProps { - filter?: IProjectFilter; + initialValues: ProjectFilterModel; + pimsRegionsOptions: MultiSelectOption[]; setFilter: (filter: IProjectFilter) => void; - initialFilter?: IProjectFilter; + onResetFilter: () => void; } -export const defaultFilter: IProjectFilter = { - projectName: '', - projectNumber: '', - projectRegionCode: '', - projectStatusCode: '', -}; - /** * Filter bar for Projects. * @param {IProjectFilterProps} props */ export const ProjectFilter: React.FunctionComponent< React.PropsWithChildren -> = ({ setFilter, initialFilter }) => { - const onSearchSubmit = (values: IProjectFilter, { setSubmitting }: any) => { - setFilter(values); - setSubmitting(false); - }; - - const resetFilter = () => { - setFilter(initialFilter ?? defaultFilter); +> = ({ initialValues, pimsRegionsOptions, setFilter, onResetFilter }) => { + const onSearchSubmit = ( + values: ProjectFilterModel, + formikHelpers: FormikHelpers, + ) => { + setFilter(values.toApi()); + formikHelpers.setSubmitting(false); }; const lookupCodes = useLookupCodeHelpers(); @@ -46,9 +41,9 @@ export const ProjectFilter: React.FunctionComponent< .map(c => mapLookupCode(c)); return ( - enableReinitialize - initialValues={initialFilter ?? defaultFilter} + initialValues={initialValues} onSubmit={onSearchSubmit} > {formikProps => ( @@ -75,14 +70,18 @@ export const ProjectFilter: React.FunctionComponent< - + @@ -476,7 +483,7 @@ exports[`Acquisition Filter > matches snapshot 1`] = ` class="row" >
matches snapshot 1`] = ` />
- -
+
@@ -513,6 +517,53 @@ exports[`Acquisition Filter > matches snapshot 1`] = ` />
+
+
+
+
+
+
+
+ +
+
+
    + + No Options Available + +
+
+
+
+
+
diff --git a/source/frontend/src/features/acquisition/list/interfaces.ts b/source/frontend/src/features/acquisition/list/interfaces.ts index 650b71c6a6..ccb03ff8ef 100644 --- a/source/frontend/src/features/acquisition/list/interfaces.ts +++ b/source/frontend/src/features/acquisition/list/interfaces.ts @@ -35,14 +35,19 @@ export class AcquisitionFilterModel { } toApi(): ApiGen_Concepts_AcquisitionFilter { + const acquisitionTeamPersonId = getParameterIdFromOptions(this.acquisitionTeamMembers, 'P'); + const acquistionTeamOrganizationId = getParameterIdFromOptions( + this.acquisitionTeamMembers, + 'O', + ); + return { acquisitionFileStatusTypeCode: this.acquisitionFileStatusTypeCode, acquisitionFileNameOrNumber: this.acquisitionFileNameOrNumber, - acquisitionTeamMemberPersonId: getParameterIdFromOptions(this.acquisitionTeamMembers, 'P'), - acquisitionTeamMemberOrganizationId: getParameterIdFromOptions( - this.acquisitionTeamMembers, - 'O', - ), + acquisitionTeamMemberPersonId: acquisitionTeamPersonId ? acquisitionTeamPersonId : null, + acquisitionTeamMemberOrganizationId: acquistionTeamOrganizationId + ? acquistionTeamOrganizationId + : null, projectNameOrNumber: this.projectNameOrNumber, ownerName: this.ownerName, searchBy: this.searchBy, diff --git a/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.test.tsx b/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.test.tsx index 2f4af09f3a..7b69e900d1 100644 --- a/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.test.tsx +++ b/source/frontend/src/features/disposition/list/DispositionFilter/DispositionFilter.test.tsx @@ -15,20 +15,27 @@ import { DispositionFilterModel } from '../models'; import DispositionFilter from './DispositionFilter'; const setFilter = vi.fn(); +const onResetFilter = vi.fn(); const fileStatusOptions = getMockLookUpsByType(DISPOSITION_FILE_STATUS_TYPES); const dispositionStatusOptions = getMockLookUpsByType(DISPOSITION_STATUS_TYPES); const dispositionTypeOptions = getMockLookUpsByType(DISPOSITION_TYPES); +const mockFilterModel = new DispositionFilterModel(); + describe('Disposition filter', () => { const setup = (renderOptions: RenderOptions = {}) => { const utils = render( , { store: { @@ -46,7 +53,7 @@ describe('Disposition filter', () => { }; beforeEach(() => { - setFilter.mockClear(); + vi.clearAllMocks(); }); it('matches snapshot', () => { @@ -55,9 +62,12 @@ describe('Disposition filter', () => { }); it('searches for active disposition files by default', async () => { - const { getResetButton } = setup(); - await act(async () => userEvent.click(getResetButton())); - expect(setFilter).toHaveBeenCalledWith(new DispositionFilterModel().toApi()); + const { getSearchButton } = setup(); + + await act(async () => userEvent.click(getSearchButton())); + expect(setFilter).toHaveBeenCalledWith( + expect.objectContaining(new DispositionFilterModel().toApi()), + ); }); it('searches by disposition file status', async () => { @@ -128,8 +138,6 @@ describe('Disposition filter', () => { await act(async () => userEvent.paste(input!, 'test disposition')); await act(async () => userEvent.click(getResetButton())); - expect(setFilter).toHaveBeenCalledWith( - expect.objectContaining(new DispositionFilterModel().toApi()), - ); + expect(onResetFilter).toHaveBeenCalledTimes(1); }); }); diff --git a/source/frontend/src/features/disposition/list/DispositionFilter/__snapshots__/DispositionFilter.test.tsx.snap b/source/frontend/src/features/disposition/list/DispositionFilter/__snapshots__/DispositionFilter.test.tsx.snap index 48ca2e7c8a..42ec579cba 100644 --- a/source/frontend/src/features/disposition/list/DispositionFilter/__snapshots__/DispositionFilter.test.tsx.snap +++ b/source/frontend/src/features/disposition/list/DispositionFilter/__snapshots__/DispositionFilter.test.tsx.snap @@ -451,6 +451,53 @@ exports[`Disposition filter > matches snapshot 1`] = `
+
+
+
+
+
+
+ +
+
+
    + + No Options Available + +
+
+
+
+
+
+
); +vi.mock('@/hooks/repositories/useUserInfoRepository'); +vi.mocked(useUserInfoRepository).mockReturnValue({ + retrieveUserInfo: vi.fn(), + retrieveUserInfoLoading: true, + retrieveUserInfoResponse: getUserMock(), +}); + + + const mockPagedResults = ( searchResults?: ApiGen_Concepts_DispositionFile[], ): Partial, any>> => { diff --git a/source/frontend/src/features/disposition/list/__snapshots__/DispositionListView.test.tsx.snap b/source/frontend/src/features/disposition/list/__snapshots__/DispositionListView.test.tsx.snap index dc4f1e03c8..f2fba2c607 100644 --- a/source/frontend/src/features/disposition/list/__snapshots__/DispositionListView.test.tsx.snap +++ b/source/frontend/src/features/disposition/list/__snapshots__/DispositionListView.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Disposition List View > matches snapshot 1`] = ` class="Toastify" />
- .c11.btn { + .c12.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,17 +35,17 @@ exports[`Disposition List View > matches snapshot 1`] = ` cursor: pointer; } -.c11.btn .Button__value { +.c12.btn .Button__value { width: auto; } -.c11.btn:hover { +.c12.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c11.btn:focus { +.c12.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -53,31 +53,31 @@ exports[`Disposition List View > matches snapshot 1`] = ` box-shadow: none; } -.c11.btn.btn-primary { +.c12.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c11.btn.btn-primary:hover, -.c11.btn.btn-primary:active, -.c11.btn.btn-primary:focus { +.c12.btn.btn-primary:hover, +.c12.btn.btn-primary:active, +.c12.btn.btn-primary:focus { background-color: #1E5189; } -.c11.btn.btn-secondary { +.c12.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c11.btn.btn-secondary:hover, -.c11.btn.btn-secondary:active, -.c11.btn.btn-secondary:focus { +.c12.btn.btn-secondary:hover, +.c12.btn.btn-secondary:active, +.c12.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c11.btn.btn-info { +.c12.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -85,66 +85,66 @@ exports[`Disposition List View > matches snapshot 1`] = ` padding-right: 0.6rem; } -.c11.btn.btn-info:hover, -.c11.btn.btn-info:active, -.c11.btn.btn-info:focus { +.c12.btn.btn-info:hover, +.c12.btn.btn-info:active, +.c12.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c11.btn.btn-light { +.c12.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c11.btn.btn-light:hover, -.c11.btn.btn-light:active, -.c11.btn.btn-light:focus { +.c12.btn.btn-light:hover, +.c12.btn.btn-light:active, +.c12.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c11.btn.btn-dark { +.c12.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c11.btn.btn-dark:hover, -.c11.btn.btn-dark:active, -.c11.btn.btn-dark:focus { +.c12.btn.btn-dark:hover, +.c12.btn.btn-dark:active, +.c12.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c11.btn.btn-danger { +.c12.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c11.btn.btn-danger:hover, -.c11.btn.btn-danger:active, -.c11.btn.btn-danger:focus { +.c12.btn.btn-danger:hover, +.c12.btn.btn-danger:active, +.c12.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c11.btn.btn-warning { +.c12.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c11.btn.btn-warning:hover, -.c11.btn.btn-warning:active, -.c11.btn.btn-warning:focus { +.c12.btn.btn-warning:hover, +.c12.btn.btn-warning:active, +.c12.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c11.btn.btn-link { +.c12.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -168,9 +168,9 @@ exports[`Disposition List View > matches snapshot 1`] = ` text-decoration: underline; } -.c11.btn.btn-link:hover, -.c11.btn.btn-link:active, -.c11.btn.btn-link:focus { +.c12.btn.btn-link:hover, +.c12.btn.btn-link:active, +.c12.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -180,15 +180,15 @@ exports[`Disposition List View > matches snapshot 1`] = ` outline: none; } -.c11.btn.btn-link:disabled, -.c11.btn.btn-link.disabled { +.c12.btn.btn-link:disabled, +.c12.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c11.btn:disabled, -.c11.btn:disabled:hover { +.c12.btn:disabled, +.c12.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -200,62 +200,62 @@ exports[`Disposition List View > matches snapshot 1`] = ` cursor: not-allowed; } -.c11.Button .Button__icon { +.c12.Button .Button__icon { margin-right: 1.6rem; } -.c11.Button--icon-only:focus { +.c12.Button--icon-only:focus { outline: none; } -.c11.Button--icon-only .Button__icon { +.c12.Button--icon-only .Button__icon { margin-right: 0; } -.c12.c12.btn { +.c13.c13.btn { background-color: unset; border: none; } -.c12.c12.btn:hover, -.c12.c12.btn:focus, -.c12.c12.btn:active { +.c13.c13.btn:hover, +.c13.c13.btn:focus, +.c13.c13.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c12.c12.btn svg { +.c13.c13.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c12.c12.btn svg:hover { +.c13.c13.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c12.c12.btn.btn-primary svg { +.c13.c13.btn.btn-primary svg { color: #013366; } -.c12.c12.btn.btn-primary svg:hover { +.c13.c13.btn.btn-primary svg:hover { color: #013366; } -.c12.c12.btn.btn-light svg { +.c13.c13.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c12.c12.btn.btn-light svg:hover { +.c13.c13.btn.btn-light svg:hover { color: #CE3E39; } -.c12.c12.btn.btn-info svg { +.c13.c13.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c12.c12.btn.btn-info svg:hover { +.c13.c13.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } @@ -287,7 +287,17 @@ exports[`Disposition List View > matches snapshot 1`] = ` border-bottom-left-radius: 0; } -.c13 { +.c10 { + margin-left: 0.5rem; + cursor: pointer; + fill: #474543; +} + +.c10:hover { + fill: #CE3E39; +} + +.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -306,21 +316,21 @@ exports[`Disposition List View > matches snapshot 1`] = ` margin-left: 0.5rem; } -.c15 { +.c16 { width: 1.6rem; height: 1.6rem; } -.c14 { +.c15 { width: 1.6rem; height: 1.6rem; } -.c17 { +.c18 { margin-top: 0.3rem; } -.c18 { +.c19 { min-width: 5rem; max-width: 5rem; margin-left: 1rem; @@ -329,11 +339,11 @@ exports[`Disposition List View > matches snapshot 1`] = ` padding: 0; } -.c18:-moz-read-only { +.c19:-moz-read-only { background: white; } -.c18:read-only { +.c19:read-only { background: white; } @@ -381,7 +391,7 @@ exports[`Disposition List View > matches snapshot 1`] = ` padding-bottom: 2rem; } -.c10 { +.c11 { border-left: 0.2rem solid white; min-width: 2.5rem; } @@ -408,7 +418,7 @@ exports[`Disposition List View > matches snapshot 1`] = ` padding: 0; } -.c16 { +.c17 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -423,7 +433,7 @@ exports[`Disposition List View > matches snapshot 1`] = ` color: inherit; } -.c16:hover { +.c17:hover { -webkit-text-decoration: underline; text-decoration: underline; } @@ -676,6 +686,87 @@ exports[`Disposition List View > matches snapshot 1`] = `
+
+
+
+
+
+
+ + South Coast Region + + + + + + + +
+
+
    +
  • + Southern Interior Region +
  • +
  • + Northern Region +
  • +
  • + Cannot determine +
  • +
+
+
+
+
+
+
matches snapshot 1`] = `
matches snapshot 1`] = ` class="pr-0 col-xl-auto" >
x.id), + projectStatusCode: this.projectStatusCode?.trim() ? this.projectStatusCode : null, + projectName: this.projectName?.trim() ? this.projectName : null, + projectNumber: this.projectNumber?.trim() ? this.projectNumber : null, + regions: this.regions?.map(x => x.id) ?? [], }; } @@ -26,7 +24,6 @@ export class ProjectFilterModel { const newModel = new ProjectFilterModel(); newModel.projectName = model.projectName; newModel.projectNumber = model.projectNumber; - newModel.projectRegionCode = model.projectRegionCode; newModel.projectStatusCode = model.projectStatusCode; newModel.regions = userRegions ?? []; diff --git a/source/frontend/src/features/projects/list/ProjectListView.test.tsx b/source/frontend/src/features/projects/list/ProjectListView.test.tsx index 58019512ab..b19714a648 100644 --- a/source/frontend/src/features/projects/list/ProjectListView.test.tsx +++ b/source/frontend/src/features/projects/list/ProjectListView.test.tsx @@ -58,7 +58,9 @@ const setup = (renderOptions: RenderOptions = { store: storeState }) => { describe('Project List View', () => { beforeEach(() => { - searchProjects.mockClear(); + vi.clearAllMocks(); + // Mock console.error to prevent test failure on React controlled/uncontrolled input warning + vi.spyOn(console, 'error').mockImplementation(() => {}); }); it('matches snapshot', async () => { @@ -85,15 +87,15 @@ describe('Project List View', () => { const { container, searchButton, findByText, getByTitle } = setup(); await waitForElementToBeRemoved(getByTitle('table-loading')); - fillInput(container, 'projectName', 'NAME'); + await fillInput(container, 'projectName', 'NAME'); await act(async () => userEvent.click(searchButton)); - expect(searchProjects).toHaveBeenCalledWith( + expect(searchProjects).toHaveBeenLastCalledWith( expect.objectContaining({ projectName: 'NAME', - projectNumber: '', - projectStatusCode: '', - projectRegionCode: '', + projectNumber: null, + projectStatusCode: null, + regions: [] }), ); diff --git a/source/frontend/src/features/projects/list/__snapshots__/ProjectListView.test.tsx.snap b/source/frontend/src/features/projects/list/__snapshots__/ProjectListView.test.tsx.snap index a14a832156..12bf073fd1 100644 --- a/source/frontend/src/features/projects/list/__snapshots__/ProjectListView.test.tsx.snap +++ b/source/frontend/src/features/projects/list/__snapshots__/ProjectListView.test.tsx.snap @@ -420,7 +420,6 @@ exports[`Project List View > matches snapshot 1`] = ` name="projectNumber" placeholder="Project number" style="word-break: break-all;" - title="" value="" />
@@ -437,7 +436,6 @@ exports[`Project List View > matches snapshot 1`] = ` name="projectName" placeholder="Project name" style="word-break: break-all;" - title="" value="" /> @@ -458,31 +456,41 @@ exports[`Project List View > matches snapshot 1`] = `
- - +
- - - - +
+
+
    + + No Options Available + +
+
+
+
Date: Fri, 17 Apr 2026 10:20:53 -0700 Subject: [PATCH 3/3] - linting --- .../management/list/ManagementFilter/ManagementFilter.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.test.tsx b/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.test.tsx index 0635aff388..039e8a5400 100644 --- a/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.test.tsx +++ b/source/frontend/src/features/management/list/ManagementFilter/ManagementFilter.test.tsx @@ -73,7 +73,7 @@ describe('Management filter', () => { }); it('searches for active management files by default', async () => { - const { getResetButton, getSearchButton } = await setup({}); + const { getSearchButton } = await setup({}); await act(async () => userEvent.click(getSearchButton())); expect(setFilter).toHaveBeenCalledWith(