Skip to content

Commit

Permalink
psp-9256 allow users added to a project to view an acquisition file. (#…
Browse files Browse the repository at this point in the history
…4555)

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

* test corrections.

Signed-off-by: devinleighsmith <[email protected]>

* snapshot updates.

* final snapshot

* add the list of project team members to acquisition files.

* Remove unecessary mapping for person entity.

---------

Signed-off-by: devinleighsmith <[email protected]>
  • Loading branch information
devinleighsmith authored Dec 30, 2024
1 parent 11bb53f commit c3a630e
Show file tree
Hide file tree
Showing 31 changed files with 427 additions and 21 deletions.
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,26 @@
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)
.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

0 comments on commit c3a630e

Please sign in to comment.