diff --git a/containers/ecr-viewer/src/app/components/BaseFilter.tsx b/containers/ecr-viewer/src/app/components/BaseFilter.tsx
index 0fdcf0957..a90955528 100644
--- a/containers/ecr-viewer/src/app/components/BaseFilter.tsx
+++ b/containers/ecr-viewer/src/app/components/BaseFilter.tsx
@@ -6,9 +6,14 @@ import React, {
useEffect,
useRef,
} from "react";
-import { Button } from "@trussworks/react-uswds";
+import { Button, Label } from "@trussworks/react-uswds";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
-import { FILTER_CLOSED, FILTER_SUBMITTED, FilterOpenContext } from "./Filters";
+import {
+ FILTER_CLOSED,
+ FILTER_SUBMITTED,
+ FilterOpenContext,
+} from "@/app/components/Filters";
+import { toKebabCase } from "@/app/services/formatService";
/**
* Custom hook to manage query parameters in the URL (set, delete, and update). Hook always resets page back to 1.
@@ -24,8 +29,7 @@ export const useQueryParam = () => {
// Set a query param with a specific value
const setQueryParam = useCallback(
- (key: string, value: string) => {
- const params = new URLSearchParams(searchParams.toString());
+ (params: URLSearchParams, key: string, value: string) => {
params.set("page", "1");
params.set(key, value);
return params;
@@ -35,10 +39,9 @@ export const useQueryParam = () => {
// Delete a query param or a specific query param value
const deleteQueryParam = useCallback(
- (name: string, value?: string) => {
- const params = new URLSearchParams(searchParams.toString());
+ (params: URLSearchParams, key: string) => {
params.set("page", "1");
- params.delete(name, value);
+ params.delete(key);
return params;
},
[searchParams],
@@ -52,9 +55,10 @@ export const useQueryParam = () => {
// Update a specific query param (set or delete if default)
const updateQueryParam = (
+ params: URLSearchParams,
key: string,
value: string | { [key: string]: boolean },
- isDefault: boolean,
+ isDefault: boolean = false,
) => {
if (typeof value === "object") {
value = Object.keys(value)
@@ -62,14 +66,13 @@ export const useQueryParam = () => {
.join("|");
}
const updatedParams = isDefault
- ? deleteQueryParam(key)
- : setQueryParam(key, value);
+ ? deleteQueryParam(params, key)
+ : setQueryParam(params, key, value);
- // Update query params
- pushQueryUpdate(updatedParams, [key]);
+ return updatedParams;
};
- return { searchParams, updateQueryParam, pushQueryUpdate };
+ return { searchParams, deleteQueryParam, updateQueryParam, pushQueryUpdate };
};
/**
@@ -210,7 +213,6 @@ const FilterLegend = ({ type }: { type: string }) => {
const ApplyFilterButton = ({ type }: { type: string }) => {
return (
);
};
+
+/**
+ * A reusable radio button component, used for the filter by date feature.
+ * @param props - The properties for the RadioDateOption component.
+ * @param props.groupName - The name of the radio button group.
+ * @param props.option - The value of the radio option.
+ * @param props.label - The label to display next to the radio button.
+ * @param props.onChange - The callback function to handle the `onChange` event when the radio button is clicked.
+ * @param props.isChecked - Determines if the radio button is selected based on the current state.
+ * @param props.classNames - (Optional) Additional custom CSS class names to apply to the radio button wrapper.
+ * @returns The rendered RadioDateOption component.
+ */
+export const RadioDateOption = ({
+ groupName,
+ option,
+ label,
+ onChange,
+ isChecked,
+ classNames,
+}: {
+ groupName: string;
+ option: string;
+ label: string;
+ onChange: (value: string) => void;
+ isChecked: boolean;
+ classNames?: string;
+}) => {
+ return (
+
+ onChange(e.target.value)}
+ checked={isChecked}
+ />
+
+ {label}
+
+
+ );
+};
+
+/**
+ * A group of radio button components, given a set of options.
+ * @param props - The properties for the RadioDateOption component.
+ * @param props.groupName - The name of the radio button group.
+ * @param props.optionsMap - A map with each option as the key, and the corresponding labels as the value.
+ * @param props.onChange - The callback function to handle the `onChange` event when the radio button is clicked.
+ * @param props.currentOption - The option currently selected.
+ * @param props.classNames - (Optional) Additional custom CSS class names to apply to the radio button wrapper.
+ * @returns The rendered RadioDateOption component.
+ */
+export const RadioDateOptions = ({
+ groupName,
+ optionsMap,
+ onChange,
+ currentOption,
+ classNames,
+}: {
+ groupName: string;
+ optionsMap: Record;
+ onChange: (value: string) => void;
+ currentOption: string;
+ classNames?: string;
+}) => {
+ return (
+ <>
+ {Object.entries(optionsMap).map(([option, label]) => (
+
+ ))}
+ >
+ );
+};
+
+/**
+ * A custom date input component for selecting a date.
+ * @param props - The properties for the CustomDateInput component.
+ * @param props.label - The label of the custom date input component.
+ * @param props.onDateChange - The function that is caulled when the date changes.
+ * @param props.defaultValue - The default value of the date input.
+ * @param props.isRequired - Boolean indicating whether or not the date is required.
+ * @returns A JSX element containing a date input field and corresponding label.
+ */
+export const CustomDateInput = ({
+ label,
+ onDateChange,
+ defaultValue,
+ isRequired,
+}: {
+ label: string;
+ onDateChange: (date: string) => void;
+ defaultValue: string;
+ isRequired: boolean;
+}) => {
+ const today = new Date().toLocaleDateString("en-CA");
+ const id = toKebabCase(label);
+ return (
+
+
+ {label}
+
+ {
+ const date = e.target.value;
+ onDateChange(date);
+ }}
+ />
+
+ );
+};
diff --git a/containers/ecr-viewer/src/app/components/EcrTable.tsx b/containers/ecr-viewer/src/app/components/EcrTable.tsx
index ea728632d..7091cbc61 100644
--- a/containers/ecr-viewer/src/app/components/EcrTable.tsx
+++ b/containers/ecr-viewer/src/app/components/EcrTable.tsx
@@ -10,7 +10,7 @@ import { DateRangePeriod } from "@/app/view-data/utils/date-utils";
* @param props.itemsPerPage - The number of items to be displayed in the table
* @param props.sortColumn - The column to sort by
* @param props.sortDirection - The direction to sort by
- * @param props.filterDate - The date range used to filter data
+ * @param props.filterDates - The date range used to filter data
* @param props.searchTerm - The search term used to list data
* @param props.filterConditions - (Optional) The reportable condition(s) used to filter the data
* @returns - eCR Table element
@@ -22,13 +22,13 @@ const EcrTable = async ({
sortDirection,
searchTerm,
filterConditions,
- filterDate,
+ filterDates,
}: {
currentPage: number;
itemsPerPage: number;
sortColumn: string;
sortDirection: string;
- filterDate: DateRangePeriod;
+ filterDates: DateRangePeriod;
searchTerm?: string;
filterConditions?: string[];
}) => {
@@ -39,7 +39,7 @@ const EcrTable = async ({
itemsPerPage,
sortColumn,
sortDirection,
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
diff --git a/containers/ecr-viewer/src/app/components/Filters.tsx b/containers/ecr-viewer/src/app/components/Filters.tsx
index c5a220f1e..424431381 100644
--- a/containers/ecr-viewer/src/app/components/Filters.tsx
+++ b/containers/ecr-viewer/src/app/components/Filters.tsx
@@ -8,12 +8,25 @@ import React, {
useState,
} from "react";
import { Button, Icon } from "@trussworks/react-uswds";
-import { useQueryParam, Filter } from "./BaseFilter";
+import {
+ useQueryParam,
+ Filter,
+ RadioDateOption,
+ RadioDateOptions,
+ CustomDateInput,
+} from "@/app/components/BaseFilter";
import {
DEFAULT_DATE_RANGE,
DateRangeOptions,
dateRangeLabels,
} from "@/app/view-data/utils/date-utils";
+import { formatDateTime } from "@/app/services/formatService";
+
+enum ParamName {
+ Condition = "condition",
+ DateRange = "dateRange",
+ Dates = "dates",
+}
// We use a context to communicate between the overall component
// and the ` ` component to avoid prop drilling
@@ -77,7 +90,7 @@ const Filters = () => {
}
}, [filterBoxOpen]);
- const paramKeys = ["condition", "dateRange"];
+ const paramKeys = Object.values(ParamName);
const resetToDefault = () => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", "1");
@@ -124,7 +137,8 @@ const Filters = () => {
* - Updates the browser's query string when the filter is applied.
*/
const FilterReportableConditions = () => {
- const { searchParams, updateQueryParam } = useQueryParam();
+ const { searchParams, updateQueryParam, pushQueryUpdate } = useQueryParam();
+ const params = new URLSearchParams(searchParams.toString());
const [filterConditions, setFilterConditions] = useState<{
[key: string]: boolean;
}>({});
@@ -174,7 +188,7 @@ const FilterReportableConditions = () => {
);
const resetFilterConditions = (conditions: string[]) => {
- const conditionParam = searchParams.get("condition");
+ const conditionParam = searchParams.get(ParamName.Condition);
const conditionsToTrue = new Set(conditionParam?.split("|") || []);
const conditionValue = (c: string) => {
@@ -211,9 +225,15 @@ const FilterReportableConditions = () => {
(key) => filterConditions[key] === true,
).length || "0"
}
- submitHandler={() =>
- updateQueryParam("condition", filterConditions, isAllSelected)
- }
+ submitHandler={() => {
+ const updatedParams = updateQueryParam(
+ params,
+ ParamName.Condition,
+ filterConditions,
+ isAllSelected,
+ );
+ pushQueryUpdate(updatedParams, [ParamName.Condition]);
+ }}
>
{/* Select All checkbox */}
@@ -260,6 +280,7 @@ const FilterReportableConditions = () => {
))}
+
);
};
@@ -271,67 +292,124 @@ const FilterReportableConditions = () => {
* - Updates the browser's query string when the filter is applied.
*/
const FilterByDate = () => {
- const { searchParams, updateQueryParam } = useQueryParam();
-
+ const today = new Date().toLocaleDateString("en-CA");
+ const { searchParams, deleteQueryParam, updateQueryParam, pushQueryUpdate } =
+ useQueryParam();
const [filterDateOption, setFilterDateOption] =
useState(DEFAULT_DATE_RANGE);
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
const isFilterDateDefault = filterDateOption === DEFAULT_DATE_RANGE;
- const handleDateOptionChange = (
- event: React.ChangeEvent,
- ) => {
- // Set option
- const { value } = event.target;
- setFilterDateOption(value);
- };
-
const resetFilterDate = () => {
- const queryDateRange = searchParams.get("dateRange");
+ const queryDateRange = searchParams.get(ParamName.DateRange);
if (!queryDateRange) {
setFilterDateOption(DEFAULT_DATE_RANGE);
+ setStartDate("");
+ setEndDate("");
} else if (queryDateRange !== filterDateOption) {
setFilterDateOption(queryDateRange);
}
+ if (queryDateRange === "custom") {
+ const queryCustomDates = searchParams.get(ParamName.Dates) || "";
+ const [startDate, endDate] = queryCustomDates?.split("|");
+ setStartDate(startDate);
+ setEndDate(endDate);
+ }
};
// on mount, make sure the filters match the search params
useEffect(resetFilterDate, []);
+ const submitHandler = () => {
+ const params = new URLSearchParams(searchParams.toString());
+ if (filterDateOption === "custom") {
+ // TODO ANGELA: Temp: Placeholder errorhandling for when Start Date > End Date
+ if (new Date(startDate) > new Date(endDate)) {
+ alert(
+ "Invalid date range. Start date must be earlier or equal to the end date.",
+ );
+ resetFilterDate();
+ return;
+ }
+ if (!endDate) {
+ setEndDate(today);
+ }
+ const datesParam = `${startDate}|${endDate || today}`;
+ let updatedParams: URLSearchParams;
+ updatedParams = updateQueryParam(
+ params,
+ ParamName.DateRange,
+ filterDateOption,
+ isFilterDateDefault,
+ );
+ updatedParams = updateQueryParam(
+ updatedParams,
+ ParamName.Dates,
+ datesParam,
+ );
+ pushQueryUpdate(updatedParams, [ParamName.DateRange, ParamName.Dates]);
+ } else {
+ let updatedParams = updateQueryParam(
+ params,
+ ParamName.DateRange,
+ filterDateOption,
+ isFilterDateDefault,
+ );
+ updatedParams = deleteQueryParam(updatedParams, ParamName.Dates);
+ pushQueryUpdate(updatedParams, [ParamName.DateRange, ParamName.Dates]);
+ setStartDate("");
+ setEndDate("");
+ }
+ };
+
return (
- updateQueryParam("dateRange", filterDateOption, isFilterDateDefault)
+ title={
+ filterDateOption === "custom"
+ ? startDate && endDate
+ ? `From ${formatDateTime(startDate)} to ${formatDateTime(endDate)}`
+ : ""
+ : dateRangeLabels[filterDateOption as DateRangeOptions] || ""
}
+ submitHandler={submitHandler}
>
- {Object.values(DateRangeOptions).map((option) => (
-
-
+
+
+ {filterDateOption === "custom" && (
+
+
+
-
- {dateRangeLabels[option]}
-
- ))}
-
+ )}
);
diff --git a/containers/ecr-viewer/src/app/page.tsx b/containers/ecr-viewer/src/app/page.tsx
index 12ad62b15..32ee759fc 100644
--- a/containers/ecr-viewer/src/app/page.tsx
+++ b/containers/ecr-viewer/src/app/page.tsx
@@ -8,6 +8,7 @@ import NotFound from "./not-found";
import Filters from "@/app/components/Filters";
import { EcrTableLoading } from "./components/EcrTableClient";
import {
+ buildCustomDateRange,
convertDateOptionToDateRange,
DEFAULT_DATE_RANGE,
} from "@/app/view-data/utils/date-utils";
@@ -34,14 +35,25 @@ const HomePage = async ({
const searchTerm = searchParams?.search as string | undefined;
const filterConditions = searchParams?.condition as string | undefined;
const filterConditionsArr = filterConditions?.split("|");
- const filterDateRange = convertDateOptionToDateRange(
- (searchParams?.dateRange as string) || DEFAULT_DATE_RANGE,
- );
+ const filterDateRange =
+ (searchParams?.dateRange as string) || DEFAULT_DATE_RANGE;
+ let filterDates;
+
+ if (filterDateRange === "custom") {
+ const datesParam = searchParams?.dates as string | undefined;
+ if (datesParam) {
+ filterDates = buildCustomDateRange(datesParam);
+ } else {
+ filterDates = convertDateOptionToDateRange(DEFAULT_DATE_RANGE); // TODO ANGELA: Change Default for invalid or missing dates
+ }
+ } else {
+ filterDates = convertDateOptionToDateRange(filterDateRange);
+ }
let totalCount: number = 0;
if (isNonIntegratedViewer) {
totalCount = await getTotalEcrCount(
- filterDateRange,
+ filterDates,
searchTerm,
filterConditionsArr,
);
@@ -68,7 +80,7 @@ const HomePage = async ({
sortDirection={sortDirection}
searchTerm={searchTerm}
filterConditions={filterConditionsArr}
- filterDate={filterDateRange}
+ filterDates={filterDates}
/>
diff --git a/containers/ecr-viewer/src/app/services/listEcrDataService.ts b/containers/ecr-viewer/src/app/services/listEcrDataService.ts
index bd3445a60..e56c69662 100644
--- a/containers/ecr-viewer/src/app/services/listEcrDataService.ts
+++ b/containers/ecr-viewer/src/app/services/listEcrDataService.ts
@@ -45,7 +45,7 @@ export interface EcrDisplay {
* @param itemsPerPage - The number of items to fetch
* @param sortColumn - The column to sort by
* @param sortDirection - The direction to sort by
- * @param filterDate - The date (range) to filter on
+ * @param filterDates - The date (range) to filter on
* @param searchTerm - The search term to use
* @param filterConditions - The condition(s) to filter on
* @returns A promise resolving to a list of eCR metadata
@@ -55,7 +55,7 @@ export async function listEcrData(
itemsPerPage: number,
sortColumn: string,
sortDirection: string,
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise
{
@@ -68,7 +68,7 @@ export async function listEcrData(
itemsPerPage,
sortColumn,
sortDirection,
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
@@ -78,7 +78,7 @@ export async function listEcrData(
itemsPerPage,
sortColumn,
sortDirection,
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
@@ -92,7 +92,7 @@ async function listEcrDataPostgres(
itemsPerPage: number,
sortColumn: string,
sortDirection: string,
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise {
@@ -101,7 +101,7 @@ async function listEcrDataPostgres(
"SELECT ed.eICR_ID, ed.patient_name_first, ed.patient_name_last, ed.patient_birth_date, ed.date_created, ed.report_date, ed.report_date, ARRAY_AGG(DISTINCT erc.condition) AS conditions, ARRAY_AGG(DISTINCT ers.rule_summary) AS rule_summaries FROM ecr_data ed LEFT JOIN ecr_rr_conditions erc ON ed.eICR_ID = erc.eICR_ID LEFT JOIN ecr_rr_rule_summaries ers ON erc.uuid = ers.ecr_rr_conditions_id WHERE $[whereClause] GROUP BY ed.eICR_ID, ed.patient_name_first, ed.patient_name_last, ed.patient_birth_date, ed.date_created, ed.report_date $[sortStatement] OFFSET $[startIndex] ROWS FETCH NEXT $[itemsPerPage] ROWS ONLY",
{
whereClause: generateWhereStatementPostgres(
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
),
@@ -119,7 +119,7 @@ async function listEcrDataSqlserver(
itemsPerPage: number,
sortColumn: string,
sortDirection: string,
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise {
@@ -135,7 +135,7 @@ async function listEcrDataSqlserver(
sortDirection,
);
const whereStatement = generateWhereStatementSqlServer(
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
@@ -209,13 +209,13 @@ const processExtendedMetadata = (
/**
* Retrieves the total number of eCRs stored in the ecr_data table.
- * @param filterDate - The date (range) to filter on
+ * @param filterDates - The date (range) to filter on
* @param searchTerm - The search term used to filter the count query
* @param filterConditions - The array of reportable conditions used to filter the count query
* @returns A promise resolving to the total number of eCRs.
*/
export const getTotalEcrCount = async (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise => {
@@ -223,10 +223,14 @@ export const getTotalEcrCount = async (
switch (DATABASE_TYPE) {
case "postgres":
- return getTotalEcrCountPostgres(filterDate, searchTerm, filterConditions);
+ return getTotalEcrCountPostgres(
+ filterDates,
+ searchTerm,
+ filterConditions,
+ );
case "sqlserver":
return getTotalEcrCountSqlServer(
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
@@ -236,7 +240,7 @@ export const getTotalEcrCount = async (
};
const getTotalEcrCountPostgres = async (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise => {
@@ -245,7 +249,7 @@ const getTotalEcrCountPostgres = async (
"SELECT count(DISTINCT ed.eICR_ID) FROM ecr_data as ed LEFT JOIN ecr_rr_conditions erc on ed.eICR_ID = erc.eICR_ID WHERE $[whereClause]",
{
whereClause: generateWhereStatementPostgres(
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
),
@@ -255,7 +259,7 @@ const getTotalEcrCountPostgres = async (
};
const getTotalEcrCountSqlServer = async (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
): Promise => {
@@ -263,7 +267,7 @@ const getTotalEcrCountSqlServer = async (
try {
const whereStatement = generateWhereStatementSqlServer(
- filterDate,
+ filterDates,
searchTerm,
filterConditions,
);
@@ -281,13 +285,13 @@ const getTotalEcrCountSqlServer = async (
/**
* A custom type format for where statement
- * @param filterDate - The date (range) to filter on
+ * @param filterDates - The date (range) to filter on
* @param searchTerm - Optional search term used to filter
* @param filterConditions - Optional array of reportable conditions used to filter
* @returns custom type format object for use by pg-promise
*/
export const generateWhereStatementPostgres = (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
) => ({
@@ -298,7 +302,7 @@ export const generateWhereStatementPostgres = (
? generateFilterConditionsStatement(filterConditions).toPostgres()
: "NULL IS NULL";
const statementDate =
- generateFilterDateStatementPostgres(filterDate).toPostgres();
+ generateFilterDateStatementPostgres(filterDates).toPostgres();
return `(${statementSearch}) AND (${statementDate}) AND (${statementConditions})`;
},
@@ -306,13 +310,13 @@ export const generateWhereStatementPostgres = (
/**
* Generate where statement for SQL Server
- * @param filterDate - The date (range) to filter on
+ * @param filterDates - The date (range) to filter on
* @param searchTerm - Optional search term used to filter
* @param filterConditions - Optional array of reportable conditions used to filter
* @returns - where statement for SQL Server
*/
const generateWhereStatementSqlServer = (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
searchTerm?: string,
filterConditions?: string[],
) => {
@@ -320,7 +324,7 @@ const generateWhereStatementSqlServer = (
const statementConditions = filterConditions
? generateFilterConditionsStatementSqlServer(filterConditions)
: "NULL IS NULL";
- const statementDate = generateFilterDateStatementSqlServer(filterDate);
+ const statementDate = generateFilterDateStatementSqlServer(filterDates);
return `(${statementSearch}) AND (${statementDate}) AND (${statementConditions})`;
};
@@ -414,11 +418,11 @@ const generateFilterConditionsStatementSqlServer = (
/**
* A custom type format for statement filtering by date range
- * @param filterDate - Date range to filter on
+ * @param filterDates - Date range to filter on
* @returns custom type format object for use by pg-promise
*/
export const generateFilterDateStatementPostgres = (
- filterDate: DateRangePeriod,
+ filterDates: DateRangePeriod,
) => ({
rawType: true,
toPostgres: () => {
@@ -426,19 +430,19 @@ export const generateFilterDateStatementPostgres = (
return [
pgPromise.as.format("ed.date_created >= $[startDate]", {
- startDate: filterDate.startDate,
+ startDate: filterDates.startDate,
}),
pgPromise.as.format("ed.date_created <= $[endDate]", {
- endDate: filterDate.endDate,
+ endDate: filterDates.endDate,
}),
].join(" AND ");
},
});
-const generateFilterDateStatementSqlServer = (filterDate: DateRangePeriod) => {
+const generateFilterDateStatementSqlServer = (filterDates: DateRangePeriod) => {
return [
- `ed.date_created >= '${filterDate.startDate.toISOString()}'`,
- `ed.date_created <= '${filterDate.endDate.toISOString()}'`,
+ `ed.date_created >= '${filterDates.startDate.toISOString()}'`,
+ `ed.date_created <= '${filterDates.endDate.toISOString()}'`,
].join(" AND ");
};
diff --git a/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx b/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx
index 96112e2a3..f64d27c8b 100644
--- a/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx
+++ b/containers/ecr-viewer/src/app/tests/components/EcrTable.test.tsx
@@ -49,7 +49,7 @@ describe("EcrTable", () => {
itemsPerPage: 25,
sortColumn: "date_created",
sortDirection: "DESC",
- filterDate: mockDateRange,
+ filterDates: mockDateRange,
}),
);
expect(container).toMatchSnapshot();
@@ -63,7 +63,7 @@ describe("EcrTable", () => {
itemsPerPage: 25,
sortColumn: "date_created",
sortDirection: "DESC",
- filterDate: mockDateRange,
+ filterDates: mockDateRange,
}),
);
await act(async () => {
@@ -80,7 +80,7 @@ describe("EcrTable", () => {
itemsPerPage: 25,
sortColumn: "date_created",
sortDirection: "DESC",
- filterDate: mockDateRange,
+ filterDates: mockDateRange,
searchTerm: "blah",
filterConditions: ["Anthrax (disorder)"],
}),
diff --git a/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx b/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx
index 51155df07..1fcc05d93 100644
--- a/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx
+++ b/containers/ecr-viewer/src/app/tests/components/Filters.test.tsx
@@ -455,6 +455,147 @@ describe("Filter by Date Component", () => {
});
});
+describe("Filter by Date Component - custom dates", () => {
+ it("Renders correctly after opening Filter by Date box and clicking Custom date range option", async () => {
+ const mockDate = new Date("2025-01-09T13:00:00");
+ jest
+ .spyOn(global, "Date")
+ .mockImplementation(() => mockDate as unknown as Date);
+
+ const { container } = render( );
+
+ const toggleFilterButton = screen.getByRole("button", {
+ name: /Filter by Received Date/i,
+ });
+ fireEvent.click(toggleFilterButton);
+
+ await waitFor(() =>
+ screen.getByRole("radio", {
+ name: "Custom date range",
+ }),
+ );
+ const radio = screen.getByRole("radio", {
+ name: "Custom date range",
+ });
+ fireEvent.click(radio);
+
+ expect(container).toMatchSnapshot();
+
+ jest.restoreAllMocks();
+ });
+ it("Display start and end date fields when 'Custom date range' is selected", async () => {
+ const mockPush = jest.fn();
+ (useRouter as jest.Mock).mockReturnValue({ push: mockPush });
+
+ render( );
+ const toggleButton = screen.getByRole("button", {
+ name: /Filter by Received Date/i,
+ });
+ fireEvent.click(toggleButton);
+
+ await waitFor(() =>
+ screen.getByRole("radio", {
+ name: "Custom date range",
+ }),
+ );
+ const radio = screen.getByRole("radio", {
+ name: "Custom date range",
+ });
+ fireEvent.click(radio);
+
+ expect(screen.getByText("Start date")).toBeInTheDocument();
+ expect(screen.getByText("End date")).toBeInTheDocument();
+ });
+ it("Navigates with the correct query string on applying custom dates", async () => {
+ const mockPush = jest.fn();
+ (useRouter as jest.Mock).mockReturnValue({ push: mockPush });
+
+ render( );
+ const toggleButton = screen.getByRole("button", {
+ name: /Filter by Received Date/i,
+ });
+ fireEvent.click(toggleButton);
+
+ await waitFor(() =>
+ screen.getByRole("radio", {
+ name: "Custom date range",
+ }),
+ );
+ const radio = screen.getByRole("radio", {
+ name: "Custom date range",
+ });
+ fireEvent.click(radio);
+
+ const startDateInput = screen.getByTestId("start-date");
+ const endDateInput = screen.getByTestId("end-date");
+
+ fireEvent.change(startDateInput, { target: { value: "2025-01-01" } });
+ fireEvent.change(endDateInput, { target: { value: "2025-01-02" } });
+
+ const applyButton = screen.getByRole("button", { name: /Apply Filter/i });
+ fireEvent.click(applyButton);
+
+ expect(toggleButton).toHaveFocus();
+
+ // Filter by Date button title should include custom date range
+ expect(
+ screen.getByRole("button", {
+ name: /Filter by Received Date/i,
+ }),
+ ).toHaveTextContent("From 01/01/2025 to 01/02/2025");
+
+ // Should have custom date range in search param
+ expect(mockPush).toHaveBeenCalledWith(
+ expect.stringContaining("dateRange=custom"),
+ );
+ expect(mockPush).toHaveBeenCalledWith(
+ expect.stringContaining("dates=2025-01-01%7C2025-01-02"),
+ );
+ });
+ it("If no end date is given, end date defaults to today", async () => {
+ const mockDateString = "2025-01-09";
+ const mockDate = new Date("2025-01-09T13:00:00");
+ jest
+ .spyOn(global, "Date")
+ .mockImplementation(() => mockDate as unknown as Date);
+
+ const mockPush = jest.fn();
+ (useRouter as jest.Mock).mockReturnValue({ push: mockPush });
+
+ render( );
+ const toggleButton = screen.getByRole("button", {
+ name: /Filter by Received Date/i,
+ });
+ fireEvent.click(toggleButton);
+
+ await waitFor(() =>
+ screen.getByRole("radio", {
+ name: "Custom date range",
+ }),
+ );
+ const radio = screen.getByRole("radio", {
+ name: "Custom date range",
+ });
+ fireEvent.click(radio);
+
+ const startDateInput = screen.getByTestId("start-date");
+ fireEvent.change(startDateInput, { target: { value: "2025-01-01" } });
+
+ const applyButton = screen.getByRole("button", {
+ name: /Apply Filter/i,
+ });
+ fireEvent.click(applyButton);
+
+ // Should have custom date range in search param
+ expect(mockPush).toHaveBeenCalledWith(
+ expect.stringContaining("dateRange=custom"),
+ );
+ expect(mockPush).toHaveBeenCalledWith(
+ expect.stringContaining(`dates=2025-01-01%7C${mockDateString}`),
+ );
+ });
+});
+
describe("Filter Opening/Closing Controls", () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -669,7 +810,9 @@ describe("Reset button", () => {
fireEvent.click(radio);
expect(radio).toBeChecked();
- fireEvent.submit(radio);
+ const applyButton = screen.getByRole("button", { name: /Apply Filter/i });
+ fireEvent.click(applyButton);
+
await waitFor(() => screen.findByText("Last 7 days"));
// should be closed
diff --git a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap
index f19680ceb..911f0e6bf 100644
--- a/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap
+++ b/containers/ecr-viewer/src/app/tests/components/__snapshots__/Filters.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Filter by Date Component Renders correctly after opening Filter by Date box 1`] = `
+exports[`Filter by Date Component - custom dates Renders correctly after opening Filter by Date box and clicking Custom date range option 1`] = `
- Last year
+ Received Date
@@ -84,12 +84,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -101,12 +101,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -118,12 +118,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -135,12 +135,12 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -152,13 +152,13 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
@@ -170,15 +170,326 @@ exports[`Filter by Date Component Renders correctly after opening Filter by Date
+
+
+
+ Custom date range
+
+
+
+
+ Apply Filter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reportable Condition
+
+
+ 2
+
+
+
+
+
+
+
+`;
+
+exports[`Filter by Date Component Renders correctly after opening Filter by Date box 1`] = `
+
+
+
+
+
+ FILTERS:
+
+
+
+
+
+
+
+
+
+
+ Last year
+
+
+
+
-
{
expect(endDate).toEqual(leapYearDay);
});
});
+
+describe("buildCustomDateRange", () => {
+ it("should return the correct start and end date given the dates string", () => {
+ const dates = "2025-01-10|2025-01-11";
+ const expectedCustomDates = {
+ startDate: new Date("2025-01-10T00:00:00"),
+ endDate: new Date("2025-01-11T23:59:59.999"),
+ };
+
+ const result = buildCustomDateRange(dates);
+ expect(result).toEqual(expectedCustomDates);
+ });
+});
diff --git a/containers/ecr-viewer/src/app/view-data/utils/date-utils.ts b/containers/ecr-viewer/src/app/view-data/utils/date-utils.ts
index 87ed766f3..d2a33056a 100644
--- a/containers/ecr-viewer/src/app/view-data/utils/date-utils.ts
+++ b/containers/ecr-viewer/src/app/view-data/utils/date-utils.ts
@@ -106,3 +106,23 @@ export function convertDateOptionToDateRange(
throw new Error("Invalid filter option");
}
}
+
+/**
+ * Builds object with start and end dates (as Date objects) from custom date range string.
+ * @param datesString - A string representing the date range in the format "YYYY-MM-DD|YYYY-MM-DD" (start_date|end_date).
+ * @returns An object containing the `startDate` and `endDate` as Date objects.
+ * The `startDate` is set to the start of the day (00:00:00.000),
+ * and the `endDate` is set to the end of the day (23:59:59.999).
+ */
+export function buildCustomDateRange(datesString: string) {
+ const [startDate, endDate] = datesString.split("|").map((date) => {
+ // Split the input and parse the date as local time
+ const [year, month, day] = date.split("-"); // YYYY-MM-DD
+ const localDate = new Date();
+ localDate.setFullYear(parseInt(year), parseInt(month) - 1, parseInt(day));
+ return localDate;
+ });
+ startDate.setHours(0, 0, 0, 0);
+ endDate.setHours(23, 59, 59, 999);
+ return { startDate: startDate, endDate: endDate };
+}