diff --git a/frontend/cypress/e2e/canList.cy.js b/frontend/cypress/e2e/canList.cy.js index c947491673..0444ce86c5 100644 --- a/frontend/cypress/e2e/canList.cy.js +++ b/frontend/cypress/e2e/canList.cy.js @@ -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(() => { @@ -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); }); diff --git a/frontend/src/components/CANs/CANBudgetSummary/CANBudgetSummary.jsx b/frontend/src/components/CANs/CANBudgetSummary/CANBudgetSummary.jsx index 7ea39313cc..56f38933ce 100644 --- a/frontend/src/components/CANs/CANBudgetSummary/CANBudgetSummary.jsx +++ b/frontend/src/components/CANs/CANBudgetSummary/CANBudgetSummary.jsx @@ -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); @@ -54,7 +55,7 @@ const CANBudgetSummary = () => {

Budget summary

@@ -67,7 +68,7 @@ const CANBudgetSummary = () => { - Total FY {selectedFiscalYear.value || constants.notFilledInText} Funding + Total FY {fiscalYear || constants.notFilledInText} Funding {totalFiscalYearFundingTableData} diff --git a/frontend/src/components/CANs/CANTable/CANTable.helpers.js b/frontend/src/components/CANs/CANTable/CANTable.helpers.js index b3790efd4e..c83b848c02 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.helpers.js +++ b/frontend/src/components/CANs/CANTable/CANTable.helpers.js @@ -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; +} diff --git a/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js b/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js index 3349b089e6..d4751264d7 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js +++ b/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js @@ -1,4 +1,8 @@ -import { formatObligateBy } from "./CANTable.helpers"; +import { + findFundingBudgetBudgetByFiscalYear, + findFundingBudgetFYByFiscalYear, + formatObligateBy +} from "./CANTable.helpers"; describe("formatObligateBy", () => { test('returns "TBD" for undefined input', () => { @@ -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); + }); +}); diff --git a/frontend/src/components/CANs/CANTable/CANTable.jsx b/frontend/src/components/CANs/CANTable/CANTable.jsx index e55043a643..dde05cad1b 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.jsx +++ b/frontend/src/components/CANs/CANTable/CANTable.jsx @@ -1,10 +1,14 @@ 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 @@ -12,15 +16,22 @@ import { formatObligateBy } from "./CANTable.helpers"; * @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

No CANs found

; } @@ -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)} /> ))} - {cans.length > 0 && ( + {filteredCANsByFiscalYear.length > CANS_PER_PAGE && ( { }; CANTable.propTypes = { - cans: PropTypes.array.isRequired + cans: PropTypes.array.isRequired, + fiscalYear: PropTypes.number }; export default CANTable; diff --git a/frontend/src/components/CANs/CANTable/CANTable.test.jsx b/frontend/src/components/CANs/CANTable/CANTable.test.jsx index 0d77969497..a6a47a6ba1 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.test.jsx +++ b/frontend/src/components/CANs/CANTable/CANTable.test.jsx @@ -66,16 +66,6 @@ describe("CANTable", () => { expect(screen.getByText("No CANs found")).toBeInTheDocument(); }); - it("renders PaginationNav when there are CANs", () => { - render( - - - - ); - - expect(screen.getByTestId("pagination-nav")).toBeInTheDocument(); - }); - it("does not render PaginationNav when there are no CANs", () => { render( diff --git a/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx b/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx index 0b6bed8bb5..d17427fb6f 100644 --- a/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx +++ b/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx @@ -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 ( <> @@ -52,7 +54,10 @@ export const TablePageLayout = ({ )} - {TabsSection} +
+ {TabsSection} + {FYSelect && FYSelect} +

{subtitle}

@@ -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 }; diff --git a/frontend/src/components/UI/FiscalYear/FiscalYear.jsx b/frontend/src/components/UI/FiscalYear/FiscalYear.jsx index 4140173d68..6675354deb 100644 --- a/frontend/src/components/UI/FiscalYear/FiscalYear.jsx +++ b/frontend/src/components/UI/FiscalYear/FiscalYear.jsx @@ -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} event - The change event + */ const onChangeFiscalYear = (event) => { dispatch(handleChangeFiscalYear({ value: event.target.value })); }; - const fiscalYearClasses = `usa-select ${styles.fiscalYearSelector}`; - return ( -
+