Skip to content

Commit

Permalink
feat: adds CAN budget form (#3212)
Browse files Browse the repository at this point in the history
* feat: adds can budget form

* feat: adds CANBudgetForm

* feat: starts can budget submit

* feat: adds post patch logic

* feat: adds validation

* feat: adds reset to validation suite

* test: adds e2e for can budget form

* test: adds can budget form component test

* style: match figma design

* chore: updates tag on CAN funding calls

---------

Co-authored-by: Santi-3rd <[email protected]>
Co-authored-by: maiyerlee <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 4ad202d commit 1bf6b5c
Show file tree
Hide file tree
Showing 18 changed files with 660 additions and 99 deletions.
57 changes: 22 additions & 35 deletions backend/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1191,45 +1191,32 @@ paths:
available_funding:
type: string
carry_forward_funding:
type: integer
can:
type: object
type: string
cans:
type: array
properties:
can:
type: array
items:
type: object
properties:
appropriation_term:
type: integer
authorizer_id:
type: integer
arrangement_type_id:
type: integer
number:
type: string
purpose:
type: string
managing_portfolio_id:
type: integer
nickname:
type: string
appropriation_date:
type: string
description:
type: string
id:
type: integer
expiration_date:
type: string
managing_project_id:
type: integer
properties:
active_period:
type: integer
description:
type: string
display_name:
type: string
id:
type: integer
nick_name:
type: string
number:
type: string
portfolio_id:
type: integer
projects:
type: array
carry_forward_label:
type: string
example: ""
expiration_date:
type: string
example: ""
expected_funding:
type: string
in_draft_funding:
Expand All @@ -1239,9 +1226,9 @@ paths:
new_funding:
type: string
obligated_funding:
type: integer
type: string
planned_funding:
type: integer
type: string
received_funding:
type: string
total_funding:
Expand Down
54 changes: 54 additions & 0 deletions frontend/cypress/e2e/canDetail.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ afterEach(() => {

const can502Nickname = "SSRD";
const can502Description = "Social Science Research and Development";
const can504 = {
number: 504,
nickname: "G994426",
budgetAmount: "5_000_000"
};

const currentFiscalYear = getCurrentFiscalYear();

Expand Down Expand Up @@ -167,4 +172,53 @@ describe("CAN detail page", () => {
.and("contain", "$6,000,000.00")
.and("contain", "60%");
});
it("handles budget form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
cy.get("#save-changes").should("be.disabled");
cy.get("#add-fy-budget").should("be.disabled");
cy.get("#carry-forward-card").should("contain", "0");
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();
cy.get("[data-cy='can-budget-fy-card']").should("contain", "5,000,000.00");
cy.get("#save-changes").should("be.enabled");
cy.get("#save-changes").click();
cy.get(".usa-alert__body").should("contain", `The CAN ${can504.nickname} has been successfully updated.`);
cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "Received $0.00 of $5,000,000.00");
cy.get("[data-cy=can-budget-fy-card]")
.should("exist")
.and("contain", "CAN Budget by FY")
.and("contain", `FY ${currentFiscalYear}`)
.and("contain", "$5,000,000.00");
});
it("handles cancelling from budget form", () => {
cy.visit(`/cans/${can504.number}/funding`);
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").click();
cy.get("#carry-forward-card").should("contain", "0");
cy.get("[data-cy='can-budget-fy-card']").should("contain", "5,000,000.00");
cy.get("#budget-amount").type("6_000_000");
cy.get("#add-fy-budget").click();
cy.get("[data-cy='can-budget-fy-card']").should("contain", "6,000,000.00");
cy.get("#save-changes").should("be.enabled");
cy.get("[data-cy=cancel-button]").should("be.enabled");
cy.get("[data-cy=cancel-button]").click();
cy.get(".usa-modal__heading").should(
"contain",
"Are you sure you want to cancel editing? Your changes will not be saved."
);
cy.get("[data-cy='confirm-action']").click();
cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "Received $0.00 of $5,000,000.00");
cy.get("[data-cy=can-budget-fy-card]")
.should("exist")
.and("contain", "CAN Budget by FY")
.and("contain", `FY ${currentFiscalYear}`)
.and("contain", "$5,000,000.00");
});
});
26 changes: 23 additions & 3 deletions frontend/src/api/opsAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,24 @@ export const opsApi = createApi({
}),
invalidatesTags: ["Cans"]
}),
addCanFundingBudgets: builder.mutation({
query: ({ data }) => ({
url: `/can-funding-budgets/`,
method: "POST",
headers: { "Content-Type": "application/json" },
body: data
}),
invalidatesTags: ["Cans", "CanFunding"]
}),
updateCanFundingBudget: builder.mutation({
query: ({ id, data }) => ({
url: `/can-funding-budgets/${id}`,
method: "PATCH",
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 @@ -243,7 +261,7 @@ export const opsApi = createApi({

return `/can-funding-summary?${queryParams.join("&")}`;
},
providesTags: ["CanFunding"]
providesTags: ["Cans", "CanFunding"]
}),
getNotificationsByUserId: builder.query({
query: ({ id, auth_header }) => {
Expand Down Expand Up @@ -331,8 +349,8 @@ export const opsApi = createApi({
invalidatesTags: ["ServicesComponents", "Agreements", "BudgetLineItems", "AgreementHistory"]
}),
getChangeRequestsList: builder.query({
query: ({userId}) => ({
url: `/change-requests/${userId ? `?userId=${userId}` : ""}`,
query: ({ userId }) => ({
url: `/change-requests/${userId ? `?userId=${userId}` : ""}`
}),
providesTags: ["ChangeRequests"]
}),
Expand Down Expand Up @@ -413,6 +431,8 @@ export const {
useGetCansQuery,
useGetCanByIdQuery,
useUpdateCanMutation,
useAddCanFundingBudgetsMutation,
useUpdateCanFundingBudgetMutation,
useGetCanFundingSummaryQuery,
useGetNotificationsByUserIdQuery,
useGetNotificationsByUserIdAndAgreementIdQuery,
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import CurrencyInput from "../../UI/Form/CurrencyInput";
import icons from "../../../uswds/img/sprite.svg";

/**
* @typedef {Object} CANBudgetFormProps
* @property {string} budgetAmount
* @property {(arg: string) => string} cn
* @property {Object} res
* @property {number} fiscalYear
* @property {(e: React.FormEvent<HTMLFormElement>) => void} handleAddBudget
* @property {(name: string, value: string) => void} runValidate
* @property { React.Dispatch<React.SetStateAction<string>>} setBudgetAmount
*/

/**
* @component - The CAN Budget Form component.
* @param {CANBudgetFormProps} props
* @returns {JSX.Element} - The component JSX.
*/
const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => {
const fillColor = budgetAmount ? "#005ea2" : "#757575";

return (
<form
onSubmit={(e) => {
handleAddBudget(e);
setBudgetAmount("");
}}
>
<div style={{ width: "383px" }}>
<CurrencyInput
name="budget-amount"
label={`FY ${fiscalYear} CAN Budget`}
onChange={(name, value) => {
runValidate("budget-amount", value);
}}
setEnteredAmount={setBudgetAmount}
value={budgetAmount || ""}
messages={res.getErrors("budget-amount")}
className={cn("budget-amount")}
/>
</div>
<button
id="add-fy-budget"
className="usa-button usa-button--outline margin-top-4"
disabled={!budgetAmount}
data-cy="add-fy-budget"
>
<svg
className="height-2 width-2 margin-right-05 cursor-pointer"
style={{ fill: fillColor }}
>
<use xlinkHref={`${icons}#add`}></use>
</svg>
Add FY Budget
</button>
</form>
);
};
export default CANBudgetForm;
72 changes: 72 additions & 0 deletions frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, test, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import CANBudgetForm from "./CANBudgetForm";

describe("CANBudgetForm", () => {
const defaultProps = {
budgetAmount: "",
cn: (name) => name,
res: { getErrors: () => [] },
fiscalYear: 2024,
handleAddBudget: vi.fn(),
runValidate: vi.fn(),
setBudgetAmount: vi.fn()
};

test("renders with required props", () => {
render(<CANBudgetForm {...defaultProps} />);
expect(screen.getByLabelText(/FY 2024 CAN Budget/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /add fy budget/i })).toBeInTheDocument();
});

test("button is disabled when budgetAmount is empty", () => {
render(<CANBudgetForm {...defaultProps} />);
expect(screen.getByRole("button", { name: /add fy budget/i })).toBeDisabled();
});

test("button is enabled when budgetAmount has value", () => {
render(
<CANBudgetForm
{...defaultProps}
budgetAmount="1000"
/>
);
expect(screen.getByRole("button", { name: /add fy budget/i })).toBeEnabled();
});

test("calls handleAddBudget and setBudgetAmount on form submission", async () => {
const user = userEvent.setup();
render(
<CANBudgetForm
{...defaultProps}
budgetAmount="1000"
/>
);

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", () => {
render(<CANBudgetForm {...defaultProps} />);

fireEvent.change(screen.getByLabelText(/FY 2024 CAN Budget/i), {
target: { value: "1000" }
});

expect(defaultProps.runValidate).toHaveBeenCalledWith("budget-amount", "1,000");
});

test("displays validation errors when present", () => {
const propsWithError = {
...defaultProps,
res: { getErrors: () => ["This is required information"] }
};

render(<CANBudgetForm {...propsWithError} />);
expect(screen.getByText("This is required information")).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions frontend/src/components/CANs/CANBudgetForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default} from "./CANBudgetForm"
11 changes: 11 additions & 0 deletions frontend/src/components/CANs/CANBudgetForm/suite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { create, test, enforce, only } from "vest";

const suite = create((data = {}, fieldName) => {
only(fieldName);

test("budget-amount", "This is required information", () => {
enforce(data["budget-amount"]).isNotBlank();
});
});

export default suite;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import classnames from "vest/classnames";
import { useUpdateCanMutation } from "../../../api/opsAPI";
import useAlert from "../../../hooks/use-alert.hooks";
import suite from "./suite.js";

/**
* @description - Custom hook for the CAN Detail Form.
* @param {number} canId
* @param {string} canNumber
* @param {string} canNickname
* @param {string} canDescription
* @param {number} portfolioId
* @param {() => void} toggleEditMode
*/
export default function useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode) {
const [nickName, setNickName] = React.useState(canNickname);
const [description, setDescription] = React.useState(canDescription);
Expand Down Expand Up @@ -70,6 +78,7 @@ export default function useCanDetailForm(canId, canNumber, canNickname, canDescr
handleConfirm: () => {}
});
toggleEditMode();
suite.reset();
};

const runValidate = (name, value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { NO_DATA } from "../../../constants";

/**
* @typedef {Object} CANFundingReceivedTableProps
* @property {number} totalFunding
* @property {string} totalFunding
* @property {FundingReceived[]} fundingReceived data for table
*/

Expand Down
Loading

0 comments on commit 1bf6b5c

Please sign in to comment.