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
49 changes: 44 additions & 5 deletions 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 @@ -737,6 +738,25 @@ export class ComplaintService {
return Promise.resolve(results);
};

private _getComplaintsByActionTaken = async (token: string, actionTaken: string): Promise<string[]> => {
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<WildlifeComplaintDto> | Array<AllegationComplaintDto>> => {
Expand Down Expand Up @@ -843,6 +863,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 +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);
Expand Down Expand Up @@ -946,6 +976,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,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);

Expand All @@ -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") {
Expand All @@ -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 };

Expand Down
58 changes: 58 additions & 0 deletions frontend/cypress/e2e/complaint-search.v2.cy.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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");
});
});
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,15 +13,17 @@ 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";
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";
afwilcox marked this conversation as resolved.
Show resolved Hide resolved
import Roles from "../../../types/app/roles";

type Props = {
type: string;
Expand All @@ -42,11 +44,12 @@ export const ComplaintFilter: FC<Props> = ({ 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" });
Expand All @@ -62,6 +65,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 +228,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 +249,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 +326,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