Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

PIMS-1985: Agency selection on add property #2629

Merged
merged 11 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions express-api/src/services/buildings/buildingServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ export const updateBuildingById = async (building: DeepPartial<Building>, 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(
Expand Down
9 changes: 3 additions & 6 deletions express-api/src/services/parcels/parcelServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,9 @@ const updateParcel = async (incomingParcel: DeepPartial<Parcel>, 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => building);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 () => {});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AppDataSource } from '@/appDataSource';
import { Roles } from '@/constants/roles';
import propertyServices, {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 7 additions & 7 deletions react-app/src/components/property/AddProperty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ 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<PropertyType>('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,
);
const { submit: submitBuilding, submitting: submittingBuilding } = useDataSubmitter(
api.buildings.addBuilding,
);
const { menuItems: agencyOptions } = useUserAgencies();

const formMethods = useForm({
defaultValues: {
Expand All @@ -68,6 +68,7 @@ const AddProperty = () => {
BuildingTenancyUpdatedOn: dayjs(),
Fiscals: [],
Evaluations: [],
AgencyId: null,
},
});

Expand Down Expand Up @@ -112,6 +113,7 @@ const AddProperty = () => {
/>
</RadioGroup>
<GeneralInformationForm
agencies={agencyOptions}
defaultLocationValue={undefined}
propertyType={propertyType}
adminAreas={
Expand Down Expand Up @@ -149,7 +151,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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -208,7 +208,7 @@ const AddProperty = () => {
}
} else {
console.log('Error!');
setShowErrorTest(true);
setShowErrorText(true);
}
}}
variant="contained"
Expand Down
33 changes: 5 additions & 28 deletions react-app/src/components/property/PropertyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -76,8 +74,6 @@ export const ParcelInformationEditDialog = (props: IParcelInformationEditDialog)
});
}, [initialValues]);

const isAdmin = keycloak.hasRoles([Roles.ADMIN]);

return (
<ConfirmDialog
title={'Edit Parcel Information'}
Expand All @@ -99,6 +95,7 @@ export const ParcelInformationEditDialog = (props: IParcelInformationEditDialog)
<FormProvider {...infoFormMethods}>
<Box display={'flex'} flexDirection={'column'} gap={'1rem'}>
<GeneralInformationForm
agencies={agencyOptions ?? []}
propertyType={'Parcel'}
defaultLocationValue={initialValues?.Location}
adminAreas={
Expand All @@ -116,15 +113,6 @@ export const ParcelInformationEditDialog = (props: IParcelInformationEditDialog)
})) ?? []
}
/>
{isAdmin && (
<AutocompleteFormField
name={'AgencyId'}
label={'Agency'}
options={
lookupData?.Agencies.map((agc) => ({ value: agc.Id, label: agc.Name })) ?? []
}
/>
)}
</Box>
</FormProvider>
</ConfirmDialog>
Expand All @@ -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;
Expand Down Expand Up @@ -193,8 +180,6 @@ export const BuildingInformationEditDialog = (props: IBuildingInformationEditDia
});
}, [initialValues]);

const isAdmin = keycloak.hasRoles([Roles.ADMIN]);

return (
<ConfirmDialog
title={'Edit Building Information'}
Expand All @@ -218,6 +203,7 @@ export const BuildingInformationEditDialog = (props: IBuildingInformationEditDia
<FormProvider {...infoFormMethods}>
<Box display={'flex'} flexDirection={'column'} gap={'1rem'}>
<GeneralInformationForm
agencies={agencyOptions}
propertyType={'Building'}
defaultLocationValue={initialValues?.Location}
adminAreas={
Expand All @@ -232,15 +218,6 @@ export const BuildingInformationEditDialog = (props: IBuildingInformationEditDia
constructionOptions={lookupData?.ConstructionTypes as BuildingConstructionType[]}
predominateUseOptions={lookupData?.PredominateUses as BuildingPredominateUse[]}
/>
{isAdmin && (
<AutocompleteFormField
name={'AgencyId'}
label={'Agency'}
options={
lookupData?.Agencies.map((agc) => ({ value: agc.Id, label: agc.Name })) ?? []
}
/>
)}
</Box>
</FormProvider>
</ConfirmDialog>
Expand Down
10 changes: 10 additions & 0 deletions react-app/src/components/property/PropertyForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface IGeneralInformationForm {
propertyType: PropertyType;
defaultLocationValue: GeoPoint | null;
adminAreas: ISelectMenuItem[];
agencies: ISelectMenuItem[];
}

export const GeneralInformationForm = (props: IGeneralInformationForm) => {
Expand Down Expand Up @@ -258,6 +259,15 @@ export const GeneralInformationForm = (props: IGeneralInformationForm) => {
}}
/>
</Grid>
<Grid item xs={12}>
<AutocompleteFormField
allowNestedIndent
required
name={'AgencyId'}
label={'Agency'}
options={props.agencies ?? []}
/>
</Grid>
<Grid item xs={12}>
<ParcelMap
height={'500px'}
Expand Down
45 changes: 45 additions & 0 deletions react-app/src/hooks/api/useUserAgencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ISelectMenuItem } from '@/components/form/SelectFormField';
import { Roles } from '@/constants/roles';
import { useContext, useEffect, useMemo } from 'react';
import useDataLoader from '../useDataLoader';
import { AuthContext } from '@/contexts/authContext';
import usePimsApi from '../usePimsApi';
import useGroupedAgenciesApi from './useGroupedAgenciesApi';

const useUserAgencies = () => {
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;
Loading