Skip to content

Commit

Permalink
feat: ✨filter and sort CANs list by user (#2867)
Browse files Browse the repository at this point in the history
  • Loading branch information
fpigeonjr authored Oct 3, 2024
1 parent e979bdd commit 98cf127
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 8 deletions.
13 changes: 11 additions & 2 deletions backend/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2836,8 +2836,17 @@ components:
type: string
full_name:
type: string
role:
type: string
roles:
type: array
items:
type: string
enum:
- admin
- BUDGET_TEAM
- division-director
- unassigned
- user
- USER_ADMIN
last_name:
type: string
id:
Expand Down
10 changes: 9 additions & 1 deletion frontend/cypress/e2e/canList.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { terminalLog, testLogin } from "./utils";

beforeEach(() => {
testLogin("admin");
testLogin("division-director");
cy.visit("/cans").wait(1000);
});

Expand All @@ -28,6 +28,14 @@ describe("CAN List", () => {
cy.get("h1").should("contain", canNumber);
});

it("should correctly filter all cans or my cans", () => {
cy.visit("/cans/");
cy.get("tbody").children().should("have.length.greaterThan", 2);

cy.visit("/cans/?filter=my-cans");
cy.get("tbody").children().should("have.length", 1);
});

it("pagination on the bli table works as expected", () => {
cy.get("ul").should("have.class", "usa-pagination__list");
cy.get("li").should("have.class", "usa-pagination__item").contains("1");
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/CANs/CANTable/CANTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ const CANTable = ({ cans }) => {
<CANTableRow
key={can.id}
canId={can.id}
name={can.display_name ?? ""}
nickname={can.nick_name ?? ""}
name={can.display_name ?? "TBD"}
nickname={can.nick_name ?? "TBD"}
portfolio={can.portfolio.abbreviation}
fiscalYear={can.funding_budgets[0]?.fiscal_year ?? "TBD"}
activePeriod={can.active_period?.toString() ?? "TBD"}
activePeriod={can.active_period ?? 0}
obligateBy={formatObligateBy(can.obligate_by)}
transfer={can.funding_details.method_of_transfer ?? "TBD"}
fyBudget={can.funding_budgets[0]?.budget ?? 0}
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/CANs/CANTable/CANTableRow.helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Display active period in years
* @param {number} activePeriod
* @returns {string}
*/
export const displayActivePeriod = (activePeriod) => {
switch (activePeriod) {
case 0:
return "TBD";
case 1:
return "1 year";
default:
return `${activePeriod} years`;
}
};
22 changes: 22 additions & 0 deletions frontend/src/components/CANs/CANTable/CANTableRow.helpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { displayActivePeriod } from "./CANTableRow.helpers";

describe("displayActivePeriod", () => {
it('should return "TBD" when activePeriod is 0', () => {
expect(displayActivePeriod(0)).toBe("TBD");
});

it('should return "1 year" when activePeriod is 1', () => {
expect(displayActivePeriod(1)).toBe("1 year");
});

it('should return "X years" for any activePeriod greater than 1', () => {
expect(displayActivePeriod(2)).toBe("2 years");
expect(displayActivePeriod(5)).toBe("5 years");
expect(displayActivePeriod(10)).toBe("10 years");
});

it("should handle negative values", () => {
expect(displayActivePeriod(-2)).toBe("-2 years");
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/CANs/CANTable/CANTableRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useGetCanFundingSummaryQuery } from "../../../api/opsAPI";
import { getDecimalScale } from "../../../helpers/currencyFormat.helpers";
import { convertCodeForDisplay } from "../../../helpers/utils";
import Tooltip from "../../UI/USWDS/Tooltip";
import { displayActivePeriod } from "./CANTableRow.helpers";

/**
* CanTableRow component of CANTable
Expand Down Expand Up @@ -65,7 +66,7 @@ const CANTableRow = ({
</th>
<td>{portfolio}</td>
<td>{fiscalYear}</td>
<td>{activePeriod > 1 ? `${activePeriod} years` : `${activePeriod} year`}</td>
<td>{displayActivePeriod(activePeriod)}</td>
<td>{obligateBy}</td>
<td>{convertCodeForDisplay("methodOfTransfer", transfer)}</td>
<td>
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/Users/UserTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,24 @@ export type SafeUser = {
full_name: string;
id: number;
};

export type User = {
display_name: string;
division: number;
email: string;
first_name: string;
full_name: string;
hhs_id?: number;
id: number;
last_name: string;
oidc_id: string;
roles: UserRoles[];
status: UserStatus;
created_by?: number;
created_on: Date;
updated_by?: number;
updated_on: Date;
};

type UserRoles = "USER_ADMIN" | "BUDGET_TEAM" | "admin" | "division-director" | "user" | "unassigned";
type UserStatus = "ACTIVE" | "INACTIVE" | "LOCKED";
33 changes: 33 additions & 0 deletions frontend/src/pages/cans/list/CanList.helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Sorts an array of CANs by obligateBy date in descending order.
* @typedef {import("../../../components/CANs/CANTypes").CAN} CAN
* @param {CAN[]} cans - The array of CANs to sort.
* @param {boolean} myCANsUrl - The URL parameter to filter by "my-CANs".
* @param {import("../../../components/Users/UserTypes").User} activeUser - The active user.
* @returns {CAN[] | undefined} - The sorted array of CANs.
*/
export const sortAndFilterCANs = (cans, myCANsUrl, activeUser) => {
if (!cans || cans.length === 0) {
return [];
}
let filteredCANs = myCANsUrl ? cans.filter((can) => can.portfolio.division_id === activeUser.division) : cans;

return sortCANs(filteredCANs);
};

/**
* Sorts an array of CANs by obligateBy date in descending order.
* @param {CAN[]} cans - The array of CANs to sort.
* @returns {CAN[] | []} - The sorted array of CANs.
*/
const sortCANs = (cans) => {
if (!cans) {
return [];
}

return [...cans].sort((a, b) => {
const dateA = a.obligate_by ? new Date(a.obligate_by).getTime() : 0;
const dateB = b.obligate_by ? new Date(b.obligate_by).getTime() : 0;
return dateB - dateA;
});
};
39 changes: 39 additions & 0 deletions frontend/src/pages/cans/list/CanList.helpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { sortAndFilterCANs } from "./CanList.helpers";

describe("sortAndFilterCANs", () => {
const mockUser = { division: 1 };

const mockCANs = [
{ id: 1, obligate_by: "2023", portfolio: { division_id: 1 } },
{ id: 2, obligate_by: "2023", portfolio: { division_id: 2 } },
{ id: 3, obligate_by: "2023", portfolio: { division_id: 1 } },
{ id: 4, obligate_by: null, portfolio: { division_id: 1 } }
];

it("should return an empty array when input is null or empty", () => {
expect(sortAndFilterCANs(null, false, mockUser)).toEqual([]);
expect(sortAndFilterCANs([], false, mockUser)).toEqual([]);
});

it("should sort CANs by obligate_by date in descending order", () => {
const result = sortAndFilterCANs(mockCANs, false, mockUser);
expect(result.map((can) => can.id)).toEqual([1, 2, 3, 4]);
});

it("should filter CANs by user division when myCANsUrl is true", () => {
const result = sortAndFilterCANs(mockCANs, true, mockUser);
expect(result.length).toBe(3);
expect(result.every((can) => can.portfolio.division_id === 1)).toBe(true);
});

it("should not filter CANs when myCANsUrl is false", () => {
const result = sortAndFilterCANs(mockCANs, false, mockUser);
expect(result.length).toBe(4);
});

it("should handle CANs with null obligate_by dates", () => {
const result = sortAndFilterCANs(mockCANs, false, mockUser);
expect(result[result.length - 1].id).toBe(4);
});
});
13 changes: 12 additions & 1 deletion frontend/src/pages/cans/list/CanList.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { useSelector } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { useGetCansQuery } from "../../../api/opsAPI";
import App from "../../../App";
import CANTable from "../../../components/CANs/CANTable";
import CANTags from "../../../components/CANs/CanTabs";
import TablePageLayout from "../../../components/Layouts/TablePageLayout";
import ErrorPage from "../../ErrorPage";
import { sortAndFilterCANs } from "./CanList.helpers";

/**
* Page for the CAN List.
* @component
* @typedef {import("../../../components/CANs/CANTypes").CAN} CAN
* @returns {JSX.Element | boolean} - The component JSX.
*/
const CanList = () => {
const [searchParams] = useSearchParams();
const myCANsUrl = searchParams.get("filter") === "my-cans";
const { data: canList, isError, isLoading } = useGetCansQuery({});
const activeUser = useSelector((state) => state.auth.activeUser);
const sortedCANs = sortAndFilterCANs(canList, myCANsUrl, activeUser) || [];

if (isLoading) {
return (
<App>
Expand All @@ -33,7 +44,7 @@ const CanList = () => {
: "This is a list of all CANs across OPRE that are or were active within the selected Fiscal Year."
}
TabsSection={<CANTags />}
TableSection={<CANTable cans={canList} />}
TableSection={<CANTable cans={sortedCANs} />}
/>
</App>
)
Expand Down

0 comments on commit 98cf127

Please sign in to comment.