diff --git a/backend/data_tools/data/can_data.json5 b/backend/data_tools/data/can_data.json5 index b6a4de71d5..151852a73a 100644 --- a/backend/data_tools/data/can_data.json5 +++ b/backend/data_tools/data/can_data.json5 @@ -5,87 +5,104 @@ fiscal_year: 2023, fund_code: "AAXXXX20231DAD", funding_source: "OPRE", + method_of_transfer: "DIRECT", }, { // 2 fiscal_year: 2021, fund_code: "BBXXXX20215DAD", + method_of_transfer: "COST_SHARE", }, { // 3 fiscal_year: 2022, - fund_code: "CCXXXX20225DAD" + fund_code: "CCXXXX20225DAD", + method_of_transfer: "IDDA", }, { // 4 fiscal_year: 2021, fund_code: "DDXXXX20215DAD", + method_of_transfer: "IAA", }, { // 5 fiscal_year: 2021, fund_code: "EEXXXX20215DAD", + method_of_transfer: "IDDA", }, { // 6 fiscal_year: 2021, - fund_code: "FFXXXX20215DAD" + fund_code: "FFXXXX20215DAD", + method_of_transfer: "DIRECT", }, { // 7 fiscal_year: 2023, - fund_code: "GGXXXX20231DAD" + fund_code: "GGXXXX20231DAD", + method_of_transfer: "DIRECT", }, { // 8 fiscal_year: 2023, - fund_code: "HHXXXX20231DAD" + fund_code: "HHXXXX20231DAD", + method_of_transfer: "IDDA", }, { // 9 fiscal_year: 2023, - fund_code: "IIXXXX20231DAD" + fund_code: "IIXXXX20231DAD", + method_of_transfer: "IAA", }, { // 10 fiscal_year: 2023, - fund_code: "JJXXXX20231DAD" + fund_code: "JJXXXX20231DAD", + method_of_transfer: "DIRECT", }, { // 11 fiscal_year: 2023, - fund_code: "KKXXXX20235DAD" + fund_code: "KKXXXX20235DAD", + method_of_transfer: "IDDA", }, { // 12 fiscal_year: 2022, - fund_code: "LLXXXX20225DAD" + fund_code: "LLXXXX20225DAD", + method_of_transfer: "IAA", }, { // 13 fiscal_year: 2023, - fund_code: "MMXXXX20235DAD" + fund_code: "MMXXXX20235DAD", + method_of_transfer: "DIRECT", }, { // 14 fiscal_year: 2023, - fund_code: "NNXXXX20231DAD" + fund_code: "NNXXXX20231DAD", + method_of_transfer: "IDDA", }, { // 15 fiscal_year: 2023, - fund_code: "OOXXXX20235DAD" + fund_code: "OOXXXX20235DAD", + method_of_transfer: "DIRECT", }, { // 16 fiscal_year: 2023, - fund_code: "PPXXXX20235DAD" + fund_code: "PPXXXX20235DAD", + method_of_transfer: "DIRECT", }, { // 17 fiscal_year: 2023, - fund_code: "QQXXXX20235DAD" - } + fund_code: "QQXXXX20235DAD", + method_of_transfer: "IDDA", + }, ], can: [ { @@ -94,7 +111,7 @@ description: "Healthy Marriages Responsible Fatherhood - OPRE", nick_name: "HMRF-OPRE", portfolio_id: 6, - funding_details_id: 1 + funding_details_id: 1, }, { // 501 @@ -102,7 +119,7 @@ description: "Incoming Interagency Agreements", nick_name: "IAA-Incoming", portfolio_id: 1, - funding_details_id: 2 + funding_details_id: 2, }, { // 502 @@ -110,7 +127,7 @@ description: "Social Science Research and Development", nick_name: "SSRD", portfolio_id: 8, - funding_details_id: 3 + funding_details_id: 3, }, { // 503 @@ -118,7 +135,7 @@ description: "Child Development Research Fellowship Grant Program", nick_name: "ASPE SRCD-IDDA", portfolio_id: 1, - funding_details_id: 4 + funding_details_id: 4, }, { // 504 @@ -126,7 +143,7 @@ description: "Head Start Research", nick_name: "HS", portfolio_id: 2, - funding_details_id: 5 + funding_details_id: 5, }, { // 505 @@ -134,7 +151,7 @@ description: "Kinship Navigation", nick_name: "Kin-Nav", portfolio_id: 6, - funding_details_id: 6 + funding_details_id: 6, }, { // 506 @@ -142,7 +159,7 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 7 + funding_details_id: 7, }, { // 507 @@ -150,7 +167,7 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 8 + funding_details_id: 8, }, { // 508 @@ -158,7 +175,7 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 9 + funding_details_id: 9, }, { // 509 @@ -166,7 +183,7 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 10 + funding_details_id: 10, }, { // 510 @@ -174,7 +191,7 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 11 + funding_details_id: 11, }, { // 511 @@ -182,21 +199,21 @@ description: "Healthy Marriages Responsible Fatherhood - OFA", nick_name: "HMRF-OFA", portfolio_id: 6, - funding_details_id: 12 + funding_details_id: 12, }, { // 512 number: "G99XXX8", description: "Example CAN", - nick_name: "", + nick_name: "Next Generation Leadership Program", portfolio_id: 3, funding_details_id: 13, projects: [ { - "tablename": "project", - "id": 1000 - } - ] + tablename: "project", + id: 1000, + }, + ], }, { // 513 @@ -204,7 +221,7 @@ description: "MIHOPE Check-in 2023", nick_name: "MIHOPE 23", portfolio_id: 3, - funding_details_id: 14 + funding_details_id: 14, }, { // 514 @@ -212,7 +229,7 @@ description: "MIHOPE Check-in 2024", nick_name: "MIHOPE 24", portfolio_id: 3, - funding_details_id: 15 + funding_details_id: 15, }, { // 515 @@ -220,7 +237,7 @@ description: "MOHOPE Long-Term", nick_name: "MIHOPE LT", portfolio_id: 3, - funding_details_id: 16 + funding_details_id: 16, }, { // 516 @@ -228,8 +245,8 @@ description: "Shared CAN", nick_name: "SHARED", portfolio_id: 3, - funding_details_id: 17 - } + funding_details_id: 17, + }, ], can_funding_budget: [ { @@ -386,7 +403,7 @@ fiscal_year: 2023, can_id: 516, budget: 500000.0, - } + }, ], can_funding_received: [ { @@ -518,6 +535,6 @@ fiscal_year: 2023, can_id: 515, funding: 1000000.0, - } + }, ], } diff --git a/backend/ops_api/ops/schemas/cans.py b/backend/ops_api/ops/schemas/cans.py index 216a5e96ba..2ed35ca7cb 100644 --- a/backend/ops_api/ops/schemas/cans.py +++ b/backend/ops_api/ops/schemas/cans.py @@ -1,6 +1,7 @@ from marshmallow import Schema, fields from models import CANMethodOfTransfer, PortfolioStatus + from ops_api.ops.schemas.budget_line_items import BudgetLineItemResponseSchema from ops_api.ops.schemas.projects import ProjectSchema from ops_api.ops.schemas.users import SafeUserSchema diff --git a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py index f3b02cff32..2ab354bc56 100644 --- a/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py +++ b/backend/ops_api/tests/ops/funding_summary/test_can_funding_summary.py @@ -53,7 +53,7 @@ def test_get_can_funding_summary_no_fiscal_year(loaded_db, test_can) -> None: "funding_partner": None, "funding_source": "OPRE", "id": 1, - "method_of_transfer": None, + "method_of_transfer": "DIRECT", "sub_allowance": None, "updated_by": None, "updated_by_user": None, @@ -141,7 +141,7 @@ def test_get_can_funding_summary_with_fiscal_year(loaded_db, test_can) -> None: "funding_partner": None, "funding_source": "OPRE", "id": 1, - "method_of_transfer": None, + "method_of_transfer": "DIRECT", "sub_allowance": None, "updated_by": None, "updated_by_user": None, diff --git a/docs/developers/frontend/types.md b/docs/developers/frontend/types.md new file mode 100644 index 0000000000..34925ba505 --- /dev/null +++ b/docs/developers/frontend/types.md @@ -0,0 +1,118 @@ +# Process for Creating and Using Types + +This document describes the process for creating and using types in OPS based on TypeScript best practices and conventions in a JS Doc format. + +## Creating a Type Definition + +1. **Create a new file co-located with the component directory**. The file should be named after the type it defines that type names should be in PascalCase. For example, a type definition for a user object should be named `UserTypes.d.ts`. + +```markdown +src/components/Users +├── UsersEmailComboBox +├── UserInfo +├── IserInfoForm +└── UserTypes.d.ts +``` + +The `.d.ts` extension is used to indicate that the file contains type definitions and does not contain any executable code. + +2. **Define the type**. The type definition should be exported and named. The type should be defined using the `type` keyword. + +```typescript +export type SafeUser = { + email: string; + full_name: string; + id: number; +}; +``` + + +## Using a Type Definition + +1. **Import the type**. Import the type in the component file where it is used. Since we are using JS Doc, the type should be imported using the `@typedef` tag and imported as a module. + +```jsx +// src/components/Users/UserInfo.jsx + +/** + * Renders the User information. + * @component + * @typedef {import("../UserTypes").SafeUser} User + * @param {Object} props - The component props. + * @param {User} props.user - The user object. + * @param {Boolean} props.isEditable - Whether the user information is editable. + * @returns {JSX.Element} - The rendered component. + */ +const UserInfo = ({ user, isEditable }) => { +... +``` + +2. Another example of using a type definition with a list or array: + +Let's say you have a CANs component ytree like this: + +```markdown +src/components/CANs +├── CANBudgetSummary +├── CANTable +├── CanTypes.d.ts +``` + +The `CanTypes.d.ts` file would look like this: + +```typescript +import { BudgetLine } from "../BudgetLineItems/BudgetLineTypes"; +import { Portfolio } from "../Portfolios/PortfolioTypes"; +import { Project } from "../Projects/ProjectTypes"; + +export type CAN = { + active_period: number; + budget_line_items: BudgetLine[]; + created_by: number | null; + created_by_user: number | null; + created_on: Date; + description: string; + display_name: string; + funding_budgets: CANFundingBudget[]; + funding_details: CANFundingDetails; + funding_details_id: number; + funding_received: CANFundingReceived[]; + id: number; + nick_name: string; + number: string; + portfolio: Portfolio; + portfolio_id: number; + projects: Project[]; + updated_by: number | null; + updated_by_user: number | null; + updated_on: Date; +}; + +... +``` + +And then you would import the `CAN` type in the `CANTable` component like this: + +```jsx +// src/components/CANs/CANTable.jsx + +/** + * CANTable component of CanList + * @component + * @typedef {import("../CANTypes").CAN} CAN + * @param {Object} props + * @param {CAN[]} props.cans - Array of CANs + * @returns {JSX.Element} + */ +const CANTable = ({ cans }) => { + const CANS_PER_PAGE = 10; +... +``` + +## Resources + +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [JSDocs TypeDefinition](https://jsdoc.app/tags-typedef) +- [JSDocs Types](https://jsdoc.app/tags-type) +- [ThePrimeTimeagen inspiration TY Short](https://www.youtube.com/shorts/tj5VW2xJsqU) +``` diff --git a/frontend/cypress/e2e/canList.cy.js b/frontend/cypress/e2e/canList.cy.js index 2d65d72e1c..0f16e1df20 100644 --- a/frontend/cypress/e2e/canList.cy.js +++ b/frontend/cypress/e2e/canList.cy.js @@ -3,7 +3,7 @@ import { terminalLog, testLogin } from "./utils"; beforeEach(() => { testLogin("admin"); - cy.visit("/cans"); + cy.visit("/cans").wait(1000); }); afterEach(() => { @@ -11,18 +11,48 @@ afterEach(() => { cy.checkA11y(null, null, terminalLog); }); -it("loads", () => { - // beforeEach has ran... - cy.get("h1").should("have.text", "CANs"); - cy.get('a[href="/cans/502"]').should("exist"); -}); +describe("CAN List", () => { + it("loads", () => { + // beforeEach has ran... + cy.get("h1").should("have.text", "CANs"); + cy.get('a[href="/cans/502"]').should("exist"); + }); + + it("clicking on a CAN takes you to the detail page", () => { + // beforeEach has ran... + const canNumber = "G99PHS9"; + + cy.contains(canNumber).click(); + + cy.url().should("include", "/cans/502"); + cy.get("h1").should("contain", canNumber); + }); -it("clicking on a CAN takes you to the detail page", () => { - // beforeEach has ran... - const canNumber = "G99PHS9"; + it("pagination on the bli table works as expected", () => { + cy.get("ul").should("have.class", "usa-pagination__list"); + cy.get("li").should("have.class", "usa-pagination__item").contains("1"); + cy.get("button").should("have.class", "usa-current").contains("1"); + cy.get("li").should("have.class", "usa-pagination__item").contains("2"); + cy.get("li").should("have.class", "usa-pagination__item").contains("Next"); + cy.get("tbody").find("tr").should("have.length", 10); + cy.get("li") + .should("have.class", "usa-pagination__item") + .contains("Previous") + .find("svg") + .should("have.attr", "aria-hidden", "true"); - cy.contains(canNumber).click(); + // go to the second page + cy.get("li").should("have.class", "usa-pagination__item").contains("2").click(); + cy.get("button").should("have.class", "usa-current").contains("2"); + cy.get("li").should("have.class", "usa-pagination__item").contains("Previous"); + cy.get("li") + .should("have.class", "usa-pagination__item") + .contains("Next") + .find("svg") + .should("have.attr", "aria-hidden", "true"); - cy.url().should("include", "/cans/502"); - cy.get("h1").should("contain", canNumber); + // go back to the first page + cy.get("li").should("have.class", "usa-pagination__item").contains("1").click(); + cy.get("button").should("have.class", "usa-current").contains("1"); + }); }); diff --git a/frontend/cypress/e2e/loginPage.cy.js b/frontend/cypress/e2e/loginPage.cy.js index a358f2e4a8..7224293618 100644 --- a/frontend/cypress/e2e/loginPage.cy.js +++ b/frontend/cypress/e2e/loginPage.cy.js @@ -6,7 +6,6 @@ it("has expected state on initial load", () => { cy.visit("/login"); cy.fixture("initial-state").then((initState) => { const currentFY = getCurrentFiscalYear(); - initState.canDetail.selectedFiscalYear.value = currentFY; initState.portfolio.selectedFiscalYear.value = currentFY; initState.researchProjectFunding.selectedFiscalYear.value = currentFY; diff --git a/frontend/cypress/fixtures/initial-state.json b/frontend/cypress/fixtures/initial-state.json index ceb1e8a769..e1373fd96e 100644 --- a/frontend/cypress/fixtures/initial-state.json +++ b/frontend/cypress/fixtures/initial-state.json @@ -1,15 +1,4 @@ { - "canList": { - "cans": [] - }, - "canDetail": { - "can": {}, - "canFiscalYearObj": {}, - "pendingFunds": "--", - "selectedFiscalYear": { - "value": null - } - }, "portfolioList": { "portfolios": [] }, diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index ed3d5d2eaa..c33186023e 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -17,7 +17,7 @@ "skipLibCheck": true, "strict": true, "target": "ESNext", - "types": ["vite/client", "vite-plugin-svgr/client", "vite-plugin-babel-macros/client", "vitest/globals"] + "types": ["vite/client", "vite-plugin-svgr/client", "vite-plugin-babel-macros", "vitest/globals"] }, "display": "Recommended", "include": ["src", "src/setupTests.js"] diff --git a/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBLIRow.jsx b/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBLIRow.jsx index d764c1a4d4..e9434cc4a3 100644 --- a/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBLIRow.jsx +++ b/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBLIRow.jsx @@ -24,8 +24,9 @@ import ChangeIcons from "../ChangeIcons"; /** * BLIRow component that represents a single row in the Budget Lines table. * @component + * @typedef {import("../../BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine * @param {Object} props - The props for the BLIRow component. - * @param {Object} props.budgetLine - The budget line object. + * @param {BudgetLine} props.budgetLine - The budget line object. * @param {boolean} [props.canUserEditBudgetLines] - Whether the user can edit budget lines. * @param {Function} [props.handleSetBudgetLineForEditing] - The function to set the budget line for editing. * @param {Function} [props.handleDeleteBudgetLine] - The function to delete the budget line. @@ -113,7 +114,6 @@ const AllBLIRow = ({ prefix={"$"} decimalScale={getDecimalScale(budgetLineTotalPlusFees)} fixedDecimalScale={true} - renderText={(value) => value} /> value} /> @@ -206,7 +205,6 @@ const AllBLIRow = ({ prefix={"$"} decimalScale={getDecimalScale(feeTotal)} fixedDecimalScale={true} - renderText={(value) => value} /> diff --git a/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBudgetLinesTable.jsx b/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBudgetLinesTable.jsx index d78502be8f..2d534a62ed 100644 --- a/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBudgetLinesTable.jsx +++ b/frontend/src/components/BudgetLineItems/AllBudgetLinesTable/AllBudgetLinesTable.jsx @@ -12,8 +12,9 @@ import ConfirmationModal from "../../UI/Modals/ConfirmationModal"; /** * TableRow component that represents a single row in the budget lines table. * @component + * @typedef {import("../../BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine * @param {Object} props - The props for the TableRow component. - * @param {Object[]} props.budgetLines - The budget line data for the row. + * @param {BudgetLine[]} props.budgetLines - The budget line data for the row. * @returns {JSX.Element} The TableRow component. */ const AllBudgetLinesTable = ({ budgetLines }) => { diff --git a/frontend/src/components/BudgetLineItems/BLIDiffTable/BLIDiffTable.jsx b/frontend/src/components/BudgetLineItems/BLIDiffTable/BLIDiffTable.jsx index 71595b8b7d..87ecd9fc78 100644 --- a/frontend/src/components/BudgetLineItems/BLIDiffTable/BLIDiffTable.jsx +++ b/frontend/src/components/BudgetLineItems/BLIDiffTable/BLIDiffTable.jsx @@ -6,11 +6,12 @@ import "./BLIDiffTable.scss"; /** * A table component that displays budget lines. + * @typedef {import("../../BudgetLineItems/BudgetLineTypes").BudgetLine} BudgetLine * @param {Object} props - The component props. - * @param {Array} [props.budgetLines] - An array of budget lines to display. - optional + * @param {BudgetLine[]} [props.budgetLines=[]] - The budget lines to display. * @param {string} props.changeType - The type of change request. - * @param {string} [props.statusChangeTo=""] - The status change to. - optional - * @returns {JSX.Element} - The rendered table component. + * @param {string} [props.statusChangeTo=""] - The status change to. + * @returns {JSX.Element} The rendered table component. */ const BLIDiffTable = ({ budgetLines = [], changeType, statusChangeTo = "" }) => { const sortedBudgetLines = budgetLines diff --git a/frontend/src/components/BudgetLineItems/BudgetLineTypes.d.ts b/frontend/src/components/BudgetLineItems/BudgetLineTypes.d.ts new file mode 100644 index 0000000000..79e0f5102f --- /dev/null +++ b/frontend/src/components/BudgetLineItems/BudgetLineTypes.d.ts @@ -0,0 +1,25 @@ +import { SimpleCAN } from "../CANs/CANTypes"; +import { ChangeRequest } from "../ChangeRequests/ChangeRequestsTypes"; +import { SafeUser } from "../Users/UserTypes"; + +export type BudgetLine = { + agreement_id: number; + amount: number; + can: SimpleCAN; + can_id: number; + change_requests_in_review: ChangeRequest[]; + comments: string; + created_by: number; + created_on: Date; + date_needed: Date; + fiscal_year: number; + id: number; + in_review: boolean; + line_description: string; + portfolio_id: number; + proc_shop_fee_percentage: number; + services_component_id: number | null; + status: string; + team_members: SafeUser[]; + updated_on: Date; +}; diff --git a/frontend/src/components/CANs/CANTable/CANTable.jsx b/frontend/src/components/CANs/CANTable/CANTable.jsx new file mode 100644 index 0000000000..5ac792fffa --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTable.jsx @@ -0,0 +1,124 @@ +import _ from "lodash"; +import PropTypes from "prop-types"; +import React from "react"; +import PaginationNav from "../../UI/PaginationNav"; +import Tooltip from "../../UI/USWDS/Tooltip"; +import CANTableRow from "./CANTableRow"; +import styles from "./style.module.css"; +/** + * CANTable component of CanList + * @component + * @typedef {import("../CANTypes").CAN} CAN + * @param {Object} props + * @param {CAN[]} props.cans - Array of CANs + * @returns {JSX.Element} + */ +const CANTable = ({ cans }) => { + const CANS_PER_PAGE = 10; + const [currentPage, setCurrentPage] = React.useState(1); + let cansPerPage = _.cloneDeep(cans); + cansPerPage = cansPerPage.slice((currentPage - 1) * CANS_PER_PAGE, currentPage * CANS_PER_PAGE); + + if (cans.length === 0) { + return

No CANs found

; + } + + return ( + <> + + + + {cansPerPage.map((can) => ( + + ))} + +
+ {cans.length > 0 && ( + + )} + + ); +}; + +const TableHead = () => { + const availbleTooltip = + "$ Available is the remaining amount of the total budget that is available to plan from (Total FY Budget minus budget lines in Planned, Executing or Obligated Status)"; + return ( + + + + CAN + + + Portfolio + + + FY + + + Active Period + + + Obligate By + + + Transfer + + + FY Budget + + + + $ Available + + + + + ); +}; + +CANTable.propTypes = { + cans: PropTypes.array.isRequired +}; + +export default CANTable; diff --git a/frontend/src/components/CANs/CANTable/CANTable.test.jsx b/frontend/src/components/CANs/CANTable/CANTable.test.jsx new file mode 100644 index 0000000000..ce39e3b8fb --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTable.test.jsx @@ -0,0 +1,88 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import CANTable from "./CANTable"; +import { cans } from "../../../tests/data"; +import { useGetCansQuery, useGetCanFundingSummaryQuery } from "../../../api/opsAPI"; +import { MemoryRouter } from "react-router-dom"; + +// Mock the PaginationNav component +vi.mock("../../UI/PaginationNav", () => ({ + default: () =>
+})); + +// Mock the Tooltip component +vi.mock("../../UI/USWDS/Tooltip", () => ({ + default: ({ children }) =>
{children}
+})); +vi.mock("../../../api/opsAPI"); + +describe("CANTable", () => { + useGetCansQuery.mockReturnValue({ + data: cans + }); + useGetCanFundingSummaryQuery.mockReturnValue({ + data: { + fundingSummary: { + available_funding: 1000 + } + } + }); + it("renders the table with correct headers", () => { + render( + + + + ); + + expect(screen.getByText("CAN")).toBeInTheDocument(); + expect(screen.getByText("Portfolio")).toBeInTheDocument(); + expect(screen.getByText("FY")).toBeInTheDocument(); + expect(screen.getByText("Active Period")).toBeInTheDocument(); + expect(screen.getByText("Obligate By")).toBeInTheDocument(); + expect(screen.getByText("Transfer")).toBeInTheDocument(); + expect(screen.getByText("FY Budget")).toBeInTheDocument(); + expect(screen.getByText("$ Available")).toBeInTheDocument(); + }); + + it("renders the correct number of rows", () => { + render( + + + + ); + + const rows = screen.getAllByRole("row"); + // +1 for the header row + expect(rows.length).toBe(cans.length + 1); + }); + + it('displays "No CANs found" when cans array is empty', () => { + render( + + + + ); + + expect(screen.getByText("No CANs found")).toBeInTheDocument(); + }); + + it("renders PaginationNav when there are CANs", () => { + render( + + + + ); + + expect(screen.getByTestId("pagination-nav")).toBeInTheDocument(); + }); + + it("does not render PaginationNav when there are no CANs", () => { + render( + + + + ); + + expect(screen.queryByTestId("pagination-nav")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/CANs/CANTable/CANTableRow.jsx b/frontend/src/components/CANs/CANTable/CANTableRow.jsx new file mode 100644 index 0000000000..fc9a2080cc --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTableRow.jsx @@ -0,0 +1,107 @@ +import PropTypes from "prop-types"; +import CurrencyFormat from "react-currency-format"; +import { Link } from "react-router-dom"; +import { useGetCanFundingSummaryQuery } from "../../../api/opsAPI"; +import { getDecimalScale } from "../../../helpers/currencyFormat.helpers"; +import { convertCodeForDisplay } from "../../../helpers/utils"; +import Tooltip from "../../UI/USWDS/Tooltip"; + +/** + * CanTableRow component of CANTable + * @component + * @param {Object} props + * @param {string} props.name - CAN name + * @param {string} props.nickname - CAN nickname + * @param {string} props.portfolio - Portfolio abbreviation + * @param {number} props.fiscalYear - Fiscal Year + * @param {number} props.activePeriod - Active Period in years + * @param {string} props.obligateBy - Obligate By Date + * @param {string} props.transfer - Method of Transfer + * @param {number} props.fyBudget - Fiscal Year Budget + * @param {number} props.canId - CAN ID + * @returns {JSX.Element} + */ +const CANTableRow = ({ + name, + nickname, + portfolio, + fiscalYear, + activePeriod, + obligateBy, + transfer, + fyBudget, + canId +}) => { + const { data: fundingSummary, isError, isLoading } = useGetCanFundingSummaryQuery(canId); + const availableFunds = fundingSummary?.available_funding ?? 0; + + if (isLoading) + return ( + + Loading... + + ); + if (isError) + return ( + + Error: {isError.valueOf()} + + ); + + return ( + + + + + {name} + + + + {portfolio} + {fiscalYear} + {activePeriod > 1 ? `${activePeriod} years` : `${activePeriod} year`} + {obligateBy} + {convertCodeForDisplay("methodOfTransfer", transfer)} + + + + + + + + ); +}; + +CANTableRow.propTypes = { + name: PropTypes.string.isRequired, + nickname: PropTypes.string.isRequired, + portfolio: PropTypes.string.isRequired, + fiscalYear: PropTypes.number.isRequired, + activePeriod: PropTypes.number.isRequired, + obligateBy: PropTypes.string.isRequired, + transfer: PropTypes.string.isRequired, + fyBudget: PropTypes.number.isRequired, + canId: PropTypes.number.isRequired +}; + +export default CANTableRow; diff --git a/frontend/src/components/CANs/CANTable/CANTableRow.test.jsx b/frontend/src/components/CANs/CANTable/CANTableRow.test.jsx new file mode 100644 index 0000000000..7bc3b3a66f --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTableRow.test.jsx @@ -0,0 +1,124 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import CANTableRow from "./CANTableRow"; +import { MemoryRouter } from "react-router-dom"; +import { useGetCanFundingSummaryQuery } from "../../../api/opsAPI"; + +// Mock the Tooltip component +vi.mock("../../UI/USWDS/Tooltip", () => ({ + default: ({ children }) =>
{children}
+})); + +// Mock the API hook +vi.mock("../../../api/opsAPI"); + +describe("CANTableRow", () => { + const mockProps = { + name: "Test CAN", + nickname: "Test Nickname", + portfolio: "Test Portfolio", + fiscalYear: 2023, + activePeriod: 2, + obligateBy: "2023-09-30", + transfer: "RWA", + fyBudget: 1_000_000, + canId: 1 + }; + + beforeEach(() => { + useGetCanFundingSummaryQuery.mockReturnValue({ + data: { available_funding: 500000 }, + isLoading: false, + isError: false + }); + }); + + it("renders the row with correct data", () => { + render( + + + + + +
+
+ ); + + expect(screen.getByText("Test CAN")).toBeInTheDocument(); + expect(screen.getByText("Test Portfolio")).toBeInTheDocument(); + expect(screen.getByText("2023")).toBeInTheDocument(); + expect(screen.getByText("2 years")).toBeInTheDocument(); + expect(screen.getByText("2023-09-30")).toBeInTheDocument(); + expect(screen.getByText("$1,000,000.00")).toBeInTheDocument(); + expect(screen.getByText("$500,000.00")).toBeInTheDocument(); + }); + + it("renders 'Loading...' when data is loading", () => { + useGetCanFundingSummaryQuery.mockReturnValue({ + isLoading: true + }); + + render( + + + + + +
+
+ ); + + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + it("renders error message when there's an error", () => { + useGetCanFundingSummaryQuery.mockReturnValue({ + isError: true, + error: { message: "Test error" } + }); + + render( + + + + + +
+
+ ); + + expect(screen.getByText("Error:")).toBeInTheDocument(); + }); + + it("renders correct active period text for single year", () => { + render( + + + + + +
+
+ ); + + expect(screen.getByText("1 year")).toBeInTheDocument(); + }); + + it("renders tooltip with nickname", () => { + render( + + + + + +
+
+ ); + + expect(screen.getByTestId("tooltip")).toBeInTheDocument(); + expect(screen.getByText("Test CAN")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/CANs/CANTable/index.js b/frontend/src/components/CANs/CANTable/index.js new file mode 100644 index 0000000000..ed3769afde --- /dev/null +++ b/frontend/src/components/CANs/CANTable/index.js @@ -0,0 +1 @@ +export { default } from "./CANTable"; diff --git a/frontend/src/components/CANs/CANTable/style.module.css b/frontend/src/components/CANs/CANTable/style.module.css new file mode 100644 index 0000000000..cfb7dcf40c --- /dev/null +++ b/frontend/src/components/CANs/CANTable/style.module.css @@ -0,0 +1,4 @@ +.tableHover tbody tr:hover td, +.tableHover tbody tr:hover th { + background-color: var(--base-light-variant); +} diff --git a/frontend/src/components/CANs/CANTypes.d.ts b/frontend/src/components/CANs/CANTypes.d.ts new file mode 100644 index 0000000000..abdb289e9b --- /dev/null +++ b/frontend/src/components/CANs/CANTypes.d.ts @@ -0,0 +1,101 @@ +import { BudgetLine } from "../BudgetLineItems/BudgetLineTypes"; +import { Portfolio } from "../Portfolios/PortfolioTypes"; +import { Project } from "../Projects/ProjectTypes"; + +export type CAN = { + active_period?: number; + budget_line_items: BudgetLine[]; + created_by: number | null; + created_by_user: number | null; + created_on: Date; + description?: string; + display_name?: string; + funding_budgets: CANFundingBudget[]; + funding_details: CANFundingDetails; + funding_details_id: number; + funding_received: CANFundingReceived[]; + id: number; + nick_name?: string; + number: string; + obligate_by?: Date; + portfolio: Portfolio; + portfolio_id: number; + projects: Project[]; + updated_by: number | null; + updated_by_user: number | null; + updated_on: Date; +}; + +export type SimpleCAN = { + active_period: number; + description: string; + display_name: string; + id: number; + nick_name: string; + number: string; + portfolio_id: number; + projects: Project[]; +}; + +export type URL = { + created_by: number | null; + created_by_user: number | null; + created_on: Date; + id: number; + portfolio_id: number; + updated_by: number | null; + updated_by_user: number | null; + updated_on: Date; + url: string; +}; + +export type CANFundingBudget = { + budget: number; + can: SimpleCAN; + can_id: number; + created_by: number | null; + created_by_user: number | null; + created_on: Date; + display_name: string; + fiscal_year: number; + id: number; + notes: string | null; + updated_by: number | null; + updated_by_user: number | null; + updated_on: Date; +}; + +export type CANFundingDetails = { + allotment: null; + allowance: null; + created_by: null; + created_by_user: null; + created_on: Date; + display_name: string; + fiscal_year: number; + fund_code: string; + funding_partner: null; + funding_source: string; + id: number; + method_of_transfer: "DIRECT" | "COST_SHARE" | "IDDA" | "IAA"; + sub_allowance: null; + updated_by: null; + updated_by_user: null; + updated_on: Date; +}; + +export type CANFundingReceived = { + can: SimpleCAN; + can_id: number; + created_by: number | null; + created_by_user: number | null; + created_on: Date; + display_name: string; + fiscal_year: number; + funding: number; + id: number; + notes: string | null; + updated_by: number | null; + updated_by_user: number | null; + updated_on: Date; +}; diff --git a/frontend/src/components/CANs/CanTabs/CanTabs.jsx b/frontend/src/components/CANs/CanTabs/CanTabs.jsx new file mode 100644 index 0000000000..3a7d27db49 --- /dev/null +++ b/frontend/src/components/CANs/CanTabs/CanTabs.jsx @@ -0,0 +1,49 @@ +import { Link, useLocation } from "react-router-dom"; +import styles from "../../../components/Portfolios/PortfolioTabsSection/PortfolioTabsSection.module.scss"; +import TabsSection from "../../../components/UI/TabsSection"; + +/** + * A header section of the Budget lines list page that contains the filters. + * @component + * @returns {JSX.Element} - The procurement shop select element. + */ +const CANTags = () => { + const location = useLocation(); + const selected = `font-sans-2xs text-bold ${styles.listItemSelected}`; + const notSelected = `font-sans-2xs text-bold ${styles.listItemNotSelected}`; + + const paths = [ + { + name: "", + label: "All CANs" + }, + { + name: "?filter=my-cans", + label: "My CANs" + } + ]; + + const links = paths.map((path) => { + const queryString = `${path.name}`; + + return ( + + {path.label} + + ); + }); + + return ( + + ); +}; + +export default CANTags; diff --git a/frontend/src/components/CANs/CanTabs/index.js b/frontend/src/components/CANs/CanTabs/index.js new file mode 100644 index 0000000000..ccab092012 --- /dev/null +++ b/frontend/src/components/CANs/CanTabs/index.js @@ -0,0 +1 @@ +export { default } from "./CanTabs"; diff --git a/frontend/src/components/ChangeRequests/ChangeRequest.hooks.js b/frontend/src/components/ChangeRequests/ChangeRequests.hooks.js similarity index 100% rename from frontend/src/components/ChangeRequests/ChangeRequest.hooks.js rename to frontend/src/components/ChangeRequests/ChangeRequests.hooks.js diff --git a/frontend/src/components/ChangeRequests/ChangeRequests.jsx b/frontend/src/components/ChangeRequests/ChangeRequests.jsx index 1fa2c06f87..3dbe3b4868 100644 --- a/frontend/src/components/ChangeRequests/ChangeRequests.jsx +++ b/frontend/src/components/ChangeRequests/ChangeRequests.jsx @@ -1,5 +1,5 @@ import ConfirmationModal from "../UI/Modals/ConfirmationModal"; -import useChangeRequest from "./ChangeRequest.hooks"; +import useChangeRequest from "./ChangeRequests.hooks"; import ChangeRequestsList from "./ChangeRequestsList"; function ChangeRequests() { diff --git a/frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequestsList.jsx b/frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequestsList.jsx index 81da62c890..457e24449b 100644 --- a/frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequestsList.jsx +++ b/frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequestsList.jsx @@ -25,7 +25,7 @@ function ChangeRequestsList({ handleReviewChangeRequest }) { } /** - * @typedef {import('./ChangeRequests').ChangeRequest} ChangeRequest + * @typedef {import('../ChangeRequestsTypes').ChangeRequest} ChangeRequest * @type {ChangeRequest[]} */ return changeRequests.length > 0 ? ( diff --git a/frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequests.d.ts b/frontend/src/components/ChangeRequests/ChangeRequestsTypes.d.ts similarity index 100% rename from frontend/src/components/ChangeRequests/ChangeRequestsList/ChangeRequests.d.ts rename to frontend/src/components/ChangeRequests/ChangeRequestsTypes.d.ts diff --git a/frontend/src/components/ChangeRequests/ReviewChangeRequestAccordion/ReviewChangeRequestAccordion.jsx b/frontend/src/components/ChangeRequests/ReviewChangeRequestAccordion/ReviewChangeRequestAccordion.jsx index 2ec6748125..258576c8ec 100644 --- a/frontend/src/components/ChangeRequests/ReviewChangeRequestAccordion/ReviewChangeRequestAccordion.jsx +++ b/frontend/src/components/ChangeRequests/ReviewChangeRequestAccordion/ReviewChangeRequestAccordion.jsx @@ -7,7 +7,7 @@ import { CHANGE_REQUEST_TYPES } from "../ChangeRequests.constants"; import StatusChangeReviewCard from "../StatusChangeReviewCard"; /** - * @typedef {import('../ChangeRequestsList/ChangeRequests').ChangeRequest} ChangeRequest + * @typedef {import('../ChangeRequestsTypes').ChangeRequest} ChangeRequest * @type {ChangeRequest[]} */ /** diff --git a/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx b/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx index b38cfcf271..0b6bed8bb5 100644 --- a/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx +++ b/frontend/src/components/Layouts/TablePageLayout/TablePageLayout.jsx @@ -16,8 +16,8 @@ import icons from "../../../uswds/img/sprite.svg"; * @param {React.ReactNode} [props.FilterButton] - The filter button to display. * @param {React.ReactNode} [props.TableSection] - The table to display. * @param {React.ReactNode} [props.SummaryCardsSection] - The summary cards to display. - * @param {string} props.buttonText - The text to display on the button. - * @param {string} props.buttonLink - The link to navigate to when the button is clicked. + * @param {string} [props.buttonText] - The text to display on the button. + * @param {string} [props.buttonLink] - The link to navigate to when the button is clicked. * @returns {JSX.Element} - The rendered component. */ export const TablePageLayout = ({ @@ -37,18 +37,20 @@ export const TablePageLayout = ({ <>

{title}

- - - - - {buttonText} - + + + + {buttonText} + + )}
{TabsSection}
@@ -78,6 +80,6 @@ TablePageLayout.propTypes = { FilterButton: PropTypes.node, TableSection: PropTypes.node, SummaryCardsSection: PropTypes.node, - buttonText: PropTypes.string.isRequired, - buttonLink: PropTypes.string.isRequired + buttonText: PropTypes.string, + buttonLink: PropTypes.string }; diff --git a/frontend/src/components/Portfolios/PortfolioTypes.d.ts b/frontend/src/components/Portfolios/PortfolioTypes.d.ts new file mode 100644 index 0000000000..9f78c79cc3 --- /dev/null +++ b/frontend/src/components/Portfolios/PortfolioTypes.d.ts @@ -0,0 +1,18 @@ +import { URL } from "../CANs/CANTypes"; +import { SafeUser } from "../Users/UserTypes"; + +export type Portfolio = { + abbreviation: string; + created_by: number | null; + created_by_user: number | null; + created_on: Date; + division_id: number; + id: number; + name: string; + status: string; + team_leaders: SafeUser[]; + updated_by: null; + updated_by_user: null; + updated_on: Date; + urls: URL[]; +}; diff --git a/frontend/src/components/Projects/ProjectTypes.d.ts b/frontend/src/components/Projects/ProjectTypes.d.ts new file mode 100644 index 0000000000..4b03f87faa --- /dev/null +++ b/frontend/src/components/Projects/ProjectTypes.d.ts @@ -0,0 +1,16 @@ +import { SafeUser } from "../Users/UserTypes"; + +export type Project = { + created_by: number | null; + created_on: Date; + description: string; + id: number; + methodologies: string[]; + origination_date: Date; + populations: string[]; + short_title: string; + team_leaders: SafeUser[]; + title: string; + updated_on: Date; + url: string; +}; diff --git a/frontend/src/components/Users/UserInfo/UserInfo.jsx b/frontend/src/components/Users/UserInfo/UserInfo.jsx index 1f0efad9d7..d79802e8fb 100644 --- a/frontend/src/components/Users/UserInfo/UserInfo.jsx +++ b/frontend/src/components/Users/UserInfo/UserInfo.jsx @@ -10,8 +10,11 @@ import { setIsActive } from "../../UI/Alert/alertSlice.js"; /** * Renders the user information. - * @param {Object} user - The user object. - * @param {Boolean} isEditable - Whether the user information is editable. + * @component + * @typedef {import("../UserTypes").SafeUser} User + * @param {Object} props - The component props. + * @param {User} props.user - The user object. + * @param {Boolean} props.isEditable - Whether the user information is editable. * @returns {JSX.Element} - The rendered component. */ const UserInfo = ({ user, isEditable }) => { diff --git a/frontend/src/components/Users/UserTypes.d.ts b/frontend/src/components/Users/UserTypes.d.ts new file mode 100644 index 0000000000..b0fef34740 --- /dev/null +++ b/frontend/src/components/Users/UserTypes.d.ts @@ -0,0 +1,5 @@ +export type SafeUser = { + email: string; + full_name: string; + id: number; +}; diff --git a/frontend/src/helpers/utils.js b/frontend/src/helpers/utils.js index 2794807498..08e2757cf1 100644 --- a/frontend/src/helpers/utils.js +++ b/frontend/src/helpers/utils.js @@ -29,7 +29,10 @@ export const calculatePercent = (numerator, denominator) => { return Math.round((numerator / denominator) * 100); }; - +/** + * This function formats a date into a string in the format MM/DD/YYYY. + * @param {Date} date - The date to format. This parameter is required. + */ export const formatDate = (date) => { const options = { timeZone: "UTC" }; @@ -38,8 +41,8 @@ export const formatDate = (date) => { /** * Formats a date string into a date string in the format MM/DD/YYYY. - * @param {string} dateNeeded - The date string to format. This parameter is required. - * @returns {string} The formatted date string. + * @param {Date | string} dateNeeded - The date string to format. This parameter is required. + * @returns {string | undefined} The formatted date string or undefined if input is invalid. */ export const formatDateNeeded = (dateNeeded) => { let formatted_date_needed; @@ -176,12 +179,18 @@ export const codesToDisplayText = { can_id: "CAN", date_needed: "Obligate By Date", status: "Status" + }, + methodOfTransfer: { + COST_SHARE: "Cost Share", + DIRECT: "Direct", + IAA: "IAA", + IDDA: "IDDA" } }; /** * Converts a code value into a display text value based on a predefined mapping. - * @param {("agreementType" | "agreementReason" | "budgetLineStatus" | "validation" | "classNameLabels" | "baseClassNameLabels"| "agreementPropertyLabels" | "budgetLineItemPropertyLabels" | "changeToTypes")} listName - The name of the list to retrieve the mapping from the codesToDisplayText object. This parameter is required. + * @param {("agreementType" | "agreementReason" | "budgetLineStatus" | "validation" | "classNameLabels" | "baseClassNameLabels"| "agreementPropertyLabels" | "budgetLineItemPropertyLabels" | "changeToTypes" | "methodOfTransfer")} listName - The name of the list to retrieve the mapping from the codesToDisplayText object. This parameter is required. * @param {string} code - The code value to convert. This parameter is required. * @returns {string} The display text value for the code, or the original code value if no mapping is found. * @throws {Error} If either the listName or code parameter is not provided. diff --git a/frontend/src/hooks/useChangeRequests.hooks.js b/frontend/src/hooks/useChangeRequests.hooks.js index d0899b2c5d..8f3bbcd818 100644 --- a/frontend/src/hooks/useChangeRequests.hooks.js +++ b/frontend/src/hooks/useChangeRequests.hooks.js @@ -1,7 +1,7 @@ import { useGetAgreementByIdQuery, useGetCansQuery, useGetChangeRequestsListQuery } from "../api/opsAPI"; import { renderField } from "../helpers/utils"; /** - * @typedef {import ('../components/ChangeRequests/ChangeRequestsList/ChangeRequests.d.ts').ChangeRequest} ChangeRequest + * @typedef {import ('../components/ChangeRequests/ChangeRequests').ChangeRequest} ChangeRequest */ /** @@ -61,7 +61,7 @@ export const useChangeRequestsForBudgetLines = (budgetLines, targetStatus, isBud * Custom hook that returns the change requests for a budget line. * @param {Object} budgetLine - The budget line. * @returns {string} The change requests messages. - + */ export const useChangeRequestsForTooltip = (budgetLine) => { const { data: cans, isSuccess: cansSuccess } = useGetCansQuery(); diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index d4268be8a1..95c8bce709 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -231,24 +231,29 @@ const router = createBrowserRouter( ) }} /> - } - /> - } - handle={{ - crumb: () => ( - - Cans - - ) - }} - /> + {/*TODO: remove flag once CANS are ready */} + {import.meta.env.DEV && ( + <> + } + /> + } + handle={{ + crumb: () => ( + + Cans + + ) + }} + /> + + )} } diff --git a/frontend/src/pages/agreements/approve/ApproveAgreement.hooks.js b/frontend/src/pages/agreements/approve/ApproveAgreement.hooks.js index 341a2c0b79..e89694c076 100644 --- a/frontend/src/pages/agreements/approve/ApproveAgreement.hooks.js +++ b/frontend/src/pages/agreements/approve/ApproveAgreement.hooks.js @@ -20,7 +20,7 @@ import useToggle from "../../../hooks/useToggle"; import { getTotalByCans } from "../review/ReviewAgreement.helpers"; /** - * @typedef {import('../../../components/ChangeRequests/ChangeRequestsList/ChangeRequests').ChangeRequest} ChangeRequest + * @typedef {import('../../../components/ChangeRequests/ChangeRequests').ChangeRequest} ChangeRequest * @type {ChangeRequest[]} */ /** diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx index 4b28c5ace1..3868d0b34a 100644 --- a/frontend/src/pages/cans/list/CanList.jsx +++ b/frontend/src/pages/cans/list/CanList.jsx @@ -1,48 +1,42 @@ -import { useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Link, Outlet } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; +import { useGetCansQuery } from "../../../api/opsAPI"; import App from "../../../App"; -import { getCanList } from "./getCanList"; -import TextClip from "../../../components/UI/Text/TextClip"; +import CANTable from "../../../components/CANs/CANTable"; +import CANTags from "../../../components/CANs/CanTabs"; +import TablePageLayout from "../../../components/Layouts/TablePageLayout"; +import ErrorPage from "../../ErrorPage"; const CanList = () => { - const dispatch = useDispatch(); - const canList = useSelector((state) => state.canList.cans); - - const tableClasses = "usa-table usa-table--borderless margin-x-auto"; - - useEffect(() => { - dispatch(getCanList()); - }, [dispatch]); - + const [searchParams] = useSearchParams(); + const myCANsUrl = searchParams.get("filter") === "my-cans"; + const { data: canList, isError, isLoading } = useGetCansQuery({}); + if (isLoading) { + return ( + +

Loading...

+
+ ); + } + if (isError) { + return ; + } + // TODO: remove flag once CANS are ready return ( - -

CANs

- - -
+ import.meta.env.DEV && ( + + } + TableSection={} + /> + + ) ); }; diff --git a/frontend/src/pages/cans/list/CanList.test.jsx b/frontend/src/pages/cans/list/CanList.test.jsx new file mode 100644 index 0000000000..b43267a3e7 --- /dev/null +++ b/frontend/src/pages/cans/list/CanList.test.jsx @@ -0,0 +1,24 @@ +import { screen, waitFor } from "@testing-library/react"; +import { renderWithProviders } from "../../../test-utils"; +import { server } from "../../../tests/mocks"; +import CanList from "./CanList"; + +server.listen(); +// TODO: Fix this test +describe.skip("opsApi", () => { + test("should GET /cans using mocks", async () => { + const { container } = renderWithProviders(); + + await waitFor(() => { + expect(container).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText("G99HRF2")).toBeInTheDocument(); + }); + }); +}); + +function TestComponent() { + return ; +} diff --git a/frontend/src/pages/cans/list/canListSlice.js b/frontend/src/pages/cans/list/canListSlice.js deleted file mode 100644 index 2692848b76..0000000000 --- a/frontend/src/pages/cans/list/canListSlice.js +++ /dev/null @@ -1,19 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; - -const initialState = { - cans: [] -}; - -const canListSlice = createSlice({ - name: "canList", - initialState, - reducers: { - setCanList: (state, action) => { - state.cans = action.payload; - } - } -}); - -export const { setCanList } = canListSlice.actions; - -export default canListSlice.reducer; diff --git a/frontend/src/pages/cans/list/getCanList.js b/frontend/src/pages/cans/list/getCanList.js deleted file mode 100644 index 79b9842ed8..0000000000 --- a/frontend/src/pages/cans/list/getCanList.js +++ /dev/null @@ -1,12 +0,0 @@ -import { setCanList } from "./canListSlice"; -import ApplicationContext from "../../../applicationContext/ApplicationContext"; - -// TODO: Replace with RTK Query - -export const getCanList = () => { - return async (dispatch) => { - const api_version = ApplicationContext.get().helpers().backEndConfig.apiVersion; - const responseData = await ApplicationContext.get().helpers().callBackend(`/api/${api_version}/cans/`, "get"); - dispatch(setCanList(responseData)); - }; -}; diff --git a/frontend/src/pages/cans/list/getCanList.test.js b/frontend/src/pages/cans/list/getCanList.test.js deleted file mode 100644 index 9dac35da3a..0000000000 --- a/frontend/src/pages/cans/list/getCanList.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { getCanList } from "./getCanList"; -import store from "../../../store"; -import TestApplicationContext from "../../../applicationContext/TestApplicationContext"; -import { dispatchUsecase } from "../../../helpers/test"; - -test("successfully gets the CAN list from the backend and directly puts it into state", async () => { - const mockBackendResponse = [ - { - id: 1, - number: "G99HRF2", - otherStuff: "Moof" - }, - { - id: 2, - number: "G99IA14", - otherStuff: "DogCow" - }, - { - id: 3, - number: "G99PHS9", - otherStuff: "Clarus" - } - ]; - TestApplicationContext.helpers().callBackend.mockImplementation(async () => { - return mockBackendResponse; - }); - - const actualGetCanList = getCanList(); - - await dispatchUsecase(actualGetCanList); - - const canList = store.getState().canList.cans; - expect(canList).toEqual(mockBackendResponse); -}); diff --git a/frontend/src/store.js b/frontend/src/store.js index 046cba4fad..b9ec9b73ed 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -1,5 +1,4 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; -import canListSlice from "./pages/cans/list/canListSlice"; import canDetailSlice from "./pages/cans/detail/canDetailSlice"; import portfolioListSlice from "./pages/portfolios/list/portfolioListSlice"; import portfolioBudgetSummarySlice from "./components/Portfolios/PortfolioBudgetSummary/portfolioBudgetSummarySlice"; @@ -17,7 +16,6 @@ import { opsAuthApi } from "./api/opsAuthAPI.js"; const rootReducer = combineReducers({ [opsApi.reducerPath]: opsApi.reducer, [opsAuthApi.reducerPath]: opsAuthApi.reducer, - canList: canListSlice, canDetail: canDetailSlice, portfolioList: portfolioListSlice, portfolioBudgetSummary: portfolioBudgetSummarySlice, diff --git a/frontend/src/tests/data.js b/frontend/src/tests/data.js index 741f3a4ceb..545cb1f3cd 100644 --- a/frontend/src/tests/data.js +++ b/frontend/src/tests/data.js @@ -892,3 +892,548 @@ export const budgetLineWithStatusChangeRequestToExecuting = { ], updated_on: "2024-07-26T14:07:14.544417" }; + +export const cans = [ + { + active_period: 1, + budget_line_items: [ + { + agreement_id: 2, + amount: 2000000, + can: { + active_period: 1, + description: "Healthy Marriages Responsible Fatherhood - OPRE", + display_name: "G99HRF2", + id: 500, + nick_name: "HMRF-OPRE", + number: "G99HRF2", + portfolio_id: 6 + }, + can_id: 500, + change_requests_in_review: null, + comments: "", + created_by: 503, + created_on: "2024-09-17T18:12:32.976627", + date_needed: "2043-06-13", + fiscal_year: 2043, + id: 15008, + in_review: false, + line_description: "Line Item 2", + portfolio_id: 6, + proc_shop_fee_percentage: 0.005, + services_component_id: null, + status: "IN_EXECUTION", + team_members: [ + { + email: "chris.fortunato@example.com", + full_name: "Chris Fortunato", + id: 500 + }, + { + email: "Amelia.Popham@example.com", + full_name: "Amelia Popham", + id: 503 + }, + { + email: "admin.demo@email.com", + full_name: "Admin Demo", + id: 520 + } + ], + updated_on: "2024-09-17T18:12:32.976627" + } + ], + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.558006Z", + description: "Healthy Marriages Responsible Fatherhood - OPRE", + display_name: "G99HRF2", + funding_budgets: [ + { + budget: 1140000, + can: { + active_period: 1, + description: "Healthy Marriages Responsible Fatherhood - OPRE", + display_name: "G99HRF2", + id: 500, + nick_name: "HMRF-OPRE", + number: "G99HRF2", + portfolio_id: 6, + projects: [] + }, + can_id: 500, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.781382Z", + display_name: "CANFundingBudget#1", + fiscal_year: 2023, + id: 1, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.781382Z", + versions: [ + { + budget: 1140000, + can: { + description: "Healthy Marriages Responsible Fatherhood - OPRE", + id: 500, + nick_name: "HMRF-OPRE", + number: "G99HRF2", + portfolio_id: 6, + projects: [] + }, + can_id: 500, + created_by: null, + created_on: "2024-09-17T18:12:25.781382Z", + end_transaction_id: null, + fiscal_year: 2023, + id: 1, + notes: null, + operation_type: 0, + transaction_id: 186, + updated_by: null, + updated_on: "2024-09-17T18:12:25.781382Z" + } + ] + } + ], + funding_details: { + allotment: null, + allowance: null, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.370020Z", + display_name: "CANFundingDetails#1", + fiscal_year: 2023, + fund_code: "AAXXXX20231DAD", + funding_partner: null, + funding_source: "CANFundingSource.OPRE", + id: 1, + method_of_transfer: "DIRECT", + sub_allowance: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.370020Z" + }, + funding_details_id: 1, + funding_received: [ + { + can: { + active_period: 1, + description: "Healthy Marriages Responsible Fatherhood - OPRE", + display_name: "G99HRF2", + id: 500, + nick_name: "HMRF-OPRE", + number: "G99HRF2", + portfolio_id: 6, + projects: [] + }, + can_id: 500, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:26.088324Z", + display_name: "CANFundingReceived#500", + fiscal_year: 2023, + funding: 880000, + id: 500, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:26.088324Z" + } + ], + id: 500, + nick_name: "HMRF-OPRE", + number: "G99HRF2", + portfolio: { + abbreviation: "HMRF", + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:16.659182Z", + division_id: 5, + id: 6, + name: "Healthy Marriage & Responsible Fatherhood", + status: "IN_PROCESS", + team_leaders: [ + { + full_name: "Katie Pahigiannis", + id: 505 + } + ], + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:16.659182Z", + urls: [ + { + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:16.802256Z", + id: 6, + portfolio_id: 6, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:16.802256Z", + url: "https://www.acf.hhs.gov/opre/topic/strengthening-families-healthy-marriage-responsible-fatherhood" + } + ] + }, + portfolio_id: 6, + projects: [], + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.558006Z" + }, + { + active_period: 5, + budget_line_items: [ + { + agreement_id: 2, + amount: 2000000, + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1 + }, + can_id: 501, + change_requests_in_review: null, + comments: "", + created_by: 503, + created_on: "2024-09-17T18:12:33.000154", + date_needed: "2043-06-13", + fiscal_year: 2043, + id: 15010, + in_review: false, + line_description: "Line Item 2", + portfolio_id: 1, + proc_shop_fee_percentage: 0.005, + services_component_id: null, + status: "IN_EXECUTION", + team_members: [ + { + email: "chris.fortunato@example.com", + full_name: "Chris Fortunato", + id: 500 + }, + { + email: "Amelia.Popham@example.com", + full_name: "Amelia Popham", + id: 503 + }, + { + email: "admin.demo@email.com", + full_name: "Admin Demo", + id: 520 + } + ], + updated_on: "2024-09-17T18:12:33.000154" + }, + { + agreement_id: 2, + amount: 1000000, + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1 + }, + can_id: 501, + change_requests_in_review: null, + comments: "", + created_by: 503, + created_on: "2024-09-17T18:12:33.060480", + date_needed: "2043-06-13", + fiscal_year: 2043, + id: 15015, + in_review: false, + line_description: "Line Item 2", + portfolio_id: 1, + proc_shop_fee_percentage: 0.005, + services_component_id: null, + status: "PLANNED", + team_members: [ + { + email: "chris.fortunato@example.com", + full_name: "Chris Fortunato", + id: 500 + }, + { + email: "Amelia.Popham@example.com", + full_name: "Amelia Popham", + id: 503 + }, + { + email: "admin.demo@email.com", + full_name: "Admin Demo", + id: 520 + } + ], + updated_on: "2024-09-17T18:12:33.060480" + }, + { + agreement_id: 2, + amount: 3000000, + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1 + }, + can_id: 501, + change_requests_in_review: null, + comments: "", + created_by: 503, + created_on: "2024-09-17T18:12:33.073854", + date_needed: "2043-06-13", + fiscal_year: 2043, + id: 15016, + in_review: false, + line_description: "Line Item 2", + portfolio_id: 1, + proc_shop_fee_percentage: 0.005, + services_component_id: null, + status: "OBLIGATED", + team_members: [ + { + email: "chris.fortunato@example.com", + full_name: "Chris Fortunato", + id: 500 + }, + { + email: "Amelia.Popham@example.com", + full_name: "Amelia Popham", + id: 503 + }, + { + email: "admin.demo@email.com", + full_name: "Admin Demo", + id: 520 + } + ], + updated_on: "2024-09-17T18:12:33.073854" + } + ], + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.579527Z", + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + funding_budgets: [ + { + budget: 200000, + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.799918Z", + display_name: "CANFundingBudget#2", + fiscal_year: 2021, + id: 2, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.799918Z", + versions: [ + { + budget: 200000, + can: { + description: "Incoming Interagency Agreements", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_on: "2024-09-17T18:12:25.799918Z", + end_transaction_id: null, + fiscal_year: 2021, + id: 2, + notes: null, + operation_type: 0, + transaction_id: 187, + updated_by: null, + updated_on: "2024-09-17T18:12:25.799918Z" + } + ] + }, + { + budget: 10000000, + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.903789Z", + display_name: "CANFundingBudget#13", + fiscal_year: 2023, + id: 13, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.903789Z", + versions: [ + { + budget: 10000000, + can: { + description: "Incoming Interagency Agreements", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_on: "2024-09-17T18:12:25.903789Z", + end_transaction_id: null, + fiscal_year: 2023, + id: 13, + notes: null, + operation_type: 0, + transaction_id: 198, + updated_by: null, + updated_on: "2024-09-17T18:12:25.903789Z" + } + ] + } + ], + funding_details: { + allotment: null, + allowance: null, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:25.394257Z", + display_name: "CANFundingDetails#2", + fiscal_year: 2021, + fund_code: "BBXXXX20215DAD", + funding_partner: null, + funding_source: null, + id: 2, + method_of_transfer: "COST_SHARE", + sub_allowance: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.394257Z" + }, + funding_details_id: 2, + funding_received: [ + { + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:26.106152Z", + display_name: "CANFundingReceived#501", + fiscal_year: 2021, + funding: 200000, + id: 501, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:26.106152Z" + }, + { + can: { + active_period: 5, + description: "Incoming Interagency Agreements", + display_name: "G99IA14", + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio_id: 1, + projects: [] + }, + can_id: 501, + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:26.216160Z", + display_name: "CANFundingReceived#512", + fiscal_year: 2023, + funding: 6000000, + id: 512, + notes: null, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:26.216160Z" + } + ], + id: 501, + nick_name: "IAA-Incoming", + number: "G99IA14", + portfolio: { + abbreviation: "CWR", + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:16.472498Z", + division_id: 4, + id: 1, + name: "Child Welfare Research", + status: "IN_PROCESS", + team_leaders: [ + { + full_name: "Chris Fortunato", + id: 500 + } + ], + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:16.472498Z", + urls: [ + { + created_by: null, + created_by_user: null, + created_on: "2024-09-17T18:12:16.760426Z", + id: 1, + portfolio_id: 1, + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:16.760426Z", + url: "https://www.acf.hhs.gov/opre/topic/overview/abuse-neglect-adoption-foster-care" + } + ] + }, + portfolio_id: 1, + projects: [], + updated_by: null, + updated_by_user: null, + updated_on: "2024-09-17T18:12:25.579527Z" + } +]; diff --git a/frontend/src/tests/mocks.js b/frontend/src/tests/mocks.js index 1481f93719..4f9a740970 100644 --- a/frontend/src/tests/mocks.js +++ b/frontend/src/tests/mocks.js @@ -1,6 +1,6 @@ import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; -import { changeRequests, divisions, roles } from "./data"; +import { changeRequests, divisions, roles, cans } from "./data"; export const handlers = [ http.get(`https://localhost:8000/api/v1/agreements/`, () => { @@ -49,6 +49,10 @@ export const handlers = [ const { body } = req; return res(ctx.status(200), ctx.json({ id, ...body }), ctx.delay(150)); + }), + + http.get("https://localhost:8000/api/v1/cans/", () => { + return HttpResponse.json(cans); }) ];