diff --git a/src/components/CustomDateInput/DateInput.tsx b/src/components/CustomDateInput/DateInput.tsx index 1c7060c83..2103ea7d8 100644 --- a/src/components/CustomDateInput/DateInput.tsx +++ b/src/components/CustomDateInput/DateInput.tsx @@ -1,5 +1,5 @@ import { Dispatch } from "react" -import { SerializedCourtDateRange } from "types/CaseListQueryParams" +import { SerializedDateRange } from "types/CaseListQueryParams" import { FilterAction } from "types/CourtCaseFilter" import { SmallButton } from "./DateInput.styles" @@ -7,7 +7,7 @@ interface Props { dateType: "from" | "to" dispatch: Dispatch value: string - dateRange: SerializedCourtDateRange | undefined + dateRange: SerializedDateRange | undefined } const DateInput: React.FC = ({ dateType, dispatch, value, dateRange }: Props) => { diff --git a/src/components/SearchFilters/CourtDateFilter.tsx b/src/components/SearchFilters/CourtDateFilter.tsx index 5174aff13..84e9cd807 100644 --- a/src/components/SearchFilters/CourtDateFilter.tsx +++ b/src/components/SearchFilters/CourtDateFilter.tsx @@ -3,7 +3,7 @@ import RadioButton from "components/RadioButton/RadioButton" import ExpandingFilters from "features/CourtCaseFilters/ExpandingFilters" import { FormGroup } from "govuk-react" import type { Dispatch } from "react" -import { SerializedCourtDateRange } from "types/CaseListQueryParams" +import { SerializedDateRange } from "types/CaseListQueryParams" import type { FilterAction } from "types/CourtCaseFilter" import { CaseAgeOptions } from "utils/caseAgeOptions" import { formatDisplayedDate } from "utils/date/formattedDate" @@ -14,7 +14,7 @@ interface Props { caseAges?: string[] caseAgeCounts: Record dispatch: Dispatch - dateRange: SerializedCourtDateRange | undefined + dateRange: SerializedDateRange | undefined } const getCaseAgeWithFormattedDate = (namedCaseAge: string): string => { diff --git a/src/features/CourtCaseFilters/AppliedFilters.tsx b/src/features/CourtCaseFilters/AppliedFilters.tsx index 1a0790c34..d607be7df 100644 --- a/src/features/CourtCaseFilters/AppliedFilters.tsx +++ b/src/features/CourtCaseFilters/AppliedFilters.tsx @@ -2,7 +2,7 @@ import ConditionalRender from "components/ConditionalRender" import FilterTag from "components/FilterTag/FilterTag" import { useRouter } from "next/router" import { encode } from "querystring" -import { LockedState, Reason, SerializedCourtDateRange } from "types/CaseListQueryParams" +import { LockedState, Reason, SerializedDateRange } from "types/CaseListQueryParams" import { caseStateLabels } from "utils/caseStateFilters" import { deleteQueryParam, deleteQueryParamsByName } from "utils/deleteQueryParam" import { formatStringDateAsDisplayedDate } from "utils/date/formattedDate" @@ -15,7 +15,7 @@ interface Props { reasonCodes?: string[] ptiurn?: string | null caseAge?: string[] - dateRange?: SerializedCourtDateRange | null + dateRange?: SerializedDateRange | null lockedState?: string | null caseState?: string | null } diff --git a/src/features/CourtCaseFilters/CourtCaseFilter.tsx b/src/features/CourtCaseFilters/CourtCaseFilter.tsx index d88e85301..a2c7dd761 100644 --- a/src/features/CourtCaseFilters/CourtCaseFilter.tsx +++ b/src/features/CourtCaseFilters/CourtCaseFilter.tsx @@ -9,7 +9,7 @@ import TriggerGroups from "components/SearchFilters/TriggerGroups" import { useCurrentUser } from "context/CurrentUserContext" import { FormGroup } from "govuk-react" import { useReducer } from "react" -import { CaseListQueryParams, LockedState, SerializedCourtDateRange } from "types/CaseListQueryParams" +import { CaseListQueryParams, LockedState, SerializedDateRange } from "types/CaseListQueryParams" import type { Filter } from "types/CourtCaseFilter" import Permission from "types/Permission" import { anyFilterChips } from "utils/filterChips" @@ -26,7 +26,7 @@ const Divider = () => ( type Props = CaseListQueryParams & { caseAge: string[] caseAgeCounts: Record - dateRange: SerializedCourtDateRange | null + dateRange: SerializedDateRange | null } const CourtCaseFilter: React.FC = ({ diff --git a/src/pages/index.tsx b/src/pages/index.tsx index dec1009d8..b389bcbbf 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -31,7 +31,7 @@ import { LockedState, QueryOrder, Reason, - SerializedCourtDateRange + SerializedDateRange } from "types/CaseListQueryParams" import Permission from "types/Permission" import { isError } from "types/Result" @@ -63,14 +63,15 @@ type Props = { caseAgeCounts: Record courtCases: DisplayPartialCourtCase[] csrfToken: string - dateRange: SerializedCourtDateRange | null + dateRange: SerializedDateRange | null displaySwitchingSurveyFeedback: boolean environment: string | null oppositeOrder: QueryOrder queryStringCookieName: string totalCases: number user: DisplayFullUser -} & Omit + caseResolvedDateRange: SerializedDateRange | null +} & Omit const validateOrder = (param: unknown): param is QueryOrder => param === "asc" || param === "desc" @@ -104,7 +105,9 @@ const extractSearchParamsFromQuery = (query: ParsedUrlQuery, currentUser: User): const resolvedByUsername = caseState === "Resolved" && !currentUser.hasAccessTo[Permission.ListAllCases] ? currentUser.username : null const courtDateRange = caseAges || dateRange + const resolvedDateRange = validateDateRange({ from: query.resolvedFrom, to: query.resolvedTo }) const asn = validateQueryParams(query.asn) ? sanitise(query.asn) : null + return { ...(defendantName && { defendantName: defendantName }), ...(courtName && { courtName: courtName }), @@ -120,7 +123,8 @@ const extractSearchParamsFromQuery = (query: ParsedUrlQuery, currentUser: User): lockedState, ...(caseState && { caseState }), ...(resolvedByUsername && { resolvedByUsername }), - ...(allocatedToUserName && { allocatedToUserName }) + ...(allocatedToUserName && { allocatedToUserName }), + ...(resolvedDateRange && { resolvedDateRange }) } } @@ -204,7 +208,7 @@ export const getServerSideProps = withMultipleServerSideProps( logCaseListRenderTime(startTime, currentUser, caseListQueryParams) // Remove courtDateRange from the props because the dates don't serialise - const { courtDateRange: _, ...caseListQueryProps } = caseListQueryParams + const { courtDateRange: _, resolvedDateRange: __, ...caseListQueryProps } = caseListQueryParams return { props: { build: process.env.NEXT_PUBLIC_BUILD || null, @@ -225,6 +229,12 @@ export const getServerSideProps = withMultipleServerSideProps( queryStringCookieName, totalCases: courtCases.totalCases, user: userToDisplayFullUserDto(currentUser), + caseResolvedDateRange: caseListQueryParams.resolvedDateRange + ? { + from: formatFormInputDateString(caseListQueryParams.resolvedDateRange.from), + to: formatFormInputDateString(caseListQueryParams.resolvedDateRange.to) + } + : null, ...caseListQueryProps } } diff --git a/src/services/listCourtCases.ts b/src/services/listCourtCases.ts index 9a837f3a4..f07b7c2a1 100644 --- a/src/services/listCourtCases.ts +++ b/src/services/listCourtCases.ts @@ -29,6 +29,7 @@ const listCourtCases = async ( allocatedToUserName, reasonCodes, resolvedByUsername, + resolvedDateRange, asn }: CaseListQueryParams, user: User @@ -150,6 +151,12 @@ const listCourtCases = async ( filterByReasonAndResolutionStatus(query, user, reason, reasonCodes, caseState, resolvedByUsername) + if (caseState === "Resolved" && resolvedDateRange) { + query + .andWhere({ resolutionTimestamp: MoreThanOrEqual(resolvedDateRange?.from) }) + .andWhere({ resolutionTimestamp: LessThanOrEqual(resolvedDateRange?.to) }) + } + if (allocatedToUserName) { query.andWhere( new Brackets((qb) => { diff --git a/src/types/CaseListQueryParams.ts b/src/types/CaseListQueryParams.ts index e1a85a5cf..bb4304f69 100644 --- a/src/types/CaseListQueryParams.ts +++ b/src/types/CaseListQueryParams.ts @@ -13,12 +13,12 @@ export enum LockedState { LockedToMe = "LockedToMe" } -export type CourtDateRange = { +export type DateRange = { from: Date to: Date } -export type SerializedCourtDateRange = { +export type SerializedDateRange = { from?: string to?: string } @@ -28,7 +28,7 @@ export type CaseState = "Resolved" | "Unresolved" | undefined export type CaseListQueryParams = { allocatedToUserName?: string caseState?: CaseState - courtDateRange?: CourtDateRange | CourtDateRange[] + courtDateRange?: DateRange | DateRange[] courtName?: string defendantName?: string lockedState?: string @@ -40,5 +40,6 @@ export type CaseListQueryParams = { reason?: Reason reasonCodes?: string[] resolvedByUsername?: string + resolvedDateRange?: DateRange asn?: string } diff --git a/src/utils/caseAgeOptions.ts b/src/utils/caseAgeOptions.ts index c0f8a06d7..9d53670aa 100644 --- a/src/utils/caseAgeOptions.ts +++ b/src/utils/caseAgeOptions.ts @@ -1,7 +1,7 @@ import { subDays } from "date-fns" -import { CourtDateRange } from "types/CaseListQueryParams" +import { DateRange } from "types/CaseListQueryParams" -export const CaseAgeOptions: Record CourtDateRange> = { +export const CaseAgeOptions: Record DateRange> = { Today: () => { const today = new Date() return { from: today, to: today } diff --git a/src/utils/validators/validateCaseAges.ts b/src/utils/validators/validateCaseAges.ts index 83bc38123..9754019b4 100644 --- a/src/utils/validators/validateCaseAges.ts +++ b/src/utils/validators/validateCaseAges.ts @@ -1,7 +1,7 @@ -import { CourtDateRange } from "types/CaseListQueryParams" +import { DateRange } from "types/CaseListQueryParams" import { CaseAgeOptions } from "utils/caseAgeOptions" -export const mapCaseAges = (caseAge: string | string[] | undefined): CourtDateRange[] | undefined => { +export const mapCaseAges = (caseAge: string | string[] | undefined): DateRange[] | undefined => { if (!caseAge) { return undefined } diff --git a/test/services/listCourtCases/listCourtCases.integration.test.ts b/test/services/listCourtCases/listCourtCases.integration.test.ts index 4886af495..6909dcb86 100644 --- a/test/services/listCourtCases/listCourtCases.integration.test.ts +++ b/test/services/listCourtCases/listCourtCases.integration.test.ts @@ -6,7 +6,7 @@ import User from "services/entities/User" import courtCasesByOrganisationUnitQuery from "services/queries/courtCasesByOrganisationUnitQuery" import leftJoinAndSelectTriggersQuery from "services/queries/leftJoinAndSelectTriggersQuery" import { DataSource } from "typeorm" -import { LockedState } from "types/CaseListQueryParams" +import { DateRange, LockedState } from "types/CaseListQueryParams" import { ListCourtCaseResult } from "types/ListCourtCasesResult" import { ResolutionStatus } from "types/ResolutionStatus" import CourtCase from "../../../src/services/entities/CourtCase" @@ -816,4 +816,38 @@ describe("listCourtCases", () => { expect(unlockedCases.map((c) => c.errorId)).toStrictEqual([2]) }) }) + + describe("Filter cases by resolved date", () => { + it("Returns resolved cases within given date range", async () => { + const resolvedDateRange = { + from: new Date("2024-05-3"), + to: new Date("2024-05-7 23:59:59.000") + } as DateRange + await insertCourtCasesWithFields( + Array.from(Array(8)).map((_, index) => ({ + orgForPoliceFilter: orgCode, + resolutionTimestamp: new Date(`2024-05-0${index + 1}`), + errorResolvedTimestamp: new Date(`2024-05-0${index + 1}`), + triggerResolvedTimestamp: new Date(`2024-05-0${index + 1}`) + })) + ) + + const { result, totalCases } = (await listCourtCases( + dataSource, + { resolvedDateRange, caseState: "Resolved" }, + testUser + )) as ListCourtCaseResult + + console.log( + "Result: ", + result.map((r) => r.resolutionTimestamp) + ) + + expect(result).toHaveLength(5) + expect(totalCases).toBe(5) + expect(result[0].resolutionTimestamp?.toString()).toBe(new Date(`2024-05-03`).toString()) + expect(result[2].resolutionTimestamp?.toString()).toBe(new Date(`2024-05-05`).toString()) + expect(result[4].resolutionTimestamp?.toString()).toBe(new Date(`2024-05-07`).toString()) + }) + }) })