diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 1d41be75aa..d476f78e5c 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -7,7 +7,8 @@ WORKDIR /home/app
COPY --chown=app:app ./package.json ./bun.lockb /home/app/
-RUN bun install --production --frozen-lockfile
+# RUN bun install --production --frozen-lockfile
+RUN bun install
COPY --chown=app:app index.html /home/app/
COPY --chown=app:app src /home/app/src
diff --git a/frontend/bun.lockb b/frontend/bun.lockb
index f5f30a04cc..10926b48c8 100755
Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ
diff --git a/frontend/package.json b/frontend/package.json
index 66c27507bc..e734387977 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,111 +1,113 @@
{
- "name": "opre-ops",
- "version": "1.0.1",
- "license": "CC0-1.0",
- "private": true,
- "type": "module",
- "dependencies": {
- "@azure/storage-blob": "^12.24.0",
- "@fortawesome/fontawesome-svg-core": "6.5.2",
- "@fortawesome/free-regular-svg-icons": "6.5.2",
- "@fortawesome/free-solid-svg-icons": "6.5.2",
- "@fortawesome/react-fontawesome": "0.2.2",
- "@nivo/bar": "0.87.0",
- "@nivo/core": "0.87.0",
- "@nivo/pie": "0.87.0",
- "@reduxjs/toolkit": "2.2.8",
- "@uswds/uswds": "3.9.0",
- "axios": "1.7.7",
- "clsx": "2.1.1",
- "crypto-random-string": "5.0.0",
- "js-cookie": "3.0.5",
- "jwt-decode": "4.0.0",
- "lodash": "4.17.21",
- "react": "18.3.1",
- "react-currency-format": "1.1.0",
- "react-dom": "18.3.1",
- "react-markdown": "9.0.1",
- "react-modal": "3.16.1",
- "react-redux": "9.1.2",
- "react-router-dom": "6.27.0",
- "react-select": "5.8.1",
- "sass": "1.79.6",
- "sass-loader": "14.2.1",
- "vest": "5.4.3",
- "@eslint/compat": "1.2.0",
- "@eslint/js": "9.12.0",
- "@vitejs/plugin-react": "4.3.2",
- "eslint": "9.12.0",
- "eslint-config-prettier": "9.1.0",
- "eslint-plugin-cypress": "4.0.0",
- "eslint-plugin-import": "2.29.1",
- "eslint-plugin-jest": "28.8.3",
- "eslint-plugin-jsx-a11y": "6.10.0",
- "eslint-plugin-prettier": "5.2.1",
- "eslint-plugin-react": "7.37.1",
- "eslint-plugin-react-hooks": "5.0.0",
- "eslint-plugin-react-refresh": "0.4.12",
- "eslint-plugin-testing-library": "6.3.2",
- "jose": "5.9.4",
- "jsdom": "25.0.1",
- "vite": "5.4.9",
- "vite-jsconfig-paths": "2.0.1",
- "vite-plugin-babel-macros": "1.0.6",
- "vite-plugin-eslint": "1.8.1",
- "vite-plugin-svgr": "4.2.0"
- },
- "overrides": {
- "rollup": "4.24.0"
- },
- "devDependencies": {
- "@testing-library/jest-dom": "6.5.0",
- "@testing-library/react": "16.0.1",
- "@testing-library/user-event": "14.5.2",
- "@types/testing-library__jest-dom": "6.0.0",
- "@types/testing-library__react": "10.2.0",
- "@vitest/coverage-istanbul": "2.1.3",
- "@vitest/ui": "2.1.3",
- "axe-core": "4.10.1",
- "cypress": "13.13.3",
- "cypress-axe": "1.5.0",
- "cypress-localstorage-commands": "2.2.6",
- "globals": "15.9.0",
- "history": "5.3.0",
- "msw": "2.3.5",
- "prettier": "3.3.3",
- "redux-mock-store": "1.5.4",
- "@uswds/compile": "1.2.0",
- "vitest": "2.1.3"
- },
- "scripts": {
- "start": "vite",
- "start:debug": "vite --inspect=0.0.0.0:9229",
- "build": "vite build",
- "test": "vitest",
- "test:coverage": "vitest --coverage",
- "test:ui": "vitest --ui --coverage.enabled=true",
- "test:e2e:interactive": "cypress open --config-file ./cypress.config.js",
- "test:e2e": "cypress run --config-file ./cypress.config.js --headless",
- "test:e2e:debug": "DEBUG=cypress:* cypress run --config-file ./cypress.config.js --headless",
- "lint": "eslint './src/**'",
- "cypress:open": "cypress open",
- "uswds:update": "bunx gulp compile"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- },
- "babelMacros": {
- "fontawesome-svg-core": {
- "license": "free"
+ "name": "opre-ops",
+ "version": "1.0.1",
+ "license": "CC0-1.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@azure/storage-blob": "12.24.0",
+ "@eslint/compat": "1.2.0",
+ "@eslint/js": "9.12.0",
+ "@fortawesome/fontawesome-svg-core": "6.5.2",
+ "@fortawesome/free-regular-svg-icons": "6.5.2",
+ "@fortawesome/free-solid-svg-icons": "6.5.2",
+ "@fortawesome/react-fontawesome": "0.2.2",
+ "@nivo/bar": "0.87.0",
+ "@nivo/core": "0.87.0",
+ "@nivo/pie": "0.87.0",
+ "@reduxjs/toolkit": "2.2.8",
+ "@uswds/uswds": "3.9.0",
+ "@vitejs/plugin-react": "4.3.2",
+ "axios": "1.7.7",
+ "clsx": "2.1.1",
+ "crypto-random-string": "5.0.0",
+ "eslint": "9.12.0",
+ "eslint-config-prettier": "9.1.0",
+ "eslint-plugin-cypress": "4.0.0",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-jest": "28.8.3",
+ "eslint-plugin-jsx-a11y": "6.10.0",
+ "eslint-plugin-prettier": "5.2.1",
+ "eslint-plugin-react": "7.37.1",
+ "eslint-plugin-react-hooks": "5.0.0",
+ "eslint-plugin-react-refresh": "0.4.12",
+ "eslint-plugin-testing-library": "6.3.2",
+ "jose": "5.9.4",
+ "js-cookie": "3.0.5",
+ "jsdom": "25.0.1",
+ "jwt-decode": "4.0.0",
+ "lodash": "4.17.21",
+ "react": "18.3.1",
+ "react-currency-format": "1.1.0",
+ "react-dom": "18.3.1",
+ "react-markdown": "9.0.1",
+ "react-modal": "3.16.1",
+ "react-redux": "9.1.2",
+ "react-router-dom": "6.27.0",
+ "react-select": "5.8.1",
+ "react-slider": "2.0.6",
+ "sass": "1.79.6",
+ "sass-loader": "14.2.1",
+ "styled-components": "6.1.13",
+ "vest": "5.4.3",
+ "vite": "5.4.9",
+ "vite-jsconfig-paths": "2.0.1",
+ "vite-plugin-babel-macros": "1.0.6",
+ "vite-plugin-eslint": "1.8.1",
+ "vite-plugin-svgr": "4.2.0"
+ },
+ "overrides": {
+ "rollup": "4.24.0"
+ },
+ "devDependencies": {
+ "@testing-library/jest-dom": "6.5.0",
+ "@testing-library/react": "16.0.1",
+ "@testing-library/user-event": "14.5.2",
+ "@types/testing-library__jest-dom": "6.0.0",
+ "@types/testing-library__react": "10.2.0",
+ "@vitest/coverage-istanbul": "2.1.3",
+ "@vitest/ui": "2.1.3",
+ "axe-core": "4.10.1",
+ "cypress": "13.13.3",
+ "cypress-axe": "1.5.0",
+ "cypress-localstorage-commands": "2.2.6",
+ "globals": "15.9.0",
+ "history": "5.3.0",
+ "msw": "2.3.5",
+ "prettier": "3.3.3",
+ "redux-mock-store": "1.5.4",
+ "@uswds/compile": "1.2.0",
+ "vitest": "2.1.3"
+ },
+ "scripts": {
+ "start": "vite",
+ "start:debug": "vite --inspect=0.0.0.0:9229",
+ "build": "vite build",
+ "test": "vitest",
+ "test:coverage": "vitest --coverage",
+ "test:ui": "vitest --ui --coverage.enabled=true",
+ "test:e2e:interactive": "cypress open --config-file ./cypress.config.js",
+ "test:e2e": "cypress run --config-file ./cypress.config.js --headless",
+ "test:e2e:debug": "DEBUG=cypress:* cypress run --config-file ./cypress.config.js --headless",
+ "lint": "eslint './src/**'",
+ "cypress:open": "cypress open",
+ "uswds:update": "bunx gulp compile"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "babelMacros": {
+ "fontawesome-svg-core": {
+ "license": "free"
+ }
}
- }
}
diff --git a/frontend/src/api/opsAPI.js b/frontend/src/api/opsAPI.js
index 6bc78b60bd..ac4c02a2f6 100644
--- a/frontend/src/api/opsAPI.js
+++ b/frontend/src/api/opsAPI.js
@@ -24,7 +24,8 @@ export const opsApi = createApi({
"ServicesComponents",
"ChangeRequests",
"Divisions",
- "Documents"
+ "Documents",
+ "Cans"
],
baseQuery: fetchBaseQuery({
baseUrl: `${BACKEND_DOMAIN}/api/v1/`,
diff --git a/frontend/src/components/CANs/CANFYBudgetRangeSlider/CANFYBudgetRangeSlider.jsx b/frontend/src/components/CANs/CANFYBudgetRangeSlider/CANFYBudgetRangeSlider.jsx
new file mode 100644
index 0000000000..9b409b5e13
--- /dev/null
+++ b/frontend/src/components/CANs/CANFYBudgetRangeSlider/CANFYBudgetRangeSlider.jsx
@@ -0,0 +1,104 @@
+import React from "react";
+import DoubleRangeSlider from "../../UI/DoubleRangeSlider";
+import CurrencyFormat from "react-currency-format";
+/**
+ * @typedef {Object} CANFYBudgetRangeSliderProps
+ * @property {[number, number]} fyBudgetRange - The min and max of the fiscal year budget range
+ * @property {string} [legendClassname] - CSS class for the legend
+ * @property {[number, number]} budget - The current budget range
+ * @property {function([number, number]): void} setBudget - Function to update the budget
+ */
+
+/**
+ * @description CANFYBudgetRangeSlider component
+ * @component
+ * @param {CANFYBudgetRangeSliderProps} props
+ * @returns {JSX.Element} - The CAN FY Budget Range Slider component
+ */
+const CANFYBudgetRangeSlider = ({ fyBudgetRange, legendClassname = "usa-label margin-top-0", budget, setBudget }) => {
+ const [minValue, maxValue] = budget;
+ const [fyBudgetMin, fyBudgetMax] = fyBudgetRange;
+ const [sliderValue, setSliderValue] = React.useState([0, 100]);
+ /**
+ * Calculate percentage of a value within a range
+ * @param {number} value - The value to calculate percentage for
+ * @param {number} min - The minimum value of the range
+ * @param {number} max - The maximum value of the range
+ * @returns {number} The calculated percentage
+ */
+ const calculatePercentage = (value, min, max) => {
+ return ((value - min) / (max - min)) * 100;
+ };
+ /**
+ * Calculate value based on percentage within a range
+ * @param {number} percentage - The percentage to calculate value for
+ * @param {number} min - The minimum value of the range
+ * @param {number} max - The maximum value of the range
+ * @returns {number} The calculated value
+ */
+ const calculateValue = (percentage, min, max) => {
+ return Math.round(min + (percentage / 100) * (max - min));
+ };
+
+ /**
+ * Calculate the new budget range based on slider values
+ * @param {[number, number]} newRange - The new range from the slider
+ */
+ const calculateBudgetRange = (newRange) => {
+ const [minPercentage, maxPercentage] = newRange;
+ const selectedMinFYBudget = calculateValue(minPercentage, fyBudgetMin, fyBudgetMax);
+ const selectedMaxFYBudget = calculateValue(maxPercentage, fyBudgetMin, fyBudgetMax);
+
+ setBudget([selectedMinFYBudget, selectedMaxFYBudget]);
+ setSliderValue(newRange);
+ };
+
+ React.useEffect(() => {
+ const minPercentage = calculatePercentage(minValue, fyBudgetMin, fyBudgetMax);
+ const maxPercentage = calculatePercentage(maxValue, fyBudgetMin, fyBudgetMax);
+
+ setSliderValue([minPercentage, maxPercentage]);
+ }, [budget, fyBudgetRange]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ >
+ );
+};
+
+export default CANFYBudgetRangeSlider;
diff --git a/frontend/src/components/CANs/CANFYBudgetRangeSlider/index.js b/frontend/src/components/CANs/CANFYBudgetRangeSlider/index.js
new file mode 100644
index 0000000000..608bcedba0
--- /dev/null
+++ b/frontend/src/components/CANs/CANFYBudgetRangeSlider/index.js
@@ -0,0 +1 @@
+export { default } from "./CANFYBudgetRangeSlider.jsx";
diff --git a/frontend/src/components/CANs/CANTable/CANTable.jsx b/frontend/src/components/CANs/CANTable/CANTable.jsx
index 36a3e7180b..6963ee0663 100644
--- a/frontend/src/components/CANs/CANTable/CANTable.jsx
+++ b/frontend/src/components/CANs/CANTable/CANTable.jsx
@@ -16,15 +16,10 @@ import styles from "./style.module.css";
* @returns {JSX.Element}
*/
const CANTable = ({ cans, fiscalYear }) => {
- // Filter CANs by fiscal year
- const filteredCANsByFiscalYear = React.useMemo(() => {
- if (!fiscalYear) return cans;
- return cans.filter((can) => can.funding_details.fiscal_year === fiscalYear);
- }, [cans, fiscalYear]);
// TODO: once in prod, change this to 25
const CANS_PER_PAGE = 10;
const [currentPage, setCurrentPage] = React.useState(1);
- let cansPerPage = [...filteredCANsByFiscalYear];
+ let cansPerPage = [...cans];
cansPerPage = cansPerPage.slice((currentPage - 1) * CANS_PER_PAGE, currentPage * CANS_PER_PAGE);
if (cansPerPage.length === 0) {
@@ -52,7 +47,7 @@ const CANTable = ({ cans, fiscalYear }) => {
))}
- {filteredCANsByFiscalYear.length > CANS_PER_PAGE && (
+ {cans.length > CANS_PER_PAGE && (
{displayActivePeriod(activePeriod)}
{obligateBy} |
{convertCodeForDisplay("methodOfTransfer", transfer)} |
-
-
- |
+ {fyBudget === 0 ? (
+ TBD |
+ ) : (
+
+
+ |
+ )}
(
+
+);
+const StyledSlider = styled(ReactSlider)`
+ width: 100%;
+ height: 25px;
+ z-index: 0;
+`;
+
+const Thumb = ({ key, ...props }) => (
+
+);
+
+const Track = ({ key, ...props }) => (
+
+);
+
+const StyledTrack = styled.div`
+ top: 5px;
+ bottom: 0;
+ background: white;
+ border-radius: 999px;
+ border: 1px solid black;
+`;
+
+const StyledThumb = styled.div`
+ height: 30px;
+ line-height: 30px;
+ width: 30px;
+ background-color: whitesmoke;
+ border: 2px solid gray;
+ border-radius: 50%;
+ cursor: grab;
+`;
+export default DoubleRangeSlider;
diff --git a/frontend/src/components/UI/DoubleRangeSlider/index.js b/frontend/src/components/UI/DoubleRangeSlider/index.js
new file mode 100644
index 0000000000..f6ff31b58f
--- /dev/null
+++ b/frontend/src/components/UI/DoubleRangeSlider/index.js
@@ -0,0 +1 @@
+export { default } from "./DoubleRangeSlider";
diff --git a/frontend/src/components/UI/FilterButton/FilterButton.jsx b/frontend/src/components/UI/FilterButton/FilterButton.jsx
index 974368e5e5..2446bca39e 100644
--- a/frontend/src/components/UI/FilterButton/FilterButton.jsx
+++ b/frontend/src/components/UI/FilterButton/FilterButton.jsx
@@ -9,9 +9,10 @@ import customStyles from "./FilterButton.module.css";
* @param {Function} props.applyFilter - A function to call after clicking the Apply button.
* @param {Function} props.resetFilter - A function to call after clicking the Reset button.
* @param {Object []} props.fieldsetList - An array of fieldsets to display in the modal.
+ * @param {boolean} props.disabled - Whether the button is disabled.
* @returns {JSX.Element} - The procurement shop select element.
*/
-export const FilterButton = ({ applyFilter, resetFilter, fieldsetList }) => {
+export const FilterButton = ({ applyFilter, resetFilter, fieldsetList, disabled = false }) => {
const [showModal, setShowModal] = React.useState(false);
const handleApplyFilter = () => {
@@ -35,10 +36,11 @@ export const FilterButton = ({ applyFilter, resetFilter, fieldsetList }) => {
!showModal ? "usa-button--outline text-primary" : "bg-primary-darker"
} display-flex flex-align-center margin-right-0 ${customStyles.filterButton}`}
onClick={() => (showModal ? setShowModal(false) : setShowModal(true))}
+ disabled={disabled}
>
diff --git a/frontend/src/pages/cans/list/CANFilterButton/CANFiilterButton.jsx b/frontend/src/pages/cans/list/CANFilterButton/CANFiilterButton.jsx
index be34469d18..67ad39dd8f 100644
--- a/frontend/src/pages/cans/list/CANFilterButton/CANFiilterButton.jsx
+++ b/frontend/src/pages/cans/list/CANFilterButton/CANFiilterButton.jsx
@@ -4,6 +4,7 @@ import CANPortfolioComboBox from "../../../../components/CANs/CANPortfolioComboB
import CANTransferComboBox from "../../../../components/CANs/CANTransferComboBox";
import FilterButton from "../../../../components/UI/FilterButton";
import useCANFilterButton from "./CANFilterButton.hooks";
+import CANFYBudgetRangeSlider from "../../../../components/CANs/CANFYBudgetRangeSlider";
/**
* @typedef {import('./CANFilterTypes').FilterOption} FilterOption
@@ -14,11 +15,23 @@ import useCANFilterButton from "./CANFilterButton.hooks";
* @param {import ('./CANFilterTypes').Filters} props.filters - The current filters.
* @param {Function} props.setFilters - A function to call to set the filters.
* @param {FilterOption[]} props.portfolioOptions - The portfolio options.
+ * @param {[number, number]} props.fyBudgetRange - The fiscal year budget range.
+ * @param {boolean} props.disabled - Whether the button is disabled.
* @returns {JSX.Element} - The CAN filter button.
*/
-export const CANFilterButton = ({ filters, setFilters, portfolioOptions }) => {
- const { activePeriod, setActivePeriod, transfer, setTransfer, portfolio, setPortfolio, applyFilter, resetFilter } =
- useCANFilterButton(filters, setFilters);
+export const CANFilterButton = ({ filters, setFilters, portfolioOptions, fyBudgetRange, disabled }) => {
+ const {
+ activePeriod,
+ setActivePeriod,
+ transfer,
+ setTransfer,
+ portfolio,
+ setPortfolio,
+ budget,
+ setBudget,
+ applyFilter,
+ resetFilter
+ } = useCANFilterButton(filters, setFilters, fyBudgetRange);
const fieldStyles = "usa-fieldset margin-bottom-205";
const legendStyles = "usa-legend font-sans-3xs margin-top-0 padding-bottom-1 text-base-dark";
@@ -56,6 +69,17 @@ export const CANFilterButton = ({ filters, setFilters, portfolioOptions }) => {
legendClassname={legendStyles}
overrideStyles={{ width: "22.7rem" }}
/>
+ ,
+
];
@@ -66,6 +90,7 @@ export const CANFilterButton = ({ filters, setFilters, portfolioOptions }) => {
applyFilter={applyFilter}
resetFilter={resetFilter}
fieldsetList={fieldsetList}
+ disabled={disabled}
/>
);
};
diff --git a/frontend/src/pages/cans/list/CANFilterButton/CANFilterButton.hooks.js b/frontend/src/pages/cans/list/CANFilterButton/CANFilterButton.hooks.js
index 27d3269931..e30fdf99be 100644
--- a/frontend/src/pages/cans/list/CANFilterButton/CANFilterButton.hooks.js
+++ b/frontend/src/pages/cans/list/CANFilterButton/CANFilterButton.hooks.js
@@ -3,12 +3,14 @@ import React from "react";
/**
* A filter for CANs list.
* @param {import ('./CANFilterTypes').Filters} filters - The current filters.
+ * @param{[number, number]} fyBudgetRange - The fiscal year budget range.
* @param {Function} setFilters - A function to call to set the filters.
*/
-export const useCANFilterButton = (filters, setFilters) => {
+export const useCANFilterButton = (filters, setFilters, fyBudgetRange) => {
const [activePeriod, setActivePeriod] = React.useState([]);
const [transfer, setTransfer] = React.useState([]);
const [portfolio, setPortfolio] = React.useState([]);
+ const [budget, setBudget] = React.useState([]);
// The useEffect() hook calls below are used to set the state appropriately when the filter tags (X) are clicked.
React.useEffect(() => {
@@ -29,13 +31,23 @@ export const useCANFilterButton = (filters, setFilters) => {
}
}, [filters.portfolio]);
+ React.useEffect(() => {
+ if (fyBudgetRange !== undefined) {
+ setBudget(fyBudgetRange);
+ }
+ if (filters.budget && Array.isArray(filters.budget) && filters.budget.length === 2) {
+ setBudget([filters.budget[0], filters.budget[1]]);
+ }
+ }, [fyBudgetRange, filters.budget]);
+
const applyFilter = () => {
setFilters((prevState) => {
return {
...prevState,
activePeriod: activePeriod,
transfer: transfer,
- portfolio: portfolio
+ portfolio: portfolio,
+ budget: budget
};
});
};
@@ -43,11 +55,9 @@ export const useCANFilterButton = (filters, setFilters) => {
setFilters({
activePeriod: [],
transfer: [],
- portfolio: []
+ portfolio: [],
+ budget: []
});
- setActivePeriod([]);
- setTransfer([]);
- setPortfolio([]);
};
return {
@@ -57,6 +67,8 @@ export const useCANFilterButton = (filters, setFilters) => {
setTransfer,
portfolio,
setPortfolio,
+ budget,
+ setBudget,
applyFilter,
resetFilter
};
diff --git a/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.hooks.js b/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.hooks.js
index d7432c1119..40191c8952 100644
--- a/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.hooks.js
+++ b/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.hooks.js
@@ -9,6 +9,7 @@ import { useState, useEffect, useCallback } from "react";
* @property {FilterItem[]} activePeriod
* @property {FilterItem[]} portfolio
* @property {FilterItem[]} transfer
+ * @property {[number, number]} budget
*/
/**
@@ -20,9 +21,10 @@ import { useState, useEffect, useCallback } from "react";
/**
* Custom hook for managing tags list
* @param {Filters} filters
+ * @param {[number, number]} fyBudgetRange
* @returns {Tag[]}
*/
-export const useTagsList = (filters) => {
+export const useTagsList = (filters, fyBudgetRange) => {
const [tagsList, setTagsList] = useState([]);
/**
@@ -31,16 +33,26 @@ export const useTagsList = (filters) => {
*/
const updateTags = useCallback(
(filterKey, filterName) => {
- if (!Array.isArray(filters[filterKey])) return;
+ if (filterKey === "budget") {
+ if (Array.isArray(filters.budget) && filters.budget.length === 2) {
+ const [min, max] = filters.budget;
+ setTagsList((prevState) => [
+ ...prevState.filter((t) => t.filter !== filterName),
+ { tagText: `$${min} - $${max}`, filter: filterName }
+ ]);
+ } else {
+ setTagsList((prevState) => prevState.filter((t) => t.filter !== filterName));
+ }
+ } else if (Array.isArray(filters[filterKey])) {
+ const selectedTags = filters[filterKey].map((item) => ({
+ tagText: item.title,
+ filter: filterName
+ }));
- const selectedTags = filters[filterKey].map((item) => ({
- tagText: item.title,
- filter: filterName
- }));
-
- setTagsList((prevState) => [...prevState.filter((t) => t.filter !== filterName), ...selectedTags]);
+ setTagsList((prevState) => [...prevState.filter((t) => t.filter !== filterName), ...selectedTags]);
+ }
},
- [filters]
+ [filters, fyBudgetRange]
);
useEffect(() => {
@@ -55,6 +67,10 @@ export const useTagsList = (filters) => {
updateTags("transfer", "transfer");
}, [filters.transfer, updateTags]);
+ useEffect(() => {
+ updateTags("budget", "budget");
+ }, [filters.budget, updateTags]);
+
return tagsList;
};
@@ -83,6 +99,12 @@ export const removeFilter = (tag, setFilters) => {
transfer: prevState.transfer.filter((transfer) => transfer.title !== tag.tagText)
}));
break;
+ case "budget":
+ setFilters((prevState) => ({
+ ...prevState,
+ budget: []
+ }));
+ break;
default:
console.warn(`Unknown filter type: ${tag.filter}`);
}
diff --git a/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.jsx b/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.jsx
index 821bfcb849..35765e403d 100644
--- a/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.jsx
+++ b/frontend/src/pages/cans/list/CANFilterTags/CANFilterTags.jsx
@@ -8,15 +8,14 @@ import { useTagsList, removeFilter } from "./CANFilterTags.hooks";
* @param {Object} props - The component props.
* @param {import('./CANFilterTags.hooks').Filters} props.filters - The current filters.
* @param {() => void} props.setFilters - A function to call to set the filters.
+ * @param {[number, number]} props.fyBudgetRange - The initial budget range.
* @returns {JSX.Element|null} The filter tags component or null if no tags.
*/
-export const CANFilterTags = ({ filters, setFilters }) => {
- const tagsList = useTagsList(filters);
+const CANFilterTags = ({ filters, setFilters, fyBudgetRange }) => {
+ const tagsList = useTagsList(filters, fyBudgetRange);
const tagsListByFilter = _.groupBy(tagsList, "filter");
- const tagsListByFilterMerged = Object.values(tagsListByFilter)
- .flat()
- .sort((a, b) => a.tagText.localeCompare(b.tagText));
+ const tagsListByFilterMerged = Object.values(tagsListByFilter).flat();
if (tagsList.length === 0) {
return null;
diff --git a/frontend/src/pages/cans/list/CanList.helpers.js b/frontend/src/pages/cans/list/CanList.helpers.js
index 74c1b7534a..29b5244983 100644
--- a/frontend/src/pages/cans/list/CanList.helpers.js
+++ b/frontend/src/pages/cans/list/CanList.helpers.js
@@ -72,6 +72,7 @@ const sortCANs = (cans) => {
*/
const applyAdditionalFilters = (cans, filters) => {
let filteredCANs = cans;
+ console.log({ filters, cans });
// Filter by active period
if (filters.activePeriod && filters.activePeriod.length > 0) {
@@ -94,6 +95,15 @@ const applyAdditionalFilters = (cans, filters) => {
)
);
}
+
+ if (filters.budget && filters.budget.length > 0) {
+ filteredCANs = filteredCANs.filter((can) => {
+ return can.funding_budgets.some((budget) => {
+ return budget.budget >= filters.budget[0] && budget.budget <= filters.budget[1];
+ });
+ });
+ }
+
// TODO: Add other filters here
// Example:
// if (filters.someOtherFilter && filters.someOtherFilter.length > 0) {
@@ -126,3 +136,18 @@ export const getPortfolioOptions = (cans) => {
title: portfolio
}));
};
+
+export const getSortedFYBudgets = (cans) => {
+ if (!cans || cans.length === 0) {
+ return [];
+ }
+
+ const funding_budgets = cans.reduce((acc, can) => {
+ acc.add(can.funding_budgets);
+ return acc;
+ }, new Set());
+
+ return Array.from(funding_budgets)
+ .flatMap((itemArray) => itemArray.map((item) => item.budget))
+ .sort((a, b) => a - b);
+};
diff --git a/frontend/src/pages/cans/list/CanList.jsx b/frontend/src/pages/cans/list/CanList.jsx
index f12e1e5964..0cf8dd700b 100644
--- a/frontend/src/pages/cans/list/CanList.jsx
+++ b/frontend/src/pages/cans/list/CanList.jsx
@@ -10,8 +10,8 @@ import FiscalYear from "../../../components/UI/FiscalYear";
import { setSelectedFiscalYear } from "../../../pages/cans/detail/canDetailSlice";
import ErrorPage from "../../ErrorPage";
import CANFilterButton from "./CANFilterButton";
+import { sortAndFilterCANs, getPortfolioOptions, getSortedFYBudgets } from "./CanList.helpers";
import CANFilterTags from "./CANFilterTags";
-import { getPortfolioOptions, sortAndFilterCANs } from "./CanList.helpers";
/**
* Page for the CAN List.
@@ -29,10 +29,17 @@ const CanList = () => {
const [filters, setFilters] = React.useState({
activePeriod: [],
transfer: [],
- portfolio: []
+ portfolio: [],
+ budget: []
});
- const sortedCANs = sortAndFilterCANs(canList, myCANsUrl, activeUser, filters) || [];
+ const filteredCANsByFiscalYear = React.useMemo(() => {
+ if (!fiscalYear || !canList) return [];
+ return canList.filter((can) => can.funding_details.fiscal_year === fiscalYear);
+ }, [canList, fiscalYear]);
+ const sortedCANs = sortAndFilterCANs(filteredCANsByFiscalYear, myCANsUrl, activeUser, filters) || [];
const portfolioOptions = getPortfolioOptions(canList);
+ const sortedFYBudgets = getSortedFYBudgets(filteredCANsByFiscalYear);
+ const [minFYBudget, maxFYBudget] = [sortedFYBudgets[0], sortedFYBudgets[sortedFYBudgets.length - 1]];
if (isLoading) {
return (
@@ -44,7 +51,6 @@ const CanList = () => {
if (isError) {
return ;
}
-
const CANFiscalYearSelect = () => {
return (
{
filters={filters}
setFilters={setFilters}
portfolioOptions={portfolioOptions}
+ fyBudgetRange={[minFYBudget, maxFYBudget]}
+ disabled={sortedCANs.length === 0}
/>
}
FYSelect={}
@@ -84,6 +92,7 @@ const CanList = () => {
}
/>
|