Skip to content

Commit

Permalink
feat: ✨ adds Budget Summary Card (#2974)
Browse files Browse the repository at this point in the history
* feat: BudgetSummaryCard

---------

Co-authored-by: Santi-3rd <[email protected]>
  • Loading branch information
fpigeonjr and Santi-3rd authored Oct 24, 2024
1 parent 08adb3c commit 4bcf572
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 2 deletions.
6 changes: 6 additions & 0 deletions frontend/cypress/e2e/canList.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 *")
});
});
12 changes: 10 additions & 2 deletions frontend/src/components/CANs/CANFundingBar/CANFundingBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/CANs/CANSummaryCards/CANSummaryCards.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="display-flex flex-justify">
<p> Summary Cards Left</p>
<BudgetSummaryCard
title={`FY ${fiscalYear} CANs Available Budget *`}
totalSpending={1_500_000}
totalFunding={2_000_000}
/>
</div>
);
};

export default CANSummaryCards;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
describe("CANSummaryCards", () => {
it.todo("renders all summary cards", () => {});
});
1 change: 1 addition & 0 deletions frontend/src/components/CANs/CANSummaryCards/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./CANSummaryCards";
Original file line number Diff line number Diff line change
@@ -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 (
<RoundedBox
className={"padding-y-205 padding-x-4 display-inline-block"}
dataCy={`budget-summary-card`}
style={{ height: "14.5rem" }}
>
<h3
className="margin-0 margin-bottom-2 font-12px text-base-dark text-normal"
style={{ whiteSpace: "pre-line", lineHeight: "20px" }}
>
{title}
</h3>

<div className="font-32px margin-0 display-flex flex-justify flex-align-end">
<CurrencyWithSmallCents
amount={remainingBudget}
dollarsClasses="font-sans-xl text-bold margin-bottom-0"
centsStyles={{ fontSize: "10px" }}
/>
{overBudget ? (
<Tag tagStyle={"lightTextRedBackground"}>
<FontAwesomeIcon
icon={faTriangleExclamation}
title="Over Budget"
/>{" "}
Over Budget
</Tag>
) : (
<Tag tagStyle={"budgetAvailable"}>Available</Tag>
)}
</div>
<div
id="currency-summary-card"
className="margin-top-2"
>
<CANFundingBar
data={graphData}
isStriped={true}
overBudget={overBudget}
/>
</div>
<div className="font-12px margin-top-2 display-flex flex-justify-end">
<div>
Spending {""}
<CurrencyFormat
value={totalSpending || 0}
displayType={"text"}
thousandSeparator={true}
prefix={"$"}
renderText={(totalSpending) => <span>{totalSpending}</span>}
/>{" "}
of{" "}
<CurrencyFormat
value={totalFunding || 0}
displayType={"text"}
thousandSeparator={true}
prefix={"$"}
renderText={(totalFunding) => <span>{totalFunding}</span>}
/>
</div>
</div>
</RoundedBox>
);
};
export default BudgetSummaryCard;
Original file line number Diff line number Diff line change
@@ -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(<BudgetSummaryCard {...defaultProps} />);

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(<BudgetSummaryCard {...overBudgetProps} />);

expect(screen.getByTitle("Over Budget")).toBeInTheDocument();
});

it("displays correct spending and funding amounts", () => {
render(<BudgetSummaryCard {...defaultProps} />);

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(<BudgetSummaryCard {...zeroProps} />);

expect(screen.queryAllByText("$0")).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default} from "./BudgetSummaryCard"
3 changes: 3 additions & 0 deletions frontend/src/pages/cans/list/CanList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -59,6 +60,7 @@ const CanList = () => {
/>
);
};

// TODO: remove flag once CANS are ready
return (
import.meta.env.DEV && (
Expand Down Expand Up @@ -95,6 +97,7 @@ const CanList = () => {
fyBudgetRange={[minFYBudget, maxFYBudget]}
/>
}
SummaryCardsSection={<CANSummaryCards fiscalYear={fiscalYear} />}
/>
</App>
)
Expand Down

0 comments on commit 4bcf572

Please sign in to comment.