diff --git a/source/backend/api/Areas/Reports/Models/Acquisition/AgreementReportModel.cs b/source/backend/api/Areas/Reports/Models/Acquisition/AgreementReportModel.cs index a510f752d7..08d5dff577 100644 --- a/source/backend/api/Areas/Reports/Models/Acquisition/AgreementReportModel.cs +++ b/source/backend/api/Areas/Reports/Models/Acquisition/AgreementReportModel.cs @@ -105,8 +105,14 @@ public AgreementReportModel(PimsAgreement agreement, ClaimsPrincipal user) private static string GetTeamMemberName(PimsAcquisitionFile file, string teamProfileTypeCode) { - PimsPerson matchingPerson = file?.PimsAcquisitionFileTeams?.FirstOrDefault(x => x.AcqFlTeamProfileTypeCode == teamProfileTypeCode)?.Person; - return matchingPerson?.GetFullName() ?? string.Empty; + var matchingTeamMember = file?.PimsAcquisitionFileTeams?.FirstOrDefault(x => x.AcqFlTeamProfileTypeCode == teamProfileTypeCode); + + if(matchingTeamMember is not null) + { + return matchingTeamMember.PersonId.HasValue ? matchingTeamMember.Person?.GetFullName() : matchingTeamMember.Organization.Name; + } + + return string.Empty; } private static string GetNullableDate(DateTime? dateTime) diff --git a/source/backend/api/Services/AcquisitionFileService.cs b/source/backend/api/Services/AcquisitionFileService.cs index 541deaf73f..94e4f8b1c1 100644 --- a/source/backend/api/Services/AcquisitionFileService.cs +++ b/source/backend/api/Services/AcquisitionFileService.cs @@ -121,7 +121,7 @@ public List GetAcquisitionFileExport(AcquisitionFilt FileAcquisitionCompleted = fileProperty.file.CompletionDate.HasValue ? fileProperty.file.CompletionDate.Value.ToString("dd-MMM-yyyy") : string.Empty, FilePhysicalStatus = fileProperty.file.AcqPhysFileStatusTypeCodeNavigation is not null ? fileProperty.file.AcqPhysFileStatusTypeCodeNavigation.Description : string.Empty, FileAcquisitionType = fileProperty.file.AcquisitionTypeCodeNavigation is not null ? fileProperty.file.AcquisitionTypeCodeNavigation.Description : string.Empty, - FileAcquisitionTeam = string.Join(", ", fileProperty.file.PimsAcquisitionFileTeams.Select(x => x.Person.GetFullName(true))), + FileAcquisitionTeam = string.Join(", ", fileProperty.file.PimsAcquisitionFileTeams.Select(x => x.PersonId.HasValue ? x.Person.GetFullName(true) : x.Organization.Name)), FileAcquisitionOwners = string.Join(", ", fileProperty.file.PimsAcquisitionOwners.Select(x => x.FormatOwnerName())), }).ToList(); } diff --git a/source/backend/dal/Models/AcquisitionReportFilterModel.cs b/source/backend/dal/Models/AcquisitionReportFilterModel.cs index 796b2368c6..8425775ba4 100644 --- a/source/backend/dal/Models/AcquisitionReportFilterModel.cs +++ b/source/backend/dal/Models/AcquisitionReportFilterModel.cs @@ -7,5 +7,7 @@ public class AcquisitionReportFilterModel public IEnumerable Projects { get; set; } public IEnumerable AcquisitionTeamPersons { get; set; } + + public IEnumerable AcquisitionTeamOrganizations { get; set; } } } diff --git a/source/backend/dal/Repositories/AgreementRepository.cs b/source/backend/dal/Repositories/AgreementRepository.cs index 0f66dfd1e7..1fd13ee88b 100644 --- a/source/backend/dal/Repositories/AgreementRepository.cs +++ b/source/backend/dal/Repositories/AgreementRepository.cs @@ -53,16 +53,25 @@ public List SearchAgreements(AcquisitionReportFilterModel filter) { predicate.And(a => a.AcquisitionFile.ProjectId.HasValue && filter.Projects.Contains(a.AcquisitionFile.ProjectId.Value)); } + if (filter.AcquisitionTeamPersons != null && filter.AcquisitionTeamPersons.Any()) { predicate.And(a => a.AcquisitionFile.PimsAcquisitionFileTeams.Any(afp => afp.PersonId.HasValue && filter.AcquisitionTeamPersons.Contains((long)afp.PersonId))); } + if (filter.AcquisitionTeamOrganizations != null && filter.AcquisitionTeamOrganizations.Any()) + { + predicate.And(a => a.AcquisitionFile.PimsAcquisitionFileTeams.Any(o => o.OrganizationId.HasValue && filter.AcquisitionTeamOrganizations.Contains((long)o.OrganizationId))); + } + var query = Context.PimsAgreements .Include(a => a.AgreementTypeCodeNavigation) .Include(a => a.AcquisitionFile) .ThenInclude(a => a.PimsAcquisitionFileTeams) .ThenInclude(afp => afp.Person) + .Include(a => a.AcquisitionFile) + .ThenInclude(a => a.PimsAcquisitionFileTeams) + .ThenInclude(o => o.Organization) .Include(a => a.AcquisitionFile) .ThenInclude(a => a.Project) .Include(a => a.AcquisitionFile) diff --git a/source/backend/dal/Repositories/CompReqFinancialRepository.cs b/source/backend/dal/Repositories/CompReqFinancialRepository.cs index d0369bf701..e55c67147f 100644 --- a/source/backend/dal/Repositories/CompReqFinancialRepository.cs +++ b/source/backend/dal/Repositories/CompReqFinancialRepository.cs @@ -57,6 +57,10 @@ public IEnumerable SearchCompensationRequisitionFinancials .ThenInclude(cr => cr.AcquisitionFile) .ThenInclude(a => a.PimsAcquisitionFileTeams) .ThenInclude(afp => afp.Person) + .Include(f => f.CompensationRequisition) + .ThenInclude(cr => cr.AcquisitionFile) + .ThenInclude(a => a.PimsAcquisitionFileTeams) + .ThenInclude(o => o.Organization) .Include(f => f.CompensationRequisition) .ThenInclude(cr => cr.AcquisitionFile) .ThenInclude(a => a.Project) @@ -71,11 +75,17 @@ public IEnumerable SearchCompensationRequisitionFinancials (f.CompensationRequisition.AlternateProjectId.HasValue && filter.Projects.Contains(f.CompensationRequisition.AlternateProjectId.Value)) || (!f.CompensationRequisition.AlternateProjectId.HasValue && f.CompensationRequisition.AcquisitionFile.ProjectId.HasValue && filter.Projects.Contains(f.CompensationRequisition.AcquisitionFile.ProjectId.Value))); } + if (filter.AcquisitionTeamPersons != null && filter.AcquisitionTeamPersons.Any()) { query = query.Where(f => f.CompensationRequisition.AcquisitionFile.PimsAcquisitionFileTeams.Any(afp => afp.PersonId.HasValue && filter.AcquisitionTeamPersons.Contains((long)afp.PersonId))); } + if (filter.AcquisitionTeamOrganizations != null && filter.AcquisitionTeamOrganizations.Any()) + { + query = query.Where(f => f.CompensationRequisition.AcquisitionFile.PimsAcquisitionFileTeams.Any(o => o.OrganizationId.HasValue && filter.AcquisitionTeamOrganizations.Contains((long)o.OrganizationId))); + } + return query.ToList(); } } diff --git a/source/backend/tests/unit/api/Models/AgreementReportModelTest.cs b/source/backend/tests/unit/api/Models/AgreementReportModelTest.cs index ad5a7d6711..7d759716cd 100644 --- a/source/backend/tests/unit/api/Models/AgreementReportModelTest.cs +++ b/source/backend/tests/unit/api/Models/AgreementReportModelTest.cs @@ -10,11 +10,11 @@ namespace Pims.Api.Test public class AgreementReportModelTest { [Fact] - public void AgreementReportModel_TeamMember() + public void AgreementReportModel_TeamMember_Person() { // Arrange var testAgreement = new Dal.Entities.PimsAgreement(); - var propCoord = new PimsAcquisitionFileTeam() { AcqFlTeamProfileTypeCode = "PROPCOORD", Person = new PimsPerson() { Surname = "test" } }; + var propCoord = new PimsAcquisitionFileTeam() { AcqFlTeamProfileTypeCode = "PROPCOORD", PersonId = 1, Person = new PimsPerson() { PersonId = 1, Surname = "test" } }; testAgreement.AcquisitionFile = new Dal.Entities.PimsAcquisitionFile() { PimsAcquisitionFileTeams = new List() { propCoord } }; // Act @@ -24,6 +24,21 @@ public void AgreementReportModel_TeamMember() model.PropertyCoordinator.Should().Be("test"); } + [Fact] + public void AgreementReportModel_TeamMember_Organization() + { + // Arrange + var testAgreement = new Dal.Entities.PimsAgreement(); + var propCoord = new PimsAcquisitionFileTeam() { AcqFlTeamProfileTypeCode = "PROPCOORD", OrganizationId = 100, Organization = new PimsOrganization() { OrganizationId = 100, Name = "FORTIS BC" } }; + testAgreement.AcquisitionFile = new Dal.Entities.PimsAcquisitionFile() { PimsAcquisitionFileTeams = new List() { propCoord } }; + + // Act + var model = new AgreementReportModel(testAgreement, new System.Security.Claims.ClaimsPrincipal()); + + // Assert + model.PropertyCoordinator.Should().Be("FORTIS BC"); + } + [Fact] public void AgreementReportModel_TeamMember_Null() { diff --git a/source/backend/tests/unit/dal/Repositories/AgreementRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/AgreementRepositoryTest.cs index 0e040b999a..1696526035 100644 --- a/source/backend/tests/unit/dal/Repositories/AgreementRepositoryTest.cs +++ b/source/backend/tests/unit/dal/Repositories/AgreementRepositoryTest.cs @@ -49,7 +49,7 @@ public void SearchAgreement_Project() } [Fact] - public void SearchAgreement_Team() + public void SearchAgreement_Team_Person() { // Arrange var helper = new TestHelper(); @@ -70,6 +70,28 @@ public void SearchAgreement_Team() result.Should().HaveCount(1); } + [Fact] + public void SearchAgreement_Team_Organization() + { + // Arrange + var helper = new TestHelper(); + var user = PrincipalHelper.CreateForPermission(Permissions.AcquisitionFileAdd); + var acqFile = EntityHelper.CreateAcquisitionFile(); + acqFile.PimsAcquisitionFileTeams = new List() { new PimsAcquisitionFileTeam() { OrganizationId = 100 } }; + var agreement = new PimsAgreement() { AcquisitionFile = acqFile, AgreementTypeCodeNavigation = new PimsAgreementType() { AgreementTypeCode = "test" } }; + var filter = new AcquisitionReportFilterModel() { AcquisitionTeamOrganizations = new List { 100 } }; + + helper.CreatePimsContext(user, true).AddAndSaveChanges(agreement); + + var repository = helper.CreateRepository(user); + + // Act + var result = repository.SearchAgreements(filter); + + // Assert + result.Should().HaveCount(1); + } + [Fact] public void SearchAgreement_TeamAndProject() { diff --git a/source/backend/tests/unit/dal/Repositories/CompReqFinancialRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/CompReqFinancialRepositoryTest.cs index 5d09e8b711..796af0b625 100644 --- a/source/backend/tests/unit/dal/Repositories/CompReqFinancialRepositoryTest.cs +++ b/source/backend/tests/unit/dal/Repositories/CompReqFinancialRepositoryTest.cs @@ -158,7 +158,7 @@ public void SearchCompensationRequisitionFinancials_AlternateProject() } [Fact] - public void SearchCompensationRequisitionFinancials_Team() + public void SearchCompensationRequisitionFinancials_Team_Person() { // Arrange var acqFile = EntityHelper.CreateAcquisitionFile(); @@ -184,5 +184,33 @@ public void SearchCompensationRequisitionFinancials_Team() // Assert result.Should().HaveCount(1); } + + [Fact] + public void SearchCompensationRequisitionFinancials_Team_Organization() + { + // Arrange + var acqFile = EntityHelper.CreateAcquisitionFile(); + acqFile.PimsAcquisitionFileTeams = new List() { new PimsAcquisitionFileTeam() { OrganizationId = 100 } }; + var financial = new PimsCompReqFinancial + { + FinancialActivityCode = new PimsFinancialActivityCode { Code = "test" }, + CompensationRequisitionId = 1, + CompensationRequisition = new PimsCompensationRequisition + { + AcquisitionFileId = acqFile.Internal_Id, + AcquisitionFile = acqFile, + }, + }; + + var repository = this.CreateWithPermissions(Permissions.AcquisitionFileAdd); + this._helper.AddAndSaveChanges(financial); + + // Act + var filter = new AcquisitionReportFilterModel() { AcquisitionTeamOrganizations = new List { 100 } }; + var result = repository.SearchCompensationRequisitionFinancials(filter); + + // Assert + result.Should().HaveCount(1); + } } } diff --git a/source/frontend/src/features/projects/reports/ProjectExportForm.test.tsx b/source/frontend/src/features/projects/reports/ProjectExportForm.test.tsx index f701dd484b..a5f1cfd851 100644 --- a/source/frontend/src/features/projects/reports/ProjectExportForm.test.tsx +++ b/source/frontend/src/features/projects/reports/ProjectExportForm.test.tsx @@ -2,6 +2,7 @@ import { createMemoryHistory } from 'history'; import { getMockPerson } from '@/mocks/contacts.mock'; import { mockLookups } from '@/mocks/lookups.mock'; +import { getMockOrganization } from '@/mocks/organization.mock'; import { mockProjects } from '@/mocks/projects.mock'; import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { act, render, RenderOptions, screen, userEvent } from '@/utils/test-utils'; @@ -97,9 +98,14 @@ describe('ProjectExportForm component', () => { expect(screen.getByText(/776/g)).not.toBeNull(); }); - it('displays team members when passed', async () => { + it('displays team members when passed person', async () => { const { getByDisplayValue } = setup({ - teamMembers: [getMockPerson({ id: 1, surname: 'last', firstName: 'first' })], + teamMembers: [ + { + personId: 1, + person: getMockPerson({ id: 1, surname: 'last', firstName: 'first' }), + }, + ], } as any); const select = getByDisplayValue(/Select Export Type.../i); @@ -111,6 +117,25 @@ describe('ProjectExportForm component', () => { expect(screen.getByText(/first last/g)).not.toBeNull(); }); + it('displays team members when passed organization', async () => { + const { getByDisplayValue } = setup({ + teamMembers: [ + { + organizationId: 100, + organization: getMockOrganization({ id: 100, name: 'FORTIS BC' }), + }, + ], + } as any); + + const select = getByDisplayValue(/Select Export Type.../i); + + await act(async () => { + userEvent.selectOptions(select, ProjectExportTypes.AGREEMENT); + }); + + expect(screen.getByText(/FORTIS BC/g)).not.toBeNull(); + }); + it('hides submit, project, team members by default', async () => { setup({} as any); diff --git a/source/frontend/src/features/projects/reports/ProjectExportForm.tsx b/source/frontend/src/features/projects/reports/ProjectExportForm.tsx index 35d27de5fd..62340e48d6 100644 --- a/source/frontend/src/features/projects/reports/ProjectExportForm.tsx +++ b/source/frontend/src/features/projects/reports/ProjectExportForm.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { Select, SelectOption } from '@/components/common/form'; import { SectionField } from '@/components/common/Section/SectionField'; -import { Api_Person } from '@/models/api/Person'; +import { Api_AcquisitionFileTeam } from '@/models/api/AcquisitionFile'; import { Api_Project } from '@/models/api/Project'; import { Api_ExportProjectFilter } from '@/models/api/ProjectFilter'; @@ -14,7 +14,7 @@ export interface IProjectExportFormProps { onExportTypeSelected: () => void; onExport: (filter: Api_ExportProjectFilter) => Promise; projects: Api_Project[]; - teamMembers: Api_Person[]; + teamMembers: Api_AcquisitionFileTeam[]; loading: boolean; } diff --git a/source/frontend/src/features/projects/reports/ProjectExportFormContent.tsx b/source/frontend/src/features/projects/reports/ProjectExportFormContent.tsx index 147b1f7b1a..21b1512443 100644 --- a/source/frontend/src/features/projects/reports/ProjectExportFormContent.tsx +++ b/source/frontend/src/features/projects/reports/ProjectExportFormContent.tsx @@ -28,8 +28,8 @@ export const ProjectExportFormContent: React.FunctionComponent(x => { return { - codeType: x?.id?.toString() ?? '', - codeTypeDescription: formatApiPersonNames(x), + codeType: x.personId ? `P-${x.personId}` : `O-${x.organizationId}`, + codeTypeDescription: x.personId ? formatApiPersonNames(x.person) : x.organization?.name ?? '', }; }); diff --git a/source/frontend/src/features/projects/reports/models.ts b/source/frontend/src/features/projects/reports/models.ts index a85e01ea15..69a07c5925 100644 --- a/source/frontend/src/features/projects/reports/models.ts +++ b/source/frontend/src/features/projects/reports/models.ts @@ -1,6 +1,8 @@ import { CodeTypeSelectOption } from '@/components/maps/leaflet/Control/AdvancedFilter/models'; import { Api_ExportProjectFilter } from '@/models/api/ProjectFilter'; +type IdSelector = 'O' | 'P'; + export class ExportProjectModel { public exportType: keyof typeof ProjectExportTypes | '' = ''; public projects: CodeTypeSelectOption[] = []; @@ -10,7 +12,8 @@ export class ExportProjectModel { return { type: this.exportType === '' ? undefined : this.exportType, projects: this.projects.map(p => +p.codeType), - acquisitionTeamPersons: this.acquisitionTeam.map(t => +t.codeType), + acquisitionTeamPersons: getParameterIdFromOptions(this.acquisitionTeam), + acquisitionTeamOrganizations: getParameterIdFromOptions(this.acquisitionTeam, 'O'), }; } } @@ -19,3 +22,22 @@ export enum ProjectExportTypes { COMPENSATION = 'Compensation Requisition Export', AGREEMENT = 'Agreement Export', } + +const getParameterIdFromOptions = ( + options: CodeTypeSelectOption[], + selector: IdSelector = 'P', +): number[] => { + if (!options.length) { + return []; + } + + var filteredItems = options.filter(option => String(option.codeType).startsWith(selector)); + if (!filteredItems.length) { + return []; + } + + return filteredItems.map(x => { + var number = x.codeType.split('-').pop() ?? ''; + return parseInt(number) ?? 0; + }); +}; diff --git a/source/frontend/src/models/api/ProjectFilter.ts b/source/frontend/src/models/api/ProjectFilter.ts index d7d883893f..fd014cdb9d 100644 --- a/source/frontend/src/models/api/ProjectFilter.ts +++ b/source/frontend/src/models/api/ProjectFilter.ts @@ -3,5 +3,6 @@ import { ProjectExportTypes } from '@/features/projects/reports/models'; export interface Api_ExportProjectFilter { projects: number[]; acquisitionTeamPersons: number[]; + acquisitionTeamOrganizations: number[]; type?: keyof typeof ProjectExportTypes; }