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 CAN Funding Received form #3246

Merged
merged 20 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
40 changes: 38 additions & 2 deletions frontend/cypress/e2e/canDetail.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ describe("CAN detail page", () => {
.and("contain", "$6,000,000.00")
.and("contain", "60%");
});
it("handles budget form", () => {
it.only("handles budget form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
Expand All @@ -182,7 +182,6 @@ describe("CAN detail page", () => {
cy.get("[data-cy='can-budget-fy-card']").should("contain", "0");
cy.get("#budget-amount").type(can504.budgetAmount);
cy.get("#budget-amount").clear();
cy.get(".usa-error-message").should("exist").contains("This is required information");
cy.get("#budget-amount").type(can504.budgetAmount);
cy.get(".usa-error-message").should("not.exist");
cy.get("#add-fy-budget").click();
Expand All @@ -197,6 +196,43 @@ describe("CAN detail page", () => {
.and("contain", `FY ${currentFiscalYear}`)
.and("contain", "$5,000,000.00");
});
it.only("handle funding received form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
// check that all buttons (saved, all funding received) are disabled
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
cy.get("[data-cy=save-btn]").should("be.disabled");
// enter amount into input
cy.get("#funding-received-amount").type("1_000_000");
cy.get("#funding-received-amount").blur();
cy.get("[data-cy=add-funding-received-btn]").should("be.enabled");
// clear and check validation
cy.get("#funding-received-amount").clear();
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
// Test received amount over budget amount
cy.get("#funding-received-amount").type("6_000_000");
cy.get("[data-cy=add-funding-received-btn]").should("be.disabled");
cy.get(".usa-error-message").should("exist").contains("Amount cannot exceed FY Budget");
cy.get("#funding-received-amount").clear();
cy.get("#funding-received-amount").type("1_000_000");
cy.get("#funding-received-amount").blur();
cy.get("[data-cy=add-funding-received-btn]").should("be.enabled");
// enter and click on add funding received
cy.get("#notes").type("Test notes");
cy.get("[data-cy=add-funding-received-btn]").click();
// check card on the right
cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "1,000,000.00");
// click on button at bottom of form
cy.get("[data-cy=save-btn]").click();
// check success alert
cy.get(".usa-alert__body").should("contain", `The CAN ${can504.nickname} has been successfully updated.`);
// check that table and card are updated
cy.get("[data-cy=budget-received-card]")
.should("exist")
.and("contain", "Received $1,000,000.00 of $5,000,000.00");
cy.get("tbody").children().should("contain", "2025").and("contain", "$1,000,000.00").and("contain", "20%");
});
it("handles cancelling from budget form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api/opsAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,15 @@ export const opsApi = createApi({
}),
invalidatesTags: ["Cans", "CanFunding"]
}),
addCanFundingReceived: builder.mutation({
query: ({ data }) => ({
url: `/cans-funding-received/`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: data
}),
invalidatesTags: ["Cans", "CanFunding"]
}),
getCanFundingSummary: builder.query({
query: ({ ids, fiscalYear, activePeriod, transfer, portfolio, fyBudgets }) => {
const queryParams = [];
Expand Down Expand Up @@ -433,6 +442,7 @@ export const {
useUpdateCanMutation,
useAddCanFundingBudgetsMutation,
useUpdateCanFundingBudgetMutation,
useAddCanFundingReceivedMutation,
useGetCanFundingSummaryQuery,
useGetNotificationsByUserIdQuery,
useGetNotificationsByUserIdAndAgreementIdQuery,
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import icons from "../../../uswds/img/sprite.svg";

/**
* @typedef {Object} CANBudgetFormProps
* @property {string} totalFunding
* @property {string} budgetAmount
* @property {(arg: string) => string} cn
* @property {Object} res
Expand All @@ -17,14 +18,13 @@ import icons from "../../../uswds/img/sprite.svg";
* @param {CANBudgetFormProps} props
* @returns {JSX.Element} - The component JSX.
*/
const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => {
const CANBudgetForm = ({ totalFunding, budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => {
const fillColor = budgetAmount ? "#005ea2" : "#757575";

return (
<form
onSubmit={(e) => {
handleAddBudget(e);
setBudgetAmount("");
}}
>
<div style={{ width: "383px" }}>
Expand All @@ -38,6 +38,7 @@ const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, run
value={budgetAmount || ""}
messages={res.getErrors("budget-amount")}
className={cn("budget-amount")}
placeholder={`$${totalFunding}`}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ nice addition @weimiao67

/>
</div>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ describe("CANBudgetForm", () => {
await user.click(screen.getByRole("button", { name: /add fy budget/i }));

expect(defaultProps.handleAddBudget).toHaveBeenCalled();
expect(defaultProps.setBudgetAmount).toHaveBeenCalledWith("");
});

test("calls runValidate when currency input changes", () => {
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/components/CANs/CANBudgetForm/suite.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import CurrencyInput from "../../UI/Form/CurrencyInput";
import TextArea from "../../UI/Form/TextArea";
import icons from "../../../uswds/img/sprite.svg";

/**
* @typedef {Object} CANFundingReceivedFormProps
* @property {(arg: string) => string} cn
* @property {Object} res
* @property {string} receivedFundingAmount
* @property {(e: React.FormEvent<HTMLFormElement>) => void} handleSubmit
* @property {(name: string, value: string) => void} runValidate
* @property { React.Dispatch<React.SetStateAction<string>>} setReceivedFundingAmount
* @property {string} notes
* @property { React.Dispatch<React.SetStateAction<string>>} setNotes
*/

/**
* @component - The CAN Funding Received Form component.
* @param {CANFundingReceivedFormProps} props
* @returns {JSX.Element} - The component JSX.
*/

const CANFundingReceivedForm = ({
cn,
res,
runValidate,
handleSubmit,
receivedFundingAmount,
setReceivedFundingAmount,
notes,
setNotes
}) => {
const isFormInValid = !receivedFundingAmount || res.hasErrors("funding-received-amount");
const fillColor = !isFormInValid ? "#005ea2" : "#757575";

return (
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
<div style={{ width: "383px" }}>
<CurrencyInput
name="funding-received-amount"
label="Funding Received"
onChange={(name, value) => {
runValidate("funding-received-amount", value);
}}
setEnteredAmount={setReceivedFundingAmount}
value={receivedFundingAmount || ""}
messages={res.getErrors("funding-received-amount")}
className={`${cn("funding-received-amount")} margin-top-0`}
/>
<TextArea
maxLength={75}
name="notes"
label="Notes (optional)"
value={notes}
onChange={(name, value) => setNotes(value)}
textAreaStyle={{ height: "51px" }}
/>{" "}
</div>
<button
className="usa-button usa-button--outline margin-top-4"
disabled={isFormInValid}
data-cy="add-funding-received-btn"
>
<svg
className="height-2 width-2 margin-right-05 cursor-pointer"
style={{ fill: fillColor }}
>
<use xlinkHref={`${icons}#add`}></use>
</svg>
Add Funding Received
</button>
</form>
);
};

export default CANFundingReceivedForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import CANFundingReceivedForm from "./CanFundingReceivedForm";

describe("CANFundingReceivedForm", () => {
let user;
const defaultProps = {
cn: vi.fn(),
res: {
hasErrors: vi.fn().mockReturnValue(false),
getErrors: vi.fn().mockReturnValue([])
},
receivedFundingAmount: "",
handleSubmit: vi.fn(),
runValidate: vi.fn(),
setReceivedFundingAmount: vi.fn(),
notes: "",
setNotes: vi.fn()
};

beforeEach(() => {
user = userEvent.setup();
vi.clearAllMocks();
});

it("renders the form correctly", () => {
render(<CANFundingReceivedForm {...defaultProps} />);

expect(screen.getByLabelText(/Funding Received/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Notes \(optional\)/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Add Funding Received/i })).toBeInTheDocument();
});

it("calls setNotes when typing in notes field", async () => {
render(<CANFundingReceivedForm {...defaultProps} />);
const textarea = screen.getByLabelText(/Notes \(optional\)/i);

await user.type(textarea, "Test note");
expect(defaultProps.setNotes).toHaveBeenCalledTimes(9);
});

it("calls setReceivedFundingAmount when typing amount", async () => {
render(<CANFundingReceivedForm {...defaultProps} />);
const input = screen.getByLabelText(/Funding Received/i);

await user.type(input, "1000");

expect(defaultProps.setReceivedFundingAmount).toHaveBeenCalledWith(1000);
});

it("calls handleSubmit when form is submitted", async () => {
render(
<CANFundingReceivedForm
{...defaultProps}
receivedFundingAmount="1000"
/>
);

await user.click(screen.getByRole("button", { name: /Add Funding Received/i }));

expect(defaultProps.handleSubmit).toHaveBeenCalled();
});

it("disables submit button when form is invalid", () => {
render(
<CANFundingReceivedForm
{...defaultProps}
res={{
...defaultProps.res,
hasErrors: vi.fn().mockReturnValue(true)
}}
/>
);

expect(screen.getByRole("button", { name: /Add Funding Received/i })).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./CanFundingReceivedForm";
Loading
Loading