From 04d24f95e41928a451db4bcb8f15c08c809aa39a Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 9 Jan 2025 11:00:57 -0800 Subject: [PATCH 1/3] Add Inspector and province-wide roles --- .../containers/complaints/complaints.tsx | 44 +++++++++++-------- .../components/containers/layout/side-bar.tsx | 6 +-- .../providers/complaint-filter-provider.tsx | 8 ++-- frontend/src/app/store/reducers/app.ts | 13 ++++++ frontend/src/app/types/app/menu-item.ts | 4 +- frontend/src/app/types/app/profile.ts | 1 + frontend/src/app/types/app/roles.ts | 2 + 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/frontend/src/app/components/containers/complaints/complaints.tsx b/frontend/src/app/components/containers/complaints/complaints.tsx index 91ecbf986..f9c388d5e 100644 --- a/frontend/src/app/components/containers/complaints/complaints.tsx +++ b/frontend/src/app/components/containers/complaints/complaints.tsx @@ -17,6 +17,7 @@ import { selectActiveTab, selectActiveComplaintsViewType, setActiveComplaintsViewType, + selectDefaultRegion, } from "../../../store/reducers/app"; import { ComplaintMapWithServerSideClustering } from "./complaint-map-with-server-side-clustering"; @@ -28,6 +29,8 @@ import UserService from "@service/user-service"; import Roles from "@apptypes/app/roles"; import Option from "@apptypes/app/option"; import { resetComplaintSearchParameters, selectComplaintSearchParameters } from "@/app/store/reducers/complaints"; +import { AgencyType } from "@/app/types/app/agency-types"; +import { DropdownOption } from "@/app/types/app/drop-down-option"; type Props = { defaultComplaintType: string; @@ -45,7 +48,7 @@ export const Complaints: FC = ({ defaultComplaintType }) => { if (!storedComplaintType) dispatch(setActiveTab(defaultComplaintType)); }, [storedComplaintType, dispatch, defaultComplaintType]); const [complaintType, setComplaintType] = useState( - UserService.hasRole([Roles.CEEB]) ? CEEB_TYPES.ERS : (storedComplaintType ?? defaultComplaintType), + UserService.hasRole([Roles.CEEB]) ? CEEB_TYPES.ERS : storedComplaintType ?? defaultComplaintType, ); const storedComplaintViewType = useAppSelector(selectActiveComplaintsViewType); @@ -57,6 +60,7 @@ export const Complaints: FC = ({ defaultComplaintType }) => { const currentOfficer = useAppSelector(selectCurrentOfficer(), shallowEqual); const defaultZone = useAppSelector(selectDefaultZone); + const defaultRegion = useAppSelector(selectDefaultRegion); //-- this is used to apply the search to the pager component const storedSearchParams = useAppSelector(selectComplaintSearchParameters); @@ -65,7 +69,7 @@ export const Complaints: FC = ({ defaultComplaintType }) => { const handleComplaintTabChange = (complaintType: string) => { setComplaintType(complaintType); dispatch(resetComplaintSearchParameters()); - let filters = getFilters(currentOfficer, defaultZone); + let filters = getFilters(currentOfficer, defaultZone, defaultRegion); const payload: Array = [ ...Object.entries(filters).map(([filter, value]) => ({ @@ -180,14 +184,13 @@ export const Complaints: FC = ({ defaultComplaintType }) => { export const ComplaintsWrapper: FC = ({ defaultComplaintType }) => { const defaultZone = useAppSelector(selectDefaultZone, shallowEqual); + const defaultRegion = useAppSelector(selectDefaultRegion); const currentOfficer = useAppSelector(selectCurrentOfficer(), shallowEqual); const storedSearchParams = useAppSelector(selectComplaintSearchParameters); // If the search is fresh, there are only 2 default parameters set. If more than 2 exist, // this is not a fresh search as the search funtion itself sets more filters, even if blank. const freshSearch = Object.keys(storedSearchParams).length === 2; - const defaultFilters = getFilters(currentOfficer, defaultZone); - const complaintFilters = freshSearch ? defaultFilters : storedSearchParams; - + const complaintFilters = freshSearch ? getFilters(currentOfficer, defaultZone, defaultRegion) : storedSearchParams; return ( <> {currentOfficer && ( @@ -205,24 +208,27 @@ const getComplaintTypes = () => { return UserService.hasRole(Roles.CEEB) ? CEEB_TYPES : COMPLAINT_TYPES; }; -const getFilters = (currentOfficer: any, defaultZone: any) => { +const getFilters = (currentOfficer: any, defaultZone: DropdownOption | null, defaultRegion: DropdownOption | null) => { let filters: any = {}; - if (UserService.hasRole([Roles.CEEB]) && !UserService.hasRole([Roles.CEEB_COMPLIANCE_COORDINATOR])) { - if (currentOfficer) { - let { - person: { firstName, lastName, id }, - } = currentOfficer; - const officer = { label: `${lastName}, ${firstName}`, value: id }; - - filters = { ...filters, officer }; - } + // Province-wide role defaults to only "Open" so skip the other checks + if (UserService.hasRole(Roles.PROVINCE_WIDE)) { + return filters; } - if (UserService.hasRole([Roles.CEEB, Roles.CEEB_COMPLIANCE_COORDINATOR])) { - // No additional filters needed, default will apply - } else if (defaultZone) { - filters = { ...filters, zone: defaultZone }; + const userAgency = UserService.getUserAgency(); + if (userAgency === AgencyType.COS) { + if (UserService.hasRole(Roles.INSPECTOR)) { + filters = { ...filters, region: defaultRegion }; + } else { + filters = { ...filters, zone: defaultZone }; + } + } else if (currentOfficer && !UserService.hasRole(Roles.CEEB_COMPLIANCE_COORDINATOR)) { + const { + person: { firstName, lastName, id }, + } = currentOfficer; + const officer = { label: `${lastName}, ${firstName}`, value: id }; + filters = { ...filters, officer }; } return filters; diff --git a/frontend/src/app/components/containers/layout/side-bar.tsx b/frontend/src/app/components/containers/layout/side-bar.tsx index e6c840169..83947e1db 100644 --- a/frontend/src/app/components/containers/layout/side-bar.tsx +++ b/frontend/src/app/components/containers/layout/side-bar.tsx @@ -20,21 +20,19 @@ export const SideBar: FC = () => { name: "Complaints", icon: "bi bi-file-earmark-medical", route: "/complaints", - roles: ["COS", "CEEB"], }, { id: "create-complaints-link", name: "Create Complaint", icon: "bi bi-plus-circle", route: "complaint/createComplaint", - roles: ["COS", "CEEB"], }, { id: "zone-at-a-glance-link", name: "Zone at a Glance", icon: "bi bi-buildings", route: "/zone/at-a-glance", - roles: ["COS"], + excludedRoles: [Roles.CEEB, Roles.PROVINCE_WIDE], }, ]; @@ -103,7 +101,7 @@ export const SideBar: FC = () => { {/* */}
    {menueItems.map((item, idx) => { - if (UserService.hasRole(Roles.CEEB) && !item.roles.includes("CEEB")) { + if (item.excludedRoles && UserService.hasRole(item.excludedRoles)) { // Do not display this hence return null return null; } diff --git a/frontend/src/app/providers/complaint-filter-provider.tsx b/frontend/src/app/providers/complaint-filter-provider.tsx index ab1229205..f01ad0c2e 100644 --- a/frontend/src/app/providers/complaint-filter-provider.tsx +++ b/frontend/src/app/providers/complaint-filter-provider.tsx @@ -32,7 +32,7 @@ let initialState: ComplaintFilters = { outcomeAnimalEndDate: undefined, }; -const mapFilters = (complaintFilters: Partial) => { +const convertFilterNames = (complaintFilters: Partial) => { /** * This funtion takes a partial set of filters in the shape of the ComplaintSearchParameters from * the store, and maps them into the initial state for this provider. This enables the search page @@ -119,11 +119,9 @@ const ComplaintFilterContext = createContext({ const ComplaintFilterProvider: FC = ({ children, freshSearch, complaintFilters }) => { let startingState = { ...initialState }; if (freshSearch) { - startingState = complaintFilters.zone - ? { ...startingState, status: { value: "OPEN", label: "Open" }, zone: complaintFilters.zone } - : { ...startingState, status: { value: "OPEN", label: "Open" } }; + startingState = { ...startingState, ...complaintFilters, status: { value: "OPEN", label: "Open" } }; } else { - const activeFilters = mapFilters(complaintFilters); + const activeFilters = convertFilterNames(complaintFilters); startingState = { ...startingState, ...activeFilters }; } diff --git a/frontend/src/app/store/reducers/app.ts b/frontend/src/app/store/reducers/app.ts index a1a851a30..668e67449 100644 --- a/frontend/src/app/store/reducers/app.ts +++ b/frontend/src/app/store/reducers/app.ts @@ -187,6 +187,14 @@ export const selectDefaultZone = (state: RootState): DropdownOption | null => { return value && label ? { value, label } : null; }; +export const selectDefaultRegion = (state: RootState): DropdownOption | null => { + const { + profile: { region: value, regionDescription: label }, + } = state.app; + + return value && label ? { value, label } : null; +}; + export const selectModalOpenState = (state: RootState): boolean => { const { app } = state; return app.modalIsOpen; @@ -366,6 +374,7 @@ export const getTokenProfile = (): AppThunk => async (dispatch) => { let office = ""; let region = ""; + let regionDescription = ""; let zone = ""; let zoneDescription = ""; let agency = ""; @@ -380,6 +389,7 @@ export const getTokenProfile = (): AppThunk => async (dispatch) => { office = unit.office_location_code; region = unit.region_code; + regionDescription = unit.region_name; zone = unit.zone_code; zoneDescription = unit.zone_name; agency = agencyCode.agency_code; @@ -402,6 +412,7 @@ export const getTokenProfile = (): AppThunk => async (dispatch) => { idir_username: idir_username, office: office, region: region, + regionDescription: regionDescription, zone: zone, zoneDescription: zoneDescription, agency, @@ -537,6 +548,7 @@ const initialState: AppState = { idir_username: "", office: "", region: "", + regionDescription: "", zone: "", zoneDescription: "", agency: "", @@ -592,6 +604,7 @@ const reducer = (state: AppState = initialState, action: any): AppState => { idir_username: payload.idir_username, office: payload.office, region: payload.region, + regionDescription: payload.regionDescription, zone: payload.zone, zoneDescription: payload.zoneDescription, agency: payload.agency, diff --git a/frontend/src/app/types/app/menu-item.ts b/frontend/src/app/types/app/menu-item.ts index 6d30a29df..96e694db2 100644 --- a/frontend/src/app/types/app/menu-item.ts +++ b/frontend/src/app/types/app/menu-item.ts @@ -1,7 +1,9 @@ +import Roles from "@/app/types/app/roles"; + export default interface MenuItem { id?: string; name: string; icon: string; route?: string; - roles: Array; + excludedRoles?: Array; } diff --git a/frontend/src/app/types/app/profile.ts b/frontend/src/app/types/app/profile.ts index 838a4a581..d6a5b9f4b 100644 --- a/frontend/src/app/types/app/profile.ts +++ b/frontend/src/app/types/app/profile.ts @@ -8,6 +8,7 @@ export default interface Profile { idir_username: string; office: string; region: string; + regionDescription: string; zone: string; zoneDescription: string; agency: string; diff --git a/frontend/src/app/types/app/roles.ts b/frontend/src/app/types/app/roles.ts index 3154ec7a5..772dd774f 100644 --- a/frontend/src/app/types/app/roles.ts +++ b/frontend/src/app/types/app/roles.ts @@ -7,6 +7,8 @@ enum Roles { COS_OFFICER = "COS Officer", TEMPORARY_TEST_ADMIN = "TEMPORARY_TEST_ADMIN", READ_ONLY = "READ ONLY", + INSPECTOR = "Inspector", + PROVINCE_WIDE = "Province-wide", } export default Roles; From eb3dea1365661dc2c4978376a4b1c924aea201ff Mon Sep 17 00:00:00 2001 From: afwilcox Date: Thu, 9 Jan 2025 12:56:46 -0800 Subject: [PATCH 2/3] fix: support required roles --- frontend/src/app/types/app/menu-item.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/types/app/menu-item.ts b/frontend/src/app/types/app/menu-item.ts index 96e694db2..a03a5a42f 100644 --- a/frontend/src/app/types/app/menu-item.ts +++ b/frontend/src/app/types/app/menu-item.ts @@ -6,4 +6,5 @@ export default interface MenuItem { icon: string; route?: string; excludedRoles?: Array; + requiredRoles?: Array; } From ad30e8dd9acd955d6cb730c6e71af0b06cc3d034 Mon Sep 17 00:00:00 2001 From: Mike Vesprini Date: Thu, 9 Jan 2025 15:16:57 -0800 Subject: [PATCH 3/3] Clear officer assigned filter in test --- frontend/cypress/e2e/complaint-filters-ceeb.cy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/cypress/e2e/complaint-filters-ceeb.cy.ts b/frontend/cypress/e2e/complaint-filters-ceeb.cy.ts index 56fe2600c..ecdb4ab75 100644 --- a/frontend/cypress/e2e/complaint-filters-ceeb.cy.ts +++ b/frontend/cypress/e2e/complaint-filters-ceeb.cy.ts @@ -40,6 +40,8 @@ describe("Verify CEEB specific search filters work", () => { cy.get("#comp-filter-btn").should("exist").click({ force: true }); cy.selectItemById("action-taken-select-id", actionTaken); cy.waitForSpinner(); + cy.clearFilterById("comp-officer-filter"); + cy.waitForSpinner(); cy.get(`#${complaintWithActionTakenID}`).should("exist"); }); });