From 4bcf572aacae803521ad697d40a301dde05e4005 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Thu, 24 Oct 2024 13:56:51 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20adds=20Budget=20Summary=20C?= =?UTF-8?q?ard=20(#2974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BudgetSummaryCard --------- Co-authored-by: Santi-3rd --- frontend/cypress/e2e/canList.cy.js | 6 ++ .../CANs/CANFundingBar/CANFundingBar.jsx | 12 ++- .../CANs/CANSummaryCards/CANSummaryCards.jsx | 22 +++++ .../CANSummaryCards/CANSummaryCards.test.jsx | 3 + .../components/CANs/CANSummaryCards/index.js | 1 + .../BudgetSummaryCard/BudgetSummaryCard.jsx | 96 +++++++++++++++++++ .../BudgetSummaryCard.test.jsx | 53 ++++++++++ .../UI/SummaryCard/BudgetSummaryCard/index.js | 1 + frontend/src/pages/cans/list/CanList.jsx | 3 + 9 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.jsx create mode 100644 frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.test.jsx create mode 100644 frontend/src/components/CANs/CANSummaryCards/index.js create mode 100644 frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.jsx create mode 100644 frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.test.jsx create mode 100644 frontend/src/components/UI/SummaryCard/BudgetSummaryCard/index.js diff --git a/frontend/cypress/e2e/canList.cy.js b/frontend/cypress/e2e/canList.cy.js index ead18ef142..8f410d7d2e 100644 --- a/frontend/cypress/e2e/canList.cy.js +++ b/frontend/cypress/e2e/canList.cy.js @@ -147,4 +147,10 @@ describe("CAN List", () => { cy.get("li").should("have.class", "usa-pagination__item").contains("1").click(); cy.get("button").should("have.class", "usa-current").contains("1"); }); + + it("should display the can budget summary card", () => { + cy.get("#fiscal-year-select").select("2023"); + cy.get("[data-cy='budget-summary-card']").should("exist") + cy.get("[data-cy='budget-summary-card']").contains("FY 2023 CANs Available Budget *") + }); }); diff --git a/frontend/src/components/CANs/CANFundingBar/CANFundingBar.jsx b/frontend/src/components/CANs/CANFundingBar/CANFundingBar.jsx index a5597813ea..7c47886187 100644 --- a/frontend/src/components/CANs/CANFundingBar/CANFundingBar.jsx +++ b/frontend/src/components/CANs/CANFundingBar/CANFundingBar.jsx @@ -2,11 +2,19 @@ import PropTypes from "prop-types"; import styles from "./styles.module.scss"; import { useEffect, useState } from "react"; import { calculateRatio } from "./util"; + +/** + * @typedef {Object} Data + * @property {number} id + * @property {number} value + * @property {string} color + */ + /** - * + * @description A bar that displays the funding status of a CAN. * @component * @param {Object} props - * @param {Object[]} props.data + * @param {Data[]} props.data * @param {Function} [props.setActiveId] * @param {boolean} [props.isStriped] * @param {boolean} [props.overBudget] diff --git a/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.jsx b/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.jsx new file mode 100644 index 0000000000..b9fa79d7e9 --- /dev/null +++ b/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.jsx @@ -0,0 +1,22 @@ +import BudgetSummaryCard from "../../UI/SummaryCard/BudgetSummaryCard"; + +/** + * @component + * @param {Object} props - Properties passed to component + * @param {number} props.fiscalYear - The fiscal year. + * @returns {JSX.Element} - The CANSummaryCards component. + */ +const CANSummaryCards = ({ fiscalYear }) => { + return ( +
+

Summary Cards Left

+ +
+ ); +}; + +export default CANSummaryCards; diff --git a/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.test.jsx b/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.test.jsx new file mode 100644 index 0000000000..f9a0848990 --- /dev/null +++ b/frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.test.jsx @@ -0,0 +1,3 @@ +describe("CANSummaryCards", () => { + it.todo("renders all summary cards", () => {}); +}); diff --git a/frontend/src/components/CANs/CANSummaryCards/index.js b/frontend/src/components/CANs/CANSummaryCards/index.js new file mode 100644 index 0000000000..c44dc2e2f2 --- /dev/null +++ b/frontend/src/components/CANs/CANSummaryCards/index.js @@ -0,0 +1 @@ +export { default } from "./CANSummaryCards"; diff --git a/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.jsx b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.jsx new file mode 100644 index 0000000000..c8e888bfe0 --- /dev/null +++ b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.jsx @@ -0,0 +1,96 @@ +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import CurrencyFormat from "react-currency-format"; +import CANFundingBar from "../../../CANs/CANFundingBar"; +import CurrencyWithSmallCents from "../../CurrencyWithSmallCents/CurrencyWithSmallCents"; +import RoundedBox from "../../RoundedBox"; +import Tag from "../../Tag"; +/** + * @component + * @param {Object} props - Properties passed to component + * @param {string} props.title - The title of the card. + * @param {number} props.totalSpending - The total spending. + * @param {number} props.totalFunding - The total funding. + * @returns {JSX.Element} - The BudgetSummaryCard component. + */ +const BudgetSummaryCard = ({ title, totalSpending, totalFunding }) => { + const overBudget = totalSpending > totalFunding; + const remainingBudget = overBudget ? 0 : totalFunding - totalSpending; + const graphData = [ + { + id: 1, + value: totalSpending, + color: overBudget ? "var(--feedback-error)" : "var(--data-viz-budget-graph-2)" + }, + { + id: 2, + value: remainingBudget, + color: overBudget ? "var(--feedback-error)" : "var(--data-viz-budget-graph-1)" + } + ]; + + return ( + +

+ {title} +

+ +
+ + {overBudget ? ( + + {" "} + Over Budget + + ) : ( + Available + )} +
+
+ +
+
+
+ Spending {""} + {totalSpending}} + />{" "} + of{" "} + {totalFunding}} + /> +
+
+
+ ); +}; +export default BudgetSummaryCard; diff --git a/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.test.jsx b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.test.jsx new file mode 100644 index 0000000000..7660ba27d7 --- /dev/null +++ b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/BudgetSummaryCard.test.jsx @@ -0,0 +1,53 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import BudgetSummaryCard from "./BudgetSummaryCard"; + +describe("BudgetSummaryCard", () => { + const defaultProps = { + title: "Budget Summary", + totalSpending: 15000, + totalFunding: 20000 + }; + + it("renders with all required props", () => { + render(); + + expect(screen.getByText("Budget Summary")).toBeInTheDocument(); + expect(screen.getByText("$ 5,000")).toBeInTheDocument(); + expect(screen.getByText("Available")).toBeInTheDocument(); + }); + + it("displays over budget warning when spending exceeds funding", () => { + const overBudgetProps = { + ...defaultProps, + totalSpending: 25000, + totalFunding: 20000 + }; + + render(); + + expect(screen.getByTitle("Over Budget")).toBeInTheDocument(); + }); + + it("displays correct spending and funding amounts", () => { + render(); + + const spendingText = screen.getByText(/\$15,000/); + const fundingText = screen.getByText(/\$20,000/); + + expect(spendingText).toBeInTheDocument(); + expect(fundingText).toBeInTheDocument(); + }); + + it("handles zero values correctly", () => { + const zeroProps = { + ...defaultProps, + totalSpending: 0, + totalFunding: 0 + }; + + render(); + + expect(screen.queryAllByText("$0")).toHaveLength(2); + }); +}); diff --git a/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/index.js b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/index.js new file mode 100644 index 0000000000..bd54f18ccc --- /dev/null +++ b/frontend/src/components/UI/SummaryCard/BudgetSummaryCard/index.js @@ -0,0 +1 @@ +export {default} from "./BudgetSummaryCard" \ No newline at end of file diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx index f7831322be..e12299a0c0 100644 --- a/frontend/src/pages/cans/list/CanList.jsx +++ b/frontend/src/pages/cans/list/CanList.jsx @@ -12,6 +12,7 @@ import ErrorPage from "../../ErrorPage"; import CANFilterButton from "./CANFilterButton"; import { sortAndFilterCANs, getPortfolioOptions, getSortedFYBudgets } from "./CanList.helpers"; import CANFilterTags from "./CANFilterTags"; +import CANSummaryCards from "../../../components/CANs/CANSummaryCards"; /** * Page for the CAN List. @@ -59,6 +60,7 @@ const CanList = () => { /> ); }; + // TODO: remove flag once CANS are ready return ( import.meta.env.DEV && ( @@ -95,6 +97,7 @@ const CanList = () => { fyBudgetRange={[minFYBudget, maxFYBudget]} /> } + SummaryCardsSection={} /> )