Skip to content

Commit

Permalink
Add Action Taken filter to complaint search
Browse files Browse the repository at this point in the history
Added the Action Taken filter to the Complaint list for CEEB users. The
filter also applies to the map view. In both the complaint search and
map search, if and only if the filters contain an Action Taken, a call
to the Case Management service is made requesting the lead identifiers
(which in NATCom are complaint identifiers) of all leads belonging to
a case_file that has had the specified action taken on it. If the list
is not empty, we add an AND clause to the search to filter the results
against the list. If the list is empty then the search should return
no results, however SQL errors if you pass an empty list to an IN.
To avoid this, a list of a single value that is not a valid complaint
identifier is passed in its stead, giving the desired result.
  • Loading branch information
mikevespi committed Oct 15, 2024
1 parent 46d6c1d commit 3bf09f6
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 13 deletions.
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];

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];

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
5 changes: 5 additions & 0 deletions frontend/cypress/e2e/complaint-list.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ describe("CEEB User Stuff?", () => {
beforeEach(function () {
cy.viewport("macbook-16");
cy.kcLogout().kcLogin();
cy.applyRoles(roles);
});

afterEach(function () {
cy.resetRoles();
});

Cypress._.times(roles.length, (index) => {
Expand Down
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";
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) && (
<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
4 changes: 4 additions & 0 deletions frontend/src/app/store/reducers/complaints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export const getComplaints =
girTypeFilter,
complaintStatusFilter,
complaintMethodFilter,
actionTakenFilter,
page,
pageSize,
query,
Expand All @@ -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,
Expand Down Expand Up @@ -331,6 +333,7 @@ export const getMappedComplaints =
violationFilter,
complaintStatusFilter,
complaintMethodFilter,
actionTakenFilter,
page,
pageSize,
query,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/types/complaints/complaint-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ComplaintFilters {
endDateFilter?: Date;
complaintStatusFilter?: Option;
complaintMethodFilter?: Option;
actionTakenFilter?: Option;
page?: number;
pageSize?: number;
query?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ export type ComplaintFilters = {

complaintMethod: DropdownOption | null;

actionTaken?: DropdownOption | null;

filters: Array<any>;
};

0 comments on commit 3bf09f6

Please sign in to comment.