From 604f0542561f871c3d8e3d47807c8242a5a8450b Mon Sep 17 00:00:00 2001 From: GrahamS-Quartech <112989452+GrahamS-Quartech@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:42:51 -0700 Subject: [PATCH] PIMS-1985: Agency selection on add property (#2629) Co-authored-by: dbarkowsky --- .../services/buildings/buildingServices.ts | 5 ++- .../src/services/parcels/parcelServices.ts | 9 ++-- .../buildings/buildingService.test.ts | 4 +- .../services/parcels/parcelsService.test.ts | 1 + .../projects/projectsServices.test.ts | 6 ++- .../properties/propertyServices.test.ts | 4 +- .../src/components/property/AddProperty.tsx | 14 +++--- .../components/property/PropertyDialog.tsx | 33 +++----------- .../src/components/property/PropertyForms.tsx | 10 +++++ react-app/src/hooks/api/useUserAgencies.ts | 45 +++++++++++++++++++ 10 files changed, 82 insertions(+), 49 deletions(-) create mode 100644 react-app/src/hooks/api/useUserAgencies.ts diff --git a/express-api/src/services/buildings/buildingServices.ts b/express-api/src/services/buildings/buildingServices.ts index 21209a0b6..2f61a6592 100644 --- a/express-api/src/services/buildings/buildingServices.ts +++ b/express-api/src/services/buildings/buildingServices.ts @@ -67,8 +67,9 @@ export const updateBuildingById = async (building: DeepPartial, ssoUse if (!existingBuilding) { throw new ErrorWithCode('Building does not exists.', 404); } - if (building.AgencyId && building.AgencyId !== existingBuilding.AgencyId && !isAdmin(ssoUser)) { - throw new ErrorWithCode('Changing agency is not permitted.', 403); + const validUserAgencies = await userServices.getAgencies(ssoUser.preferred_username); + if (!isAdmin(ssoUser) && !validUserAgencies.includes(building.AgencyId)) { + throw new ErrorWithCode('This agency change is not permitted.', 403); } if (building.Fiscals && building.Fiscals.length) { building.Fiscals = await Promise.all( diff --git a/express-api/src/services/parcels/parcelServices.ts b/express-api/src/services/parcels/parcelServices.ts index 20f46cd8e..f423df0a1 100644 --- a/express-api/src/services/parcels/parcelServices.ts +++ b/express-api/src/services/parcels/parcelServices.ts @@ -163,12 +163,9 @@ const updateParcel = async (incomingParcel: DeepPartial, ssoUser: SSOUse if (findParcel == null || findParcel.Id !== incomingParcel.Id) { throw new ErrorWithCode('Parcel not found', 404); } - if ( - incomingParcel.AgencyId && - incomingParcel.AgencyId !== findParcel.AgencyId && - !isAdmin(ssoUser) - ) { - throw new ErrorWithCode('Changing agency is not permitted.', 403); + const validUserAgencies = await userServices.getAgencies(ssoUser.preferred_username); + if (!isAdmin(ssoUser) && !validUserAgencies.includes(incomingParcel.AgencyId)) { + throw new ErrorWithCode('This agency change is not permitted.', 403); } if (incomingParcel.Fiscals && incomingParcel.Fiscals.length) { incomingParcel.Fiscals = await Promise.all( diff --git a/express-api/tests/unit/services/buildings/buildingService.test.ts b/express-api/tests/unit/services/buildings/buildingService.test.ts index a7000bfce..88b5ebe7b 100644 --- a/express-api/tests/unit/services/buildings/buildingService.test.ts +++ b/express-api/tests/unit/services/buildings/buildingService.test.ts @@ -16,8 +16,10 @@ import userServices from '@/services/users/usersServices'; import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty'; import { Roles } from '@/constants/roles'; -const buildingRepo = AppDataSource.getRepository(Building); jest.spyOn(userServices, 'getUser').mockImplementation(async () => produceUser()); +jest.spyOn(userServices, 'getAgencies').mockImplementation(async () => []); + +const buildingRepo = AppDataSource.getRepository(Building); const _buildingSave = jest .spyOn(buildingRepo, 'save') .mockImplementation(async (building: DeepPartial & Building) => building); diff --git a/express-api/tests/unit/services/parcels/parcelsService.test.ts b/express-api/tests/unit/services/parcels/parcelsService.test.ts index a169d6f51..fb3059c05 100644 --- a/express-api/tests/unit/services/parcels/parcelsService.test.ts +++ b/express-api/tests/unit/services/parcels/parcelsService.test.ts @@ -56,6 +56,7 @@ jest.spyOn(AppDataSource.getRepository(ProjectProperty), 'find').mockImplementat jest.spyOn(parcelRepo, 'find').mockImplementation(async () => [produceParcel(), produceParcel()]); jest.spyOn(userServices, 'getUser').mockImplementation(async () => produceUser()); +jest.spyOn(userServices, 'getAgencies').mockImplementation(async () => []); jest .spyOn(AppDataSource.getRepository(ParcelEvaluation), 'find') diff --git a/express-api/tests/unit/services/projects/projectsServices.test.ts b/express-api/tests/unit/services/projects/projectsServices.test.ts index 349f7ade9..2667919d3 100644 --- a/express-api/tests/unit/services/projects/projectsServices.test.ts +++ b/express-api/tests/unit/services/projects/projectsServices.test.ts @@ -2,6 +2,7 @@ import { AppDataSource } from '@/appDataSource'; import { ProjectStatus } from '@/constants/projectStatus'; import { ProjectType } from '@/constants/projectType'; import { ProjectWorkflow } from '@/constants/projectWorkflow'; +import { Roles } from '@/constants/roles'; import projectServices from '@/services/projects/projectsServices'; import userServices from '@/services/users/usersServices'; import { Agency } from '@/typeorm/Entities/Agency'; @@ -109,6 +110,7 @@ const _getNextSequence = jest.spyOn(AppDataSource, 'query').mockImplementation(a ]); jest.spyOn(userServices, 'getUser').mockImplementation(async () => produceUser()); +jest.spyOn(userServices, 'getAgencies').mockImplementation(async () => [1]); const _mockStartTransaction = jest.fn(async () => {}); const _mockRollbackTransaction = jest.fn(async () => {}); @@ -526,7 +528,7 @@ describe('UNIT - Project Services', () => { parcels: [1, 3], buildings: [4, 5], }, - produceSSO(), + produceSSO({ client_roles: [Roles.ADMIN] }), ); expect(result.StatusId).toBe(2); expect(result.Name).toBe('New Name'); @@ -610,7 +612,7 @@ describe('UNIT - Project Services', () => { parcels: [1, 3], buildings: [4, 5], }, - produceSSO(), + produceSSO({ client_roles: [Roles.ADMIN] }), ), ).rejects.toThrow(new ErrorWithCode('Error updating project: bad save', 500)); }); diff --git a/express-api/tests/unit/services/properties/propertyServices.test.ts b/express-api/tests/unit/services/properties/propertyServices.test.ts index c958cfc24..f65d9dcd9 100644 --- a/express-api/tests/unit/services/properties/propertyServices.test.ts +++ b/express-api/tests/unit/services/properties/propertyServices.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { AppDataSource } from '@/appDataSource'; import { Roles } from '@/constants/roles'; import propertyServices, { @@ -46,7 +47,6 @@ import { import { DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import xlsx, { WorkSheet } from 'xlsx'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const _parcelsCreateQueryBuilder: any = { select: () => _parcelsCreateQueryBuilder, leftJoinAndSelect: () => _parcelsCreateQueryBuilder, @@ -59,7 +59,6 @@ const _parcelsCreateQueryBuilder: any = { getMany: () => [produceParcel()], }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const _buildingsCreateQueryBuilder: any = { select: () => _buildingsCreateQueryBuilder, leftJoinAndSelect: () => _buildingsCreateQueryBuilder, @@ -72,7 +71,6 @@ const _buildingsCreateQueryBuilder: any = { getMany: () => [produceBuilding()], }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const _propertyUnionCreateQueryBuilder: any = { select: () => _propertyUnionCreateQueryBuilder, leftJoinAndSelect: () => _propertyUnionCreateQueryBuilder, diff --git a/react-app/src/components/property/AddProperty.tsx b/react-app/src/components/property/AddProperty.tsx index 3cc2c3b43..b5d155777 100644 --- a/react-app/src/components/property/AddProperty.tsx +++ b/react-app/src/components/property/AddProperty.tsx @@ -21,21 +21,20 @@ import { BuildingConstructionType, BuildingPredominateUse, } from '@/hooks/api/useBuildingsApi'; -import { AuthContext } from '@/contexts/authContext'; import { parseFloatOrNull, parseIntOrNull } from '@/utilities/formatters'; import useDataSubmitter from '@/hooks/useDataSubmitter'; import { LoadingButton } from '@mui/lab'; import { LookupContext } from '@/contexts/lookupContext'; import { Classification } from '@/hooks/api/useLookupApi'; import useHistoryAwareNavigate from '@/hooks/useHistoryAwareNavigate'; +import useUserAgencies from '@/hooks/api/useUserAgencies'; const AddProperty = () => { //const years = [new Date().getFullYear(), new Date().getFullYear() - 1]; const [propertyType, setPropertyType] = useState('Parcel'); - const [showErrorText, setShowErrorTest] = useState(false); + const [showErrorText, setShowErrorText] = useState(false); const { goToFromStateOrSetRoute } = useHistoryAwareNavigate(); const api = usePimsApi(); - const userContext = useContext(AuthContext); const { data: lookupData } = useContext(LookupContext); const { submit: submitParcel, submitting: submittingParcel } = useDataSubmitter( api.parcels.addParcel, @@ -43,6 +42,7 @@ const AddProperty = () => { const { submit: submitBuilding, submitting: submittingBuilding } = useDataSubmitter( api.buildings.addBuilding, ); + const { menuItems: agencyOptions } = useUserAgencies(); const formMethods = useForm({ defaultValues: { @@ -68,6 +68,7 @@ const AddProperty = () => { BuildingTenancyUpdatedOn: dayjs(), Fiscals: [], Evaluations: [], + AgencyId: null, }, }); @@ -112,6 +113,7 @@ const AddProperty = () => { /> { onClick={async () => { const isValid = await formMethods.trigger(); if (isValid && formMethods.getValues()['Location'] != null) { - setShowErrorTest(false); + setShowErrorText(false); if (propertyType === 'Parcel') { const formValues = formMethods.getValues(); const addParcel: ParcelAdd = { @@ -159,7 +161,6 @@ const AddProperty = () => { PIN: parseIntOrNull(formValues.PIN), Postal: formValues.Postal.replace(/ /g, '').toUpperCase(), PropertyTypeId: 0, - AgencyId: userContext.pimsUser.data.AgencyId, IsVisibleToOtherAgencies: false, Fiscals: formValues.Fiscals.map((a) => ({ ...a, @@ -187,7 +188,6 @@ const AddProperty = () => { TotalArea: parseFloatOrNull(formValues.TotalArea), BuildingFloorCount: 0, PropertyTypeId: 1, - AgencyId: userContext.pimsUser.data.AgencyId, IsVisibleToOtherAgencies: false, Fiscals: formValues.Fiscals.map((a) => ({ ...a, @@ -208,7 +208,7 @@ const AddProperty = () => { } } else { console.log('Error!'); - setShowErrorTest(true); + setShowErrorText(true); } }} variant="contained" diff --git a/react-app/src/components/property/PropertyDialog.tsx b/react-app/src/components/property/PropertyDialog.tsx index da5e7055a..efbea5c1d 100644 --- a/react-app/src/components/property/PropertyDialog.tsx +++ b/react-app/src/components/property/PropertyDialog.tsx @@ -24,9 +24,7 @@ import { parseFloatOrNull, parseIntOrNull, pidFormatter } from '@/utilities/form import useDataSubmitter from '@/hooks/useDataSubmitter'; import { LookupContext } from '@/contexts/lookupContext'; import { Classification } from '@/hooks/api/useLookupApi'; -import { AuthContext } from '@/contexts/authContext'; -import { Roles } from '@/constants/roles'; -import AutocompleteFormField from '../form/AutocompleteFormField'; +import useUserAgencies from '@/hooks/api/useUserAgencies'; interface IParcelInformationEditDialog { initialValues: Parcel; @@ -39,8 +37,8 @@ export const ParcelInformationEditDialog = (props: IParcelInformationEditDialog) const { initialValues, postSubmit } = props; const api = usePimsApi(); + const { menuItems: agencyOptions } = useUserAgencies(); const { data: lookupData } = useContext(LookupContext); - const { keycloak } = useContext(AuthContext); const { submit, submitting } = useDataSubmitter(api.parcels.updateParcelById); @@ -76,8 +74,6 @@ export const ParcelInformationEditDialog = (props: IParcelInformationEditDialog) }); }, [initialValues]); - const isAdmin = keycloak.hasRoles([Roles.ADMIN]); - return ( - {isAdmin && ( - ({ value: agc.Id, label: agc.Name })) ?? [] - } - /> - )} @@ -140,9 +128,8 @@ interface IBuildingInformationEditDialog { export const BuildingInformationEditDialog = (props: IBuildingInformationEditDialog) => { const api = usePimsApi(); + const { menuItems: agencyOptions } = useUserAgencies(); const { data: lookupData } = useContext(LookupContext); - const { keycloak } = useContext(AuthContext); - const { submit, submitting } = useDataSubmitter(api.buildings.updateBuildingById); const { initialValues, open, onCancel, postSubmit } = props; @@ -193,8 +180,6 @@ export const BuildingInformationEditDialog = (props: IBuildingInformationEditDia }); }, [initialValues]); - const isAdmin = keycloak.hasRoles([Roles.ADMIN]); - return ( - {isAdmin && ( - ({ value: agc.Id, label: agc.Name })) ?? [] - } - /> - )} diff --git a/react-app/src/components/property/PropertyForms.tsx b/react-app/src/components/property/PropertyForms.tsx index 2fd0a8ae1..fe6d52ab0 100644 --- a/react-app/src/components/property/PropertyForms.tsx +++ b/react-app/src/components/property/PropertyForms.tsx @@ -38,6 +38,7 @@ interface IGeneralInformationForm { propertyType: PropertyType; defaultLocationValue: GeoPoint | null; adminAreas: ISelectMenuItem[]; + agencies: ISelectMenuItem[]; } export const GeneralInformationForm = (props: IGeneralInformationForm) => { @@ -258,6 +259,15 @@ export const GeneralInformationForm = (props: IGeneralInformationForm) => { }} /> + + + { + const userContext = useContext(AuthContext); + const { ungroupedAgencies, agencyOptions } = useGroupedAgenciesApi(); + const api = usePimsApi(); + const isAdmin = userContext.keycloak.hasRoles([Roles.ADMIN]); + const { data: userAgencies, refreshData: refreshUserAgencies } = useDataLoader(() => + api.users.getUsersAgencyIds(userContext.keycloak.user.preferred_username), + ); + + useEffect(() => { + refreshUserAgencies(); + }, [userContext.keycloak]); + + const userAgencyObjects = useMemo(() => { + if (ungroupedAgencies && userAgencies) { + return ungroupedAgencies.filter((a) => userAgencies.includes(a.Id)); + } else { + return []; + } + }, [ungroupedAgencies, userAgencies]); + + const menuItems: ISelectMenuItem[] = useMemo(() => { + if (isAdmin) { + return agencyOptions; + } else if (userAgencyObjects) { + return agencyOptions.filter((agc) => + userAgencyObjects.some((useragc) => useragc.Id === agc.value), + ); + } else { + return []; + } + }, [agencyOptions, userAgencyObjects, isAdmin]); + + return { menuItems, userAgencies: userAgencyObjects }; +}; + +export default useUserAgencies;