diff --git a/source/backend/api/Areas/Acquisition/Controllers/AcquisitionFileController.cs b/source/backend/api/Areas/Acquisition/Controllers/AcquisitionFileController.cs index e0b7b4f76d..477881d1fa 100644 --- a/source/backend/api/Areas/Acquisition/Controllers/AcquisitionFileController.cs +++ b/source/backend/api/Areas/Acquisition/Controllers/AcquisitionFileController.cs @@ -83,6 +83,21 @@ public IActionResult GetAcquisitionFile(long id) return new JsonResult(_mapper.Map(acqFile)); } + /// + /// Gets the specified acquisition file last updated-by information. + /// + /// + [HttpGet("{id:long}/updateInfo")] + [HasPermission(Permissions.AcquisitionFileView)] + [Produces("application/json")] + [ProducesResponseType(typeof(Dal.Entities.Models.LastUpdatedByModel), 200)] + [SwaggerOperation(Tags = new[] { "acquisitionfile" })] + public IActionResult GetLastUpdatedBy(long id) + { + var lastUpdated = _acquisitionService.GetLastUpdateInformation(id); + return new JsonResult(lastUpdated); + } + /// /// Adds the specified acquisition file. /// diff --git a/source/backend/api/Services/AcquisitionFileService.cs b/source/backend/api/Services/AcquisitionFileService.cs index a4da0fc0ba..43def3cd26 100644 --- a/source/backend/api/Services/AcquisitionFileService.cs +++ b/source/backend/api/Services/AcquisitionFileService.cs @@ -134,6 +134,14 @@ public PimsAcquisitionFile GetById(long id) return acqFile; } + public LastUpdatedByModel GetLastUpdateInformation(long acquisitionFileId) + { + _logger.LogInformation("Retrieving last updated-by information..."); + _user.ThrowIfNotAuthorized(Permissions.AcquisitionFileView); + + return _acqFileRepository.GetLastUpdateBy(acquisitionFileId); + } + public IEnumerable GetProperties(long id) { _logger.LogInformation("Getting acquisition file with id {id}", id); diff --git a/source/backend/api/Services/IAcquisitionFileService.cs b/source/backend/api/Services/IAcquisitionFileService.cs index f684d477b2..3459355667 100644 --- a/source/backend/api/Services/IAcquisitionFileService.cs +++ b/source/backend/api/Services/IAcquisitionFileService.cs @@ -11,6 +11,8 @@ public interface IAcquisitionFileService PimsAcquisitionFile GetById(long id); + LastUpdatedByModel GetLastUpdateInformation(long acquisitionFileId); + PimsAcquisitionFile Add(PimsAcquisitionFile acquisitionFile, IEnumerable userOverrides); PimsAcquisitionFile Update(PimsAcquisitionFile acquisitionFile, IEnumerable userOverrides); diff --git a/source/backend/dal/Repositories/AcquisitionFileRepository.cs b/source/backend/dal/Repositories/AcquisitionFileRepository.cs index 3977e13d71..f78f2efab5 100644 --- a/source/backend/dal/Repositories/AcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/AcquisitionFileRepository.cs @@ -124,6 +124,438 @@ public PimsAcquisitionFile GetById(long id) .FirstOrDefault(x => x.AcquisitionFileId == id) ?? throw new KeyNotFoundException(); } + /// + /// Retrieves the acquisition file with the specified id last update information. + /// + /// + /// + public LastUpdatedByModel GetLastUpdateBy(long id) + { + // Acquisition File + var lastUpdatedByAggregate = new List(); + var fileLastUpdatedBy = this.Context.PimsAcquisitionFiles.AsNoTracking() + .Where(a => a.AcquisitionFileId == id) + .Select(a => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = a.AppLastUpdateUserid, + AppLastUpdateUserGuid = a.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = a.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(fileLastUpdatedBy); + + // Acquisition Owners + var ownersLastUpdatedBy = this.Context.PimsAcquisitionOwners.AsNoTracking() + .Where(ao => ao.AcquisitionFileId == id) + .Select(ao => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = ao.AppLastUpdateUserid, + AppLastUpdateUserGuid = ao.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = ao.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(ownersLastUpdatedBy); + + // Acquisition Deleted Owners + var ownersHistLastUpdatedBy = this.Context.PimsAcquisitionOwnerHists.AsNoTracking() + .Where(aoh => aoh.AcquisitionFileId == id) + .Select(aoh => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aoh.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aoh.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aoh.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(ownersHistLastUpdatedBy); + + // Acquisition Checklist items + var checklistLastUpdatedBy = this.Context.PimsAcquisitionChecklistItems.AsNoTracking() + .Where(ac => ac.AcquisitionFileId == id) + .Select(ac => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = ac.AppLastUpdateUserid, + AppLastUpdateUserGuid = ac.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = ac.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(checklistLastUpdatedBy); + + // Acquisition Team + var teamLastUpdatedBy = this.Context.PimsAcquisitionFilePeople.AsNoTracking() + .Where(ap => ap.AcquisitionFileId == id) + .Select(ap => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = ap.AppLastUpdateUserid, + AppLastUpdateUserGuid = ap.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = ap.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(teamLastUpdatedBy); + + // Acquisition Deleted Team + // This is needed to get the acquisition team last-updated-by when deleted + var teamHistLastUpdatedBy = this.Context.PimsAcquisitionFilePersonHists.AsNoTracking() + .Where(aph => aph.AcquisitionFileId == id) + .Select(aph => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aph.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aph.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aph.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(teamHistLastUpdatedBy); + + // Acquisition Interest Holders + var interestHolderLastUpdatedBy = this.Context.PimsInterestHolders.AsNoTracking() + .Where(aih => aih.AcquisitionFileId == id) + .Select(aih => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aih.AppLastUpdateUserid, + AppLastUpdateUserGuid = aih.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = aih.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(interestHolderLastUpdatedBy); + + // Acquisition Deleted Interest Holders + // This is needed to get the acquisition interest holder last-updated-by when deleted + var interestHolderHistLastUpdatedBy = this.Context.PimsInterestHolderHists.AsNoTracking() + .Where(aihh => aihh.AcquisitionFileId == id) + .Select(aihh => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aihh.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aihh.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aihh.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(interestHolderHistLastUpdatedBy); + + // Acquisition Documents + var documentsLastUpdatedBy = this.Context.PimsAcquisitionFileDocuments.AsNoTracking() + .Where(ad => ad.AcquisitionFileId == id) + .Include(ad => ad.Document) + .Select(ad => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = ad.Document.AppLastUpdateUserid, + AppLastUpdateUserGuid = ad.Document.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = ad.Document.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(documentsLastUpdatedBy); + + // Acquisition Deleted Documents + // This is needed to get the document last-updated-by from the document that where deleted + var documentsHistoryLastUpdatedBy = this.Context.PimsAcquisitionFileDocumentHists.AsNoTracking() + .Where(adh => adh.AcquisitionFileId == id) + .Select(adh => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = adh.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = adh.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = adh.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(documentsHistoryLastUpdatedBy); + + // Acquisition Notes + var notesLastUpdatedBy = this.Context.PimsAcquisitionFileNotes.AsNoTracking() + .Where(an => an.AcquisitionFileId == id) + .Include(an => an.Note) + .Select(an => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = an.Note.AppLastUpdateUserid, + AppLastUpdateUserGuid = an.Note.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = an.Note.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(notesLastUpdatedBy); + + // Acquisition Deleted Notes + // This is needed to get the document last-updated-by from the document that where deleted + var notesHistoryLastUpdatedBy = this.Context.PimsAcquisitionFileNoteHists.AsNoTracking() + .Where(anh => anh.AcquisitionFileId == id) + .Select(anh => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = anh.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = anh.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = anh.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(notesHistoryLastUpdatedBy); + + // Acquisition Properties + var propertiesLastUpdatedBy = this.Context.PimsPropertyAcquisitionFiles.AsNoTracking() + .Where(ap => ap.AcquisitionFileId == id) + .Select(ap => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = ap.AppLastUpdateUserid, + AppLastUpdateUserGuid = ap.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = ap.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(propertiesLastUpdatedBy); + + // Acquisition Deleted Properties + // This is needed to get the notes last-updated-by from the notes that where deleted + var propertiesHistoryLastUpdatedBy = this.Context.PimsPropertyAcquisitionFileHists.AsNoTracking() + .Where(aph => aph.AcquisitionFileId == id) + .Select(aph => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aph.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aph.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aph.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(propertiesHistoryLastUpdatedBy); + + // Acquisition Takes + var takesLastUpdatedBy = this.Context.PimsTakes.AsNoTracking() + .Include(at => at.PropertyAcquisitionFile) + .Where(at => at.PropertyAcquisitionFile.AcquisitionFileId == id) + .Select(apt => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = apt.AppLastUpdateUserid, + AppLastUpdateUserGuid = apt.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = apt.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(takesLastUpdatedBy); + + // Acquisition Deleted Takes + // This is needed to get the notes last-updated-by from the notes that where deleted + var takeHists = this.Context.PimsTakeHists.AsNoTracking(); + var takesHistoryLastUpdatedBy = this.Context.PimsPropertyAcquisitionFileHists.AsNoTracking() + .Where(at => at.AcquisitionFileId == id) + .Join( + takeHists, + propAcqHist => propAcqHist.PropertyAcquisitionFileId, + takeHist => takeHist.PropertyAcquisitionFileId, + (acqPropHist, takeHist) => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = takeHist.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = takeHist.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = takeHist.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(takesHistoryLastUpdatedBy); + + // Acquisition Compensation Requisition + var compensationLastUpdatedBy = this.Context.PimsCompensationRequisitions.AsNoTracking() + .Where(acr => acr.AcquisitionFileId == id) + .Select(acr => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = acr.AppLastUpdateUserid, + AppLastUpdateUserGuid = acr.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = acr.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(compensationLastUpdatedBy); + + // Acquisition Deleted Compensation Requisition + // This is needed to get the notes last-updated-by from the notes that where deleted + var compensationHistoryLastUpdatedBy = this.Context.PimsCompensationRequisitionHists.AsNoTracking() + .Where(acrh => acrh.AcquisitionFileId == id) + .Select(acrh => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = acrh.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = acrh.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = acrh.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(compensationHistoryLastUpdatedBy); + + // Acquisition Compensation Requsition Financials + var financialsLastUpdatedBy = this.Context.PimsCompReqFinancials.AsNoTracking() + .Include(acrf => acrf.CompensationRequisition) + .Where(acrf => acrf.CompensationRequisition.AcquisitionFileId == id) + .Select(acrf => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = acrf.AppLastUpdateUserid, + AppLastUpdateUserGuid = acrf.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = acrf.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(financialsLastUpdatedBy); + + // Acquisition Deleted Compensation Requsition Financials + // This is needed to get the notes last-updated-by from the notes that where deleted + var financialHists = this.Context.PimsCompReqFinancialHists.AsNoTracking(); + var compreqHistoryLastUpdatedBy = this.Context.PimsCompensationRequisitionHists.AsNoTracking() + .Where(at => at.AcquisitionFileId == id) + .Join( + financialHists, + compReqHist => compReqHist.CompensationRequisitionId, + financialHist => financialHist.CompensationRequisitionId, + (compReqHist, compReqFinHist) => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = compReqFinHist.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = compReqFinHist.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = compReqFinHist.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(compreqHistoryLastUpdatedBy); + + // Acquisition Expropiation Payments + var expPaymentsLastUpdatedBy = this.Context.PimsExpropriationPayments.AsNoTracking() + .Where(aep => aep.AcquisitionFileId == id) + .Select(aep => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aep.AppLastUpdateUserid, + AppLastUpdateUserGuid = aep.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = aep.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(expPaymentsLastUpdatedBy); + + // Acquisition Deleted Expropiation Payments + // This is needed to get the notes last-updated-by from the notes that where deleted + var expPaymentsHistoryLastUpdatedBy = this.Context.PimsExpropriationPaymentHists.AsNoTracking() + .Where(aeph => aeph.AcquisitionFileId == id) + .Select(aeph => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aeph.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aeph.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aeph.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(expPaymentsHistoryLastUpdatedBy); + + // Acquisition Expropiation payments + var expPaymentsItemsLastUpdatedBy = this.Context.PimsExpropPmtPmtItems.AsNoTracking() + .Include(aepi => aepi.ExpropriationPayment) + .Where(aepi => aepi.ExpropriationPayment.AcquisitionFileId == id) + .Select(aepi => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aepi.AppLastUpdateUserid, + AppLastUpdateUserGuid = aepi.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = aepi.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(expPaymentsItemsLastUpdatedBy); + + // Acquisition Deleted Expropiation payments + // This is needed to get the notes last-updated-by from the notes that where deleted + var expItemHists = this.Context.PimsExpropPmtPmtItemHists.AsNoTracking(); + var expHistoryLastUpdatedBy = this.Context.PimsExpropriationPaymentHists.AsNoTracking() + .Where(at => at.AcquisitionFileId == id) + .Join( + expItemHists, + expPaymentHist => expPaymentHist.ExpropriationPaymentId, + expItemHist => expItemHist.ExpropriationPaymentId, + (expPaymentHist, expItemHist) => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = expItemHist.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = expItemHist.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = expItemHist.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(expHistoryLastUpdatedBy); + + // Acquisition Agreements + var agreementsLastUpdatedBy = this.Context.PimsAgreements.AsNoTracking() + .Where(aa => aa.AcquisitionFileId == id) + .Select(aa => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aa.AppLastUpdateUserid, + AppLastUpdateUserGuid = aa.AppLastUpdateUserGuid, + AppLastUpdateTimestamp = aa.AppLastUpdateTimestamp, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(agreementsLastUpdatedBy); + + // Acquisition Deleted Agreements + // This is needed to get the notes last-updated-by from the notes that where deleted + var agreementsHistoryLastUpdatedBy = this.Context.PimsAgreementHists.AsNoTracking() + .Where(aah => aah.AcquisitionFileId == id) + .Select(aah => new LastUpdatedByModel() + { + ParentId = id, + AppLastUpdateUserid = aah.AppLastUpdateUserid, // TODO: Update this once the DB tracks the user + AppLastUpdateUserGuid = aah.AppLastUpdateUserGuid, // TODO: Update this once the DB tracks the user + AppLastUpdateTimestamp = aah.EndDateHist ?? DateTime.UnixEpoch, + }) + .OrderByDescending(lu => lu.AppLastUpdateTimestamp) + .Take(1) + .ToList(); + lastUpdatedByAggregate.AddRange(agreementsHistoryLastUpdatedBy); + + return lastUpdatedByAggregate.OrderByDescending(x => x.AppLastUpdateTimestamp).FirstOrDefault(); + } + public List GetOwnersByAcquisitionFileId(long acquisitionFileId) { return Context.PimsAcquisitionOwners diff --git a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs index c4bae1b78a..328fb4380a 100644 --- a/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IAcquisitionFileRepository.cs @@ -10,6 +10,8 @@ public interface IAcquisitionFileRepository : IRepository PimsAcquisitionFile GetById(long id); + LastUpdatedByModel GetLastUpdateBy(long id); + List GetOwnersByAcquisitionFileId(long acquisitionFileId); List GetTeamMembers(HashSet regions, long? contractorPersonId = null); diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx index 98a407b4b2..1bf5470cd4 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.test.tsx @@ -10,6 +10,7 @@ import { mockAcquisitionFileOwnersResponse, mockAcquisitionFileResponse, } from '@/mocks/acquisitionFiles.mock'; +import { mockLastUpdatedBy } from '@/mocks/lastUpdatedBy.mock'; import { mockLookups } from '@/mocks/lookups.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { mockNotesResponse } from '@/mocks/noteResponses.mock'; @@ -39,7 +40,6 @@ jest.mock('@/features/documents/hooks/useDocumentGenerationRepository'); })); jest.mock('@/components/common/mapFSM/MapStateMachineContext'); -(useMapStateMachine as jest.Mock).mockImplementation(() => mapMachineBaseMock); const onClose = jest.fn(); @@ -96,10 +96,13 @@ describe('AcquisitionContainer component', () => { }; beforeEach(() => { + (useMapStateMachine as jest.Mock).mockImplementation(() => mapMachineBaseMock); + mockAxios.onGet(new RegExp('users/info/*')).reply(200, {}); mockAxios .onGet(new RegExp('acquisitionfiles/1/properties')) .reply(200, mockAcquisitionFileResponse().fileProperties); + mockAxios.onGet(new RegExp('acquisitionfiles/1/updateInfo')).reply(200, mockLastUpdatedBy(1)); mockAxios .onGet(new RegExp('acquisitionfiles/1/owners')) .reply(200, mockAcquisitionFileOwnersResponse()); diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx index 025efb0471..c29dd5e79a 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx @@ -51,11 +51,21 @@ const initialState: AcquisitionContainerState = { export const AcquisitionContainer: React.FunctionComponent = props => { // Load state from props and side-bar context const { acquisitionFileId, onClose, View } = props; - const { setFile, setFileLoading, staleFile, setStaleFile, file } = useContext(SideBarContext); + const { + setFile, + setFileLoading, + staleFile, + setStaleFile, + file, + setLastUpdatedBy, + lastUpdatedBy, + staleLastUpdatedBy, + } = useContext(SideBarContext); const [isValid, setIsValid] = useState(true); const withUserOverride = useApiUserOverride< (userOverrideCodes: UserOverrideCode[]) => Promise >('Failed to update Acquisition File'); + const { getAcquisitionFile: { execute: retrieveAcquisitionFile, loading: loadingAcquisitionFile }, updateAcquisitionProperties, @@ -64,6 +74,7 @@ export const AcquisitionContainer: React.FunctionComponent { + var retrieved = await getLastUpdatedBy(acquisitionFileId); + if (retrieved !== undefined) { + setLastUpdatedBy(retrieved); + } else { + setLastUpdatedBy(null); + } + }, [acquisitionFileId, getLastUpdatedBy, setLastUpdatedBy]); + + React.useEffect(() => { + if ( + lastUpdatedBy === undefined || + acquisitionFileId !== lastUpdatedBy?.parentId || + staleLastUpdatedBy + ) { + fetchLastUpdatedBy(); + } + }, [fetchLastUpdatedBy, lastUpdatedBy, acquisitionFileId, staleLastUpdatedBy]); + useEffect(() => { if (acquisitionFile === undefined || acquisitionFileId !== acquisitionFile.id || staleFile) { fetchAcquisitionFile(); @@ -138,8 +168,16 @@ export const AcquisitionContainer: React.FunctionComponent setFileLoading(loadingAcquisitionFile || loadingAcquisitionFileProperties), - [loadingAcquisitionFile, setFileLoading, loadingAcquisitionFileProperties], + () => + setFileLoading( + loadingAcquisitionFile || loadingAcquisitionFileProperties || loadingGetLastUpdatedBy, + ), + [ + loadingAcquisitionFile, + setFileLoading, + loadingAcquisitionFileProperties, + loadingGetLastUpdatedBy, + ], ); const close = useCallback(() => onClose && onClose(), [onClose]); @@ -207,6 +245,7 @@ export const AcquisitionContainer: React.FunctionComponent { fetchAcquisitionFile(); + fetchLastUpdatedBy(); mapMachine.refreshMapProperties(); setIsEditing(false); }; diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.test.tsx index 7d60596e5e..b293c0a46f 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.test.tsx @@ -11,6 +11,7 @@ import { mockAcquisitionFileResponse, } from '@/mocks/acquisitionFiles.mock'; import { getMockApiInterestHolders } from '@/mocks/interestHolders.mock'; +import { mockLastUpdatedBy } from '@/mocks/lastUpdatedBy.mock'; import { mockLookups } from '@/mocks/lookups.mock'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { rest, server } from '@/mocks/msw/server'; @@ -89,6 +90,9 @@ describe('AcquisitionView component', () => { ...mockAcquisitionFileResponse(), fileType: FileTypes.Acquisition, }} + lastUpdatedBy={{ + ...mockLastUpdatedBy(1), + }} > @@ -149,7 +153,7 @@ describe('AcquisitionView component', () => { expect(getByText('1-12345-01 - Test ACQ File')).toBeVisible(); expect(getByText(prettyFormatUTCDate(testAcquisitionFile.appCreateTimestamp))).toBeVisible(); expect( - getByText(prettyFormatUTCDate(testAcquisitionFile.appLastUpdateTimestamp)), + getByText(prettyFormatUTCDate(mockLastUpdatedBy(1).appLastUpdateTimestamp)), ).toBeVisible(); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx index 0c252e05f6..eefbde3863 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx @@ -71,7 +71,7 @@ export const AcquisitionView: React.FunctionComponent = ( const location = useLocation(); const history = useHistory(); const match = useRouteMatch(); - const { file } = useContext(SideBarContext); + const { file, lastUpdatedBy } = useContext(SideBarContext); if (!!file && file?.fileType !== FileTypes.Acquisition) { throw Error('Context file is not an acquisition file'); } @@ -130,7 +130,9 @@ export const AcquisitionView: React.FunctionComponent = ( className="mr-2" /> } - header={} + header={ + + } footer={ isEditing && ( Last updated: - Jul 27, 2022 + Oct 6, 2023 by - admin + MARODRIG diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.test.tsx index fe29f71b24..1e33d265d9 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.test.tsx @@ -12,9 +12,15 @@ const mockAxios = new MockAdapter(axios); describe('AcquisitionHeader component', () => { // render component under test const setup = (props: IAcquisitionHeaderProps, renderOptions: RenderOptions = {}) => { - const utils = render(, { - ...renderOptions, - }); + const utils = render( + , + { + ...renderOptions, + }, + ); return { ...utils }; }; @@ -29,13 +35,21 @@ describe('AcquisitionHeader component', () => { }); it('renders as expected when no data is provided', () => { - const { asFragment } = setup({}); + const { asFragment } = setup({ lastUpdatedBy: null }); expect(asFragment()).toMatchSnapshot(); }); it('renders as expected when an acquisition file is provided', async () => { const testAcquisitionFile = mockAcquisitionFileResponse(); - const { getByText } = setup({ acquisitionFile: testAcquisitionFile }); + const { getByText } = setup({ + acquisitionFile: testAcquisitionFile, + lastUpdatedBy: { + parentId: testAcquisitionFile.id || 0, + appLastUpdateUserid: testAcquisitionFile.appLastUpdateUserid || '', + appLastUpdateUserGuid: testAcquisitionFile.appLastUpdateUserGuid || '', + appLastUpdateTimestamp: testAcquisitionFile.appLastUpdateTimestamp || '', + }, + }); expect(getByText('1-12345-01 - Test ACQ File')).toBeVisible(); expect(getByText(prettyFormatUTCDate(testAcquisitionFile.appCreateTimestamp))).toBeVisible(); @@ -46,7 +60,7 @@ describe('AcquisitionHeader component', () => { it('renders the file number and name concatenated', async () => { const testAcquisitionFile = mockAcquisitionFileResponse(); - const { getByText } = setup({ acquisitionFile: testAcquisitionFile }); + const { getByText } = setup({ acquisitionFile: testAcquisitionFile, lastUpdatedBy: null }); expect(getByText('File:')).toBeVisible(); expect(getByText('1-12345-01 - Test ACQ File')).toBeVisible(); @@ -54,7 +68,7 @@ describe('AcquisitionHeader component', () => { it('renders the file Project Number and name concatenated', async () => { const testAcquisitionFile = mockAcquisitionFileResponse(); - const { getByText } = setup({ acquisitionFile: testAcquisitionFile }); + const { getByText } = setup({ acquisitionFile: testAcquisitionFile, lastUpdatedBy: null }); expect(getByText('Ministry project:')).toBeVisible(); expect( @@ -66,7 +80,7 @@ describe('AcquisitionHeader component', () => { it('renders the file Product code and description concatenated', async () => { const testAcquisitionFile = mockAcquisitionFileResponse(); - const { getByText } = setup({ acquisitionFile: testAcquisitionFile }); + const { getByText } = setup({ acquisitionFile: testAcquisitionFile, lastUpdatedBy: null }); expect(getByText('Ministry product:')).toBeVisible(); expect(getByText('00048 - MISCELLANEOUS CLAIMS')).toBeVisible(); @@ -81,9 +95,28 @@ describe('AcquisitionHeader component', () => { productId: null, projectId: null, }, + lastUpdatedBy: null, }); expect(getByText('Ministry product:')).toBeVisible(); expect(getByTestId('acq-header-product-val')).toHaveTextContent(''); }); + + it('renders the last-update-time when provided', async () => { + const testDate = new Date().toISOString(); + const testAcquisitionFile = mockAcquisitionFileResponse(); + const { getByText } = setup({ + acquisitionFile: testAcquisitionFile, + lastUpdatedBy: { + parentId: testAcquisitionFile.id || 0, + appLastUpdateUserid: 'Test User Id', + appLastUpdateUserGuid: 'TEST GUID', + appLastUpdateTimestamp: testDate, + }, + }); + + expect(getByText('1-12345-01 - Test ACQ File')).toBeVisible(); + expect(getByText(prettyFormatUTCDate(testAcquisitionFile.appCreateTimestamp))).toBeVisible(); + expect(getByText(prettyFormatUTCDate(testDate))).toBeVisible(); + }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.tsx b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.tsx index 228744d7bd..6fe161a8a2 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/common/AcquisitionHeader.tsx @@ -5,15 +5,17 @@ import styled from 'styled-components'; import { HeaderField } from '@/components/common/HeaderField/HeaderField'; import { UserNameTooltip } from '@/components/common/UserNameTooltip'; import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { Api_LastUpdatedBy } from '@/models/api/File'; import { prettyFormatUTCDate } from '@/utils'; export interface IAcquisitionHeaderProps { acquisitionFile?: Api_AcquisitionFile; + lastUpdatedBy: Api_LastUpdatedBy | null; } export const AcquisitionHeader: React.FunctionComponent< React.PropsWithChildren -> = ({ acquisitionFile }) => { +> = ({ acquisitionFile, lastUpdatedBy }) => { const leftColumnWidth = '7'; const leftColumnLabel = '3'; @@ -71,10 +73,10 @@ export const AcquisitionHeader: React.FunctionComponent< Last updated:{' '} - {prettyFormatUTCDate(acquisitionFile?.appLastUpdateTimestamp)} by{' '} + {prettyFormatUTCDate(lastUpdatedBy?.appLastUpdateTimestamp)} by{' '} diff --git a/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx b/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx index 3f68d1663f..f7527d7cea 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/router/AcquisitionRouter.tsx @@ -64,7 +64,7 @@ export const AcquisitionRouter: React.FC = props => { acquisitionFileId={props.acquisitionFile.id || -1} View={UpdateAgreementsForm} formikRef={props.formikRef} - onSuccess={() => props.setIsEditing(false)} + onSuccess={props.onSuccess} /> @@ -72,7 +72,7 @@ export const AcquisitionRouter: React.FC = props => { View={UpdateStakeHolderForm} formikRef={props.formikRef} acquisitionFile={props.acquisitionFile} - onSuccess={() => props.setIsEditing(false)} + onSuccess={props.onSuccess} /> {/* Ignore property-related routes (which are handled in separate FilePropertyRouter) */} @@ -100,7 +100,8 @@ export const AcquisitionRouter: React.FC = props => { + onSuccess={props.onSuccess} + /> )} claim={Claims.ACQUISITION_EDIT} key={'expropriation'} @@ -112,7 +113,8 @@ export const AcquisitionRouter: React.FC = props => { + onSuccess={props.onSuccess} + /> )} claim={Claims.ACQUISITION_EDIT} key={'expropriation'} diff --git a/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx b/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx index 1f60da67bb..2f18d1217e 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/router/FilePropertyRouter.tsx @@ -1,5 +1,5 @@ import { FormikProps } from 'formik'; -import React from 'react'; +import React, { useContext } from 'react'; import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -8,6 +8,7 @@ import { InventoryTabNames, InventoryTabs } from '@/features/mapSideBar/property import { FileTabType } from '@/features/mapSideBar/shared/detail/FileTabs'; import { Api_AcquisitionFile } from '@/models/api/AcquisitionFile'; +import { SideBarContext } from '../../context/sidebarContext'; import { UpdatePropertyDetailsContainer } from '../../property/tabs/propertyDetails/update/UpdatePropertyDetailsContainer'; import { TakesUpdateContainer } from '../../property/tabs/takes/update/TakesUpdateContainer'; import { TakesUpdateForm } from '../../property/tabs/takes/update/TakesUpdateForm'; @@ -27,6 +28,13 @@ export interface IFilePropertyRouterProps { export const FilePropertyRouter: React.FC = props => { const { path, url } = useRouteMatch(); + const { setStaleLastUpdatedBy } = useContext(SideBarContext); + + const onChildSucess = () => { + props.setIsEditing(false); + setStaleLastUpdatedBy(true); + }; + if (props.acquisitionFile === undefined || props.acquisitionFile === null) { return null; } @@ -63,7 +71,7 @@ export const FilePropertyRouter: React.FC = props => { fileProperty={fileProperty} View={TakesUpdateForm} ref={props.formikRef} - onSuccess={() => props.setIsEditing(false)} + onSuccess={onChildSucess} /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx index 5cd41a32d9..fc50eff60b 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/AcquisitionFileTabs.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { Claims } from '@/constants/claims'; @@ -9,6 +9,7 @@ import NoteListView from '@/features/notes/list/NoteListView'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { Api_AcquisitionFile, EnumAcquisitionFileType } from '@/models/api/AcquisitionFile'; +import { SideBarContext } from '../../context/sidebarContext'; import DocumentsTab from '../../shared/tabs/DocumentsTab'; import AgreementContainer from './agreement/detail/AgreementContainer'; import AgreementView from './agreement/detail/AgreementView'; @@ -35,6 +36,8 @@ export const AcquisitionFileTabs: React.FC = ({ const tabViews: TabFileView[] = []; const { hasClaim } = useKeycloakWrapper(); + const { setStaleLastUpdatedBy } = useContext(SideBarContext); + const location = useLocation(); const history = useHistory(); const { tab } = useParams<{ tab?: string }>(); @@ -46,6 +49,10 @@ export const AcquisitionFileTabs: React.FC = ({ } }; + const onChildSuccess = () => { + setStaleLastUpdatedBy(true); + }; + tabViews.push({ content: ( setIsEditing(true)} /> @@ -71,6 +78,7 @@ export const AcquisitionFileTabs: React.FC = ({ ), key: FileTabType.DOCUMENTS, @@ -80,7 +88,13 @@ export const AcquisitionFileTabs: React.FC = ({ if (acquisitionFile?.id && hasClaim(Claims.NOTE_VIEW)) { tabViews.push({ - content: , + content: ( + + ), key: FileTabType.NOTES, name: 'Notes', }); @@ -93,7 +107,7 @@ export const AcquisitionFileTabs: React.FC = ({ acquisitionFileId={acquisitionFile.id} View={AgreementView} onEdit={() => setIsEditing(true)} - > + /> ), key: FileTabType.AGREEMENTS, name: 'Agreements', @@ -107,7 +121,7 @@ export const AcquisitionFileTabs: React.FC = ({ View={StakeHolderView} onEdit={() => setIsEditing(true)} acquisitionFile={acquisitionFile} - > + /> ), key: FileTabType.STAKEHOLDERS, name: 'Stakeholders', @@ -121,7 +135,7 @@ export const AcquisitionFileTabs: React.FC = ({ fileId={acquisitionFile.id} file={acquisitionFile} View={CompensationListView} - > + /> ), key: FileTabType.COMPENSATIONS, name: 'Compensation', @@ -138,7 +152,7 @@ export const AcquisitionFileTabs: React.FC = ({ + /> ), key: FileTabType.EXPROPRIATION, name: 'Expropriation', diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/CompensationRequisitionTrayContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/CompensationRequisitionTrayContainer.tsx index 25aa8c217d..6a56b1b9f7 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/CompensationRequisitionTrayContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/CompensationRequisitionTrayContainer.tsx @@ -19,7 +19,8 @@ export const CompensationRequisitionTrayContainer: React.FunctionComponent< React.PropsWithChildren > = ({ compensationRequisitionId, onClose, View }) => { const { getSystemConstant } = useSystemConstants(); - const { file, project, setProject, setProjectLoading, setStaleFile } = useContext(SideBarContext); + const { file, project, setProject, setProjectLoading, setStaleFile, setStaleLastUpdatedBy } = + useContext(SideBarContext); const [editMode, setEditMode] = useState(false); const [show, setShow] = useState(true); @@ -85,6 +86,7 @@ export const CompensationRequisitionTrayContainer: React.FunctionComponent< setShow={setShow} onUpdate={() => { fetchCompensationReq(); + setStaleLastUpdatedBy(true); setStaleFile(true); }} > diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListContainer.tsx index 8ea554919d..3c6045796a 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/compensation/list/CompensationListContainer.tsx @@ -37,7 +37,7 @@ export const CompensationListContainer: React.FunctionComponent< deleteCompensation: { execute: deleteCompensation }, } = useCompensationRequisitionRepository(); - const { staleFile, setStaleFile, setFile } = useContext(SideBarContext); + const { staleFile, setStaleFile, setFile, setStaleLastUpdatedBy } = useContext(SideBarContext); const { setModalContent, setDisplayModal } = useModalContext(); const fetchData = useCallback(async () => { @@ -60,6 +60,7 @@ export const CompensationListContainer: React.FunctionComponent< rowVersion: response.rowVersion, fileType: FileTypes.Acquisition, }); + setStaleLastUpdatedBy(true); setStaleFile(true); return response.totalAllowableCompensation ?? null; } @@ -113,6 +114,7 @@ export const CompensationListContainer: React.FunctionComponent< postAcquisitionCompensationRequisition(fileId, defaultCompensationRequisition).then( async newCompensationReq => { if (newCompensationReq?.id) { + setStaleLastUpdatedBy(true); setStaleFile(true); } }, @@ -123,7 +125,7 @@ export const CompensationListContainer: React.FunctionComponent< if (compensations === undefined || staleFile) { fetchData(); } - }, [fetchData, staleFile, setStaleFile, compensations]); + }, [fetchData, staleFile, compensations]); return ( { const result = await deleteCompensation(compensationId); if (result === true) { + setStaleLastUpdatedBy(true); setStaleFile(true); } setDisplayModal(false); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/ExpropriationTabContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/ExpropriationTabContainer.tsx index 1cb02957f0..6657ddd327 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/ExpropriationTabContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/ExpropriationTabContainer.tsx @@ -16,7 +16,7 @@ export interface IExpropriationTabContainerProps { export const ExpropriationTabContainer: React.FunctionComponent< React.PropsWithChildren > = ({ View, acquisitionFile }) => { - const { fileLoading } = useContext(SideBarContext); + const { fileLoading, setStaleLastUpdatedBy } = useContext(SideBarContext); const [form8s, setForm8s] = useState([]); const { @@ -42,6 +42,7 @@ export const ExpropriationTabContainer: React.FunctionComponent< const handleForm8Deleted = async (form8Id: number) => { await deleteForm8(form8Id); + setStaleLastUpdatedBy(true); var updatedForms = await getAcquisitionFileForm8s(acquisitionFile.id!); if (updatedForms) { setForm8s(updatedForms); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.test.tsx index 30f40957b0..37e0f517aa 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.test.tsx @@ -1,4 +1,5 @@ import { createMemoryHistory } from 'history'; +import { noop } from 'lodash'; import { Claims } from '@/constants'; import { mockAcquisitionFileOwnersResponse } from '@/mocks/acquisitionFiles.mock'; @@ -67,16 +68,19 @@ describe('Add Form8 Container component', () => { props?: Partial; } = {}, ) => { - const component = render(, { - history, - store: { - [lookupCodesSlice.name]: { lookupCodes: mockLookups }, - [systemConstantsSlice.name]: { systemConstants: [{ name: 'GST', value: '5.0' }] }, + const component = render( + , + { + history, + store: { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, + [systemConstantsSlice.name]: { systemConstants: [{ name: 'GST', value: '5.0' }] }, + }, + useMockAuthentication: true, + claims: renderOptions?.claims ?? [Claims.ACQUISITION_EDIT], + ...renderOptions, }, - useMockAuthentication: true, - claims: renderOptions?.claims ?? [Claims.ACQUISITION_EDIT], - ...renderOptions, - }); + ); return { ...component, diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.tsx index cd0c94b8d0..ea29daa1cd 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/add/AddForm8Container.tsx @@ -14,11 +14,12 @@ import { IForm8FormProps } from '../UpdateForm8Form'; export interface IAddForm8ContainerProps { acquisitionFileId: number; View: React.FC; + onSuccess: () => void; } export const AddForm8Container: React.FunctionComponent< React.PropsWithChildren -> = ({ acquisitionFileId, View }) => { +> = ({ acquisitionFileId, View, onSuccess }) => { const initialValues = new Form8FormModel(null, acquisitionFileId); const history = useHistory(); @@ -74,6 +75,7 @@ export const AddForm8Container: React.FunctionComponent< const onAddSuccess = async () => { history.push(backUrl); + onSuccess(); }; useEffect(() => { diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.test.tsx index 55ebde459b..f48a7166b2 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.test.tsx @@ -1,4 +1,5 @@ import { createMemoryHistory } from 'history'; +import { noop } from 'lodash'; import { Claims } from '@/constants'; import { mockGetExpropriationPaymentApi } from '@/mocks/ExpropriationPayment.mock'; @@ -52,6 +53,7 @@ describe('Update Form8 Container component', () => { , { history, diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx index 087521556a..165eced9a1 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/expropriation/form8/update/UpdateForm8Container.tsx @@ -16,11 +16,12 @@ import { IForm8FormProps } from '../UpdateForm8Form'; export interface IUpdateForm8ContainerProps { form8Id: number; View: React.FC; + onSuccess: () => void; } export const UpdateForm8Container: React.FunctionComponent< React.PropsWithChildren -> = ({ form8Id, View }) => { +> = ({ form8Id, View, onSuccess }) => { const history = useHistory(); const location = useLocation(); @@ -86,6 +87,7 @@ export const UpdateForm8Container: React.FunctionComponent< }; const onUpdateSuccess = async () => { + onSuccess(); history.push(backUrl); }; diff --git a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx index a04acbfe56..e64e0ad054 100644 --- a/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx +++ b/source/frontend/src/features/mapSideBar/context/sidebarContext.tsx @@ -81,11 +81,14 @@ export const SideBarContextProvider = (props: { children: React.ReactChild | React.ReactChild[] | React.ReactNode; file?: TypedFile; project?: Api_Project; + lastUpdatedBy?: Api_LastUpdatedBy; }) => { const [file, setFile] = useState(props.file); const [project, setProject] = useState(props.project); const [staleFile, setStaleFile] = useState(false); - const [lastUpdatedBy, setLastUpdatedBy] = useState(null); + const [lastUpdatedBy, setLastUpdatedBy] = useState( + props.lastUpdatedBy ?? null, + ); const [staleLastUpdatedBy, setStaleLastUpdatedBy] = useState(false); const [fileLoading, setFileLoading] = useState(false); const [projectLoading, setProjectLoading] = useState(false); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts index 438b9085bb..16d8ee73e6 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/models.ts @@ -10,7 +10,7 @@ export const TakesYupSchema = Yup.object().shape({ takes: Yup.array().of( Yup.object().shape({ description: Yup.string().max(4000, 'Description must be at most ${max} characters'), - takeTypeCode: Yup.string().required('Take type is required'), + takeTypeCode: Yup.string().required('Take type is required').nullable(), takeStatusTypeCode: Yup.string().required('Take status type is required.'), isSurplus: Yup.bool().required('Surplus flag required'), isNewRightOfWay: Yup.bool().required('Surplus flag required'), diff --git a/source/frontend/src/hooks/pims-api/useApiAcquisitionFile.ts b/source/frontend/src/hooks/pims-api/useApiAcquisitionFile.ts index fdd9b39e7f..46e84d1c5f 100644 --- a/source/frontend/src/hooks/pims-api/useApiAcquisitionFile.ts +++ b/source/frontend/src/hooks/pims-api/useApiAcquisitionFile.ts @@ -12,6 +12,7 @@ import { import { Api_CompensationFinancial } from '@/models/api/CompensationFinancial'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; import { Api_ExpropriationPayment } from '@/models/api/ExpropriationPayment'; +import { Api_LastUpdatedBy } from '@/models/api/File'; import { Api_Person } from '@/models/api/Person'; import { Api_Product, Api_Project } from '@/models/api/Project'; import { Api_ExportProjectFilter } from '@/models/api/ProjectFilter'; @@ -35,6 +36,8 @@ export const useApiAcquisitionFile = () => { ), getAcquisitionFile: (acqFileId: number) => api.get(`/acquisitionfiles/${acqFileId}`), + getLastUpdatedByApi: (acqFileId: number) => + api.get(`/acquisitionfiles/${acqFileId}/updateInfo`), getAgreementReport: (filter: Api_ExportProjectFilter) => api.post(`/reports/acquisition/agreements`, filter, { responseType: 'blob', diff --git a/source/frontend/src/hooks/repositories/useAcquisitionProvider.ts b/source/frontend/src/hooks/repositories/useAcquisitionProvider.ts index 5fa07b8e52..60f7030e91 100644 --- a/source/frontend/src/hooks/repositories/useAcquisitionProvider.ts +++ b/source/frontend/src/hooks/repositories/useAcquisitionProvider.ts @@ -12,6 +12,7 @@ import { import { Api_CompensationFinancial } from '@/models/api/CompensationFinancial'; import { Api_CompensationRequisition } from '@/models/api/CompensationRequisition'; import { Api_ExpropriationPayment } from '@/models/api/ExpropriationPayment'; +import { Api_LastUpdatedBy } from '@/models/api/File'; import { Api_Person } from '@/models/api/Person'; import { Api_Product, Api_Project } from '@/models/api/Project'; import { Api_ExportProjectFilter } from '@/models/api/ProjectFilter'; @@ -43,6 +44,7 @@ export const useAcquisitionProvider = () => { getAllAcquisitionFileTeamMembers, getAgreementReport, getCompensationReport, + getLastUpdatedByApi, } = useApiAcquisitionFile(); const addAcquisitionFileApi = useApiRequestWrapper< @@ -73,6 +75,17 @@ export const useAcquisitionProvider = () => { onError: useAxiosErrorHandler('Failed to load Acquisition File'), }); + const getLastUpdatedBy = useApiRequestWrapper< + (acqFileId: number) => Promise> + >({ + requestFunction: useCallback( + async (acqFileId: number) => await getLastUpdatedByApi(acqFileId), + [getLastUpdatedByApi], + ), + requestName: 'getLastUpdatedBy', + onError: useAxiosErrorHandler('Failed to load Acquisition File last-updated-by'), + }); + const getAgreementsReportApi = useApiRequestWrapper< (filter: Api_ExportProjectFilter) => Promise> >({ @@ -286,6 +299,7 @@ export const useAcquisitionProvider = () => { () => ({ addAcquisitionFile: addAcquisitionFileApi, getAcquisitionFile: getAcquisitionFileApi, + getLastUpdatedBy, updateAcquisitionFile: updateAcquisitionFileApi, updateAcquisitionProperties: updateAcquisitionPropertiesApi, getAcquisitionProperties: getAcquisitionPropertiesApi, @@ -305,6 +319,7 @@ export const useAcquisitionProvider = () => { }), [ addAcquisitionFileApi, + getLastUpdatedBy, getAcquisitionFileApi, updateAcquisitionFileApi, updateAcquisitionPropertiesApi, diff --git a/source/frontend/src/mocks/lastUpdatedBy.mock.ts b/source/frontend/src/mocks/lastUpdatedBy.mock.ts new file mode 100644 index 0000000000..9aa37b74a1 --- /dev/null +++ b/source/frontend/src/mocks/lastUpdatedBy.mock.ts @@ -0,0 +1,8 @@ +import { Api_LastUpdatedBy } from '@/models/api/File'; + +export const mockLastUpdatedBy: (parentId: number) => Api_LastUpdatedBy = (parentId: number) => ({ + parentId, + appLastUpdateUserid: 'MARODRIG', + appLastUpdateUserGuid: '123123-14f0-477c-a7fb-sdfwerwea', + appLastUpdateTimestamp: '2023-10-06T22:48:17.06', +});