diff --git a/_mocks/featureToggles.ts b/_mocks/featureToggles.ts index e4b2a5ce6d..cbf80c7140 100644 --- a/_mocks/featureToggles.ts +++ b/_mocks/featureToggles.ts @@ -85,4 +85,8 @@ export const featureTogglesFactory = () => [ key: 'AKSJONSPUNKT_OVERLAPPENDE_SAKER', value: process.env.VITE_AKSJONSPUNKT_OVERLAPPENDE_SAKER, }, + { + key: 'BRUK_V2_SAK_SOK', + value: process.env.VITE_BRUK_V2_SAK_SOK, + }, ]; diff --git a/deploy/dev-fss-k9saksbehandling.yml b/deploy/dev-fss-k9saksbehandling.yml index e6905812c8..79f3912143 100644 --- a/deploy/dev-fss-k9saksbehandling.yml +++ b/deploy/dev-fss-k9saksbehandling.yml @@ -111,3 +111,5 @@ spec: value: "true" - name: AKSJONSPUNKT_OVERLAPPENDE_SAKER value: "true" + - name: BRUK_V2_SAK_SOK + value: "true" diff --git a/deploy/prod-fss-k9saksbehandling.yml b/deploy/prod-fss-k9saksbehandling.yml index 05e4c7de63..5313fe27a5 100644 --- a/deploy/prod-fss-k9saksbehandling.yml +++ b/deploy/prod-fss-k9saksbehandling.yml @@ -108,3 +108,5 @@ spec: value: "true" - name: AKSJONSPUNKT_OVERLAPPENDE_SAKER value: 'false' + - name: BRUK_V2_SAK_SOK + value: 'false' diff --git a/envDir/.env.development b/envDir/.env.development index df2349f153..ec1fa96adc 100644 --- a/envDir/.env.development +++ b/envDir/.env.development @@ -19,3 +19,4 @@ VITE_BRUK_V2_SAK_DOKUMENTER=true VITE_OPPTJENING_READ_ONLY_PERIODER=true VITE_BRUK_INNTEKTSGRADERING_I_UTTAK=true VITE_AKSJONSPUNKT_OVERLAPPENDE_SAKER=true +VITE_BRUK_V2_SAK_SOK=true diff --git a/feature-toggles.json b/feature-toggles.json index 7f8e80f06c..e3c333245d 100644 --- a/feature-toggles.json +++ b/feature-toggles.json @@ -90,5 +90,9 @@ { "key": "AKSJONSPUNKT_OVERLAPPENDE_SAKER", "value": "${AKSJONSPUNKT_OVERLAPPENDE_SAKER}" + }, + { + "key": "BRUK_V2_SAK_SOK", + "value": "${BRUK_V2_SAK_SOK}" } ] diff --git a/loosely-type-checked-files.json b/loosely-type-checked-files.json index 439fde3b21..ea1fbe2566 100644 --- a/loosely-type-checked-files.json +++ b/loosely-type-checked-files.json @@ -449,7 +449,6 @@ "packages/sak-app/src/data/useVisForhandsvisningAvMelding.tsx", "packages/sak-app/src/fagsak/FagsakIndex.tsx", "packages/sak-app/src/fagsak/useHentFagsakRettigheter.tsx", - "packages/sak-app/src/fagsakSearch/FagsakSearchIndex.tsx", "packages/sak-app/src/fagsakprofile/FagsakProfileIndex.spec.tsx", "packages/sak-app/src/fagsakprofile/FagsakProfileIndex.tsx", "packages/sak-behandling-velger/src/components/BehandlingFilter.tsx", diff --git a/packages/sak-app/src/fagsakSearch/FagsakSearchIndex.tsx b/packages/sak-app/src/fagsakSearch/FagsakSearchIndex.tsx index c83a7b9f23..178150543c 100644 --- a/packages/sak-app/src/fagsakSearch/FagsakSearchIndex.tsx +++ b/packages/sak-app/src/fagsakSearch/FagsakSearchIndex.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useContext, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router'; import FagsakSokSakIndex from '@fpsak-frontend/sak-sok'; @@ -6,6 +6,9 @@ import { errorOfType, ErrorTypes, getErrorResponseData } from '@k9-sak-web/rest- import { RestApiState, useRestApiErrorDispatcher } from '@k9-sak-web/rest-api-hooks'; import { Fagsak, KodeverkMedNavn } from '@k9-sak-web/types'; +import FagsakSøkSakIndexV2 from '@k9-sak-web/gui/sak/fagsakSøk/FagsakSøkSakIndex.js'; +import FeatureTogglesContext from '@k9-sak-web/gui/utils/featureToggles/FeatureTogglesContext.js'; +import { konverterKodeverkTilKode } from '@k9-sak-web/lib/kodeverk/konverterKodeverkTilKode.js'; import { pathToFagsak } from '../app/paths'; import { K9sakApiKeys, restApiHooks } from '../data/k9sakApi'; @@ -22,6 +25,8 @@ const FagsakSearchIndex = () => { K9sakApiKeys.KODEVERK, ); + const featureToggles = useContext(FeatureTogglesContext); + const navigate = useNavigate(); const { removeErrorMessages } = useRestApiErrorDispatcher(); const goToFagsak = (saksnummer: string) => { @@ -37,7 +42,7 @@ const FagsakSearchIndex = () => { } = restApiHooks.useRestApiRunner(K9sakApiKeys.SEARCH_FAGSAK); const searchResultAccessDenied = useMemo( - () => (errorOfType(error, ErrorTypes.MANGLER_TILGANG_FEIL) ? getErrorResponseData(error) : undefined), + () => (error && errorOfType(error, ErrorTypes.MANGLER_TILGANG_FEIL) ? getErrorResponseData(error) : undefined), [error], ); @@ -49,6 +54,21 @@ const FagsakSearchIndex = () => { } }, [sokFerdig, fagsaker]); + if (featureToggles?.BRUK_V2_SAK_SOK) { + const fagsakerV2 = JSON.parse(JSON.stringify(fagsaker)); + konverterKodeverkTilKode(fagsakerV2, false); + return ( + goToFagsak(saksnummer)} + searchStarted={sokeStatus === RestApiState.LOADING} + searchResultAccessDenied={searchResultAccessDenied} + /> + ); + } + return ( ( + + + + ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + fagsaker, + searchFagsakCallback: action('button-click'), + selectFagsakCallback: action('button-click'), + searchResultReceived: false, + searchStarted: false, + }, + play: async ({ canvas }) => { + expect(canvas.getByLabelText('Saksnummer eller fødselsnummer/D-nummer')).toBeInTheDocument(); + expect(canvas.getByRole('button', { name: 'Søk' })).toBeInTheDocument(); + expect(canvas.getByText('Pleiepenger sykt barn')).toBeInTheDocument(); + expect(canvas.getByText('Omsorgspenger')).toBeInTheDocument(); + }, +}; + +export const SøkUtenTreff: Story = { + args: { + fagsaker: [], + searchFagsakCallback: action('button-click'), + selectFagsakCallback: action('button-click'), + searchResultReceived: true, + searchStarted: true, + }, + play: async ({ canvas }) => { + expect(canvas.getByText('Søket ga ingen treff')).toBeInTheDocument(); + }, +}; + +export const SøkDerEnIkkeHarAdgang: Story = { + args: { + fagsaker: [], + searchFagsakCallback: action('button-click'), + selectFagsakCallback: action('button-click'), + searchResultReceived: false, + searchStarted: false, + searchResultAccessDenied: { + feilmelding: 'Har ikke adgang', + }, + }, + play: async ({ canvas }) => { + expect(canvas.getByText('Har ikke adgang')).toBeInTheDocument(); + }, +}; diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/FagsakS\303\270kSakIndex.tsx" "b/packages/v2/gui/src/sak/fagsakS\303\270k/FagsakS\303\270kSakIndex.tsx" new file mode 100644 index 0000000000..8a9ae492f6 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/FagsakS\303\270kSakIndex.tsx" @@ -0,0 +1,37 @@ +import React from 'react'; +import FagsakSearch from './components/FagsakSearch'; +import type { Fagsak } from './types/Fagsak'; + +interface OwnProps { + fagsaker?: Fagsak[]; + searchFagsakCallback: () => void; + searchResultReceived: boolean; + selectFagsakCallback: (e: React.SyntheticEvent, saksnummer: string) => void; + searchStarted?: boolean; + searchResultAccessDenied?: { + feilmelding: string; + }; +} + +/* + * NB! Denne komponenten blir kun brukt lokalt. I alle andre miljø brukes K9LOS + */ +const FagsakSøkSakIndexV2 = ({ + fagsaker = [], + searchFagsakCallback, + searchResultReceived, + selectFagsakCallback, + searchStarted = false, + searchResultAccessDenied, +}: OwnProps) => ( + +); + +export default FagsakSøkSakIndexV2; diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.spec.tsx" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.spec.tsx" new file mode 100644 index 0000000000..70bd69fd94 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.spec.tsx" @@ -0,0 +1,38 @@ +import { + BehandlingAksjonspunktDtoFagsakStatus as fagsakStatus, + MatchFagsakYtelseType as fagsakYtelseType, +} from '@k9-sak-web/backend/k9sak/generated'; +import { sortFagsaker } from './FagsakList'; + +describe('', () => { + it('skal sortere søkeresultat der avsluttede skal vises sist, mens sist endrede skal vises først', () => { + const fagsak = { + saksnummer: '12345', + sakstype: fagsakYtelseType.PLEIEPENGER_SYKT_BARN, + status: fagsakStatus.UNDER_BEHANDLING, + opprettet: '2019-02-17T13:49:18.645', + endret: '2019-02-17T13:49:18.645', + }; + const fagsak2 = { + saksnummer: '23456', + sakstype: fagsakYtelseType.PLEIEPENGER_SYKT_BARN, + status: fagsakStatus.UNDER_BEHANDLING, + opprettet: '2019-02-18T13:49:18.645', + endret: '2019-02-18T13:49:18.645', + }; + + const fagsak3 = { + saksnummer: '34567', + sakstype: fagsakYtelseType.PLEIEPENGER_SYKT_BARN, + status: fagsakStatus.AVSLUTTET, + opprettet: '2019-02-18T13:49:18.645', + endret: '2019-02-18T13:49:18.645', + }; + + const fagsaker = [fagsak, fagsak2, fagsak3]; + const sorterteFagsaker = sortFagsaker(fagsaker); + expect(sorterteFagsaker[0]).toEqual(fagsaker[1]); + expect(sorterteFagsaker[1]).toEqual(fagsaker[0]); + expect(sorterteFagsaker[2]).toEqual(fagsaker[2]); + }); +}); diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.tsx" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.tsx" new file mode 100644 index 0000000000..fe20c5a7cd --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakList.tsx" @@ -0,0 +1,80 @@ +import { BehandlingAksjonspunktDtoFagsakStatus as fagsakStatus } from '@k9-sak-web/backend/k9sak/generated'; +import { useKodeverkContext } from '@k9-sak-web/gui/kodeverk/index.js'; +import { KodeverkType } from '@k9-sak-web/lib/kodeverk/types.js'; +import { Table } from '@navikt/ds-react'; +import React from 'react'; +import type { Fagsak } from '../types/Fagsak'; +import styles from './fagsakList.module.css'; + +const headerTextCodes = ['Saksnummer', 'Sakstype', 'Status']; +const lagFagsakSortObj = (fagsak: Fagsak) => ({ + avsluttet: fagsak.status === fagsakStatus.AVSLUTTET, + endret: fagsak.endret ? fagsak.endret : fagsak.opprettet, +}); + +export const sortFagsaker = (fagsaker: Fagsak[]) => + fagsaker.toSorted((fagsak1, fagsak2) => { + const a = lagFagsakSortObj(fagsak1); + const b = lagFagsakSortObj(fagsak2); + if (a.avsluttet && !b.avsluttet) { + return 1; + } + if (!a.avsluttet && b.avsluttet) { + return -1; + } + if (a.endret && b.endret) { + if (a.endret > b.endret) { + return -1; + } + if (a.endret < b.endret) { + return 1; + } + } + return 0; + }); + +interface OwnProps { + fagsaker: Fagsak[]; + selectFagsakCallback: (e: React.SyntheticEvent, saksnummer: string) => void; +} + +/** + * FagsakList + * + * Presentasjonskomponent. Formaterer fagsak-søkeresultatet for visning i tabell. Sortering av fagsakene blir håndtert her. + */ +const FagsakList = ({ fagsaker, selectFagsakCallback }: OwnProps) => { + const { kodeverkNavnFraKode } = useKodeverkContext(); + + return ( + + + + {headerTextCodes.map(text => ( + + {text} + + ))} + + + + {sortFagsaker(fagsaker).map(fagsak => ( + selectFagsakCallback(event, fagsak.saksnummer)} + shadeOnHover={false} + > + {fagsak.saksnummer} + {kodeverkNavnFraKode(fagsak.sakstype, KodeverkType.FAGSAK_YTELSE)} + + {fagsak.status && kodeverkNavnFraKode(fagsak.status, KodeverkType.FAGSAK_STATUS)} + + + ))} + +
+ ); +}; + +export default FagsakList; diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakSearch.tsx" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakSearch.tsx" new file mode 100644 index 0000000000..12ab08d023 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/FagsakSearch.tsx" @@ -0,0 +1,52 @@ +import { BodyShort } from '@navikt/ds-react'; +import React from 'react'; +import type { Fagsak } from '../types/Fagsak'; +import FagsakList from './FagsakList'; +import styles from './fagsakSearch.module.css'; +import SearchForm from './SearchForm'; + +interface OwnProps { + fagsaker: Fagsak[]; + searchFagsakCallback: () => void; + searchResultReceived: boolean; + selectFagsakCallback: (e: React.SyntheticEvent, saksnummer: string) => void; + searchStarted: boolean; + searchResultAccessDenied?: { + feilmelding: string; + }; +} + +/** + * FagsakSearch + * + * Presentasjonskomponent. Denne setter sammen de ulike komponentene i søkebildet. + * Er søkeresultat mottatt vises enten trefflisten og relatert person, eller en tekst som viser ingen resultater. + */ +const FagsakSearch = ({ + fagsaker, + searchFagsakCallback, + searchResultReceived, + selectFagsakCallback, + searchStarted, + searchResultAccessDenied, +}: OwnProps) => ( +
+ + + {searchResultReceived && fagsaker.length === 0 && ( + + Søket ga ingen treff + + )} + +
+ {fagsaker.length > 1 && } +
+
+); + +export default FagsakSearch; diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/SearchForm.tsx" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/SearchForm.tsx" new file mode 100644 index 0000000000..9595e3b20f --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/SearchForm.tsx" @@ -0,0 +1,69 @@ +import { Alert, Button, Heading } from '@navikt/ds-react'; +import { Form, InputField } from '@navikt/ft-form-hooks'; +import { useForm } from 'react-hook-form'; +import { hasValidSaksnummerOrFodselsnummerFormat } from '../../../utils/validation/validators'; +import styles from './searchForm.module.css'; + +const isButtonDisabled = (searchStarted: boolean, searchString?: string): boolean => + !!(searchStarted || (searchString && searchString.length < 1)); + +interface SearchFormProps { + searchStarted: boolean; + searchResultAccessDenied?: { + feilmelding: string; + }; + onSubmit: (formValues: FormState) => void; +} + +interface FormState { + searchString?: string; +} + +/** + * SearchForm + * + * Presentasjonskomponent. Definerer søkefelt og tilhørende søkeknapp. + */ +export const SearchForm = ({ searchStarted, searchResultAccessDenied, onSubmit }: SearchFormProps) => { + const formMethods = useForm({ + defaultValues: { + searchString: '', + }, + }); + const [searchString] = formMethods.watch(['searchString']); + const handleSubmit = (formValues: FormState) => { + onSubmit(formValues); + }; + return ( + formMethods={formMethods} className={styles.container} onSubmit={handleSubmit}> + + Søk på sak eller person + +
+ (typeof s === 'string' ? s.trim() : s)} + label="Saksnummer eller fødselsnummer/D-nummer" + size="medium" + /> + +
+ {searchResultAccessDenied && ( + + {searchResultAccessDenied.feilmelding} + + )} + + ); +}; + +export default SearchForm; diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.css" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.css" new file mode 100644 index 0000000000..a668ba25c3 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.css" @@ -0,0 +1,3 @@ +.table { + width: 500px; +} diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.d.css.ts" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.d.css.ts" new file mode 100644 index 0000000000..0a597f3bd3 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakList.module.d.css.ts" @@ -0,0 +1,5 @@ +declare const styles: { + readonly "table": string; +}; +export = styles; + diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.css" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.css" new file mode 100644 index 0000000000..11f5a4a2c5 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.css" @@ -0,0 +1,15 @@ +.container { + background-color: var(--a-white); + border-radius: 4px; + margin-left: 100px; + margin-top: 20px; + max-width: 800px; + min-width: 500px; + padding-bottom: 10px; + padding-left: 20px; + padding-top: 10px; +} + +.label { + margin-bottom: 20px; +} diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.d.css.ts" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.d.css.ts" new file mode 100644 index 0000000000..44a9144cec --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/fagsakSearch.module.d.css.ts" @@ -0,0 +1,6 @@ +declare const styles: { + readonly "container": string; + readonly "label": string; +}; +export = styles; + diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.css" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.css" new file mode 100644 index 0000000000..7c1db688d5 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.css" @@ -0,0 +1,15 @@ +.container { + margin-bottom: 20px; + width: 550px; +} + +button.button { + margin-top: 2rem; +} + +.advarselIcon, +.feilmelding { + display: inline-block; + margin-right: 1ex; + vertical-align: text-bottom; +} diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.d.css.ts" "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.d.css.ts" new file mode 100644 index 0000000000..46b6f59801 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/components/searchForm.module.d.css.ts" @@ -0,0 +1,8 @@ +declare const styles: { + readonly "advarselIcon": string; + readonly "button": string; + readonly "container": string; + readonly "feilmelding": string; +}; +export = styles; + diff --git "a/packages/v2/gui/src/sak/fagsakS\303\270k/types/Fagsak.ts" "b/packages/v2/gui/src/sak/fagsakS\303\270k/types/Fagsak.ts" new file mode 100644 index 0000000000..73ac343702 --- /dev/null +++ "b/packages/v2/gui/src/sak/fagsakS\303\270k/types/Fagsak.ts" @@ -0,0 +1,9 @@ +import { type FagsakDto } from '@navikt/k9-sak-typescript-client'; + +export type Fagsak = { + endret: FagsakDto['endret']; + opprettet: FagsakDto['opprettet']; + saksnummer: FagsakDto['saksnummer']; + sakstype: FagsakDto['sakstype']; + status: FagsakDto['status']; +}; diff --git a/packages/v2/gui/src/utils/validation/regexes.ts b/packages/v2/gui/src/utils/validation/regexes.ts index 1138cb0978..fd43b41f40 100644 --- a/packages/v2/gui/src/utils/validation/regexes.ts +++ b/packages/v2/gui/src/utils/validation/regexes.ts @@ -6,3 +6,5 @@ // blir sendt. Dette bør derfor avklarast nærmare før ein evt tillater [] teikn i denne valideringa. export const invalidTextRegex = /[^0-9a-zA-ZæøåÆØÅAaÁáBbCcČčDdĐđEeFfGgHhIiJjKkLlMmNnŊŋOoPpRrSsŠšTtŦŧUuVvZzŽžéôèÉöüäÖÜÄ .'\-‐–‑/%§!?@_()#+:;,="&\s<>~*]/g; + +export const saksnummerOrFodselsnummerPattern = /^[a-zA-Z0-9_-]{0,18}$/; diff --git a/packages/v2/gui/src/utils/validation/validators.ts b/packages/v2/gui/src/utils/validation/validators.ts new file mode 100644 index 0000000000..171e82de08 --- /dev/null +++ b/packages/v2/gui/src/utils/validation/validators.ts @@ -0,0 +1,7 @@ +import { Dayjs } from 'dayjs'; +import { saksnummerOrFodselsnummerPattern } from './regexes'; +export const isEmpty = (text: string | number | Dayjs | null | undefined) => + text === null || text === undefined || text.toString().trim().length === 0; + +export const hasValidSaksnummerOrFodselsnummerFormat = (text: string) => + isEmpty(text) || saksnummerOrFodselsnummerPattern.test(text) ? null : 'Ugyldig saksnummer eller fødselsnummer'; diff --git a/packages/v2/tsconfig.json b/packages/v2/tsconfig.json index 7457764ca7..a36048f9f5 100644 --- a/packages/v2/tsconfig.json +++ b/packages/v2/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "target": "es2020", "module": "es2020", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "allowJs": true, "noEmit": true, @@ -31,5 +31,5 @@ "baseUrl": "packages/v2/", "skipLibCheck": true }, - "include": ["./**/*"], + "include": ["./**/*"] }