diff --git a/frontend/cypress/e2e/canDetail.cy.js b/frontend/cypress/e2e/canDetail.cy.js index c4cca8f402..148642e622 100644 --- a/frontend/cypress/e2e/canDetail.cy.js +++ b/frontend/cypress/e2e/canDetail.cy.js @@ -1,8 +1,9 @@ /// import { terminalLog, testLogin } from "./utils"; +import { getCurrentFiscalYear } from "../../src/helpers/utils.js"; beforeEach(() => { - testLogin("system-owner"); + testLogin("budget-team"); }); afterEach(() => { @@ -10,20 +11,75 @@ afterEach(() => { cy.checkA11y(null, null, terminalLog); }); +const can502Nickname = "SSRD"; +const can502Description = "Social Science Research and Development"; + +const currentFiscalYear = getCurrentFiscalYear(); + describe("CAN detail page", () => { - it("shows relevant CAN data", () => { + it("shows the CAN details page", () => { cy.visit("/cans/502/"); cy.get("h1").should("contain", "G99PHS9"); // heading - cy.get("p").should("contain", "SSRD - 5 Years"); // sub-heading + cy.get("p").should("contain", can502Nickname); // sub-heading cy.get("span").should("contain", "Nicole Deterding"); // team member cy.get("span").should("contain", "Director Derrek"); // division director cy.get("span").should("contain", "Program Support"); // portfolio + cy.get("span").should("contain", "Division of Data and Improvement"); // division + }); + it("CAN Edit form", () => { + cy.visit("/cans/502/"); + cy.get("#fiscal-year-select").select("2024"); + cy.get("#edit").should("not.exist"); + cy.get("#fiscal-year-select").select(currentFiscalYear); + cy.get("#edit").should("exist"); + cy.get("#edit").click(); + cy.get("h2").should("contain", "Edit CAN Details"); + cy.get("#can-nickName").invoke("val").should("equal", can502Nickname); + cy.get("#can-nickName").clear(); + cy.get(".usa-error-message").should("exist").contains("This is required information"); + cy.get("#save-changes").should("be.disabled"); + cy.get("#can-nickName").type("Test Can Nickname"); + cy.get("#save-changes").should("not.be.disabled"); + cy.get(".usa-error-message").should("not.exist"); + cy.get("#description").invoke("val").should("equal", can502Description); + cy.get("#description").clear(); + cy.get("#description").type("Test description."); + cy.get("#save-changes").click(); + cy.get(".usa-alert__body").should("contain", "The CAN G99PHS9 has been successfully updated."); + cy.get("p").should("contain", "Test Can Nickname"); + cy.get("dd").should("contain", "Test description."); + // revert back to original values + cy.get("#edit").click(); + cy.get("#can-nickName").clear(); + cy.get("#can-nickName").type(can502Nickname); + cy.get("#description").clear(); + cy.get("#description").type(can502Description); + cy.get("#save-changes").click(); + }); + it("handles cancelling from CAN edit form", () => { + cy.visit("/cans/502/"); + cy.get("#fiscal-year-select").select(currentFiscalYear); + // Attempt cancel without making any changes + cy.get("#edit").click(); + 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='cancel-action']").click(); + // Exit out of the cancel modal + cy.get("[data-cy='cancel-button']").click(); + // Actual cancel event + cy.get("[data-cy='confirm-action']").click(); + cy.get("h2").should("contain", "CAN Details"); + cy.get("p").should("contain", can502Nickname); + cy.get("dd").should("contain", can502Description); }); it("shows the CAN Spending page", () => { cy.visit("/cans/504/spending"); cy.get("#fiscal-year-select").select("2043"); cy.get("h1").should("contain", "G994426"); // heading - cy.get("p").should("contain", "HS - 5 Years"); // sub-heading + cy.get("p").should("contain", "HS"); // sub-heading // should contain the budget line table cy.get("table").should("exist"); // table should have more than 1 row @@ -75,14 +131,17 @@ describe("CAN detail page", () => { cy.visit("/cans/504/funding"); cy.get("#fiscal-year-select").select("2024"); cy.get("h1").should("contain", "G994426"); // heading - cy.get("p").should("contain", "HS - 5 Years"); // sub-heading + cy.get("p").should("contain", "HS"); // sub-heading cy.get("[data-cy=can-funding-info-card]") .should("exist") .and("contain", "EEXXXX20215DAD") .and("contain", "5 Years") .and("contain", "IDDA") .and("contain", "09/30/25") - .and("contain", "2021"); + .and("contain", "2021") + .and("contain", "Quarterly") + .and("contain", "Direct") + .and("contain", "Discretionary"); cy.get("[data-cy=budget-received-card]") .should("exist") .and("contain", "FY 2024 Funding Received YTD") diff --git a/frontend/src/api/opsAPI.js b/frontend/src/api/opsAPI.js index 6598f8bb96..7007f513e6 100644 --- a/frontend/src/api/opsAPI.js +++ b/frontend/src/api/opsAPI.js @@ -203,12 +203,21 @@ export const opsApi = createApi({ query: (id) => `/cans/${id}`, providesTags: ["Cans"] }), + updateCan: builder.mutation({ + query: ({ id, data }) => ({ + url: `/cans/${id}`, + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: data + }), + invalidatesTags: ["Cans"] + }), getCanFundingSummary: builder.query({ query: ({ ids, fiscalYear, activePeriod, transfer, portfolio, fyBudgets }) => { const queryParams = []; if (ids && ids.length > 0) { - ids.forEach(id => queryParams.push(`can_ids=${id}`)); + ids.forEach((id) => queryParams.push(`can_ids=${id}`)); } if (fiscalYear) { @@ -216,15 +225,15 @@ export const opsApi = createApi({ } if (activePeriod && activePeriod.length > 0) { - activePeriod.forEach(period => queryParams.push(`active_period=${period}`)); + activePeriod.forEach((period) => queryParams.push(`active_period=${period}`)); } if (transfer && transfer.length > 0) { - transfer.forEach(t => queryParams.push(`transfer=${t}`)); + transfer.forEach((t) => queryParams.push(`transfer=${t}`)); } if (portfolio && portfolio.length > 0) { - portfolio.forEach(p => queryParams.push(`portfolio=${p}`)); + portfolio.forEach((p) => queryParams.push(`portfolio=${p}`)); } if (fyBudgets && fyBudgets.length === 2) { @@ -403,6 +412,7 @@ export const { useUpdateUserMutation, useGetCansQuery, useGetCanByIdQuery, + useUpdateCanMutation, useGetCanFundingSummaryQuery, useGetNotificationsByUserIdQuery, useGetNotificationsByUserIdAndAgreementIdQuery, diff --git a/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js new file mode 100644 index 0000000000..9ddeb84dbe --- /dev/null +++ b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js @@ -0,0 +1,97 @@ +import React from "react"; +import classnames from "vest/classnames"; +import { useUpdateCanMutation } from "../../../api/opsAPI"; +import useAlert from "../../../hooks/use-alert.hooks"; +import suite from "./suite.js"; + +export default function useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode) { + const [nickName, setNickName] = React.useState(canNickname); + const [description, setDescription] = React.useState(canDescription); + const [showModal, setShowModal] = React.useState(false); + const [modalProps, setModalProps] = React.useState({ + heading: "", + actionButtonText: "", + secondaryButtonText: "", + handleConfirm: () => {} + }); + const [updateCan] = useUpdateCanMutation(); + const { setAlert } = useAlert(); + + let res = suite.get(); + + const cn = classnames(suite.get(), { + invalid: "usa-form-group--error", + valid: "success", + warning: "warning" + }); + + const handleCancel = (e) => { + e.preventDefault(); + setShowModal(true); + setModalProps({ + heading: "Are you sure you want to cancel editing? Your changes will not be saved.", + actionButtonText: "Cancel Edits", + secondaryButtonText: "Continue Editing", + handleConfirm: () => cleanUp() + }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const payload = { + number: canNumber, + portfolio_id: portfolioId, + nick_name: nickName, + description: description + }; + + setAlert({ + type: "success", + heading: "CAN Updated", + message: `The CAN ${canNumber} has been successfully updated.` + }); + + updateCan({ + id: canId, + data: payload + }); + + cleanUp(); + }; + + const cleanUp = () => { + setNickName(""); + setDescription(""); + setShowModal(false); + setModalProps({ + heading: "", + actionButtonText: "", + secondaryButtonText: "", + handleConfirm: () => {} + }); + toggleEditMode(); + }; + + const runValidate = (name, value) => { + suite( + { + ...{ [name]: value } + }, + name + ); + }; + return { + nickName, + setNickName, + description, + setDescription, + handleCancel, + handleSubmit, + runValidate, + res, + cn, + setShowModal, + showModal, + modalProps + }; +} diff --git a/frontend/src/components/CANs/CANDetailForm/CANDetailForm.jsx b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.jsx new file mode 100644 index 0000000000..fcefce09ae --- /dev/null +++ b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.jsx @@ -0,0 +1,94 @@ +import Input from "../../UI/Form/Input"; +import TextArea from "../../UI/Form/TextArea"; +import ConfirmationModal from "../../UI/Modals/ConfirmationModal"; +import useCanDetailForm from "./CANDetailForm.hooks"; + +/** + * @typedef {Object} CANDetailFormProps + * @property {number} canId - CAN ID + * @property {string} canNumber - CAN number + * @property {string} canNickname - CAN nick name + * @property {string} canDescription - CAN description + * @property {number} portfolioId - Portfolio ID + * @property {Function} toggleEditMode - Function to toggle edit mode + */ + +/** + * @component - The CAN Details form + * @param {CANDetailFormProps} props + * @returns {JSX.Element} + */ +const CANDetailForm = ({ canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode }) => { + const { + nickName, + setNickName, + description, + setDescription, + handleCancel, + handleSubmit, + runValidate, + res, + cn, + showModal, + setShowModal, + modalProps + } = useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode); + + return ( +
{ + handleSubmit(e); + }} + > + {showModal && ( + + )} + { + runValidate("can-nickName", value); + setNickName(value); + }} + value={nickName} + isRequired + messages={res.getErrors("can-nickName")} + className={cn("can-nickName")} + /> +