diff --git a/backend/openapi.yml b/backend/openapi.yml index a752f1b67b..56910e8256 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -291,11 +291,11 @@ paths: required: true content: application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCANRequestSchema" - examples: - "0": - $ref: "#/components/examples/CreateUpdateCANRequestSchema" + schema: + $ref: "#/components/schemas/CreateUpdateCANRequestSchema" + examples: + "0": + $ref: "#/components/examples/CreateUpdateCANRequestSchema" responses: "200": description: CAN Updated @@ -329,11 +329,11 @@ paths: required: true content: application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCANRequestSchema" - examples: - "0": - $ref: "#/components/examples/CreateUpdateCANRequestSchema" + schema: + $ref: "#/components/schemas/CreateUpdateCANRequestSchema" + examples: + "0": + $ref: "#/components/examples/CreateUpdateCANRequestSchema" responses: "200": description: CAN Updated @@ -391,11 +391,11 @@ paths: required: true content: application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" - examples: - "0": - $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" + schema: + $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" + examples: + "0": + $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" responses: "201": description: Created @@ -457,11 +457,11 @@ paths: required: true content: application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" - examples: - "0": - $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" + schema: + $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" + examples: + "0": + $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" responses: "200": description: CANFundingBudget Updated @@ -495,11 +495,11 @@ paths: required: true content: application/json: - schema: - $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" - examples: - "0": - $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" + schema: + $ref: "#/components/schemas/CreateUpdateCANFundingBudgetRequest" + examples: + "0": + $ref: "#/components/examples/CreateUpdateCANFundingBudgetRequest" responses: "200": description: CAN Updated @@ -2355,6 +2355,8 @@ components: type: array items: $ref: "#/components/schemas/BudgetLineItem" + description: + type: string display_name: type: string funding_budgets: @@ -2371,8 +2373,14 @@ components: type: array items: $ref: "#/components/schemas/FundingReceived" + id: + type: integer + nick_name: + type: string number: type: string + obligate_by: + type: integer portfolio: type: integer portfolio_id: @@ -2381,12 +2389,6 @@ components: type: array items: $ref: "#/components/schemas/Project" - nick_name: - type: string - description: - type: string - id: - type: integer created_on: $ref: "#/components/parameters/created_on" updated_on: diff --git a/frontend/src/components/CANs/CANTable/CANTable.helpers.js b/frontend/src/components/CANs/CANTable/CANTable.helpers.js new file mode 100644 index 0000000000..b3790efd4e --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTable.helpers.js @@ -0,0 +1,27 @@ +/** + * Gets the last day of the fiscal year for a given year. + * @param {number} fiscalYear - The fiscal year + * @returns {Date} The last day of the fiscal year + */ +const getLastDayOfFiscalYear = (fiscalYear) => { + // Fiscal year ends on September 30 of the previous calendar year + return new Date(fiscalYear - 1, 8, 30); // Month is 0-indexed, so 8 is September +}; +/** + * Formats the obligate by date to the last day of the fiscal year. + * @param {number | undefined} obligateBy - The obligate by value + * @returns {string} Formatted date string or "TBD" + */ +export const formatObligateBy = (obligateBy) => { + if (!obligateBy) return "TBD"; // Default value + if (typeof obligateBy !== "number" || isNaN(obligateBy)) return "TBD"; // Default if parsing fails + + const lastDay = getLastDayOfFiscalYear(obligateBy); + + // Format as MM/DD/YY + return lastDay.toLocaleDateString("en-US", { + month: "2-digit", + day: "2-digit", + year: "2-digit" + }); +}; diff --git a/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js b/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js new file mode 100644 index 0000000000..3349b089e6 --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTable.helpers.test.js @@ -0,0 +1,24 @@ +import { formatObligateBy } from "./CANTable.helpers"; + +describe("formatObligateBy", () => { + test('returns "TBD" for undefined input', () => { + expect(formatObligateBy(undefined)).toBe("TBD"); + }); + + test('returns "TBD" for non-numeric input', () => { + expect(formatObligateBy("not a number")).toBe("TBD"); + }); + + test('returns "TBD" for NaN input', () => { + expect(formatObligateBy(NaN)).toBe("TBD"); + }); + + test("formats valid fiscal year correctly", () => { + expect(formatObligateBy(2023)).toBe("09/30/22"); + }); + + test("handles different fiscal years", () => { + expect(formatObligateBy(2024)).toBe("09/30/23"); + expect(formatObligateBy(2025)).toBe("09/30/24"); + }); +}); diff --git a/frontend/src/components/CANs/CANTable/CANTable.jsx b/frontend/src/components/CANs/CANTable/CANTable.jsx index 5ac792fffa..d6f20e233a 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.jsx +++ b/frontend/src/components/CANs/CANTable/CANTable.jsx @@ -1,10 +1,11 @@ -import _ from "lodash"; import PropTypes from "prop-types"; import React from "react"; import PaginationNav from "../../UI/PaginationNav"; -import Tooltip from "../../UI/USWDS/Tooltip"; +import CANTableHead from "./CANTableHead"; import CANTableRow from "./CANTableRow"; import styles from "./style.module.css"; +import { formatObligateBy } from "./CANTable.helpers"; + /** * CANTable component of CanList * @component @@ -16,7 +17,7 @@ import styles from "./style.module.css"; const CANTable = ({ cans }) => { const CANS_PER_PAGE = 10; const [currentPage, setCurrentPage] = React.useState(1); - let cansPerPage = _.cloneDeep(cans); + let cansPerPage = [...cans]; cansPerPage = cansPerPage.slice((currentPage - 1) * CANS_PER_PAGE, currentPage * CANS_PER_PAGE); if (cans.length === 0) { @@ -26,18 +27,18 @@ const CANTable = ({ cans }) => { return ( <> - + {cansPerPage.map((can) => ( @@ -56,67 +57,6 @@ const CANTable = ({ cans }) => { ); }; -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 ( - - - - - - - - - - - - - ); -}; - CANTable.propTypes = { cans: PropTypes.array.isRequired }; diff --git a/frontend/src/components/CANs/CANTable/CANTable.test.jsx b/frontend/src/components/CANs/CANTable/CANTable.test.jsx index ce39e3b8fb..0d77969497 100644 --- a/frontend/src/components/CANs/CANTable/CANTable.test.jsx +++ b/frontend/src/components/CANs/CANTable/CANTable.test.jsx @@ -1,9 +1,9 @@ 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"; +import { describe, expect, it, vi } from "vitest"; +import { useGetCanFundingSummaryQuery, useGetCansQuery } from "../../../api/opsAPI"; +import { cans } from "../../../tests/data"; +import CANTable from "./CANTable"; // Mock the PaginationNav component vi.mock("../../UI/PaginationNav", () => ({ diff --git a/frontend/src/components/CANs/CANTable/CANTableHead.jsx b/frontend/src/components/CANs/CANTable/CANTableHead.jsx new file mode 100644 index 0000000000..6a7455eaae --- /dev/null +++ b/frontend/src/components/CANs/CANTable/CANTableHead.jsx @@ -0,0 +1,65 @@ +import Tooltip from "../../UI/USWDS/Tooltip"; + +const CANTableHead = () => { + 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 ( + + + + + + + + + + + + + ); +}; + +export default CANTableHead; diff --git a/frontend/src/components/CANs/CANTable/CANTableRow.jsx b/frontend/src/components/CANs/CANTable/CANTableRow.jsx index fc9a2080cc..b491795ed0 100644 --- a/frontend/src/components/CANs/CANTable/CANTableRow.jsx +++ b/frontend/src/components/CANs/CANTable/CANTableRow.jsx @@ -10,27 +10,27 @@ import Tooltip from "../../UI/USWDS/Tooltip"; * CanTableRow component of CANTable * @component * @param {Object} props + * @param {number} props.activePeriod - Active Period in years + * @param {number} props.canId - CAN ID + * @param {number} props.fiscalYear - Fiscal Year + * @param {number} props.fyBudget - Fiscal Year Budget * @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.portfolio - Portfolio abbreviation * @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 = ({ + activePeriod, + canId, + fiscalYear, + fyBudget, name, nickname, - portfolio, - fiscalYear, - activePeriod, obligateBy, - transfer, - fyBudget, - canId + portfolio, + transfer }) => { const { data: fundingSummary, isError, isLoading } = useGetCanFundingSummaryQuery(canId); const availableFunds = fundingSummary?.available_funding ?? 0; @@ -93,15 +93,15 @@ const CANTableRow = ({ }; CANTableRow.propTypes = { + activePeriod: PropTypes.number.isRequired, + canId: PropTypes.number.isRequired, + fiscalYear: PropTypes.number.isRequired, + fyBudget: PropTypes.number.isRequired, 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 + portfolio: PropTypes.string.isRequired, + transfer: PropTypes.string.isRequired }; export default CANTableRow; diff --git a/frontend/src/components/CANs/CANTypes.d.ts b/frontend/src/components/CANs/CANTypes.d.ts index abdb289e9b..b86e71c5e5 100644 --- a/frontend/src/components/CANs/CANTypes.d.ts +++ b/frontend/src/components/CANs/CANTypes.d.ts @@ -5,8 +5,6 @@ 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; @@ -17,13 +15,15 @@ export type CAN = { id: number; nick_name?: string; number: string; - obligate_by?: Date; + obligate_by?: number; portfolio: Portfolio; portfolio_id: number; projects: Project[]; + updated_on: Date; + created_by: number | null; + created_by_user: number | null; updated_by: number | null; updated_by_user: number | null; - updated_on: Date; }; export type SimpleCAN = {
- CAN - - Portfolio - - FY - - Active Period - - Obligate By - - Transfer - - FY Budget - - - $ Available - -
+ CAN + + Portfolio + + FY + + Active Period + + Obligate By + + Transfer + + FY Budget + + + $ Available + +