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

feat: CE-1045 add action taken filter for ceeb #708

8 changes: 5 additions & 3 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export interface ComplaintFilterParameters {
status?: string;
girTypeCode?: string;
complaintMethod?: string;
actionTaken?: string;
}
11 changes: 7 additions & 4 deletions backend/src/v1/complaint/complaint.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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")
Expand Down
69 changes: 68 additions & 1 deletion backend/src/v1/complaint/complaint.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -843,6 +844,7 @@ export class ComplaintService {
complaintType: COMPLAINT_TYPE,
model: ComplaintSearchParameters,
hasCEEBRole: boolean,
token?: string,
): Promise<SearchResults> => {
try {
let results: SearchResults = { totalCount: 0, complaints: [] };
Expand Down Expand Up @@ -871,6 +873,30 @@ 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 { data, errors } = await get(token, {
query: `{getLeadsByActionTaken (actionCode: "${filters.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];
afwilcox marked this conversation as resolved.
Show resolved Hide resolved

builder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", {
complaint_identifiers: complaintIdentifiers,
});
}

//-- apply search
if (query) {
builder = this._applySearch(builder, complaintType, query);
Expand Down Expand Up @@ -946,6 +972,7 @@ export class ComplaintService {
complaintType: COMPLAINT_TYPE,
model: ComplaintSearchParameters,
hasCEEBRole: boolean,
token?: string,
): Promise<MapSearchResults> => {
const { orderBy, sortBy, page, pageSize, query, ...filters } = model;

Expand Down Expand Up @@ -985,6 +1012,29 @@ export class ComplaintService {
complaintBuilder.andWhere("ST_X(complaint.location_geometry_point) <> 0");
complaintBuilder.andWhere("ST_Y(complaint.location_geometry_point) <> 0");

// -- filter by complaint identifiers returned by case management if actionTaken filter is present
if (hasCEEBRole && filters.actionTaken) {
const { data, errors } = await get(token, {
query: `{getLeadsByActionTaken (actionCode: "${filters.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];

complaintBuilder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", {
complaint_identifiers: complaintIdentifiers,
});
}

//-- run query
const mappedComplaints = await complaintBuilder.getMany();

Expand All @@ -1008,13 +1058,30 @@ export class ComplaintService {
});
}


//-- added this for consistency with search method
//-- return Waste and Pestivide complaints for CEEB users
if (hasCEEBRole && complaintType === "ERS") {
unMappedBuilder.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 { data, errors } = await get(token, {
query: `{getLeadsByActionTaken (actionCode: "${filters.actionTaken}")}`,
});
if (errors) {
this.logger.error("GraphQL errors:", errors);
throw new Error("GraphQL errors occurred");
}
// If no complaint indentifiers are returned by the CM database, provide a non-empty non-matching list to avoid
// the SQL error of IN empty set.
const complaintIdentifiers = data.getLeadsByActionTaken.length > 0 ? data.getLeadsByActionTaken : [-1];
afwilcox marked this conversation as resolved.
Show resolved Hide resolved

unMappedBuilder.andWhere("complaint.complaint_identifier IN(:...complaint_identifiers)", {
complaint_identifiers: complaintIdentifiers,
});
}

//-- filter locations without coordinates
unMappedBuilder.andWhere("ST_X(complaint.location_geometry_point) = 0");
unMappedBuilder.andWhere("ST_Y(complaint.location_geometry_point) = 0");
Expand Down
43 changes: 43 additions & 0 deletions frontend/cypress/e2e/complaint-search.v2.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Roles from "../../src/app/types/app/roles";
/*
Tests to verify complaint list specification functionality
*/
Expand Down Expand Up @@ -146,3 +147,45 @@ 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);
});

it("allows filtering of complaints by Action Taken", function () {
// Navigate to the complaint list
const complaintWithActionTakenID = "23-030990";
const actionTaken = "Forward to lead agency";
cy.visit("/");
cy.waitForSpinner();

// Set an 'action taken' on a complaint, so it can be filtered
cy.get("#comp-officer-filter").should("exist").click();
cy.get(`#${complaintWithActionTakenID}`).should("exist").click();
afwilcox marked this conversation as resolved.
Show resolved Hide resolved
// cy.get(".input-group > .comp-form-control").should("exist").click().type("111");
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");
cy.get("#details-screen-assign-button").should("exist").click();
cy.get("#self_assign_button").should("exist").click();
cy.get(".modal").should("not.exist");
cy.get("#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");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const ComplaintFilterBar: FC<Props> = ({
violationType,
girType,
complaintMethod,
actionTaken,
} = state;

const dateRangeLabel = (): string | undefined => {
Expand Down Expand Up @@ -220,6 +221,15 @@ export const ComplaintFilterBar: FC<Props> = ({
clear={removeFilter}
/>
)}

{hasFilter("actionTaken") && (
<FilterButton
id="comp-complaint-method-filter"
label={actionTaken?.label}
name="actionTaken"
clear={removeFilter}
/>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -22,6 +23,8 @@ import { ComplaintFilterPayload, updateFilter } from "../../../store/reducers/co
import Option from "../../../types/app/option";
import { getUserAgency } from "../../../service/user-service";
import { listActiveFilters } from "../../../store/reducers/app";
import UserService from "../../../service/user-service";
afwilcox marked this conversation as resolved.
Show resolved Hide resolved
import Roles from "../../../types/app/roles";

type Props = {
type: string;
Expand All @@ -42,6 +45,7 @@ export const ComplaintFilter: FC<Props> = ({ type }) => {
endDate,
girType,
complaintMethod,
actionTaken,
},
dispatch,
} = useContext(ComplaintFilterContext);
Expand All @@ -62,6 +66,7 @@ export const ComplaintFilter: FC<Props> = ({ type }) => {
const communities = useAppSelector(selectCascadedCommunity(region?.value, zone?.value, community?.value));

const complaintMethods = useAppSelector(selectComplaintReceivedMethodDropdown);
const decisionTypeOptions = useAppSelector(selectDecisionTypeDropdown);

const activeFilters = useAppSelector(listActiveFilters());

Expand Down Expand Up @@ -224,8 +229,9 @@ export const ComplaintFilter: FC<Props> = ({ type }) => {
<div>
<button
aria-label="Previous Month"
className={`react-datepicker__navigation react-datepicker__navigation--previous ${customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
className={`react-datepicker__navigation react-datepicker__navigation--previous ${
customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
onClick={decreaseMonth}
>
<span
Expand All @@ -244,8 +250,9 @@ export const ComplaintFilter: FC<Props> = ({ type }) => {
</span>
<button
aria-label="Next Month"
className={`react-datepicker__navigation react-datepicker__navigation--next ${customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
className={`react-datepicker__navigation react-datepicker__navigation--next ${
customHeaderCount === 1 ? "datepicker-nav-hidden" : "datepicker-nav-visible"
}`}
onClick={increaseMonth}
>
<span
Expand Down Expand Up @@ -320,6 +327,28 @@ export const ComplaintFilter: FC<Props> = ({ type }) => {
</div>
</div>
)}
{UserService.hasRole(Roles.CEEB) && (
afwilcox marked this conversation as resolved.
Show resolved Hide resolved
<div id="comp-filter-action-taken-id">
<label htmlFor="action-taken-select-id">Action Taken</label>
<div className="filter-select-padding">
<CompSelect
id="action-taken-select-id"
classNamePrefix="comp-select"
onChange={(option) => {
setFilter("actionTaken", option);
}}
classNames={{
menu: () => "top-layer-select",
}}
options={decisionTypeOptions}
placeholder="Select"
enableValidation={false}
value={actionTaken}
isClearable={true}
/>
</div>
</div>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const generateComplaintRequestPayload = (
violationType,
girType,
complaintMethod,
actionTaken,
} = filters;

const common = {
Expand Down Expand Up @@ -80,6 +81,7 @@ export const generateComplaintRequestPayload = (
...common,
violationFilter: violationType,
complaintMethodFilter: complaintMethod,
actionTakenFilter: actionTaken,
} as ComplaintRequestPayload;
case COMPLAINT_TYPES.HWCR:
default:
Expand Down Expand Up @@ -114,7 +116,6 @@ export const ComplaintList: FC<Props> = ({ 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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const generateMapComplaintRequestPayload = (
natureOfComplaint,
violationType,
complaintMethod,
actionTaken,
} = filters;

const common = {
Expand All @@ -48,6 +49,7 @@ export const generateMapComplaintRequestPayload = (
startDateFilter: startDate,
endDateFilter: endDate,
complaintStatusFilter: status,
actionTakenFilter: actionTaken,
};

switch (complaintType) {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/providers/complaint-filter-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let initialState: ComplaintFilters = {
violationType: null,
filters: [],
complaintMethod: null,
actionTaken: null,
};

const ComplaintFilterContext = createContext<ComplaintFilterContextType>({
Expand Down
Loading
Loading