Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

psp-9256 allow users added to a project to view an acquisition file. #4555

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions source/backend/api/Helpers/Extensions/AcquisitionFileExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,41 @@ namespace Pims.Api.Helpers.Extensions
{
public static class AcquisitionFileExtensions
{
public static void ThrowMissingContractorInTeam(this PimsAcquisitionFile acquisitionFile, ClaimsPrincipal principal, IUserRepository userRepository)
public static void ThrowMissingContractorInTeam(this PimsAcquisitionFile acquisitionFile, ClaimsPrincipal principal, IUserRepository userRepository, IProjectRepository projectRepository)
{
ArgumentNullException.ThrowIfNull(acquisitionFile);

ArgumentNullException.ThrowIfNull(principal);

var pimsUser = userRepository.GetUserInfoByKeycloakUserId(principal.GetUserKey());

if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId))
PimsProject project = null;
if (acquisitionFile.ProjectId.HasValue)
{
project = projectRepository.TryGet(acquisitionFile.ProjectId.Value);
}

if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId) && (project == null || !project.PimsProjectPeople.Any(x => x.PersonId == pimsUser.PersonId)))
{
throw new ContractorNotInTeamException("As a Contractor your user contact information should be assigned to the Acquisition File's team");
}
}

public static void ThrowContractorRemovedFromTeam(this PimsAcquisitionFile acquisitionFile, ClaimsPrincipal principal, IUserRepository userRepository)
public static void ThrowContractorRemovedFromTeam(this PimsAcquisitionFile acquisitionFile, ClaimsPrincipal principal, IUserRepository userRepository, IProjectRepository projectRepository)
{
ArgumentNullException.ThrowIfNull(acquisitionFile);

ArgumentNullException.ThrowIfNull(principal);

var pimsUser = userRepository.GetUserInfoByKeycloakUserId(principal.GetUserKey());

if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId))
PimsProject project = null;
if (acquisitionFile.ProjectId.HasValue)
{
project = projectRepository.TryGet(acquisitionFile.ProjectId.Value);
}

if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId) && (project == null || !project.PimsProjectPeople.Any(x => x.PersonId == pimsUser.PersonId)))
{
throw new UserOverrideException(UserOverrideCode.ContractorSelfRemoved, "Contractors cannot remove themselves from a file. Please contact the admin at [email protected]");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static void ThrowInvalidAccessToAcquisitionFile(this ClaimsPrincipal prin
var pimsUser = userRepository.GetUserInfoByKeycloakUserId(principal.GetUserKey());
PimsAcquisitionFile acquisitionFile = acquisitionFileRepository.GetById(acquisitionFileId);

if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId))
if (pimsUser?.IsContractor == true && !acquisitionFile.PimsAcquisitionFileTeams.Any(x => x.PersonId == pimsUser.PersonId) && (acquisitionFile.Project == null || !acquisitionFile.Project.PimsProjectPeople.Any(x => x.PersonId == pimsUser.PersonId)))
{
throw new NotAuthorizedException("Contractor is not assigned to the Acquisition File's team");
}
Expand Down
8 changes: 6 additions & 2 deletions source/backend/api/Services/AcquisitionFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class AcquisitionFileService : IAcquisitionFileService
private readonly ITakeRepository _takeRepository;
private readonly IAcquisitionStatusSolver _statusSolver;
private readonly IPropertyService _propertyService;
private readonly IProjectRepository _projectRepository;

public AcquisitionFileService(
ClaimsPrincipal user,
Expand All @@ -55,6 +56,7 @@ public AcquisitionFileService(
ICompReqFinancialService compReqFinancialService,
IExpropriationPaymentRepository expropriationPaymentRepository,
ITakeRepository takeRepository,
IProjectRepository projectRepository,
IAcquisitionStatusSolver statusSolver,
IPropertyService propertyService)
{
Expand All @@ -75,6 +77,7 @@ public AcquisitionFileService(
_takeRepository = takeRepository;
_statusSolver = statusSolver;
_propertyService = propertyService;
_projectRepository = projectRepository;
}

public Paged<PimsAcquisitionFile> GetPage(AcquisitionFilter filter)
Expand Down Expand Up @@ -206,7 +209,8 @@ public PimsAcquisitionFile Add(PimsAcquisitionFile acquisitionFile, IEnumerable<

_logger.LogInformation("Adding acquisition file with id {id}", acquisitionFile.Internal_Id);
_user.ThrowIfNotAuthorized(Permissions.AcquisitionFileAdd);
acquisitionFile.ThrowMissingContractorInTeam(_user, _userRepository);

acquisitionFile.ThrowMissingContractorInTeam(_user, _userRepository, _projectRepository);

// validate the new acq region
var cannotDetermineRegion = _lookupRepository.GetAllRegions().FirstOrDefault(x => x.RegionName == "Cannot determine");
Expand Down Expand Up @@ -274,7 +278,7 @@ public PimsAcquisitionFile Update(PimsAcquisitionFile acquisitionFile, IEnumerab
ValidateStaff(acquisitionFile);
ValidateOrganizationStaff(acquisitionFile);

acquisitionFile.ThrowContractorRemovedFromTeam(_user, _userRepository);
acquisitionFile.ThrowContractorRemovedFromTeam(_user, _userRepository, _projectRepository);

ValidatePayeeDependency(acquisitionFile);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public void Register(TypeAdapterConfig config)
.Map(dest => dest.Description, src => src.Description)
.Map(dest => dest.Note, src => src.Note)
.Map(dest => dest.ProjectProducts, src => src.PimsProjectProducts)
.Map(dest => dest.ProjectPersons, src => src.PimsProjectPeople)
.Map(dest => dest.AppLastUpdateUserid, src => src.AppLastUpdateUserid)
.Map(dest => dest.AppLastUpdateTimestamp, src => src.AppLastUpdateTimestamp)
.Inherits<Entity.IBaseEntity, BaseConcurrentModel>();
Expand All @@ -34,6 +35,7 @@ public void Register(TypeAdapterConfig config)
.Map(dest => dest.Description, src => src.Description)
.Map(dest => dest.Note, src => src.Note)
.Map(dest => dest.PimsProjectProducts, src => src.ProjectProducts)
.Map(dest => dest.PimsProjectPeople, src => src.ProjectPersons)
.Inherits<BaseConcurrentModel, Entity.IBaseEntity>();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public class ProjectModel : BaseAuditModel
/// get/set - Project products.
/// </summary>
public List<ProjectProductModel> ProjectProducts { get; set; }

/// <summary>
/// get/set - Project persons.
/// </summary>
public List<ProjectPersonModel> ProjectPersons { get; set; }
#endregion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Mapster;
using Pims.Api.Models.Base;
using Entity = Pims.Dal.Entities;

namespace Pims.Api.Models.Concepts.Project
{
public class ProjectPersonMap : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Entity.PimsProjectPerson, ProjectPersonModel>()
.Map(dest => dest.Id, src => src.ProjectPersonId)
.Map(dest => dest.ProjectId, src => src.ProjectId)
.Map(dest => dest.Person, src => src.Person)
.Map(dest => dest.PersonId, src => src.PersonId)
.Map(dest => dest.Project, src => src.Project)
.Inherits<Entity.IBaseEntity, BaseConcurrentModel>();

config.NewConfig<ProjectPersonModel, Entity.PimsProjectPerson>()
.Map(dest => dest.ProjectPersonId, src => src.Id)
.Map(dest => dest.ProjectId, src => src.ProjectId)
.Map(dest => dest.PersonId, src => src.PersonId)
.Map(dest => dest.Person, src => src.Person)
.Inherits<BaseConcurrentModel, Entity.IBaseEntity>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Pims.Api.Models.Base;
using Pims.Api.Models.Concepts.Person;

namespace Pims.Api.Models.Concepts.Project
{
public class ProjectPersonModel : BaseAuditModel
{
#region Properties

public long? Id { get; set; }

public long? ProjectId { get; set; }

public ProjectModel Project { get; set; }

public long PersonId { get; set; }

public PersonModel Person { get; set; }

#endregion
}
}
5 changes: 4 additions & 1 deletion source/backend/dal/Repositories/AcquisitionFileRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public PimsAcquisitionFile GetById(long id)
.ThenInclude(x => x.CostTypeCode)
.Include(r => r.Project)
.ThenInclude(x => x.BusinessFunctionCode)
.Include(r => r.Project)
.ThenInclude(x => x.PimsProjectPeople)
.Include(r => r.Product)
.Include(r => r.PimsPropertyAcquisitionFiles)
.Include(r => r.PimsAcquisitionFileTeams)
Expand Down Expand Up @@ -887,7 +889,7 @@ private IQueryable<PimsAcquisitionFile> GetCommonAcquisitionFileQueryDeep(Acquis

if (contractorPersonId is not null)
{
predicate = predicate.And(acq => acq.PimsAcquisitionFileTeams.Any(x => x.PersonId == contractorPersonId));
predicate = predicate.And(acq => acq.PimsAcquisitionFileTeams.Any(x => x.PersonId == contractorPersonId) || (acq.Project != null && acq.Project.PimsProjectPeople.Any(x => x.PersonId == contractorPersonId)));
}

if (!string.IsNullOrWhiteSpace(filter.AcquisitionTeamMemberPersonId))
Expand All @@ -903,6 +905,7 @@ private IQueryable<PimsAcquisitionFile> GetCommonAcquisitionFileQueryDeep(Acquis
var query = Context.PimsAcquisitionFiles.AsNoTracking()
.Include(r => r.RegionCodeNavigation)
.Include(p => p.Project)
.ThenInclude(p => p.PimsProjectPeople)
.Include(s => s.AcquisitionFileStatusTypeCodeNavigation)
.Include(f => f.AcquisitionFundingTypeCodeNavigation)
.Include(ph => ph.AcqPhysFileStatusTypeCodeNavigation)
Expand Down
3 changes: 3 additions & 0 deletions source/backend/dal/Repositories/ProjectRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public PimsProject TryGet(long id)
.AsNoTracking()
.Include(x => x.PimsProjectProducts)
.ThenInclude(x => x.Product)
.Include(x => x.PimsProjectPeople)
.ThenInclude(x => x.Person)
.Include(x => x.ProjectStatusTypeCodeNavigation)
.Include(x => x.RegionCodeNavigation)
.Include(x => x.CostTypeCode)
Expand Down Expand Up @@ -146,6 +148,7 @@ public PimsProject Update(PimsProject project)
Func<PimsContext, PimsProjectProduct, bool> canDeleteGrandchild = (context, pa) => !context.PimsProducts.Any(o => o.Id == pa.ProductId);

this.Context.UpdateGrandchild<PimsProject, long, PimsProjectProduct>(p => p.PimsProjectProducts, pp => pp.Product, project.Id, project.PimsProjectProducts.ToArray(), canDeleteGrandchild);
this.Context.UpdateGrandchild<PimsProject, long, PimsProjectPerson>(p => p.PimsProjectPeople, pp => pp.Person, project.Id, project.PimsProjectPeople.ToArray(), true);

Context.Entry(existingProject).CurrentValues.SetValues(project);

Expand Down
13 changes: 13 additions & 0 deletions source/backend/entities/Partials/ProjectPerson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace Pims.Dal.Entities
{
/// <summary>
/// PimsProjectPerson class, provides an entity for the datamodel to manage project persons.
/// </summary>
public partial class PimsProjectPerson : StandardIdentityBaseAppEntity<long>, IBaseAppEntity
{
[NotMapped]
public override long Internal_Id { get => this.ProjectPersonId; set => this.ProjectPersonId = value; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type IContactInputContainerProps = {
restrictContactType?: RestrictContactType;
displayErrorAsTooltip?: boolean;
onContactSelected?: (contact: IContactSearchResult) => void;
placeholder?: string;
};

export const ContactInputContainer: React.FC<
Expand All @@ -27,6 +28,7 @@ export const ContactInputContainer: React.FC<
restrictContactType,
displayErrorAsTooltip = true,
onContactSelected,
placeholder,
}) => {
const [showContactManager, setShowContactManager] = useState(false);
const [selectedContacts, setSelectedContacts] = useState<IContactSearchResult[]>([]);
Expand Down Expand Up @@ -67,6 +69,7 @@ export const ContactInputContainer: React.FC<
showActiveSelector: true,
restrictContactType: restrictContactType,
}}
placeholder={placeholder}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type OptionalAttributes = {
label?: string;
required?: boolean;
displayErrorTooltips?: boolean;
placeholder?: string;
};

export type IContactInputViewProps = FormControlProps & OptionalAttributes & RequiredAttributes;
Expand All @@ -39,17 +40,18 @@ const ContactInputView: React.FunctionComponent<IContactInputViewProps> = ({
onClear,
setShowContactManager,
contactManagerProps,
placeholder,
}) => {
const { errors, touched, values } = useFormikContext<any>();
const error = getIn(errors, field);
const touch = getIn(touched, field);
const contactInfo: IContactSearchResult | undefined = getIn(values, field);
const errorTooltip = error && touch && displayErrorTooltips ? error : undefined;

let text = 'Select from contacts';
let text = placeholder ?? 'Select from contacts';

if (contactInfo !== undefined) {
text = formatContactSearchResult(contactInfo, 'Select from contacts');
text = formatContactSearchResult(contactInfo, placeholder ?? 'Select from contacts');
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LoadingBackdrop from '@/components/common/LoadingBackdrop';
import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext';
import { InventoryTabNames } from '@/features/mapSideBar/property/InventoryTabs';
import { useAcquisitionProvider } from '@/hooks/repositories/useAcquisitionProvider';
import { useProjectProvider } from '@/hooks/repositories/useProjectProvider';
import { usePropertyAssociations } from '@/hooks/repositories/usePropertyAssociations';
import { useQuery } from '@/hooks/use-query';
import useApiUserOverride from '@/hooks/useApiUserOverride';
Expand Down Expand Up @@ -84,6 +85,9 @@ export const AcquisitionContainer: React.FunctionComponent<IAcquisitionContainer
getAcquisitionFileChecklist: { execute: retrieveAcquisitionFileChecklist },
getLastUpdatedBy: { execute: getLastUpdatedBy, loading: loadingGetLastUpdatedBy },
} = useAcquisitionProvider();
const {
getProject: { execute: getProjectFunction },
} = useProjectProvider();

const { setModalContent, setDisplayModal } = useModalContext();
const { execute: getPropertyAssociations } = usePropertyAssociations();
Expand Down Expand Up @@ -132,6 +136,9 @@ export const AcquisitionContainer: React.FunctionComponent<IAcquisitionContainer
const fetchAcquisitionFile = useCallback(async () => {
const retrieved = await retrieveAcquisitionFile(acquisitionFileId);
if (exists(retrieved)) {
if (isValidId(retrieved.projectId)) {
retrieved.project = await getProjectFunction(retrieved.projectId);
}
// retrieve related entities (ie properties, checklist items) in parallel
const acquisitionPropertiesTask = retrieveAcquisitionFileProperties(acquisitionFileId);
const acquisitionChecklistTask = retrieveAcquisitionFileChecklist(acquisitionFileId);
Expand All @@ -152,6 +159,7 @@ export const AcquisitionContainer: React.FunctionComponent<IAcquisitionContainer
retrieveAcquisitionFileChecklist,
setFile,
setStaleFile,
getProjectFunction,
]);

const fetchLastUpdatedBy = React.useCallback(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class AcquisitionForm implements WithAcquisitionTeam, WithAcquisitionOwne
acquisitionTypeCode: toTypeCodeNullable(this.acquisitionType),
regionCode: toTypeCodeNullable(Number(this.region)),
projectId: isValidId(this.project?.id) ? this.project!.id : null,
productId: this.product !== '' ? Number(this.product) : null,
productId: isValidId(Number(this.product)) ? Number(this.product) : null,
fundingTypeCode: toTypeCodeNullable(this.fundingTypeCode),
fundingOther: this.fundingTypeOtherDescription,
subfileInterestTypeCode: toTypeCodeNullable(this.subfileInterestTypeCode),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ const AcquisitionSummaryView: React.FC<IAcquisitionSummaryViewProps> = ({
{acquisitionFile?.fundingTypeCode?.id === 'OTHER' && (
<SectionField label="Other funding">{acquisitionFile.fundingOther}</SectionField>
)}
{acquisitionFile?.project?.projectPersons?.map((teamMember, index) => (
<React.Fragment key={`project-team-${index}`}>
<SectionField label="Management team member">
<StyledLink
target="_blank"
rel="noopener noreferrer"
to={`/contact/P${teamMember?.personId}`}
>
<span>{formatApiPersonNames(teamMember?.person)}</span>
<FaExternalLinkAlt className="ml-2" size="1rem" />
</StyledLink>
</SectionField>
</React.Fragment>
))}
</Section>
<Section header="Schedule">
<SectionField label="Assigned date" valueTestId="assigned-date">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Section } from '@/components/common/Section/Section';
import { SectionField } from '@/components/common/Section/SectionField';

import { ProjectForm } from '../models';
import AddProjectTeamSubForm from './AddProjectTeamSubForm';
import ProductsArrayForm from './ProductsArrayForm';

export interface IAddProjectFormProps {
Expand Down Expand Up @@ -99,6 +100,9 @@ const AddProjectForm = React.forwardRef<FormikProps<ProjectForm>, IAddProjectFor
</SectionField>
</Section>
<ProductsArrayForm formikProps={formikProps} field="products" />
<Section header="Project Management Team">
<AddProjectTeamSubForm />
</Section>
</Form>
</StyledFormWrapper>
)}
Expand Down
Loading
Loading