diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts index e4eb6b9e1169..59a1ac2adec3 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.spec.ts @@ -554,7 +554,10 @@ describe('Transition Case', () => { ) describe.each(indictmentCases)('complete %s', (type) => { - const allowedFromStates = [CaseState.RECEIVED] + const allowedFromStates = [ + CaseState.WAITING_FOR_CANCELLATION, + CaseState.RECEIVED, + ] describe.each(allowedFromStates)( 'state %s - should complete', diff --git a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts index 80fa6200ae7a..9865ba48b1dd 100644 --- a/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts +++ b/apps/judicial-system/backend/src/app/modules/case/state/case.state.ts @@ -98,7 +98,10 @@ const indictmentCaseStateMachine: Map< [ IndictmentCaseTransition.COMPLETE, { - fromStates: [IndictmentCaseState.RECEIVED], + fromStates: [ + IndictmentCaseState.WAITING_FOR_CANCELLATION, + IndictmentCaseState.RECEIVED, + ], to: { state: IndictmentCaseState.COMPLETED }, }, ], diff --git a/apps/judicial-system/backend/src/app/modules/event/event.service.ts b/apps/judicial-system/backend/src/app/modules/event/event.service.ts index 398b4284edf4..3ef1bf3a02b4 100644 --- a/apps/judicial-system/backend/src/app/modules/event/event.service.ts +++ b/apps/judicial-system/backend/src/app/modules/event/event.service.ts @@ -49,7 +49,7 @@ const caseEvent = { ACCEPT: ':white_check_mark: Samþykkt', REJECT: ':negative_squared_cross_mark: Hafnað', DISMISS: ':woman-shrugging: Vísað frá', - COMPLETE: ':white_check_mark: Lokað', + COMPLETE: ':white_check_mark: Lokið', DELETE: ':fire: Afturkallað', SCHEDULE_COURT_DATE: ':timer_clock: Fyrirtökutíma úthlutað', ARCHIVE: ':file_cabinet: Sett í geymslu', diff --git a/apps/judicial-system/web/messages/Core/tables.ts b/apps/judicial-system/web/messages/Core/tables.ts index 9cf5cab7a30f..d6ce53eb22b1 100644 --- a/apps/judicial-system/web/messages/Core/tables.ts +++ b/apps/judicial-system/web/messages/Core/tables.ts @@ -103,4 +103,9 @@ export const tables = defineMessages({ description: 'Notaður sem titill fyrir birtingarstaða dálk í lista yfir mál.', }, + postponed: { + id: 'judicial.system.core:tables.postponed', + defaultMessage: 'Frestað', + description: 'Notaður sem texti þegar mál er frestað.', + }, }) diff --git a/apps/judicial-system/web/src/components/FormProvider/FormProvider.tsx b/apps/judicial-system/web/src/components/FormProvider/FormProvider.tsx index c895f9e91f67..d52ce2e8ec6f 100644 --- a/apps/judicial-system/web/src/components/FormProvider/FormProvider.tsx +++ b/apps/judicial-system/web/src/components/FormProvider/FormProvider.tsx @@ -1,6 +1,7 @@ import React, { createContext, ReactNode, + useCallback, useContext, useEffect, useState, @@ -18,8 +19,11 @@ import { api } from '@island.is/judicial-system-web/src/services' import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import { UserContext } from '../UserProvider/UserProvider' -import { useCaseLazyQuery } from './case.generated' -import { useLimitedAccessCaseLazyQuery } from './limitedAccessCase.generated' +import { CaseQuery, useCaseLazyQuery } from './case.generated' +import { + LimitedAccessCaseQuery, + useLimitedAccessCaseLazyQuery, +} from './limitedAccessCase.generated' type ProviderState = | 'fetch' @@ -36,6 +40,11 @@ interface FormProvider { caseNotFound: boolean isCaseUpToDate: boolean refreshCase: () => void + getCase: ( + id: string, + onCompleted: (theCase: Case) => void, + onError: () => void, + ) => void } interface Props { @@ -60,8 +69,12 @@ export const FormContext = createContext({ isLoadingWorkingCase: true, caseNotFound: false, isCaseUpToDate: false, - // eslint-disable-next-line @typescript-eslint/no-empty-function - refreshCase: () => {}, + refreshCase: () => { + return + }, + getCase: () => { + return + }, }) const MaybeFormProvider = ({ children }: Props) => { @@ -123,39 +136,37 @@ const FormProvider = ({ children }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.query.id, router.pathname]) - const [getCase] = useCaseLazyQuery({ + const [queryCase] = useCaseLazyQuery({ fetchPolicy: 'no-cache', errorPolicy: 'all', - onCompleted: (caseData) => { - if (caseData && caseData.case) { - setWorkingCase(caseData.case) - - // The case has been loaded from the server - setState('up-to-date') - } - }, - onError: () => { - // The case was not found - setState('not-found') - }, }) - const [getLimitedAccessCase] = useLimitedAccessCaseLazyQuery({ + const [queryLimitedAccessCase] = useLimitedAccessCaseLazyQuery({ fetchPolicy: 'no-cache', errorPolicy: 'all', - onCompleted: (caseData) => { - if (caseData && caseData.limitedAccessCase) { - setWorkingCase(caseData.limitedAccessCase) + }) - // The case has been loaded from the server - setState('up-to-date') - } - }, - onError: () => { - // The case was not found - setState('not-found') + const getCase = useCallback( + (id: string, onCompleted: (theCase: Case) => void, onError: () => void) => { + const promisedCase = limitedAccess + ? queryLimitedAccessCase({ variables: { input: { id } } }) + : queryCase({ variables: { input: { id } } }) + + promisedCase + .then((caseData) => { + if (caseData && caseData.data) { + const data = caseData.data as CaseQuery & LimitedAccessCaseQuery + const theCase = data[limitedAccess ? 'limitedAccessCase' : 'case'] + + if (theCase) { + onCompleted(theCase) + } + } + }) + .catch(onError) }, - }) + [limitedAccess, queryCase, queryLimitedAccessCase], + ) useEffect(() => { if (!isAuthenticated && router.pathname !== '/') { @@ -167,20 +178,29 @@ const FormProvider = ({ children }: Props) => { id && (state === 'fetch' || state === 'refresh') ) { - if (limitedAccess) { - getLimitedAccessCase({ variables: { input: { id } } }) - } else { - getCase({ variables: { input: { id } } }) - } + getCase( + id, + (theCase: Case) => { + setWorkingCase(theCase) + + // The case has been loaded from the server + setState('up-to-date') + }, + () => { + // The case was not found + setState('not-found') + }, + ) } }, [ - getCase, - getLimitedAccessCase, + queryCase, + queryLimitedAccessCase, id, isAuthenticated, limitedAccess, router.pathname, state, + getCase, ]) useEffect(() => { @@ -207,6 +227,7 @@ const FormProvider = ({ children }: Props) => { isCaseUpToDate: !replacingCase && !replacingPath && state === 'up-to-date', refreshCase: () => setState('refresh'), + getCase, }} > {children} diff --git a/apps/judicial-system/web/src/components/Table/CourtDate/CourtDate.tsx b/apps/judicial-system/web/src/components/Table/CourtDate/CourtDate.tsx new file mode 100644 index 000000000000..9c3cd97e8edc --- /dev/null +++ b/apps/judicial-system/web/src/components/Table/CourtDate/CourtDate.tsx @@ -0,0 +1,45 @@ +import { FC } from 'react' +import { useIntl } from 'react-intl' +import format from 'date-fns/format' +import localeIS from 'date-fns/locale/is' +import parseISO from 'date-fns/parseISO' + +import { Box, Text } from '@island.is/island-ui/core' +import { capitalize } from '@island.is/judicial-system/formatters' +import { tables } from '@island.is/judicial-system-web/messages' + +interface Props { + courtDate?: string | null + postponedIndefinitelyExplanation?: string | null +} + +const CourtDate: FC = (props) => { + const { courtDate, postponedIndefinitelyExplanation } = props + const { formatMessage } = useIntl() + + if (!courtDate && !postponedIndefinitelyExplanation) { + return null + } + + return postponedIndefinitelyExplanation ? ( + {formatMessage(tables.postponed)} + ) : ( + courtDate && ( + <> + + + {capitalize( + format(parseISO(courtDate), 'EEEE d. LLLL y', { + locale: localeIS, + }), + ).replace('dagur', 'd.')} + + + + kl. {format(parseISO(courtDate), 'kk:mm')} + + + ) + ) +} +export default CourtDate diff --git a/apps/judicial-system/web/src/components/Table/Table.tsx b/apps/judicial-system/web/src/components/Table/Table.tsx index 027599968ca0..d14dec363cc1 100644 --- a/apps/judicial-system/web/src/components/Table/Table.tsx +++ b/apps/judicial-system/web/src/components/Table/Table.tsx @@ -34,6 +34,7 @@ interface TableProps { data: CaseListEntry[] columns: { cell: (row: CaseListEntry) => ReactNode }[] generateContextMenuItems?: (row: CaseListEntry) => ContextMenuItem[] + onClick?: (row: CaseListEntry) => boolean } interface TableWrapperProps { @@ -80,7 +81,7 @@ export const useTable = () => { } const Table: React.FC = (props) => { - const { thead, data, columns, generateContextMenuItems } = props + const { thead, data, columns, generateContextMenuItems, onClick } = props const { isOpeningCaseId, handleOpenCase, LoadingIndicator, showLoading } = useCaseList() const { sortConfig, requestSort, getClassNamesFor } = useTable() @@ -122,7 +123,11 @@ const Table: React.FC = (props) => { {data.map((theCase: CaseListEntry) => ( handleOpenCase(theCase.id)} + onClick={() => { + if (!onClick?.(theCase)) { + handleOpenCase(theCase.id) + } + }} theCase={theCase} isCourtRole={isDistrictCourtUser(user)} isLoading={isOpeningCaseId === theCase.id && showLoading} @@ -185,7 +190,9 @@ const Table: React.FC = (props) => { aria-disabled={isOpeningCaseId === row.id || isTransitioningCase} className={styles.tableRowContainer} onClick={() => { - handleOpenCase(row.id) + if (!onClick?.(row)) { + handleOpenCase(row.id) + } }} > {columns.map((td) => ( @@ -195,27 +202,29 @@ const Table: React.FC = (props) => { ))} {generateContextMenuItems && ( - - {isOpeningCaseId === row.id && showLoading ? ( - - - - ) : ( - { - evt.stopPropagation() - }} - /> - } - /> - )} - + {generateContextMenuItems(row).length > 0 && ( + + {isOpeningCaseId === row.id && showLoading ? ( + + + + ) : ( + { + evt.stopPropagation() + }} + /> + } + /> + )} + + )} )} diff --git a/apps/judicial-system/web/src/components/Table/index.ts b/apps/judicial-system/web/src/components/Table/index.ts index deda840f4234..72f23966d38c 100644 --- a/apps/judicial-system/web/src/components/Table/index.ts +++ b/apps/judicial-system/web/src/components/Table/index.ts @@ -12,3 +12,4 @@ export { export { default as CreatedDate } from './CreatedDate/CreatedDate' export { default as AppealCasesTable } from './AppealCasesTable/AppealCasesTable' export { default as PastCasesTable } from './PastCasesTable/PastCasesTable' +export { default as CourtDate } from './CourtDate/CourtDate' diff --git a/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.strings.ts b/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.strings.ts new file mode 100644 index 000000000000..4a7547e2a962 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.strings.ts @@ -0,0 +1,42 @@ +import { defineMessages } from 'react-intl' + +export const strings = defineMessages({ + title: { + id: 'judicial.system.core:court.cases_in_progress.title', + defaultMessage: 'Mál í vinnslu', + description: 'Notaður sem titill í málalista', + }, + noCasesTitle: { + id: 'judicial.system.core:court.cases_in_progress.no_cases_title', + defaultMessage: 'Engin mál í vinnslu.', + description: 'Notaður sem titill þegar engin mál eru til vinnslu', + }, + noCasesMessage: { + id: 'judicial.system.core:court.cases_in_progress.no_cases_message', + defaultMessage: 'Öll mál hafa verið afgreidd.', + description: 'Notað sem skilaboð þegar engin mál eru til vinnslu', + }, + cancelCaseModalTitle: { + id: 'judicial.system.core:cases.active_requests.cancel_case_modal_title', + defaultMessage: 'Mál afturkallað', + description: 'Notaður sem titill í Afturkalla mál dómstóla modal.', + }, + cancelCaseModalText: { + id: 'judicial.system.core:cases.active_requests.cancel_case_modal_text', + defaultMessage: + 'Ákæruvaldið hefur afturkallað ákæruna. Hægt er að skrá málsnúmer og ljúka málinu hér.', + description: 'Notaður sem texti í Afturkalla mál dómstóla modal.', + }, + cancelCaseModalPrimaryButtonText: { + id: 'judicial.system.core:cases.active_requests.cancel_case_modal_primary_button_text', + defaultMessage: 'Ljúka máli', + description: + 'Notaður sem texti á Ljúka máli takka í Afturkalla mál dómstóla modal.', + }, + cancelCaseModalSecondaryButtonText: { + id: 'judicial.system.core:cases.active_requests.delete_case_modal_secondary_button_text', + defaultMessage: 'Hætta við', + description: + 'Notaður sem texti á Hætta við takka í Afturkalla mál dómstóla modal.', + }, +}) diff --git a/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.tsx b/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.tsx new file mode 100644 index 000000000000..65e95e8e4bd9 --- /dev/null +++ b/apps/judicial-system/web/src/routes/Court/components/CasesInProgressTable/CasesInProgressTable.tsx @@ -0,0 +1,210 @@ +import React, { + Dispatch, + FC, + SetStateAction, + useContext, + useEffect, + useState, +} from 'react' +import { useIntl } from 'react-intl' +import { AnimatePresence } from 'framer-motion' + +import { Box, toast } from '@island.is/island-ui/core' +import { capitalize } from '@island.is/judicial-system/formatters' +import { CaseIndictmentRulingDecision } from '@island.is/judicial-system/types' +import { core, errors, tables } from '@island.is/judicial-system-web/messages' +import { + FormContext, + Modal, + SectionHeading, + TagCaseState, +} from '@island.is/judicial-system-web/src/components' +import { useContextMenu } from '@island.is/judicial-system-web/src/components/ContextMenu/ContextMenu' +import { + ColumnCaseType, + CourtCaseNumber, + CourtDate, + CreatedDate, + DefendantInfo, +} from '@island.is/judicial-system-web/src/components/Table' +import Table, { + TableWrapper, +} from '@island.is/judicial-system-web/src/components/Table/Table' +import TableInfoContainer from '@island.is/judicial-system-web/src/components/Table/TableInfoContainer/TableInfoContainer' +import { + CaseListEntry, + CaseState, + CaseTransition, +} from '@island.is/judicial-system-web/src/graphql/schema' +import { TempCase as Case } from '@island.is/judicial-system-web/src/types' +import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' + +import CourtCaseNumberInput from '../CourtCaseNumber/CourtCaseNumberInput' +import { strings } from './CasesInProgressTable.strings' + +interface CasesInProgressTableProps { + loading: boolean + isFiltering: boolean + cases: CaseListEntry[] + refetch: () => Promise +} + +const CasesInProgressTable: FC = (props) => { + const { loading, isFiltering, cases, refetch } = props + + const { formatMessage } = useIntl() + const { openCaseInNewTabMenuItem } = useContextMenu() + const { getCase } = useContext(FormContext) + const [caseToCancelId, setCaseToCancelId] = useState() + const [caseToCancel, setCaseToCancel] = useState() + const { updateCase, isUpdatingCase, transitionCase, isTransitioningCase } = + useCase() + + useEffect(() => { + if (caseToCancelId) { + getCase(caseToCancelId, setCaseToCancel, () => + toast.error(formatMessage(errors.getCaseToOpen)), + ) + } + }, [caseToCancelId, formatMessage, getCase]) + + const handlePrimaryButtonClick = async () => { + if (!caseToCancelId) { + return + } + + const updated = await updateCase(caseToCancelId, { + indictmentRulingDecision: CaseIndictmentRulingDecision.CANCELLATION, + }) + + if (!updated) { + return + } + + const cancelled = await transitionCase( + caseToCancelId, + CaseTransition.COMPLETE, + ) + + if (!cancelled) { + return + } + + refetch() + + setCaseToCancelId(undefined) + } + + const handleSecondaryButtonClick = () => { + setCaseToCancelId(undefined) + } + + return ( + <> + + + + {cases.length > 0 ? ( + ( + + ), + }, + { + cell: (row) => , + }, + { cell: (row) => }, + { cell: (row) => }, + { + cell: (row) => ( + + ), + }, + { + cell: (row) => ( + + ), + }, + ]} + generateContextMenuItems={(row) => { + return row.state === CaseState.WAITING_FOR_CANCELLATION + ? [] + : [openCaseInNewTabMenuItem(row.id)] + }} + onClick={(row) => { + if (row.state === CaseState.WAITING_FOR_CANCELLATION) { + setCaseToCancelId(row.id) + return true + } + + return false + }} + /> + ) : ( + + )} + + + {caseToCancel && caseToCancel.id === caseToCancelId && ( + + + >} + /> + + + )} + + ) +} + +export default CasesInProgressTable diff --git a/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumber.tsx b/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumber.tsx index 32f4c224ab77..97f86eeaabdc 100644 --- a/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumber.tsx +++ b/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumber.tsx @@ -1,68 +1,16 @@ -import React from 'react' +import React, { useContext } from 'react' import { useIntl } from 'react-intl' -import { Box, Button, Input, Text } from '@island.is/island-ui/core' -import { isIndictmentCase } from '@island.is/judicial-system/types' -import { BlueBox } from '@island.is/judicial-system-web/src/components' +import { Box, Text } from '@island.is/island-ui/core' +import { FormContext } from '@island.is/judicial-system-web/src/components' import { CaseState } from '@island.is/judicial-system-web/src/graphql/schema' -import { TempCase as Case } from '@island.is/judicial-system-web/src/types' -import { - removeTabsValidateAndSet, - validateAndSendToServer, -} from '@island.is/judicial-system-web/src/utils/formHelper' -import { - UpdateCase, - useCase, -} from '@island.is/judicial-system-web/src/utils/hooks' -import { validate } from '@island.is/judicial-system-web/src/utils/validate' +import CourtCaseNumberInput from './CourtCaseNumberInput' import { courtCaseNumber } from './CourtCaseNumber.strings' -import * as styles from './CourtCaseNumber.css' -interface Props { - workingCase: Case - setWorkingCase: React.Dispatch> - courtCaseNumberEM: string - setCourtCaseNumberEM: React.Dispatch> - createCourtCaseSuccess: boolean - setCreateCourtCaseSuccess: React.Dispatch> - handleCreateCourtCase: (wc: Case) => void - isCreatingCourtCase: boolean -} - -const CourtCaseNumber: React.FC> = (props) => { - const { - workingCase, - setWorkingCase, - courtCaseNumberEM, - setCourtCaseNumberEM, - createCourtCaseSuccess, - setCreateCourtCaseSuccess, - handleCreateCourtCase, - isCreatingCourtCase, - } = props - const { updateCase } = useCase() +const CourtCaseNumber: React.FC = () => { const { formatMessage } = useIntl() - - const updateCourtCaseNumber = async (id: string, update: UpdateCase) => { - const isValid = validate([ - [ - update.courtCaseNumber, - [ - 'empty', - isIndictmentCase(workingCase.type) - ? 'S-case-number' - : 'R-case-number', - ], - ], - ]).isValid - - if (!isValid) { - return - } - - await updateCase(id, update) - } + const { workingCase, setWorkingCase } = useContext(FormContext) return ( <> @@ -74,93 +22,17 @@ const CourtCaseNumber: React.FC> = (props) => { {workingCase.state !== CaseState.SUBMITTED && + workingCase.state !== CaseState.WAITING_FOR_CANCELLATION && workingCase.state !== CaseState.RECEIVED && workingCase.state !== CaseState.MAIN_HEARING ? formatMessage(courtCaseNumber.explanationDisabled) : formatMessage(courtCaseNumber.explanation)} - -
- -
- -
-
- { - setCreateCourtCaseSuccess(false) - removeTabsValidateAndSet( - 'courtCaseNumber', - event.target.value, - [ - 'empty', - isIndictmentCase(workingCase.type) - ? 'S-case-number' - : 'R-case-number', - ], - setWorkingCase, - courtCaseNumberEM, - setCourtCaseNumberEM, - ) - }} - onBlur={(event) => { - validateAndSendToServer( - 'courtCaseNumber', - event.target.value, - [ - 'empty', - isIndictmentCase(workingCase.type) - ? 'S-case-number' - : 'R-case-number', - ], - workingCase, - updateCourtCaseNumber, - setCourtCaseNumberEM, - ) - }} - disabled={ - workingCase.state !== CaseState.SUBMITTED && - workingCase.state !== CaseState.RECEIVED && - workingCase.state !== CaseState.MAIN_HEARING - } - required - /> -
-
-
-
+ ) } diff --git a/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumberInput.tsx b/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumberInput.tsx new file mode 100644 index 000000000000..60c8eb89efcc --- /dev/null +++ b/apps/judicial-system/web/src/routes/Court/components/CourtCaseNumber/CourtCaseNumberInput.tsx @@ -0,0 +1,159 @@ +import React, { Dispatch, FC, SetStateAction, useState } from 'react' +import { useIntl } from 'react-intl' + +import { Box, Button, Input } from '@island.is/island-ui/core' +import { isIndictmentCase } from '@island.is/judicial-system/types' +import { BlueBox } from '@island.is/judicial-system-web/src/components' +import { CaseState } from '@island.is/judicial-system-web/src/graphql/schema' +import { TempCase as Case } from '@island.is/judicial-system-web/src/types' +import { + removeTabsValidateAndSet, + validateAndSendToServer, +} from '@island.is/judicial-system-web/src/utils/formHelper' +import { + UpdateCase, + useCase, +} from '@island.is/judicial-system-web/src/utils/hooks' +import { validate } from '@island.is/judicial-system-web/src/utils/validate' + +import { courtCaseNumber } from './CourtCaseNumber.strings' +import * as styles from './CourtCaseNumber.css' + +interface Props { + workingCase: Case + setWorkingCase: Dispatch> +} + +const CourtCaseNumberInput: FC = (props) => { + const { workingCase, setWorkingCase } = props + + const { formatMessage } = useIntl() + const { updateCase, createCourtCase, isCreatingCourtCase } = useCase() + const [courtCaseNumberErrorMessage, setCourtCaseNumberErrorMessage] = + useState('') + const [createCourtCaseSuccess, setCreateCourtCaseSuccess] = + useState(false) + + const handleCreateCourtCase = async (workingCase: Case) => { + const courtCaseNumber = await createCourtCase(workingCase, setWorkingCase) + + if (courtCaseNumber !== '') { + setCourtCaseNumberErrorMessage('') + setCreateCourtCaseSuccess(true) + } else { + setCourtCaseNumberErrorMessage( + 'Ekki tókst að stofna nýtt mál, reyndu aftur eða sláðu inn málsnúmer', + ) + } + } + + const updateCourtCaseNumber = async (id: string, update: UpdateCase) => { + const isValid = validate([ + [ + update.courtCaseNumber, + [ + 'empty', + isIndictmentCase(workingCase.type) + ? 'S-case-number' + : 'R-case-number', + ], + ], + ]).isValid + + if (!isValid) { + return + } + + await updateCase(id, update) + } + + return ( + +
+ +
+ +
+
+ { + setCreateCourtCaseSuccess(false) + removeTabsValidateAndSet( + 'courtCaseNumber', + event.target.value, + [ + 'empty', + isIndictmentCase(workingCase.type) + ? 'S-case-number' + : 'R-case-number', + ], + setWorkingCase, + courtCaseNumberErrorMessage, + setCourtCaseNumberErrorMessage, + ) + }} + onBlur={(event) => { + validateAndSendToServer( + 'courtCaseNumber', + event.target.value, + [ + 'empty', + isIndictmentCase(workingCase.type) + ? 'S-case-number' + : 'R-case-number', + ], + workingCase, + updateCourtCaseNumber, + setCourtCaseNumberErrorMessage, + ) + }} + disabled={ + workingCase.state !== CaseState.SUBMITTED && + workingCase.state !== CaseState.WAITING_FOR_CANCELLATION && + workingCase.state !== CaseState.RECEIVED && + workingCase.state !== CaseState.MAIN_HEARING + } + required + /> +
+
+
+
+ ) +} + +export default CourtCaseNumberInput diff --git a/apps/judicial-system/web/src/routes/Court/components/ReceptionAndAssignment/ReceptionAndAssignment.tsx b/apps/judicial-system/web/src/routes/Court/components/ReceptionAndAssignment/ReceptionAndAssignment.tsx index 5d0a48490ac5..177b9ee89677 100644 --- a/apps/judicial-system/web/src/routes/Court/components/ReceptionAndAssignment/ReceptionAndAssignment.tsx +++ b/apps/judicial-system/web/src/routes/Court/components/ReceptionAndAssignment/ReceptionAndAssignment.tsx @@ -18,8 +18,6 @@ import { PageLayout, } from '@island.is/judicial-system-web/src/components' import { Gender } from '@island.is/judicial-system-web/src/graphql/schema' -import { TempCase as Case } from '@island.is/judicial-system-web/src/types' -import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' import { getDefendantPleaText } from '@island.is/judicial-system-web/src/utils/stepHelper' import { isReceptionAndAssignmentStepValid } from '@island.is/judicial-system-web/src/utils/validate' @@ -31,27 +29,10 @@ const ReceptionAndAssignment = () => { const router = useRouter() const id = router.query.id const { formatMessage } = useIntl() - const [courtCaseNumberEM, setCourtCaseNumberEM] = useState('') - const [createCourtCaseSuccess, setCreateCourtCaseSuccess] = - useState(false) - const { workingCase, setWorkingCase, isLoadingWorkingCase, caseNotFound } = + const { workingCase, isLoadingWorkingCase, caseNotFound } = useContext(FormContext) - const { createCourtCase, isCreatingCourtCase } = useCase() - - const handleCreateCourtCase = async (workingCase: Case) => { - const courtCaseNumber = await createCourtCase( - workingCase, - setWorkingCase, - setCourtCaseNumberEM, - ) - - if (courtCaseNumber !== '') { - setCreateCourtCaseSuccess(true) - } - } - const getNextRoute = () => { return isRestrictionCase(workingCase.type) ? constants.RESTRICTION_CASE_COURT_OVERVIEW_ROUTE @@ -137,16 +118,7 @@ const ReceptionAndAssignment = () => { - + diff --git a/apps/judicial-system/web/src/routes/Court/components/index.ts b/apps/judicial-system/web/src/routes/Court/components/index.ts index 5cd387b1bf71..7eb21ae65d86 100644 --- a/apps/judicial-system/web/src/routes/Court/components/index.ts +++ b/apps/judicial-system/web/src/routes/Court/components/index.ts @@ -1,5 +1,6 @@ export { default as AppealSections } from './AppealSections/AppealSections' export { default as CourtCaseNumber } from './CourtCaseNumber/CourtCaseNumber' +export { default as CourtCaseNumberInput } from './CourtCaseNumber/CourtCaseNumberInput' export { default as DraftConclusionModal } from './DraftConclusionModal/DraftConclusionModal' export { default as ReceptionAndAssignment } from './ReceptionAndAssignment/ReceptionAndAssignment' export { default as RulingModifiedModal } from './RulingModifiedModal/RulingModifiedModal' diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx index 3d57cd455f43..fc29d1b8771a 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/ActiveCases.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import { useLocalStorage } from 'react-use' import format from 'date-fns/format' @@ -13,18 +13,13 @@ import { displayFirstPlusRemaining, formatDOB, } from '@island.is/judicial-system/formatters' -import { - isDistrictCourtUser, - isProsecutionUser, - isRequestCase, -} from '@island.is/judicial-system/types' +import { isRequestCase } from '@island.is/judicial-system/types' import { core, tables } from '@island.is/judicial-system-web/messages' import { ContextMenu, Modal, TagAppealState, TagCaseState, - UserContext, } from '@island.is/judicial-system-web/src/components' import { contextMenu as contextMenuStrings } from '@island.is/judicial-system-web/src/components/ContextMenu/ContextMenu.strings' import IconButton from '@island.is/judicial-system-web/src/components/IconButton/IconButton' @@ -62,7 +57,6 @@ interface Props { const ActiveCases: React.FC> = (props) => { const { cases, isDeletingCase, onDeleteCase } = props - const { user } = useContext(UserContext) const { formatMessage } = useIntl() const { width } = useViewport() const [sortConfig, setSortConfig] = useLocalStorage( @@ -72,12 +66,12 @@ const ActiveCases: React.FC> = (props) => { direction: 'descending', }, ) - const [displayCases, setDisplayCases] = useState([]) - const [modalVisible, setVisibleModal] = useState<'DELETE_CASE'>() - // The index of requset that's about to be removed - const [requestToRemoveIndex, setRequestToRemoveIndex] = useState(-1) const { isOpeningCaseId, showLoading, handleOpenCase, LoadingIndicator } = useCaseList() + const [displayCases, setDisplayCases] = useState([]) + const [modalVisible, setVisibleModal] = useState<'DELETE_CASE'>() + // The id of the case that's about to be removed + const [caseToRemove, setCaseToRemove] = useState() useEffect(() => { setDisplayCases(cases) @@ -138,9 +132,11 @@ const ActiveCases: React.FC> = (props) => { {displayCases.map((theCase: CaseListEntry) => ( handleOpenCase(theCase.id)} + onClick={() => { + handleOpenCase(theCase.id) + }} theCase={theCase} - isCourtRole={isDistrictCourtUser(user)} + isCourtRole={false} isLoading={isOpeningCaseId === theCase.id && showLoading} > {theCase.state && @@ -226,7 +222,7 @@ const ActiveCases: React.FC> = (props) => {
- {cases.map((c, i) => ( + {cases.map((c) => ( > = (props) => { > = (props) => { onClick: () => handleOpenCase(c.id, true), icon: 'open', }, - ...(isProsecutionUser(user) && - (isRequestCase(c.type) || - c.state === CaseState.DRAFT || - c.state === CaseState.WAITING_FOR_CONFIRMATION) + ...(isRequestCase(c.type) || + c.state === CaseState.DRAFT || + c.state === CaseState.WAITING_FOR_CONFIRMATION ? [ { title: formatMessage( contextMenuStrings.deleteCase, ), onClick: () => { - setRequestToRemoveIndex(i) + setCaseToRemove(c) setVisibleModal('DELETE_CASE') }, icon: 'trash' as IconMapIcon, @@ -426,12 +420,12 @@ const ActiveCases: React.FC> = (props) => { title={formatMessage(m.activeRequests.deleteCaseModal.title)} text={formatMessage(m.activeRequests.deleteCaseModal.text)} onPrimaryButtonClick={async () => { - if (onDeleteCase && requestToRemoveIndex !== -1) { - await onDeleteCase(cases[requestToRemoveIndex]) + if (onDeleteCase && caseToRemove) { + await onDeleteCase(caseToRemove) setDisplayCases((prev) => - prev.filter((c) => c.id !== cases[requestToRemoveIndex].id), + prev.filter((c) => c.id !== caseToRemove.id), ) - setRequestToRemoveIndex(-1) + setCaseToRemove(undefined) setVisibleModal(undefined) } }} diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx index b72884663f11..59e70732318f 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.spec.tsx @@ -289,27 +289,6 @@ describe('Cases', () => { }) describe('Court users', () => { - test('should list all cases that do not have status NEW (never returned from the server), DELETED, ACCEPTED or REJECTED in a active cases table', async () => { - render( - - - - - - - , - ) - - expect( - await waitFor( - () => screen.getAllByTestId('custody-cases-table-row').length, - ), - ).toEqual(4) - }) - test('should display the judge logo', async () => { render( { }) }) - describe('Prison users', () => { - test('should list active and past cases in separate tables based on validToDate', async () => { - render( - - - - - - - , - ) - - await waitFor(() => { - expect(screen.getAllByRole('table').length).toEqual(2) - }) - }) - }) - describe('All user types - sorting', () => { test('should order the table data by accused name in ascending order when the user clicks the accused name table header', async () => { const user = userEvent.setup() diff --git a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx index aab52dd2cf4b..cde1b2b43836 100644 --- a/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx +++ b/apps/judicial-system/web/src/routes/Shared/Cases/Cases.tsx @@ -38,6 +38,7 @@ import { import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' import CasesAwaitingAssignmentTable from '../../Court/components/CasesAwaitingAssignmentTable/CasesAwaitingAssignmentTable' +import CasesInProgressTable from '../../Court/components/CasesInProgressTable/CasesInProgressTable' import CasesAwaitingConfirmationTable from '../../Prosecutor/components/CasesAwaitingConfirmationTable/CasesAwaitingConfirmationTable' import CasesAwaitingReview from '../../PublicProsecutor/Tables/CasesAwaitingReview' import ActiveCases from './ActiveCases' @@ -101,15 +102,13 @@ const CreateCaseButton: React.FC = (props) => { export const Cases: React.FC = () => { const { formatMessage } = useIntl() - - const [isFiltering, setIsFiltering] = useState(false) - const [modalVisible, setVisibleModal] = useState() - const { user } = useContext(UserContext) - const { transitionCase, isTransitioningCase, isSendingNotification } = useCase() + const [isFiltering, setIsFiltering] = useState(false) + const [modalVisible, setVisibleModal] = useState() + const { data, error, loading, refetch } = useCasesQuery({ fetchPolicy: 'no-cache', errorPolicy: 'all', @@ -292,34 +291,47 @@ export const Cases: React.FC = () => { cases={casesAwaitingReview} /> )} + + + {activeCases.length > 0 ? ( + + ) : ( +
+ +
+ )} +
)} - {isDistrictCourtUser(user) && filter.value !== 'INVESTIGATION' && ( - - )} - - - {activeCases.length > 0 ? ( - + + - ) : ( -
- -
- )} -
+ + )} {loading || pastCases.length > 0 ? ( { async ( workingCase: Case, setWorkingCase: React.Dispatch>, - setCourtCaseNumberErrorMessage: React.Dispatch< - React.SetStateAction - >, ): Promise => { try { if (isCreatingCourtCase === false) { @@ -225,16 +222,11 @@ const useCase = () => { courtCaseNumber: (data.createCourtCase as Case).courtCaseNumber, })) - setCourtCaseNumberErrorMessage('') - return data.createCourtCase.courtCaseNumber } } } catch (error) { - // Catch all so we can set an eror message - setCourtCaseNumberErrorMessage( - 'Ekki tókst að stofna nýtt mál, reyndu aftur eða sláðu inn málsnúmer', - ) + // Catch all so we can return the empty string } return '' @@ -248,10 +240,6 @@ const useCase = () => { ? limitedAccessUpdateCaseMutation : updateCaseMutation - const resultType = limitedAccess - ? 'limitedAccessUpdateCase' - : 'updateCase' - try { if (!id || Object.keys(updateCase).length === 0) { return @@ -263,7 +251,7 @@ const useCase = () => { const res = data as UpdateCaseMutation & LimitedAccessUpdateCaseMutation - return res && res[resultType] + return res?.[limitedAccess ? 'limitedAccessUpdateCase' : 'updateCase'] } catch (error) { toast.error(formatMessage(errors.updateCase)) } @@ -304,8 +292,8 @@ const useCase = () => { const res = data as TransitionCaseMutation & LimitedAccessTransitionCaseMutation - const state = res && res[resultType]?.state - const appealState = res && res[resultType]?.appealState + const state = res?.[resultType]?.state + const appealState = res?.[resultType]?.appealState if (!state && !appealState) { return false diff --git a/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx b/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx index edd04e4b7511..d0013695b275 100644 --- a/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx +++ b/apps/judicial-system/web/src/utils/hooks/useCaseList/index.tsx @@ -21,13 +21,11 @@ import { isTrafficViolationCase, } from '@island.is/judicial-system/types' import { errors } from '@island.is/judicial-system-web/messages' -import { UserContext } from '@island.is/judicial-system-web/src/components' -import { useCaseLazyQuery } from '@island.is/judicial-system-web/src/components/FormProvider/case.generated' -import { useLimitedAccessCaseLazyQuery } from '@island.is/judicial-system-web/src/components/FormProvider/limitedAccessCase.generated' import { - CaseAppealState, - User, -} from '@island.is/judicial-system-web/src/graphql/schema' + FormContext, + UserContext, +} from '@island.is/judicial-system-web/src/components' +import { CaseAppealState } from '@island.is/judicial-system-web/src/graphql/schema' import { TempCase as Case } from '@island.is/judicial-system-web/src/types' import { findFirstInvalidStep } from '../../formHelper' @@ -39,149 +37,118 @@ const useCaseList = () => { const [clickedCase, setClickedCase] = useState< [id: string | null, showLoading: boolean] >([null, false]) - const [openCaseInNewTab, setOpenCaseInNewTab] = useState(false) - const { user, limitedAccess } = useContext(UserContext) + const { getCase } = useContext(FormContext) const { formatMessage } = useIntl() const { isTransitioningCase, isSendingNotification } = useCase() const router = useRouter() - const [getLimitedAccessCase] = useLimitedAccessCaseLazyQuery({ - fetchPolicy: 'no-cache', - errorPolicy: 'all', - onCompleted: (limitedAccessCaseData) => { - if (user && limitedAccessCaseData?.limitedAccessCase) { - openCase(limitedAccessCaseData.limitedAccessCase as Case, user) - } - }, - onError: () => { - toast.error(formatMessage(errors.getCaseToOpen)) - }, - }) - - const [getCase] = useCaseLazyQuery({ - fetchPolicy: 'no-cache', - errorPolicy: 'all', - onCompleted: (caseData) => { - if (user && caseData?.case) { - openCase(caseData.case as Case, user) - } - }, - onError: () => { - toast.error(formatMessage(errors.getCaseToOpen)) - }, - }) - - const openCase = (caseToOpen: Case, user: User) => { - let routeTo = null - const isTrafficViolation = isTrafficViolationCase(caseToOpen) + const openCase = useCallback( + (caseToOpen: Case, openCaseInNewTab?: boolean) => { + let routeTo = null + const isTrafficViolation = isTrafficViolationCase(caseToOpen) - if (isDefenceUser(user)) { - if (isRequestCase(caseToOpen.type)) { - routeTo = DEFENDER_ROUTE - } else { - routeTo = DEFENDER_INDICTMENT_ROUTE - } - } else if (isPublicProsecutorUser(user)) { - // Public prosecutor users can only see completed indictments - routeTo = constants.PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE - } else if (isCourtOfAppealsUser(user)) { - // Court of appeals users can only see appealed request cases - if (caseToOpen.appealState === CaseAppealState.COMPLETED) { - routeTo = constants.COURT_OF_APPEAL_RESULT_ROUTE - } else { - routeTo = constants.COURT_OF_APPEAL_OVERVIEW_ROUTE - } - } else if (isDistrictCourtUser(user)) { - if (isRestrictionCase(caseToOpen.type)) { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + if (isDefenceUser(user)) { + if (isRequestCase(caseToOpen.type)) { + routeTo = DEFENDER_ROUTE } else { - routeTo = findFirstInvalidStep( - constants.courtRestrictionCasesRoutes, - caseToOpen, - ) + routeTo = DEFENDER_INDICTMENT_ROUTE } - } else if (isInvestigationCase(caseToOpen.type)) { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else if (isPublicProsecutorUser(user)) { + // Public prosecutor users can only see completed indictments + routeTo = constants.PUBLIC_PROSECUTOR_STAFF_INDICTMENT_OVERVIEW_ROUTE + } else if (isCourtOfAppealsUser(user)) { + // Court of appeals users can only see appealed request cases + if (caseToOpen.appealState === CaseAppealState.COMPLETED) { + routeTo = constants.COURT_OF_APPEAL_RESULT_ROUTE } else { - routeTo = findFirstInvalidStep( - constants.courtInvestigationCasesRoutes, - caseToOpen, - ) + routeTo = constants.COURT_OF_APPEAL_OVERVIEW_ROUTE } - } else { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.INDICTMENTS_COMPLETED_ROUTE + } else if (isDistrictCourtUser(user)) { + if (isRestrictionCase(caseToOpen.type)) { + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.courtRestrictionCasesRoutes, + caseToOpen, + ) + } + } else if (isInvestigationCase(caseToOpen.type)) { + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.courtInvestigationCasesRoutes, + caseToOpen, + ) + } } else { - // Route to Indictment Overview section since it always a valid step and - // would be skipped if we route to the last valid step - routeTo = constants.INDICTMENTS_COURT_OVERVIEW_ROUTE - } - } - } else { - // The user is a prosecution user - if (isRestrictionCase(caseToOpen.type)) { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE - } else { - routeTo = findFirstInvalidStep( - constants.prosecutorRestrictionCasesRoutes, - caseToOpen, - ) - } - } else if (isInvestigationCase(caseToOpen.type)) { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE - } else { - routeTo = findFirstInvalidStep( - constants.prosecutorInvestigationCasesRoutes, - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.INDICTMENTS_COMPLETED_ROUTE + } else { + // Route to Indictment Overview section since it always a valid step and + // would be skipped if we route to the last valid step + routeTo = constants.INDICTMENTS_COURT_OVERVIEW_ROUTE + } } } else { - if (isCompletedCase(caseToOpen.state)) { - routeTo = constants.CLOSED_INDICTMENT_OVERVIEW_ROUTE + // The user is a prosecution user + if (isRestrictionCase(caseToOpen.type)) { + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorRestrictionCasesRoutes, + caseToOpen, + ) + } + } else if (isInvestigationCase(caseToOpen.type)) { + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.SIGNED_VERDICT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorInvestigationCasesRoutes, + caseToOpen, + ) + } } else { - routeTo = findFirstInvalidStep( - constants.prosecutorIndictmentRoutes(isTrafficViolation), - caseToOpen, - ) + if (isCompletedCase(caseToOpen.state)) { + routeTo = constants.CLOSED_INDICTMENT_OVERVIEW_ROUTE + } else { + routeTo = findFirstInvalidStep( + constants.prosecutorIndictmentRoutes(isTrafficViolation), + caseToOpen, + ) + } } } - } - if (openCaseInNewTab) { - window.open(`${routeTo}/${caseToOpen.id}`, '_blank') - setOpenCaseInNewTab(false) - } else if (routeTo) { - router.push(`${routeTo}/${caseToOpen.id}`) - } - } + if (openCaseInNewTab) { + window.open(`${routeTo}/${caseToOpen.id}`, '_blank') + } else if (routeTo) { + router.push(`${routeTo}/${caseToOpen.id}`) + } + }, + [router, user], + ) const handleOpenCase = useCallback( (id: string, openInNewTab?: boolean) => { Promise.all(timeouts.map((timeout) => clearTimeout(timeout))) - if (openInNewTab === true) { - setOpenCaseInNewTab(openInNewTab) - } - if (clickedCase[0] !== id && !openInNewTab) { setClickedCase([id, false]) - timeouts.push( - setTimeout(() => { - setClickedCase([id, true]) - }, 2000), - ) + timeouts.push(setTimeout(() => setClickedCase([id, true]), 2000)) } const getCaseToOpen = (id: string) => { - limitedAccess - ? getLimitedAccessCase({ variables: { input: { id } } }) - : getCase({ variables: { input: { id } } }) + getCase( + id, + (caseData) => openCase(caseData, openInNewTab), + () => toast.error(formatMessage(errors.getCaseToOpen)), + ) } if ( @@ -197,11 +164,12 @@ const useCaseList = () => { }, [ clickedCase, + formatMessage, getCase, - getLimitedAccessCase, isSendingNotification, isTransitioningCase, limitedAccess, + openCase, timeouts, ], ) diff --git a/apps/judicial-system/web/src/utils/testHelpers.tsx b/apps/judicial-system/web/src/utils/testHelpers.tsx index 2ac5b5455299..2c51369653b9 100644 --- a/apps/judicial-system/web/src/utils/testHelpers.tsx +++ b/apps/judicial-system/web/src/utils/testHelpers.tsx @@ -40,6 +40,7 @@ export const FormContextWrapper = ({ caseNotFound: false, isCaseUpToDate: true, refreshCase: jest.fn(), + getCase: jest.fn(), }} > {children}