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: ✨ adds FY Select to CANs List #2882

Merged
merged 19 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/cypress/e2e/canList.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { terminalLog, testLogin } from "./utils";
beforeEach(() => {
testLogin("division-director");
cy.visit("/cans").wait(1000);
cy.get("#fiscal-year-select").select("2023");
});

afterEach(() => {
Expand All @@ -29,10 +30,9 @@ describe("CAN List", () => {
});

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("#fiscal-year-select").select("2023");
cy.get("tbody").children().should("have.length", 1);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { useEffect } from "react";
import styles from "./CANBudgetSummary.module.css";
import constants from "../../../constants";
import { getPendingFunds } from "./util";
import FiscalYear from "../../UI/FiscalYear/FiscalYear";
import FiscalYear from "../../UI/FiscalYear";

const CANBudgetSummary = () => {
const dispatch = useDispatch();
const canFiscalYear = useSelector((state) => state.canDetail.canFiscalYearObj);
const pendingFunds = useSelector((state) => state.canDetail.pendingFunds);
const selectedFiscalYear = useSelector((state) => state.canDetail.selectedFiscalYear);
const fiscalYear = Number(selectedFiscalYear.value);
const urlPathParams = useParams();
const canFiscalYearId = parseInt(urlPathParams.id);

Expand Down Expand Up @@ -54,7 +55,7 @@ const CANBudgetSummary = () => {
<h2>Budget summary</h2>

<FiscalYear
fiscalYear={selectedFiscalYear}
fiscalYear={fiscalYear}
handleChangeFiscalYear={setSelectedFiscalYear}
/>

Expand All @@ -67,7 +68,7 @@ const CANBudgetSummary = () => {
</thead>
<tbody>
<tr>
<th scope="row">Total FY {selectedFiscalYear.value || constants.notFilledInText} Funding</th>
<th scope="row">Total FY {fiscalYear || constants.notFilledInText} Funding</th>
{totalFiscalYearFundingTableData}
</tr>
<tr>
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/CANs/CANTable/CANTable.helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ export const formatObligateBy = (obligateBy) => {
year: "2-digit"
});
};

/**
* function to filter funding_budgets fiscal year by fiscal year
* @param {import("../CANTypes").CAN} can - CAN object
* @param {number} fiscalYear - Fiscal year to filter by
* @returns {number} - Fiscal year of the funding budget
*/
export function findFundingBudgetFYByFiscalYear(can, fiscalYear) {
if (!can || !fiscalYear) return 0;
const matchingBudget = can.funding_budgets.find((budget) => budget.fiscal_year === fiscalYear);

return matchingBudget ? matchingBudget.fiscal_year : 0;
}
/**
* function to filter funding_budgets budget by fiscal year
* @param {import("../CANTypes").CAN} can - CAN object
* @param {number} fiscalYear - Fiscal year to filter by
* @returns {number} - Fiscal year of the funding budget
*/
export function findFundingBudgetBudgetByFiscalYear(can, fiscalYear) {
if (!can || !fiscalYear) return 0;
const matchingBudget = can.funding_budgets.find((budget) => budget.fiscal_year === fiscalYear);

return matchingBudget ? matchingBudget.budget : 0;
}
58 changes: 57 additions & 1 deletion frontend/src/components/CANs/CANTable/CANTable.helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { formatObligateBy } from "./CANTable.helpers";
import {
findFundingBudgetBudgetByFiscalYear,
findFundingBudgetFYByFiscalYear,
formatObligateBy
} from "./CANTable.helpers";

describe("formatObligateBy", () => {
test('returns "TBD" for undefined input', () => {
Expand All @@ -22,3 +26,55 @@ describe("formatObligateBy", () => {
expect(formatObligateBy(2025)).toBe("09/30/24");
});
});

describe("findFundingBudgetFYByFiscalYear", () => {
const mockCAN = {
funding_budgets: [
{ fiscal_year: 2022, budget: 1000 },
{ fiscal_year: 2023, budget: 2000 },
{ fiscal_year: 2024, budget: 3000 }
]
};

test("returns 0 for undefined CAN", () => {
expect(findFundingBudgetFYByFiscalYear(undefined, 2023)).toBe(0);
});

test("returns 0 for undefined fiscal year", () => {
expect(findFundingBudgetFYByFiscalYear(mockCAN, undefined)).toBe(0);
});

test("returns matching fiscal year when found", () => {
expect(findFundingBudgetFYByFiscalYear(mockCAN, 2023)).toBe(2023);
});

test("returns 0 when fiscal year not found", () => {
expect(findFundingBudgetFYByFiscalYear(mockCAN, 2025)).toBe(0);
});
});

describe("findFundingBudgetBudgetByFiscalYear", () => {
const mockCAN = {
funding_budgets: [
{ fiscal_year: 2022, budget: 1000 },
{ fiscal_year: 2023, budget: 2000 },
{ fiscal_year: 2024, budget: 3000 }
]
};

test("returns 0 for undefined CAN", () => {
expect(findFundingBudgetBudgetByFiscalYear(undefined, 2023)).toBe(0);
});

test("returns 0 for undefined fiscal year", () => {
expect(findFundingBudgetBudgetByFiscalYear(mockCAN, undefined)).toBe(0);
});

test("returns matching budget when fiscal year found", () => {
expect(findFundingBudgetBudgetByFiscalYear(mockCAN, 2023)).toBe(2000);
});

test("returns 0 when fiscal year not found", () => {
expect(findFundingBudgetBudgetByFiscalYear(mockCAN, 2025)).toBe(0);
});
});
28 changes: 20 additions & 8 deletions frontend/src/components/CANs/CANTable/CANTable.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import PropTypes from "prop-types";
import React from "react";
import PaginationNav from "../../UI/PaginationNav";
import {
formatObligateBy,
findFundingBudgetBudgetByFiscalYear,
findFundingBudgetFYByFiscalYear
} from "./CANTable.helpers";
import CANTableHead from "./CANTableHead";
import CANTableRow from "./CANTableRow";
import styles from "./style.module.css";
import { formatObligateBy } from "./CANTable.helpers";

/**
* CANTable component of CanList
* @component
* @typedef {import("../CANTypes").CAN} CAN
* @param {Object} props
* @param {CAN[]} props.cans - Array of CANs
* @param {number} props.fiscalYear - Fiscal year to filter by
* @returns {JSX.Element}
*/
const CANTable = ({ cans }) => {
const CANTable = ({ cans, fiscalYear }) => {
// Filter CANs by fiscal year
const filteredCANsByFiscalYear = React.useMemo(() => {
if (!fiscalYear) return cans;
return cans.filter((can) => can.funding_budgets.some((budget) => budget.fiscal_year === fiscalYear));
}, [cans, fiscalYear]);
// TODO: once in prod, change this to 25
const CANS_PER_PAGE = 10;
const [currentPage, setCurrentPage] = React.useState(1);
let cansPerPage = [...cans];
let cansPerPage = [...filteredCANsByFiscalYear];
cansPerPage = cansPerPage.slice((currentPage - 1) * CANS_PER_PAGE, currentPage * CANS_PER_PAGE);

if (cans.length === 0) {
if (cansPerPage.length === 0) {
return <p className="text-center">No CANs found</p>;
}

Expand All @@ -36,16 +47,16 @@ const CANTable = ({ cans }) => {
name={can.display_name ?? "TBD"}
nickname={can.nick_name ?? "TBD"}
portfolio={can.portfolio.abbreviation}
fiscalYear={can.funding_budgets[0]?.fiscal_year ?? "TBD"}
fiscalYear={findFundingBudgetFYByFiscalYear(can, fiscalYear)}
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}
fyBudget={findFundingBudgetBudgetByFiscalYear(can, fiscalYear)}
/>
))}
</tbody>
</table>
{cans.length > 0 && (
{filteredCANsByFiscalYear.length > CANS_PER_PAGE && (
<PaginationNav
currentPage={currentPage}
setCurrentPage={setCurrentPage}
Expand All @@ -58,7 +69,8 @@ const CANTable = ({ cans }) => {
};

CANTable.propTypes = {
cans: PropTypes.array.isRequired
cans: PropTypes.array.isRequired,
fiscalYear: PropTypes.number
};

export default CANTable;
10 changes: 0 additions & 10 deletions frontend/src/components/CANs/CANTable/CANTable.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,6 @@ describe("CANTable", () => {
expect(screen.getByText("No CANs found")).toBeInTheDocument();
});

it("renders PaginationNav when there are CANs", () => {
render(
<MemoryRouter>
<CANTable cans={cans} />
</MemoryRouter>
);

expect(screen.getByTestId("pagination-nav")).toBeInTheDocument();
});

it("does not render PaginationNav when there are no CANs", () => {
render(
<MemoryRouter>
Expand Down
48 changes: 27 additions & 21 deletions frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,33 @@ import icons from "../../../uswds/img/sprite.svg";
*
* @component
* @param {object} props - The component props.
* @param {string} [props.buttonLink] - The link to navigate to when the button is clicked.
* @param {string} [props.buttonText] - The text to display on the button.
* @param {React.ReactNode} [props.children] - The children to render - optional.
* @param {string} props.title - The title to display.
* @param {string} props.subtitle - The subtitle to display.
* @param {string} props.details - The details to display.
* @param {React.ReactNode} props.TabsSection - The tabs to display.
* @param {React.ReactNode} [props.FilterTags] - The filter tags to display.
* @param {React.ReactNode} [props.FilterButton] - The filter button to display.
* @param {React.ReactNode} [props.TableSection] - The table to display.
* @param {React.ReactNode} [props.FilterTags] - The filter tags to display.
* @param {React.ReactNode} [props.FYSelect] - The fiscal year select to display.
* @param {string} props.subtitle - The subtitle to display.
* @param {React.ReactNode} [props.SummaryCardsSection] - The summary cards to display.
* @param {string} [props.buttonText] - The text to display on the button.
* @param {string} [props.buttonLink] - The link to navigate to when the button is clicked.
* @param {React.ReactNode} [props.TableSection] - The table to display.
* @param {React.ReactNode} props.TabsSection - The tabs to display.
* @param {string} props.title - The title to display.
* @returns {JSX.Element} - The rendered component.
*/
export const TablePageLayout = ({
buttonLink,
buttonText,
children,
title,
subtitle,
details,
TabsSection,
FilterButton = null,
FilterTags = null,
FYSelect,
subtitle,
SummaryCardsSection,
FilterButton = null,
TableSection = null,
buttonText,
buttonLink
TabsSection,
title
}) => {
return (
<>
Expand All @@ -52,7 +54,10 @@ export const TablePageLayout = ({
</Link>
)}
</div>
{TabsSection}
<div className="display-flex flex-align-center flex-justify padding-y-1">
{TabsSection}
{FYSelect && FYSelect}
</div>
<div className="display-flex flex-justify padding-y-1">
<div>
<h2 className="margin-0">{subtitle}</h2>
Expand All @@ -71,15 +76,16 @@ export const TablePageLayout = ({
export default TablePageLayout;

TablePageLayout.propTypes = {
buttonLink: PropTypes.string,
buttonText: PropTypes.string,
children: PropTypes.node,
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
details: PropTypes.string.isRequired,
TabsSection: PropTypes.node.isRequired,
FilterTags: PropTypes.node,
FilterButton: PropTypes.node,
TableSection: PropTypes.node,
FilterTags: PropTypes.node,
FYSelect: PropTypes.node,
subtitle: PropTypes.string.isRequired,
SummaryCardsSection: PropTypes.node,
buttonText: PropTypes.string,
buttonLink: PropTypes.string
TableSection: PropTypes.node,
TabsSection: PropTypes.node.isRequired,
title: PropTypes.string.isRequired
};
28 changes: 20 additions & 8 deletions frontend/src/components/UI/FiscalYear/FiscalYear.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
import constants from "../../../constants";
import { useDispatch } from "react-redux";
import styles from "./FiscalYear.module.css";
import constants from "../../../constants";

/**
* FiscalYear component for selecting a fiscal year
* @param {Object} props - Component props
* @param {number} props.fiscalYear - Current fiscal year selected
* @param {Function} props.handleChangeFiscalYear - Function to handle fiscal year change
* @returns {JSX.Element} FiscalYear component
*/
const FiscalYear = ({ fiscalYear, handleChangeFiscalYear }) => {
const dispatch = useDispatch();

/**
* Handles the change of fiscal year
* @param {React.ChangeEvent<HTMLSelectElement>} event - The change event
*/
const onChangeFiscalYear = (event) => {
dispatch(handleChangeFiscalYear({ value: event.target.value }));
};

const fiscalYearClasses = `usa-select ${styles.fiscalYearSelector}`;

return (
<div className={styles.container}>
<div
className="display-flex flex-justify flex-align-center"
style={{ width: "10.625rem" }}
>
<label
className="font-sans-xs text-bold"
className="font-sans-xs"
htmlFor="fiscal-year-select"
>
Fiscal Year
</label>
<select
id="fiscal-year-select"
className={fiscalYearClasses}
className="usa-select margin-left-1"
style={{ width: "5rem" }}
onChange={onChangeFiscalYear}
value={fiscalYear?.value}
value={fiscalYear}
>
{constants.fiscalYears.map((year) => {
return (
Expand Down
10 changes: 0 additions & 10 deletions frontend/src/components/UI/FiscalYear/FiscalYear.module.css

This file was deleted.

Loading