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/backend/tests/api/Services/LeaseReportsServiceTest.cs b/source/backend/tests/api/Services/LeaseReportsServiceTest.cs index 979052199d..36e391b9f3 100644 --- a/source/backend/tests/api/Services/LeaseReportsServiceTest.cs +++ b/source/backend/tests/api/Services/LeaseReportsServiceTest.cs @@ -58,7 +58,7 @@ public void GetAggregatedLeases_NotAuthorized() var period = new PimsLeasePeriod() { PeriodStartDate = DateTime.Now, PeriodExpiryDate = DateTime.Now.AddDays(10) }; this.MockCommonServices(); - this.leaseRepository.Setup(x => x.GetAllByFilter(It.IsAny(), It.IsAny>(), true, null)).Returns(new List() { lease }); + this.leaseRepository.Setup(x => x.GetAllByFilter(It.IsAny(), true, null)).Returns(new List() { lease }); // Act // Assert @@ -77,7 +77,7 @@ public void GetAggregatedLeases_Success() var period = new PimsLeasePeriod() { PeriodStartDate = DateTime.Now, PeriodExpiryDate = DateTime.Now.AddDays(10) }; this.MockCommonServices(); - this.leaseRepository.Setup(x => x.GetAllByFilter(It.IsAny(), It.IsAny>(), true, null)).Returns(new List() { lease }); + this.leaseRepository.Setup(x => x.GetAllByFilter(It.IsAny(), true, null)).Returns(new List() { lease }); this.userRepository.Setup(x => x.GetByKeycloakUserId(It.IsAny())).Returns(new PimsUser() { PimsRegionUsers = new List() }); // Act @@ -86,7 +86,7 @@ public void GetAggregatedLeases_Success() // Assert leases.Should().HaveCount(1); leases.FirstOrDefault().Should().Be(lease); - this.leaseRepository.Verify(x => x.GetAllByFilter(It.IsAny(), It.IsAny>(), true, null)); + this.leaseRepository.Verify(x => x.GetAllByFilter(It.IsAny(), true, null)); this.userRepository.Verify(x => x.GetByKeycloakUserId(It.IsAny())); } #endregion diff --git a/source/backend/tests/api/Services/LeaseServiceTest.cs b/source/backend/tests/api/Services/LeaseServiceTest.cs index ef31ee730d..2aa0e67b72 100644 --- a/source/backend/tests/api/Services/LeaseServiceTest.cs +++ b/source/backend/tests/api/Services/LeaseServiceTest.cs @@ -57,7 +57,7 @@ public void GetPage_Success() user.PimsRegionUsers.Add(new PimsRegionUser() { RegionCode = lease.RegionCode.Value }); var leaseRepository = this._helper.GetService>(); - leaseRepository.Setup(x => x.GetPage(It.IsAny(), It.IsAny>(), null)) + leaseRepository.Setup(x => x.GetPage(It.IsAny(), null)) .Returns(new Paged(new List() { lease }, 1, 1, 1)); var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(user); @@ -67,7 +67,7 @@ public void GetPage_Success() var properties = service.GetPage(filter, false); // Assert - leaseRepository.Verify(x => x.GetPage(filter, It.Is>(r => r.Count == 1 && r.Contains(lease.RegionCode.Value)), null), Times.Once); + leaseRepository.Verify(x => x.GetPage(filter, null), Times.Once); } [Fact] public void GetPage_Contractor_Success() @@ -82,7 +82,7 @@ public void GetPage_Contractor_Success() user.PersonId = 1; var leaseRepository = this._helper.GetService>(); - leaseRepository.Setup(x => x.GetPage(It.IsAny(), It.IsAny>(), null)) + leaseRepository.Setup(x => x.GetPage(It.IsAny(), null)) .Returns(new Paged(new List() { lease }, 1, 1, 1)); var userRepository = this._helper.GetService>(); userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(user); @@ -93,7 +93,7 @@ public void GetPage_Contractor_Success() // Assert // PersonId should be passed to ensure contractor only sees leases they are assigned to - leaseRepository.Verify(x => x.GetPage(filter, It.Is>(r => r.Count == 1 && r.Contains(lease.RegionCode.Value)), user.PersonId), Times.Once); + leaseRepository.Verify(x => x.GetPage(filter, user.PersonId), Times.Once); } [Fact] @@ -120,7 +120,7 @@ public void GetPage_All_ShouldIgnorePagination_Success() // Assert // Pagination should be ignored and all results should be returned when "all" parameter is true - leaseRepository.Verify(x => x.GetPage(It.Is(f => f.Page == 1 && f.Quantity == 55), It.IsAny>(), null), Times.Once); + leaseRepository.Verify(x => x.GetPage(It.Is(f => f.Page == 1 && f.Quantity == 55), null), Times.Once); } #endregion diff --git a/source/backend/tests/api/Services/ManagementFileServiceTest.cs b/source/backend/tests/api/Services/ManagementFileServiceTest.cs index d6ce120add..a9829181cc 100644 --- a/source/backend/tests/api/Services/ManagementFileServiceTest.cs +++ b/source/backend/tests/api/Services/ManagementFileServiceTest.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using FluentAssertions; +using k8s.KubeConfigModels; using Moq; using NetTopologySuite.Geometries; using Pims.Api.Constants; @@ -16,6 +13,10 @@ using Pims.Dal.Entities.Models; using Pims.Dal.Exceptions; using Pims.Dal.Repositories; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Xunit; namespace Pims.Api.Test.Services @@ -1508,13 +1509,17 @@ public void GetPage_Success() var managementFile = EntityHelper.CreateManagementFile(); var repository = this._helper.GetService>(); - repository.Setup(x => x.GetPageDeep(It.IsAny())).Returns(new Paged(new[] { managementFile })); + repository.Setup(x => x.GetPageDeep(It.IsAny(), null)).Returns(new Paged(new[] { managementFile })); + + var userRepository = _helper.GetService>(); + var user = EntityHelper.CreateUser("Test"); + userRepository.Setup(x => x.GetUserInfoByKeycloakUserId(It.IsAny())).Returns(user); // Act var result = service.GetPage(new ManagementFilter()); // Assert - repository.Verify(x => x.GetPageDeep(It.IsAny()), Times.Once); + repository.Verify(x => x.GetPageDeep(It.IsAny(), null), Times.Once); } [Fact] diff --git a/source/backend/tests/api/Services/ProjectServiceTest.cs b/source/backend/tests/api/Services/ProjectServiceTest.cs index 482fb29fe1..7f78c3b428 100644 --- a/source/backend/tests/api/Services/ProjectServiceTest.cs +++ b/source/backend/tests/api/Services/ProjectServiceTest.cs @@ -100,7 +100,7 @@ public void Search_GetPage_ShouldFail_Filter_IsNull() // Assert act.Should().Throw(); - repository.Verify(x => x.GetPageAsync(It.IsAny(), It.IsAny>()), Times.Never); + repository.Verify(x => x.GetPageAsync(It.IsAny(), null), Times.Never); } [Fact] @@ -118,7 +118,7 @@ public void Search_GetPage_ShouldFail_Filter_IsInvalid() // Assert act.Should().Throw(); - repository.Verify(x => x.GetPageAsync(It.IsAny(), It.IsAny>()), Times.Never); + repository.Verify(x => x.GetPageAsync(It.IsAny(), null), Times.Never); } [Fact] @@ -128,7 +128,7 @@ public async void Search_GetPage_Success() var service = this.CreateProjectServiceWithPermissions(Permissions.ProjectView); var repository = this._helper.GetService>(); - repository.Setup(x => x.GetPageAsync(It.IsAny(), It.IsAny>())) + repository.Setup(x => x.GetPageAsync(It.IsAny(), null)) .ReturnsAsync(new Paged() { Page = 1, @@ -141,7 +141,7 @@ public async void Search_GetPage_Success() // Assert result.Should().NotBeNull(); - repository.Verify(x => x.GetPageAsync(It.IsAny(), It.IsAny>()), Times.Once); + repository.Verify(x => x.GetPageAsync(It.IsAny(), null), Times.Once); } [Fact] diff --git a/source/backend/tests/dal/Repositories/AcquisitionRepositoryTest.cs b/source/backend/tests/dal/Repositories/AcquisitionRepositoryTest.cs index 3796ebf115..73b004c13f 100644 --- a/source/backend/tests/dal/Repositories/AcquisitionRepositoryTest.cs +++ b/source/backend/tests/dal/Repositories/AcquisitionRepositoryTest.cs @@ -45,7 +45,7 @@ public void GetPage_AcquisitionName() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -66,7 +66,7 @@ public void GetPage_AcquisitionNumber() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -87,7 +87,7 @@ public void GetPage_AcquisitionHistoricalNumber() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -108,7 +108,7 @@ public void GetPage_AlternateProject() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -129,7 +129,7 @@ public void GetPage_Pid() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -150,7 +150,7 @@ public void GetPage_Pin() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -171,7 +171,7 @@ public void GetPage_Address() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -192,7 +192,7 @@ public void GetPage_Project() var repository = helper.CreateRepository(user); // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }, contractorPersonId: 1); + var result = repository.GetPageDeep(filter, contractorPersonId: 1); // Assert result.Should().HaveCount(1); @@ -220,7 +220,7 @@ public void GetOwner_Success() var filter = new AcquisitionFilter() { OwnerName = "DOE" }; // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -252,7 +252,7 @@ public void GetOwner_Rep_Success() var filter = new AcquisitionFilter() { OwnerName = "DOE" }; // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -284,7 +284,7 @@ public void GetOwner_Solicitor_Person_Success() var filter = new AcquisitionFilter() { OwnerName = "DOE" }; // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); @@ -315,7 +315,7 @@ public void GetOwner_Solicitor_Org_Success() var filter = new AcquisitionFilter() { OwnerName = "DAIRY" }; // Act - var result = repository.GetPageDeep(filter, new HashSet() { 1 }); + var result = repository.GetPageDeep(filter, null); // Assert result.Should().HaveCount(1); diff --git a/source/backend/tests/dal/Repositories/LeaseRepositoryTest.cs b/source/backend/tests/dal/Repositories/LeaseRepositoryTest.cs index e2b3626bfa..19cac70427 100644 --- a/source/backend/tests/dal/Repositories/LeaseRepositoryTest.cs +++ b/source/backend/tests/dal/Repositories/LeaseRepositoryTest.cs @@ -51,7 +51,6 @@ public LeaseRepositoryTest() new object[] { new LeaseFilter() { LeaseStatusTypes = new List() { "fake" } }, 0 }, new object[] { new LeaseFilter() { Details = "details" }, 1 }, new object[] { new LeaseFilter() { Details = "test" }, 0 }, - new object[] { new LeaseFilter() { RegionType = 2 }, 0 }, new object[] { new LeaseFilter() { ExpiryStartDate = new DateOnly(1999, 1,1) }, 1 }, new object[] { new LeaseFilter() { ExpiryStartDate = new DateOnly(2001,1,1) }, 0 }, new object[] { new LeaseFilter() { ExpiryEndDate = new DateOnly(1999, 1,1) }, 0 }, @@ -118,7 +117,7 @@ public void Get_Leases_Paged(LeaseFilter filter, int expectedCount) _helper.AddAndSaveChanges(elease); // Act - var result = repository.GetAllByFilter(filter, new HashSet()); + var result = repository.GetAllByFilter(filter); // Assert Assert.NotNull(result); @@ -137,8 +136,7 @@ public void Get_Leases_NotAuthorized() // Act // Assert - Assert.Throws(() => - service.GetAllByFilter(null, new HashSet())); + Assert.Throws(() => service.GetAllByFilter(null)); } [Fact] @@ -153,7 +151,7 @@ public void Get_Leases_InvalidFilter() // Act // Assert Assert.Throws(() => - service.GetAllByFilter(new LeaseFilter() { ExpiryStartDate = DateOnly.MaxValue, ExpiryEndDate = DateOnly.MinValue }, new HashSet())); + service.GetAllByFilter(new LeaseFilter() { ExpiryStartDate = DateOnly.MaxValue, ExpiryEndDate = DateOnly.MinValue })); } [Theory] @@ -175,7 +173,7 @@ public void Get_Leases_Filter(LeaseFilter filter, int expectedCount) _helper.AddAndSaveChanges(elease); // Act - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert Assert.NotNull(result); @@ -217,7 +215,7 @@ public void Get_Leases_Filter_Historical_LISNO() LeaseFilter filter = new LeaseFilter() { Historical = "99999" }; // Act - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert Assert.NotNull(result); @@ -257,7 +255,7 @@ public void Get_Leases_Filter_Historical_PSNO() // Act LeaseFilter filter = new LeaseFilter() { Historical = "88888" }; - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert Assert.NotNull(result); @@ -298,7 +296,7 @@ public void Get_Leases_Filter_Historical_OTHERNO() LeaseFilter filter = new LeaseFilter() { Historical = "77777" }; // Act - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert Assert.NotNull(result); @@ -339,7 +337,7 @@ public void Get_Leases_Filter_Historical_File_Numbers() LeaseFilter filter = new LeaseFilter() { Historical = "66666" }; // Act - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert Assert.NotNull(result); @@ -367,7 +365,7 @@ public void GetPage_Contractor_Success() _helper.AddAndSaveChanges(secondLease); // Act - var result = repository.GetPage(new LeaseFilter(), new HashSet(), 1); + var result = repository.GetPage(new LeaseFilter(), 1); // Assert result.Should().NotBeNull(); @@ -390,7 +388,7 @@ public void Get_Leases_Filter_LFileNo_NoPrefixNoDash_ShouldMatch() // Act var filter = new LeaseFilter() { LFileNo = "000123" }; // no prefix or dash - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); // Assert result.Items.Should().HaveCount(1); @@ -405,7 +403,7 @@ public void Get_Leases_Filter_LFileNo_WithPrefixAndDash_ShouldMatch() _helper.AddAndSaveChanges(elease); var filter = new LeaseFilter() { LFileNo = "L-000-123" }; - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); result.Items.Should().HaveCount(1); } @@ -419,7 +417,7 @@ public void Get_Leases_Filter_LFileNo_Partial_ShouldMatch() _helper.AddAndSaveChanges(elease); var filter = new LeaseFilter() { LFileNo = "123" }; // partial - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); result.Items.Should().HaveCount(1); } @@ -433,7 +431,7 @@ public void Get_Leases_Filter_LFileNo_WithSpaces_ShouldMatch() _helper.AddAndSaveChanges(elease); var filter = new LeaseFilter() { LFileNo = " 000 123 " }; // spaces - var result = repository.GetPage(filter, new HashSet()); + var result = repository.GetPage(filter); result.Items.Should().HaveCount(1); } @@ -448,7 +446,7 @@ public void Get_Leases_Filter_LFileNo_InvalidFormat_ShouldThrow() var filter = new LeaseFilter() { LFileNo = "ABC123" }; // invalid - Action act = () => repository.GetPage(filter, new HashSet()); + Action act = () => repository.GetPage(filter); act.Should().Throw() .WithMessage("Invalid L-File number*"); diff --git a/source/backend/tests/dal/Repositories/ProjectRepositoryTest.cs b/source/backend/tests/dal/Repositories/ProjectRepositoryTest.cs index 37d3e033dc..39f8c9cda6 100644 --- a/source/backend/tests/dal/Repositories/ProjectRepositoryTest.cs +++ b/source/backend/tests/dal/Repositories/ProjectRepositoryTest.cs @@ -88,7 +88,7 @@ public async void GetPageAsync_Success() var repository = helper.CreateRepository(user); // Act - var result = await repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { ProjectName = "test project", ProjectNumber = "551234", ProjectStatusCode = "ACTIVE", ProjectRegionCode = "1", Sort = new string[] { "LastUpdatedBy" } }, new HashSet() { 1 }); + var result = await repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { ProjectName = "test project", ProjectNumber = "551234", ProjectStatusCode = "ACTIVE", Sort = new string[] { "LastUpdatedBy" } }); // Assert result.Should().NotBeNull(); @@ -108,7 +108,7 @@ public void GetPageAsync_Error_Permissions() var repository = helper.CreateRepository(user); // Act - Action act = () => repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { ProjectName = "test project" }, new HashSet() { 1 }); + Action act = () => repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { ProjectName = "test project" }); // Assert act.Should().Throw(); @@ -127,7 +127,7 @@ public void GetPageAsync_Error_Null() var repository = helper.CreateRepository(user); // Act - Action act = () => repository.GetPageAsync(null, new HashSet() { 1 }); + Action act = () => repository.GetPageAsync(null); // Assert act.Should().Throw(); @@ -146,7 +146,7 @@ public void GetPageAsync_Error_Invalid() var repository = helper.CreateRepository(user); // Act - Action act = () => repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { Page = -1 }, new HashSet() { 1 }); + Action act = () => repository.GetPageAsync(new Dal.Entities.Models.ProjectFilter() { Page = -1 }); // Assert act.Should().Throw(); diff --git a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx index 0784d2a45b..0febdd6403 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx +++ b/source/frontend/src/features/acquisition/list/AcquisitionFilter/AcquisitionFilter.test.tsx @@ -1,24 +1,41 @@ import userEvent from '@testing-library/user-event'; import { Claims } from '@/constants/index'; -import { mockLookups } from '@/mocks/lookups.mock'; +import { getMockLookUpsByType, mockLookups } from '@/mocks/lookups.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { act, fillInput, render, RenderOptions, waitFor } from '@/utils/test-utils'; +import * as API from '@/constants/API'; -import { AcquisitionFilterModel, ApiGen_Concepts_AcquisitionFilter } from '../interfaces'; +import { AcquisitionFilterModel } from '../interfaces'; import { AcquisitionFilter } from './AcquisitionFilter'; const setFilter = vi.fn(); +const onResetFilter = vi.fn(); + +const mockFilterModel = new AcquisitionFilterModel(); + +const acquisitionStatusTypes = getMockLookUpsByType(API.ACQUISITION_FILE_STATUS_TYPES); + // render component under test const setup = (renderOptions: RenderOptions = {}) => { - const utils = render(, { - store: { - [lookupCodesSlice.name]: { lookupCodes: mockLookups }, + const utils = render( + , + { + store: { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, + }, + claims: [Claims.ACQUISITION_VIEW], + ...renderOptions, }, - claims: [Claims.ACQUISITION_VIEW], - ...renderOptions, - }); + ); const searchButton = utils.getByTestId('search'); const resetButton = utils.getByTestId('reset-button'); const hasNOCCheckbox = utils.container.querySelector( @@ -29,7 +46,7 @@ const setup = (renderOptions: RenderOptions = {}) => { describe('Acquisition Filter', () => { beforeEach(() => { - setFilter.mockClear(); + vi.clearAllMocks(); }); it('matches snapshot', async () => { @@ -40,10 +57,12 @@ describe('Acquisition Filter', () => { }); it('searches for active acquisition files by default', async () => { - const { resetButton } = setup(); - await act(async () => userEvent.click(resetButton)); + const { searchButton } = setup(); - expect(setFilter).toHaveBeenCalledWith(new AcquisitionFilterModel().toApi()); + await act(async () => userEvent.click(searchButton)); + expect(setFilter).toHaveBeenCalledWith( + expect.objectContaining(new AcquisitionFilterModel().toApi()), + ); }); it('searches by acquisition file status', async () => { @@ -123,10 +142,6 @@ describe('Acquisition Filter', () => { fillInput(container, 'acquisitionFileNameOrNumber', 'breaking'); await act(async () => userEvent.click(resetButton)); - expect(setFilter).toHaveBeenCalledWith( - expect.objectContaining( - new AcquisitionFilterModel().toApi(), - ), - ); + expect(onResetFilter).toHaveBeenCalledTimes(1); }); }); 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/AcquisitionFilter/__snapshots__/AcquisitionFilter.test.tsx.snap b/source/frontend/src/features/acquisition/list/AcquisitionFilter/__snapshots__/AcquisitionFilter.test.tsx.snap index 52e40cb913..80c47ab0d8 100644 --- a/source/frontend/src/features/acquisition/list/AcquisitionFilter/__snapshots__/AcquisitionFilter.test.tsx.snap +++ b/source/frontend/src/features/acquisition/list/AcquisitionFilter/__snapshots__/AcquisitionFilter.test.tsx.snap @@ -464,6 +464,13 @@ exports[`Acquisition Filter > matches snapshot 1`] = ` > Archived + @@ -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/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..ccb03ff8ef 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,16 +28,26 @@ export class AcquisitionFilterModel { pin = ''; pid = ''; address = ''; + regions: MultiSelectOption[] = []; + + constructor(initialRegions: MultiSelectOption[] = []) { + this.regions = initialRegions; + } 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, @@ -45,12 +55,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 +74,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 +105,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.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/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/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/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/__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) ?? [], + }; + } + + static fromApi(model: IProjectFilter, userRegions: MultiSelectOption[]): ProjectFilterModel { + const newModel = new ProjectFilterModel(); + newModel.projectName = model.projectName; + newModel.projectNumber = model.projectNumber; + newModel.projectStatusCode = model.projectStatusCode; + newModel.regions = userRegions ?? []; + + return newModel; + } +} 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/ProjectListView.tsx b/source/frontend/src/features/projects/list/ProjectListView.tsx index de4dce3b5a..eb9efa7176 100644 --- a/source/frontend/src/features/projects/list/ProjectListView.tsx +++ b/source/frontend/src/features/projects/list/ProjectListView.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { Col, Row } from 'react-bootstrap'; import { FaPlus } from 'react-icons/fa'; import { useHistory } from 'react-router-dom'; @@ -7,14 +7,20 @@ import styled from 'styled-components'; import ProjectIcon from '@/assets/images/projects-icon.svg?react'; import * as CommonStyled from '@/components/common/styles'; import { StyledAddButton } from '@/components/common/styles'; +import * as API from '@/constants/API'; import { Claims } from '@/constants/claims'; import { useApiProjects } from '@/hooks/pims-api/useApiProjects'; -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_Project } from '@/models/api/generated/ApiGen_Concepts_Project'; +import { formatGuid } from '@/utils/utils'; import { IProjectFilter } from '../interfaces'; -import { defaultFilter, ProjectFilter } from './ProjectFilter/ProjectFilter'; +import { ProjectFilterModel } from './ProjectFilter/models/ProjectFilterModel'; +import ProjectFilter from './ProjectFilter/ProjectFilter'; import { ProjectSearchResultModel } from './ProjectSearchResults/models'; import { ProjectSearchResults } from './ProjectSearchResults/ProjectSearchResults'; @@ -23,9 +29,27 @@ import { ProjectSearchResults } from './ProjectSearchResults/ProjectSearchResult */ export const ProjectListView: React.FunctionComponent> = () => { const { searchProjects } = useApiProjects(); - const { hasClaim } = useKeycloakWrapper(); + const { hasClaim, obj } = useKeycloakWrapper(); + const { sub } = obj.userInfo as IUserInfo; + const formattedGuid = formatGuid(sub); const history = useHistory(); + const lookupCodes = useLookupCodeHelpers(); + const { retrieveUserInfo, retrieveUserInfoResponse } = useUserInfoRepository(); + + 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, @@ -40,7 +64,7 @@ export const ProjectListView: React.FunctionComponent( - defaultFilter, + new ProjectFilterModel(userRegionsOptions).toApi(), searchProjects, 'No matching results can be found. Try widening your search criteria.', ); @@ -53,6 +77,14 @@ export const ProjectListView: React.FunctionComponent { + setFilter(new ProjectFilterModel(userRegionsOptions).toApi()); + }, [setFilter, userRegionsOptions]); + + useEffect(() => { + formattedGuid && retrieveUserInfo(formattedGuid); + }, [formattedGuid, retrieveUserInfo]); + return ( @@ -75,9 +107,10 @@ export const ProjectListView: React.FunctionComponent 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 + +
+
+
+
): string { return nameParts.filter(n => exists(n) && n.trim() !== '').join(' '); } + +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() ?? ''; +};