Skip to content

Commit

Permalink
Merge branch 'release/noble-sea-lemon' into fix/CE-909-drug-field-ent…
Browse files Browse the repository at this point in the history
…ry-inconsistency
  • Loading branch information
afwilcox authored Oct 21, 2024
2 parents 2c5e5e3 + 4f34914 commit af31cb1
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 21 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/pr-version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ jobs:
if: ${{ github.event.pull_request.merged == true }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
fetch-tags: true

- name: Fetch all tags
run: git fetch --tags --force

- name: Fetch tags from main
id: get_latest_tag
run: |
git fetch origin main --tags
git fetch origin main --tags --force
latest_tag=$(git tag --list --sort=-v:refname --merged | head -n 1)
echo "::set-output name=latest_tag::$latest_tag"
echo "Latest tag: $latest_tag"
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/release-branch-creation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ jobs:
if: startsWith(github.ref, 'refs/heads/release/') && !contains(github.ref, '/')
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
fetch-tags: true

- name: Install npm
run: sudo apt-get install -y npm
Expand All @@ -19,7 +23,7 @@ jobs:
- name: Fetch latest tag
id: latest_tag
run: |
git fetch --tags
git fetch --tags --force
latest_tag=$(git tag --list --sort=-v:refname --merged | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
if [ -z "$latest_tag" ]; then
echo "No previous tags found, starting with v0.1.0"
Expand Down
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";
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) && (
<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
Loading

0 comments on commit af31cb1

Please sign in to comment.