diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4b26b75c0..ffe9880f0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import {MiddlewareConsumer, Module, RequestMethod} from "@nestjs/common"; +import { MiddlewareConsumer, Module, RequestMethod } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ConfigModule } from "@nestjs/config"; import { AutomapperModule } from "@automapper/nestjs"; @@ -135,7 +135,9 @@ if (process.env.POSTGRESQL_PASSWORD != null) { export class AppModule { // let's add a middleware on all routes configure(consumer: MiddlewareConsumer) { - consumer.apply(HTTPLoggerMiddleware).exclude({ path: '', method: RequestMethod.ALL }).forRoutes("*"); - consumer.apply(RequestTokenMiddleware).forRoutes("v1/code-table", "v1/case", "v1/configuration"); + consumer.apply(HTTPLoggerMiddleware).exclude({ path: "", method: RequestMethod.ALL }).forRoutes("*"); + consumer + .apply(RequestTokenMiddleware) + .forRoutes("v1/code-table", "v1/case", "v1/configuration", "v1/complaint/search", "v1/complaint/map/search"); } } diff --git a/backend/src/types/models/complaints/complaint-filter-parameters.ts b/backend/src/types/models/complaints/complaint-filter-parameters.ts index c61ef73bb..d35f18387 100644 --- a/backend/src/types/models/complaints/complaint-filter-parameters.ts +++ b/backend/src/types/models/complaints/complaint-filter-parameters.ts @@ -11,4 +11,5 @@ export interface ComplaintFilterParameters { status?: string; girTypeCode?: string; complaintMethod?: string; + actionTaken?: string; } diff --git a/backend/src/v1/complaint/complaint.controller.ts b/backend/src/v1/complaint/complaint.controller.ts index 6a8663afd..20a53a4f3 100644 --- a/backend/src/v1/complaint/complaint.controller.ts +++ b/backend/src/v1/complaint/complaint.controller.ts @@ -3,6 +3,7 @@ import { ComplaintService } from "./complaint.service"; import { Role } from "../../enum/role.enum"; import { Roles } from "../../auth/decorators/roles.decorator"; import { JwtRoleGuard } from "../../auth/jwtrole.guard"; +import { Token } from "../../auth/decorators/token.decorator"; import { ApiTags } from "@nestjs/swagger"; import { COMPLAINT_TYPE } from "../../types/models/complaints/complaint-type"; import { WildlifeComplaintDto } from "../../types/models/complaints/wildlife-complaint"; @@ -45,22 +46,24 @@ export class ComplaintController { @Param("complaintType") complaintType: COMPLAINT_TYPE, @Query() model: ComplaintSearchParameters, @Request() req, + @Token() token, ) { - const hasCEEBRole = hasRole(req, Role.CEEB); - return this.service.mapSearch(complaintType, model, hasCEEBRole); + return this.service.mapSearch(complaintType, model, hasCEEBRole, token); } @Get("/search/:complaintType") @Roles(Role.COS_OFFICER, Role.CEEB) - search( + async search( @Param("complaintType") complaintType: COMPLAINT_TYPE, @Query() model: ComplaintSearchParameters, @Request() req, + @Token() token, ) { const hasCEEBRole = hasRole(req, Role.CEEB); - return this.service.search(complaintType, model, hasCEEBRole); + const result = await this.service.search(complaintType, model, hasCEEBRole, token); + return result; } @Patch("/update-status-by-id/:id") diff --git a/backend/src/v1/complaint/complaint.service.ts b/backend/src/v1/complaint/complaint.service.ts index 3f44293e7..585bf193e 100644 --- a/backend/src/v1/complaint/complaint.service.ts +++ b/backend/src/v1/complaint/complaint.service.ts @@ -4,6 +4,7 @@ import { InjectRepository } from "@nestjs/typeorm"; import { Brackets, DataSource, QueryRunner, Repository, SelectQueryBuilder } from "typeorm"; import { InjectMapper } from "@automapper/nestjs"; import { Mapper } from "@automapper/core"; +import { get } from "../../external_api/case_management"; import { applyAllegationComplaintMap, @@ -737,6 +738,25 @@ export class ComplaintService { return Promise.resolve(results); }; + private _getComplaintsByActionTaken = async (token: string, actionTaken: string): Promise => { + const { data, errors } = await get(token, { + query: `{getLeadsByActionTaken (actionCode: "${actionTaken}")}`, + }); + if (errors) { + this.logger.error("GraphQL errors:", errors); + throw new Error("GraphQL errors occurred"); + } + /** + * If no leads in the case manangement database have had the selected action taken, `getLeadsByActionTaken` + * returns an empty array, and WHERE...IN () does not accept an empty set, it throws an error. In our use + * case, if `getLeadsByActionTaken` returns an empty array, we do not want the entire search to error, it + * should simply return an empty result set. To handle this, if `getLeadsByActionTaken` returns an empty + * array, we populate the array with a value that would never match on a complaint_identifier: -1. + */ + const complaintIdentifiers = data.getLeadsByActionTaken.length > 0 ? data.getLeadsByActionTaken : ["-1"]; + return complaintIdentifiers; + }; + findAllByType = async ( complaintType: COMPLAINT_TYPE, ): Promise | Array> => { @@ -843,6 +863,7 @@ export class ComplaintService { complaintType: COMPLAINT_TYPE, model: ComplaintSearchParameters, hasCEEBRole: boolean, + token?: string, ): Promise => { try { let results: SearchResults = { totalCount: 0, complaints: [] }; @@ -871,6 +892,15 @@ export class ComplaintService { builder.andWhere("violation_code.agency_code = :agency", { agency: "EPO" }); } + // -- filter by complaint identifiers returned by case management if actionTaken filter is present + if (hasCEEBRole && filters.actionTaken) { + const complaintIdentifiers = await this._getComplaintsByActionTaken(token, filters.actionTaken); + + builder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", { + complaint_identifiers: complaintIdentifiers, + }); + } + //-- apply search if (query) { builder = this._applySearch(builder, complaintType, query); @@ -946,6 +976,7 @@ export class ComplaintService { complaintType: COMPLAINT_TYPE, model: ComplaintSearchParameters, hasCEEBRole: boolean, + token?: string, ): Promise => { const { orderBy, sortBy, page, pageSize, query, ...filters } = model; @@ -985,9 +1016,6 @@ export class ComplaintService { complaintBuilder.andWhere("ST_X(complaint.location_geometry_point) <> 0"); complaintBuilder.andWhere("ST_Y(complaint.location_geometry_point) <> 0"); - //-- run query - const mappedComplaints = await complaintBuilder.getMany(); - //-- get unmapable complaints let unMappedBuilder = this._generateQueryBuilder(complaintType); @@ -1008,7 +1036,6 @@ export class ComplaintService { }); } - //-- added this for consistency with search method //-- return Waste and Pestivide complaints for CEEB users if (hasCEEBRole && complaintType === "ERS") { @@ -1019,7 +1046,19 @@ export class ComplaintService { unMappedBuilder.andWhere("ST_X(complaint.location_geometry_point) = 0"); unMappedBuilder.andWhere("ST_Y(complaint.location_geometry_point) = 0"); - //-- run query + // -- filter by complaint identifiers returned by case management if actionTaken filter is present + if (hasCEEBRole && filters.actionTaken) { + const complaintIdentifiers = await this._getComplaintsByActionTaken(token, filters.actionTaken); + complaintBuilder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", { + complaint_identifiers: complaintIdentifiers, + }); + unMappedBuilder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", { + complaint_identifiers: complaintIdentifiers, + }); + } + + //-- run queries + const mappedComplaints = await complaintBuilder.getMany(); const unmappedComplaints = await unMappedBuilder.getCount(); results = { ...results, unmappedComplaints }; diff --git a/frontend/cypress/e2e/complaint-search.v2.cy.ts b/frontend/cypress/e2e/complaint-search.v2.cy.ts index 3e65b789d..d21cedfb2 100644 --- a/frontend/cypress/e2e/complaint-search.v2.cy.ts +++ b/frontend/cypress/e2e/complaint-search.v2.cy.ts @@ -1,3 +1,5 @@ +import Roles from "../../src/app/types/app/roles"; +import COMPLAINT_TYPES from "../../src/app/types/app/complaint-types"; /* Tests to verify complaint list specification functionality */ @@ -146,3 +148,59 @@ describe("Complaint Search Functionality", () => { }); }); }); + +/** + * Test that CEEB specific search filters work + */ +describe("Verify CEEB specific search filters work", () => { + beforeEach(function () { + cy.viewport("macbook-16"); + cy.kcLogout().kcLogin(Roles.CEEB); + }); + + function needsDecision() { + let needsDecision = false; + cy.get("#ceeb-decision").then((decisionWrapper) => { + // If the action taken input is on the page, a decision needs to be made + if (decisionWrapper.find("#outcome-decision-action-taken").length > 0) { + needsDecision = true; + } + }); + return needsDecision; + } + + it.only("allows filtering of complaints by Action Taken", function () { + // Navigate to the complaint list + const complaintWithActionTakenID = "23-030990"; + const actionTaken = "Forward to lead agency"; + + // Check if complaintWithActionTakenID already has a decision. + cy.navigateToDetailsScreen(COMPLAINT_TYPES.ERS, complaintWithActionTakenID, true); + // If the action taken input is available then the complaint does not yet have a decision made on it. + // Set an action taken so that the filter will have results to return. + if (needsDecision()) { + cy.selectItemById("outcome-decision-schedule-sector", "Other"); + cy.selectItemById("outcome-decision-sector-category", "None"); + cy.selectItemById("outcome-decision-discharge", "Pesticides"); + cy.selectItemById("outcome-decision-action-taken", actionTaken); + cy.selectItemById("outcome-decision-lead-agency", "Other"); + cy.enterDateTimeInDatePicker("outcome-decision-outcome-date", "01"); + // If the complaint is not assigned to anyone, assign it to self + if (cy.get("#comp-details-assigned-officer-name-text-id").contains("Not Assigned")) { + cy.get("#details-screen-assign-button").should("exist").click(); + cy.get("#self_assign_button").should("exist").click(); + } + cy.get(".modal").should("not.exist"); // Ensure that the quick assign modal has closed + cy.get("#ceeb-decision > .card-body > .comp-details-form-buttons > #outcome-decision-save-button").click(); + cy.contains("div", "Decision added").should("exist"); + } + + // Return to the complaints view + cy.get("#complaints-link").click(); + + // Filter by action taken + cy.get("#comp-filter-btn").should("exist").click({ force: true }); + cy.selectItemById("action-taken-select-id", actionTaken); + cy.get(`#${complaintWithActionTakenID}`).should("exist"); + }); +}); diff --git a/frontend/src/app/components/containers/complaints/complaint-filter-bar.tsx b/frontend/src/app/components/containers/complaints/complaint-filter-bar.tsx index f0ad8dd28..e1a6a9ffd 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter-bar.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter-bar.tsx @@ -41,6 +41,7 @@ export const ComplaintFilterBar: FC = ({ violationType, girType, complaintMethod, + actionTaken, } = state; const dateRangeLabel = (): string | undefined => { @@ -220,6 +221,15 @@ export const ComplaintFilterBar: FC = ({ clear={removeFilter} /> )} + + {hasFilter("actionTaken") && ( + + )} ); diff --git a/frontend/src/app/components/containers/complaints/complaint-filter.tsx b/frontend/src/app/components/containers/complaints/complaint-filter.tsx index cbc1cc817..da2156b95 100644 --- a/frontend/src/app/components/containers/complaints/complaint-filter.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-filter.tsx @@ -13,6 +13,7 @@ import { selectGirTypeCodeDropdown, selectComplaintReceivedMethodDropdown, } from "../../../store/reducers/code-table"; +import { selectDecisionTypeDropdown } from "../../../store/reducers/code-table-selectors"; import { selectOfficersByAgencyDropdown } from "../../../store/reducers/officer"; import COMPLAINT_TYPES from "../../../types/app/complaint-types"; import DatePicker from "react-datepicker"; @@ -20,8 +21,9 @@ import { CompSelect } from "../../common/comp-select"; import { ComplaintFilterContext } from "../../../providers/complaint-filter-provider"; import { ComplaintFilterPayload, updateFilter } from "../../../store/reducers/complaint-filters"; import Option from "../../../types/app/option"; -import { getUserAgency } from "../../../service/user-service"; import { listActiveFilters } from "../../../store/reducers/app"; +import UserService from "../../../service/user-service"; +import Roles from "../../../types/app/roles"; type Props = { type: string; @@ -42,11 +44,12 @@ export const ComplaintFilter: FC = ({ type }) => { endDate, girType, complaintMethod, + actionTaken, }, dispatch, } = useContext(ComplaintFilterContext); - const agency = getUserAgency(); + const agency = UserService.getUserAgency(); let officersByAgency = useAppSelector(selectOfficersByAgencyDropdown(agency)); if (officersByAgency && officersByAgency[0]?.value !== "Unassigned") { officersByAgency.unshift({ value: "Unassigned", label: "Unassigned" }); @@ -62,6 +65,7 @@ export const ComplaintFilter: FC = ({ type }) => { const communities = useAppSelector(selectCascadedCommunity(region?.value, zone?.value, community?.value)); const complaintMethods = useAppSelector(selectComplaintReceivedMethodDropdown); + const decisionTypeOptions = useAppSelector(selectDecisionTypeDropdown); const activeFilters = useAppSelector(listActiveFilters()); @@ -224,8 +228,9 @@ export const ComplaintFilter: FC = ({ type }) => {
)} + {UserService.hasRole(Roles.CEEB) && ( +
+ +
+ { + setFilter("actionTaken", option); + }} + classNames={{ + menu: () => "top-layer-select", + }} + options={decisionTypeOptions} + placeholder="Select" + enableValidation={false} + value={actionTaken} + isClearable={true} + /> +
+
+ )} ); }; diff --git a/frontend/src/app/components/containers/complaints/complaint-list.tsx b/frontend/src/app/components/containers/complaints/complaint-list.tsx index 7a5fbec05..187770ed6 100644 --- a/frontend/src/app/components/containers/complaints/complaint-list.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-list.tsx @@ -52,6 +52,7 @@ export const generateComplaintRequestPayload = ( violationType, girType, complaintMethod, + actionTaken, } = filters; const common = { @@ -80,6 +81,7 @@ export const generateComplaintRequestPayload = ( ...common, violationFilter: violationType, complaintMethodFilter: complaintMethod, + actionTakenFilter: actionTaken, } as ComplaintRequestPayload; case COMPLAINT_TYPES.HWCR: default: @@ -114,7 +116,6 @@ export const ComplaintList: FC = ({ type, searchQuery }) => { if (searchQuery) { payload = { ...payload, query: searchQuery }; } - dispatch(getComplaints(type, payload)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [filters, sortKey, sortDirection, page, pageSize]); diff --git a/frontend/src/app/components/containers/complaints/complaint-map.tsx b/frontend/src/app/components/containers/complaints/complaint-map.tsx index 4ad79b41d..d599e8ea9 100644 --- a/frontend/src/app/components/containers/complaints/complaint-map.tsx +++ b/frontend/src/app/components/containers/complaints/complaint-map.tsx @@ -36,6 +36,7 @@ export const generateMapComplaintRequestPayload = ( natureOfComplaint, violationType, complaintMethod, + actionTaken, } = filters; const common = { @@ -48,6 +49,7 @@ export const generateMapComplaintRequestPayload = ( startDateFilter: startDate, endDateFilter: endDate, complaintStatusFilter: status, + actionTakenFilter: actionTaken, }; switch (complaintType) { diff --git a/frontend/src/app/providers/complaint-filter-provider.tsx b/frontend/src/app/providers/complaint-filter-provider.tsx index 61bfd985e..b38aa4c16 100644 --- a/frontend/src/app/providers/complaint-filter-provider.tsx +++ b/frontend/src/app/providers/complaint-filter-provider.tsx @@ -26,6 +26,7 @@ let initialState: ComplaintFilters = { violationType: null, filters: [], complaintMethod: null, + actionTaken: null, }; const ComplaintFilterContext = createContext({ diff --git a/frontend/src/app/store/reducers/complaints.ts b/frontend/src/app/store/reducers/complaints.ts index 5c2460777..b761d1a7a 100644 --- a/frontend/src/app/store/reducers/complaints.ts +++ b/frontend/src/app/store/reducers/complaints.ts @@ -277,6 +277,7 @@ export const getComplaints = girTypeFilter, complaintStatusFilter, complaintMethodFilter, + actionTakenFilter, page, pageSize, query, @@ -300,6 +301,7 @@ export const getComplaints = girTypeCode: girTypeFilter?.value, status: complaintStatusFilter?.value, complaintMethod: complaintMethodFilter?.value, + actionTaken: actionTakenFilter?.value, page: page, pageSize: pageSize, query: query, @@ -331,6 +333,7 @@ export const getMappedComplaints = violationFilter, complaintStatusFilter, complaintMethodFilter, + actionTakenFilter, page, pageSize, query, @@ -353,6 +356,7 @@ export const getMappedComplaints = violationCode: violationFilter?.value, status: complaintStatusFilter?.value, complaintMethod: complaintMethodFilter?.value, + actionTaken: actionTakenFilter?.value, page: page, pageSize: pageSize, query: query, diff --git a/frontend/src/app/types/complaints/complaint-filters.ts b/frontend/src/app/types/complaints/complaint-filters.ts index 40e84c81d..21c182f2c 100644 --- a/frontend/src/app/types/complaints/complaint-filters.ts +++ b/frontend/src/app/types/complaints/complaint-filters.ts @@ -15,6 +15,7 @@ export interface ComplaintFilters { endDateFilter?: Date; complaintStatusFilter?: Option; complaintMethodFilter?: Option; + actionTakenFilter?: Option; page?: number; pageSize?: number; query?: string; diff --git a/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts b/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts index 6e8975c90..614098d0a 100644 --- a/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts +++ b/frontend/src/app/types/complaints/complaint-filters/complaint-filters.ts @@ -20,5 +20,7 @@ export type ComplaintFilters = { complaintMethod: DropdownOption | null; + actionTaken?: DropdownOption | null; + filters: Array; };