diff --git a/source/backend/api/Services/TakeService.cs b/source/backend/api/Services/TakeService.cs index def6c80ef2..025be59341 100644 --- a/source/backend/api/Services/TakeService.cs +++ b/source/backend/api/Services/TakeService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Security.Claims; using Microsoft.Extensions.Logging; +using Pims.Api.Constants; using Pims.Api.Models.CodeTypes; using Pims.Core.Exceptions; using Pims.Dal.Entities; @@ -70,6 +71,10 @@ public IEnumerable UpdateAcquisitionPropertyTakes(long acquisitionFile { throw new BusinessRuleViolationException("Retired records are referenced for historical purposes only and cannot be edited or deleted. If the take has been added in error, contact your system administrator to re-open the file, which will allow take deletion."); } + else if (takes.Any(t => t.TakeStatusTypeCode == AcquisitionTakeStatusTypes.COMPLETE.ToString() && t.CompletionDt == null)) + { + throw new BusinessRuleViolationException("A completed take must have a completion date."); + } else { // Complete Takes can only be deleted or set to InProgress by Admins when File is Active/Draft diff --git a/source/backend/apimodels/Models/Concepts/Take/TakeMap.cs b/source/backend/apimodels/Models/Concepts/Take/TakeMap.cs index 92feeb3f5e..de195e4ba0 100644 --- a/source/backend/apimodels/Models/Concepts/Take/TakeMap.cs +++ b/source/backend/apimodels/Models/Concepts/Take/TakeMap.cs @@ -32,6 +32,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.TakeTypeCode, src => src.TakeTypeCodeNavigation) .Map(dest => dest.TakeStatusTypeCode, src => src.TakeStatusTypeCodeNavigation) .Map(dest => dest.LandActTypeCode, src => src.LandActTypeCodeNavigation) + .Map(dest => dest.CompletionDt, src => src.CompletionDt) .Inherits(); config.NewConfig() @@ -58,6 +59,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.TakeTypeCode, src => src.TakeTypeCode.Id) .Map(dest => dest.TakeStatusTypeCode, src => src.TakeStatusTypeCode.Id) .Map(dest => dest.LandActTypeCode, src => src.LandActTypeCode.Id) + .Map(dest => dest.CompletionDt, src => src.CompletionDt) .Inherits(); } } diff --git a/source/backend/apimodels/Models/Concepts/Take/TakeModel.cs b/source/backend/apimodels/Models/Concepts/Take/TakeModel.cs index 5922f6ca62..b8acc48fae 100644 --- a/source/backend/apimodels/Models/Concepts/Take/TakeModel.cs +++ b/source/backend/apimodels/Models/Concepts/Take/TakeModel.cs @@ -39,6 +39,8 @@ public class TakeModel : BaseAuditModel public DateOnly? LandActEndDt { get; set; } + public DateOnly? CompletionDt { get; set; } + public AcquisitionFileModel PropertyAcquisitionFile { get; set; } public long PropertyAcquisitionFileId { get; set; } diff --git a/source/frontend/src/components/common/form/FastDatePicker.tsx b/source/frontend/src/components/common/form/FastDatePicker.tsx index a4d2774dac..78f0a87412 100644 --- a/source/frontend/src/components/common/form/FastDatePicker.tsx +++ b/source/frontend/src/components/common/form/FastDatePicker.tsx @@ -25,6 +25,8 @@ type OptionalAttributes = { innerClassName?: string; /** The minimum data allowable to be chosen in the datepicker */ minDate?: Date; + /** The maximum date allowable to be chosen in the datepicker */ + maxDate?: Date; /** form label */ label?: string; /** Whether the field is required. Makes the field border blue. */ @@ -48,6 +50,7 @@ const FormikDatePicker: FunctionComponent { diff --git a/source/frontend/src/constants/takesStatusTypes.ts b/source/frontend/src/constants/takesStatusTypes.ts deleted file mode 100644 index c7817499be..0000000000 --- a/source/frontend/src/constants/takesStatusTypes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum TakesStatusTypes { - CANCELLED = 'CANCELLED', - COMPLETE = 'COMPLETE', - INPROGRESS = 'INPROGRESS', -} diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx index 5ccdab3b13..d0628df85d 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/detail/TakesDetailView.tsx @@ -12,11 +12,11 @@ import TooltipIcon from '@/components/common/TooltipIcon'; import AreaContainer from '@/components/measurements/AreaContainer'; import * as API from '@/constants/API'; import { Claims } from '@/constants/claims'; -import { TakesStatusTypes } from '@/constants/takesStatusTypes'; import { isAcquisitionFile } from '@/features/mapSideBar/acquisition/add/models'; import StatusUpdateSolver from '@/features/mapSideBar/acquisition/tabs/fileDetails/detail/statusUpdateSolver'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; +import { ApiGen_CodeTypes_AcquisitionTakeStatusTypes } from '@/models/api/generated/ApiGen_CodeTypes_AcquisitionTakeStatusTypes'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_Take } from '@/models/api/generated/ApiGen_Concepts_Take'; import { getApiPropertyName, prettyFormatDate, prettyFormatUTCDate } from '@/utils'; @@ -39,10 +39,10 @@ export const TakesDetailView: React.FunctionComponent = ( onEdit, }) => { const cancelledTakes = takes?.filter( - t => t.takeStatusTypeCode?.id === TakesStatusTypes.CANCELLED, + t => t.takeStatusTypeCode?.id === ApiGen_CodeTypes_AcquisitionTakeStatusTypes.CANCELLED, ); const nonCancelledTakes = takes?.filter( - t => t.takeStatusTypeCode?.id !== TakesStatusTypes.CANCELLED, + t => t.takeStatusTypeCode?.id !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.CANCELLED, ); const takesNotInFile = allTakesCount - (takes?.length ?? 0); @@ -114,6 +114,11 @@ export const TakesDetailView: React.FunctionComponent = ( ? getCodeById(API.TAKE_STATUS_TYPES, take.takeStatusTypeCode.id) : ''} + {take.completionDt && ( + + {prettyFormatDate(take.completionDt)} + + )} {take.takeSiteContamTypeCode?.id ? getCodeById(API.TAKE_SITE_CONTAM_TYPES, take.takeSiteContamTypeCode.id) diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx index ba34ab026e..b953412338 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/repositories/useTakesRepository.tsx @@ -67,6 +67,7 @@ export const useTakesRepository = () => { requestName: 'UpdateTakesByAcquisitionPropertyId', onSuccess: useAxiosSuccessHandler(), onError: useAxiosErrorHandler(), + throwError: true, }); return useMemo( diff --git a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx index 1333696c52..c1a8d0bf6c 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/takes/update/TakeSubForm.tsx @@ -54,6 +54,15 @@ const TakeSubForm: React.FunctionComponent = ({ ); const takeStatusTypeCode = getIn(values, withNameSpace(nameSpace, 'takeStatusTypeCode')); + React.useEffect(() => { + if ( + currentTake.completionDt && + currentTake.takeStatusTypeCode !== ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE + ) { + setFieldValue(withNameSpace(nameSpace, 'completionDt'), ''); + } + }, [currentTake.completionDt, currentTake.takeStatusTypeCode, nameSpace, setFieldValue]); + const getModalWarning = (onOk: () => void) => { return (e: React.ChangeEvent) => { if (e.target.value === 'false') { @@ -121,6 +130,24 @@ const TakeSubForm: React.FunctionComponent = ({ disabled={!canEditTake} /> + + + + + + + +
@@ -601,7 +673,7 @@ exports[`TakesUpdateForm component renders as expected 1`] = `

isNewLandAct, then: Yup.string().required('Land Act is required'), }), + completionDt: Yup.string() + .nullable() + .when('takeStatusTypeCode', { + is: (takeStatusTypeCode: string) => + takeStatusTypeCode === ApiGen_CodeTypes_AcquisitionTakeStatusTypes.COMPLETE, + then: Yup.string().nullable().required('A completed take must have a completion date.'), + }), }), ), }); export class TakeModel { id?: number; + completionDt: string | null; description: string; isThereSurplus: 'false' | 'true'; isNewHighwayDedication: 'false' | 'true'; @@ -103,6 +113,7 @@ export class TakeModel { this.newHighwayDedicationArea = base.newHighwayDedicationArea ?? 0; this.newHighwayDedicationAreaUnitTypeCode = fromTypeCodeNullable(base.areaUnitTypeCode) ?? AreaUnitTypes.SquareMeters.toString(); + this.completionDt = base.completionDt; this.appCreateTimestamp = base.appCreateTimestamp ?? null; } @@ -157,6 +168,7 @@ export class TakeModel { isNewLicenseToConstruct: this.isNewLicenseToConstruct === 'true', isNewInterestInSrw: this.isNewInterestInSrw === 'true', ...getEmptyBaseAudit(this.rowVersion), + completionDt: stringToNull(this.completionDt), }; } } diff --git a/source/frontend/src/mocks/takes.mock.ts b/source/frontend/src/mocks/takes.mock.ts index 12ee36bb7f..64ed9cdbc8 100644 --- a/source/frontend/src/mocks/takes.mock.ts +++ b/source/frontend/src/mocks/takes.mock.ts @@ -37,6 +37,7 @@ export const getMockApiTakes = (): ApiGen_Concepts_Take[] => [ displayOrder: null, isDisabled: false, }, + completionDt: '', rowVersion: 2, }, ]; diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_Take.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_Take.ts index 7d75db225b..aad66215c1 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_Take.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_Take.ts @@ -22,6 +22,7 @@ export interface ApiGen_Concepts_Take extends ApiGen_Base_BaseAudit { ltcEndDt: string | null; landActArea: number | null; landActEndDt: string | null; + completionDt: string | null; propertyAcquisitionFile: ApiGen_Concepts_AcquisitionFile | null; propertyAcquisitionFileId: number; statutoryRightOfWayArea: number | null;