diff --git a/eslint.config.mjs b/eslint.config.mjs index 35f6b16c1..af3372744 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,6 +39,7 @@ export default [ "no-console": "warn", "@typescript-eslint/explicit-function-return-type": "off", "prettier/prettier": "warn", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], "import/order": [ "warn", diff --git a/orval.config.ts b/orval.config.ts index c4bcee9c4..e9bba3520 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -1,4 +1,6 @@ -export default { +import { defineConfig } from "orval"; + +export default defineConfig({ "innsyn-api": { input: "./innsyn-api.json", output: { @@ -24,10 +26,14 @@ export default { fetch: { includeHttpStatusReturnType: false, }, + query: { + useSuspenseQuery: true, + version: 5, + }, }, }, hooks: { afterAllFilesWrite: "prettier --write", }, }, -}; +}); diff --git a/package-lock.json b/package-lock.json index af5b5f280..e838a7d6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.17.0", "@faker-js/faker": "^8.4.1", + "@tanstack/react-query-devtools": "^5.64.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/jest": "^29.5.12", @@ -4399,26 +4400,57 @@ } }, "node_modules/@tanstack/query-core": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", + "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { "version": "5.62.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.16.tgz", - "integrity": "sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g==", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", + "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", + "dev": true, + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.63.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.63.0.tgz", - "integrity": "sha512-QWizLzSiog8xqIRYmuJRok9VELlXVBAwtINgVCgW1SNvamQwWDO5R0XFSkjoBEj53x9Of1KAthLRBUC5xmtVLQ==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", + "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz", + "integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.62.16" + "@tanstack/query-devtools": "5.62.16" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { + "@tanstack/react-query": "^5.64.1", "react": "^18 || ^19" } }, diff --git a/package.json b/package.json index a88731234..070e9f0cf 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.17.0", "@faker-js/faker": "^8.4.1", + "@tanstack/react-query-devtools": "^5.64.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/jest": "^29.5.12", diff --git a/public/locales/en/utbetalinger.json b/public/locales/en/utbetalinger.json index 4f6e98ec8..45bcc7cf9 100644 --- a/public/locales/en/utbetalinger.json +++ b/public/locales/en/utbetalinger.json @@ -14,8 +14,8 @@ "filter.format":"(dd.mm.yyyy)", "filter.mottaker":"Select recipient", "filter.alle":"All", - "filter.minKonto":"To me", - "filter.annen":"To other recipients", + "filter.mottaker.minKonto":"To me", + "filter.mottaker.annenMottaker":"To other recipients", "filter.ugylding":"The date should be in the format dd.mm.yyyy", "filter.tidligstFra":"The earliest search date is 15 months back", "filter.fraEtterTil":"The From date cannot be after the To date", @@ -24,13 +24,13 @@ "modal.avbryt": "Cancel", "modal.vis": "Show matches", "feil.fetch" : "Something went wrong when we tried to retrieve your payments. Please try again later.", - "feil.ingen":"We could not find any payments", - "feil.ingen.filter":"for selected filtering", - "feil.ingen.default.tidligere":"for the last 15 months", - "feil.ingen.default.nye":"for current or coming months", - "stoppet": "Stopped", - "planlagt": "Planned", - "utbetalt" : "Paid", + "feil.ingen.filter":"We could not find any payments for selected filtering", + "feil.ingen.default.tidligere":"We could not find any payments for the last 15 months", + "feil.ingen.default.nye":"We could not find any payments for current or coming months", + "utbetalingStatus.STOPPET": "Stopped", + "utbetalingStatus.PLANLAGT_UTBETALING": "Planned", + "utbetalingStatus.UTBETALT": "Paid", + "utbetalingStatus.ANNULLERT": "Annulled", "periode" : "Period", "mottaker" : "Recipient", "tilDeg": "To you", diff --git a/public/locales/nb/utbetalinger.json b/public/locales/nb/utbetalinger.json index d34a58997..54fd952fc 100644 --- a/public/locales/nb/utbetalinger.json +++ b/public/locales/nb/utbetalinger.json @@ -14,8 +14,8 @@ "filter.format":"(dd.mm.åååå)", "filter.mottaker":"Velg mottaker", "filter.alle":"Alle", - "filter.minKonto":"Til meg", - "filter.annen":"Til andre mottakere", + "filter.mottaker.minKonto":"Til meg", + "filter.mottaker.annenMottaker":"Til andre mottakere", "filter.ugylding":"Datoen må være i formatet dd.mm.åååå", "filter.tidligstFra":"Tidligste søkedato er 15 måneder tilbake i tid", "filter.fraEtterTil":"Fra-dato kan ikke være etter til-dato", @@ -24,13 +24,13 @@ "modal.avbryt": "Avbryt", "modal.vis": "Vis treff", "feil.fetch" : "Noe gikk galt da vi skulle hente utbetalingene dine. Vennligst prøv igjen senere.", - "feil.ingen":"Vi fant ingen utbetalinger", - "feil.ingen.filter":"for valgt filtrering", - "feil.ingen.default.tidligere":"for de siste 15 måneder", - "feil.ingen.default.nye":"for nåværende eller kommende måneder", - "stoppet": "Stoppet", - "planlagt": "Planlagt", - "utbetalt" : "Utbetalt", + "feil.ingen.filter":"Vi fant ingen utbetalinger for valgt filtrering", + "feil.ingen.default.tidligere":"Vi fant ingen utbetalinger for de siste 15 måneder", + "feil.ingen.default.nye":"Vi fant ingen utbetalinger for nåværende eller kommende måneder", + "utbetalingStatus.STOPPET": "Stoppet", + "utbetalingStatus.PLANLAGT_UTBETALING": "Planlagt", + "utbetalingStatus.UTBETALT": "Utbetalt", + "utbetalingStatus.ANNULLERT": "Annullert", "periode" : "Periode", "mottaker" : "Mottaker", "tilDeg": "Til deg", diff --git a/public/locales/nn/utbetalinger.json b/public/locales/nn/utbetalinger.json index 5bcee70b2..aa0805eba 100644 --- a/public/locales/nn/utbetalinger.json +++ b/public/locales/nn/utbetalinger.json @@ -14,8 +14,8 @@ "filter.format":"(dd.mm.åååå)", "filter.mottaker":"Vel mottakar", "filter.alle":"Alle", - "filter.minKonto":"Til meg", - "filter.annen":"Til andre mottakarar", + "filter.mottaker.minKonto":"Til meg", + "filter.mottaker.annenMottaker":"Til andre mottakarar", "filter.ugylding":"Datoen må vere i formatet dd.mm.åååå", "filter.tidligstFra":"Tidlegaste søkjedato er 15 månader bak i tid", "filter.fraEtterTil":"Frå-datoen kan ikkje vere etter til-datoen", @@ -24,13 +24,13 @@ "modal.avbryt": "Avbryt", "modal.vis": "Vis treff", "feil.fetch" : "Noko gjekk gale då vi skulle hente utbetalingane dine. Prøv igjen seinare.", - "feil.ingen":"Vi fann ingen utbetalingar", - "feil.ingen.filter":"med filteret som er valt", - "feil.ingen.default.tidligere":"for dei siste 15 månadene", - "feil.ingen.default.nye":"for inneverande eller komande månader", - "stoppet": "Stoppa", - "planlagt": "Planlagt", - "utbetalt" : "Utbetalt", + "feil.ingen.filter":"Vi fann ingen utbetalingar med filteret som er valt", + "feil.ingen.default.tidligere":"Vi fann ingen utbetalingar for dei siste 15 månadene", + "feil.ingen.default.nye":"Vi fann ingen utbetalingar for inneverande eller komande månader", + "utbetalingStatus.STOPPET": "Stoppa", + "utbetalingStatus.PLANLAGT_UTBETALING": "Planlagt", + "utbetalingStatus.UTBETALT": "Utbetalt", + "utbetalingStatus.ANNULLERT": "Annullert", "periode" : "Periode", "mottaker" : "Mottakar", "tilDeg": "Til deg", diff --git a/src/components/errors/ErrorBoundary.tsx b/src/components/errors/ErrorBoundary.tsx index e8bfe226d..f5c50536e 100644 --- a/src/components/errors/ErrorBoundary.tsx +++ b/src/components/errors/ErrorBoundary.tsx @@ -5,35 +5,30 @@ import ServerError from "../../pages/500"; interface Props { children?: ReactNode; + fallback?: ReactNode; } interface State { - hasError: boolean; + error: Error | null; } class ErrorBoundary extends React.Component { constructor(props: Props) { super(props); - // Define a state variable to track whether is an error or not - this.state = { hasError: false }; + this.state = { error: null }; } - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - static getDerivedStateFromError(_: Error) { + static getDerivedStateFromError(error: Error) { // Update state so the next render will show the fallback UI - - return { hasError: true }; + return { error }; } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { logger.error(`Uncaught clientside error: ${error}, errorInfo: ${JSON.stringify(errorInfo)}`); } public render() { - if (this.state.hasError) { - return ; - } - + if (!!this.state.error) return this.props.fallback ?? ; return this.props.children; } } diff --git a/src/pages/500.tsx b/src/pages/500.tsx index 707849bf7..0ee40753f 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -15,12 +15,10 @@ const ServerError = (): React.JSX.Element => (
- <> - - Beklager, vi har dessverre tekniske problemer. - - Vennligst prøv igjen senere. - + + Beklager, vi har dessverre tekniske problemer. + + Vennligst prøv igjen senere.
diff --git a/src/pages/api/innsyn-api/[...slug].ts b/src/pages/api/innsyn-api/[...slug].ts index f91619207..0d2acf1f1 100644 --- a/src/pages/api/innsyn-api/[...slug].ts +++ b/src/pages/api/innsyn-api/[...slug].ts @@ -16,7 +16,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } if (!token) { - return res.status(401); + res.status(401); + return; } if (!slug) { res.status(400).json({ message: "Manglende path" }); diff --git a/src/pages/utbetaling.tsx b/src/pages/utbetaling.tsx index b03452b66..2e150cb15 100644 --- a/src/pages/utbetaling.tsx +++ b/src/pages/utbetaling.tsx @@ -18,10 +18,9 @@ import { } from "../generated/soknad-med-innsyn-controller/soknad-med-innsyn-controller"; import UtbetalingsoversiktIngenSoknader from "../utbetalinger/UtbetalingsoversiktIngenSoknader"; import UtbetalingsoversiktIngenInnsyn from "../utbetalinger/UtbetalingsoversiktIngenInnsyn"; -import { FilterProvider } from "../utbetalinger/beta/filter/FilterContext"; -import UtbetalingerFilter from "../utbetalinger/beta/filter/UtbetalingerFilter"; -import UtbetalingerPanelBeta from "../utbetalinger/beta/UtbetalingerPanelBeta"; -import styles from "../utbetalinger/beta/utbetalinger.module.css"; +import UtbetalingerFilter from "../utbetalinger/filter/UtbetalingerFilter"; +import UtbetalingerPanel from "../utbetalinger/UtbetalingerPanel"; +import styles from "../utbetalinger/utbetalinger.module.css"; import useUpdateBreadcrumbs from "../hooks/useUpdateBreadcrumbs"; import pageHandler, { buildUrl } from "../pagehandler/pageHandler"; import { extractAuthHeader } from "../utils/authUtils"; @@ -32,6 +31,7 @@ import { getHentTidligereUtbetalingerQueryKey, getHentTidligereUtbetalingerUrl, } from "../generated/utbetalinger-controller/utbetalinger-controller"; +import { FilterProvider } from "../utbetalinger/filter/FilterProvider"; import Error from "./_error"; @@ -55,7 +55,7 @@ const Utbetalinger: NextPage = () => { if (isAlleSakerLoading || isHarSoknaderMedInnsynLoading) { return (
- +
); } @@ -83,13 +83,13 @@ const Utbetalinger: NextPage = () => {
-
+
{!isMobile && ( - + )} - +
diff --git a/src/utbetalinger/Utbetalinger.test.ts b/src/utbetalinger/Utbetalinger.test.ts deleted file mode 100644 index 6a5395ae7..000000000 --- a/src/utbetalinger/Utbetalinger.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { UtbetalingerResponse } from "../generated/model"; - -import { mockUtbetalinger, summerAntallUtbetalinger } from "./Utbetalinger.testdata"; -import { filtrerUtbetalingerForTidsinterval, filtrerUtbetalingerPaaMottaker } from "./utbetalingerUtils"; -import { utbetalingsdetaljerDefaultAapnet } from "./beta/tabs/UtbetalingAccordionItem"; - -it("should filter by time interval", () => { - const utbetalingerMaaned: UtbetalingerResponse[] = mockUtbetalinger; - expect(utbetalingerMaaned.length).toBe(3); - const now: Date = new Date("2019-12-01"); - expect(filtrerUtbetalingerForTidsinterval(utbetalingerMaaned, 6, now).length).toBe(3); -}); - -it("should filter by receiver of money", () => { - const utbetalingerMaaned: UtbetalingerResponse[] = mockUtbetalinger; - expect(summerAntallUtbetalinger(utbetalingerMaaned)).toBe(5); - expect(summerAntallUtbetalinger(filtrerUtbetalingerPaaMottaker(utbetalingerMaaned, true, false))).toBe(3); - expect(summerAntallUtbetalinger(filtrerUtbetalingerPaaMottaker(utbetalingerMaaned, false, false))).toBe(0); - expect(summerAntallUtbetalinger(filtrerUtbetalingerPaaMottaker(utbetalingerMaaned, false, true))).toBe(2); -}); - -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato er 18 dager tilbake i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2023-12-27")).toBe(false); -}); - -it("Utbetalingsdetaljer skal være lukket når utbetalingsdato er 16 dager tilbake i tid, datoer samme måned", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-20"), "2024-01-04")).toBe(false); -}); - -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato er 16 dager tilbake i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2023-12-29")).toBe(false); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er 15 dager tilbake i tid, datoer samme måned", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-20"), "2024-01-05")).toBe(true); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er 15 dager tilbake i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2023-12-30")).toBe(true); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er 14 dager tilbake i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2023-12-31")).toBe(true); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er dags dato", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-01-14")).toBe(true); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er 14 dager frem i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-01-28")).toBe(true); -}); - -it("Utbetalingsdetaljer skal være åpen når utbetalingsdato er 15 dager frem i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-01-29")).toBe(true); -}); - -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato er 16 dager frem i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-01-30")).toBe(false); -}); - -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato er 18 dager frem i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-02-02")).toBe(false); -}); - -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato er 18 dager frem i tid", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "2024-02-02")).toBe(false); -}); -it("Utbetalingsdetaljer skal ikke være åpen når utbetalingsdato ikke er definert", () => { - expect(utbetalingsdetaljerDefaultAapnet(new Date("2024-01-14"), "")).toBe(false); -}); diff --git a/src/utbetalinger/Utbetalinger.testdata.ts b/src/utbetalinger/Utbetalinger.testdata.ts deleted file mode 100644 index 671e54778..000000000 --- a/src/utbetalinger/Utbetalinger.testdata.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { UtbetalingerResponse } from "../generated/model"; - -// Example: getRandomInt(3) => expected output: 0, 1 or 2 -const getRandomInt = (max: number): number => { - return Math.floor(Math.random() * Math.floor(max)); -}; - -const summerAntallUtbetalinger = (utbetalingerMaaned: UtbetalingerResponse[]) => { - let antallUtbetalinger: number = 0; - utbetalingerMaaned.map((utbetalingMaaned: UtbetalingerResponse) => { - antallUtbetalinger = antallUtbetalinger + utbetalingMaaned.utbetalinger.length; - return utbetalingMaaned; - }); - return antallUtbetalinger; -}; - -const mockUtbetalinger: (UtbetalingerResponse & { sum: number })[] = [ - { - ar: 2019, - maned: 10, - foersteIManeden: "2019-10-01", - sum: 13234.0, - utbetalinger: [ - { - tittel: "Utbetaling til søker", - belop: 1234.0, - utbetalingsdato: "2019-08-20", - status: "ANNULLERT", - fiksDigisosId: "ce3f24a0-359e-45f3-a7f7-5123e70cb715", - fom: "2019-09-01", - tom: "2019-09-30", - mottaker: "søkers fnr", - annenMottaker: true, - kontonummer: "11223344556", - forfallsdato: "2019-08-20", - utbetalingsmetode: "bankoverføring", - }, - { - tittel: "Utbetaling til utleier - husleie", - belop: 12000.0, - utbetalingsdato: "2019-08-01", - status: "UTBETALT", - fiksDigisosId: "ce3f24a0-359e-45f3-a7f7-5123e70cb715", - mottaker: "Utleier", - annenMottaker: true, - forfallsdato: "2019-08-20", - utbetalingsmetode: "bankoverføring", - }, - ], - }, - { - ar: 2019, - maned: 9, - foersteIManeden: "2019-09-01", - sum: 0.0, - utbetalinger: [ - { - tittel: "Annullert utbetaling", - belop: 1234.0, - utbetalingsdato: "2019-09-04", - status: "ANNULLERT", - fiksDigisosId: "ce3f24a0-359e-45f3-a7f7-5123e70cb715", - fom: "2019-09-01", - tom: "2019-10-31", - mottaker: "søkers fnr", - annenMottaker: false, - forfallsdato: "2019-08-20", - utbetalingsmetode: "bankoverføring", - }, - ], - }, - { - ar: 2018, - maned: 8, - foersteIManeden: "2019-08-01", - sum: 13234.0, - utbetalinger: [ - { - tittel: "Utbetaling til søker", - belop: 1234.0, - utbetalingsdato: "2019-08-20", - status: "UTBETALT", - fiksDigisosId: "ce3f24a0-359e-45f3-a7f7-5123e70cb715", - fom: "2019-09-01", - tom: "2019-09-30", - mottaker: "19066711222", - annenMottaker: false, - kontonummer: "11223344556", - forfallsdato: "2019-08-20", - utbetalingsmetode: "bankoverføring", - }, - { - tittel: "Utbetaling til utleier - husleie", - belop: 12000.0, - utbetalingsdato: "2019-08-01", - status: "UTBETALT", - fiksDigisosId: "ce3f24a0-359e-45f3-a7f7-5123e70cb715", - mottaker: "Utleier", - annenMottaker: false, - forfallsdato: "2019-08-20", - utbetalingsmetode: "bankoverføring", - }, - ], - }, -]; - -export { mockUtbetalinger, getRandomInt, summerAntallUtbetalinger }; diff --git a/src/utbetalinger/UtbetalingerPanel.tsx b/src/utbetalinger/UtbetalingerPanel.tsx new file mode 100644 index 000000000..72946bb99 --- /dev/null +++ b/src/utbetalinger/UtbetalingerPanel.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { BodyLong, Heading, Panel, Tabs } from "@navikt/ds-react"; +import { useTranslation } from "next-i18next"; + +import HandCoinsIcon from "../components/ikoner/HandCoins"; +import { logAmplitudeEvent } from "../utils/amplitude"; +import useIsMobile from "../utils/useIsMobile"; + +import UtbetalingerNye from "./tabs/UtbetalingerNye"; +import { UtbetalingerTidligere } from "./tabs/UtbetalingerTidligere"; +import FilterModal from "./filter/FilterModal"; + +const TAB_UTBETALINGER = "Utbetalinger" as const; +const TAB_TIDLIGERE = "Tidligere utbetalinger" as const; + +const UtbetalingerPanel = () => { + const { t } = useTranslation("utbetalinger"); + + const logTabChange = (tab: string) => logAmplitudeEvent("Klikket tab", { tab }); + const isMobile = useIsMobile(); + return ( + + + + {t("tittel.inne")} + +
+ {isMobile && } + + + + + + + {t("utbetalingerIngress")} + + + + {t("tidligereIngress")} + + + + + ); +}; + +export default UtbetalingerPanel; diff --git a/src/utbetalinger/UtbetalingsoversiktIngenInnsyn.tsx b/src/utbetalinger/UtbetalingsoversiktIngenInnsyn.tsx index e17bdfc48..02c9417be 100644 --- a/src/utbetalinger/UtbetalingsoversiktIngenInnsyn.tsx +++ b/src/utbetalinger/UtbetalingsoversiktIngenInnsyn.tsx @@ -1,38 +1,24 @@ import React from "react"; import { BodyLong, Heading } from "@navikt/ds-react"; -import styled from "styled-components"; import { useTranslation } from "next-i18next"; import { StyledGuidePanel } from "../styles/styledGuidePanel"; import IngenSoknaderFunnet from "../components/ikoner/IngenSoknaderFunnet"; -const StyledGuidePanelContent = styled.div` - display: flex; - flex-direction: column; - align-items: center; - margin: 0 3rem; - max-width: 45rem; -`; - -const Wrapper = styled.div` - padding-top: 1rem; - padding-bottom: 50px; -`; - const UtbetalingsoversiktIngenInnsyn = () => { const { t } = useTranslation(); return ( - +
}> - +
{t("ingen_soknad.tittel")} {t("ingen_soknad.info")} - +
- +
); }; diff --git a/src/utbetalinger/beta/UtbetalingerPanelBeta.tsx b/src/utbetalinger/beta/UtbetalingerPanelBeta.tsx deleted file mode 100644 index e1b12c47d..000000000 --- a/src/utbetalinger/beta/UtbetalingerPanelBeta.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { BodyLong, Heading, Panel, Tabs } from "@navikt/ds-react"; -import { useTranslation } from "next-i18next"; -import styled from "styled-components"; - -import HandCoinsIcon from "../../components/ikoner/HandCoins"; -import { useHentNyeUtbetalinger } from "../../generated/utbetalinger-controller/utbetalinger-controller"; -import { logAmplitudeEvent } from "../../utils/amplitude"; -import useIsMobile from "../../utils/useIsMobile"; -import { ManedUtbetaling, NyeOgTidligereUtbetalingerResponse } from "../../generated/model"; - -import styles from "./utbetalinger.module.css"; -import useFiltrerteUtbetalinger from "./filter/useFiltrerteUtbetalinger"; -import NyeUtbetalinger from "./tabs/NyeUtbetalinger"; -import TidligereUtbetalinger from "./tabs/TidligereUtbetalinger"; -import FilterModal from "./filter/FilterModal"; - -enum TAB_VALUE { - UTBETALINGER = "Utbetalinger", - TIDLIGERE = "Tidligere utbetalinger", -} - -export interface UtbetalingMedId extends ManedUtbetaling { - id: string; -} - -export interface UtbetalingerResponseMedId extends Omit { - utbetalingerForManed: UtbetalingMedId[]; -} -const StyledSpace = styled.div` - @media screen and (max-width: 769px) { - padding: 1rem 0 0 0; - } - - @media screen and (min-width: 769px) { - padding: 3rem 0 0 0; - } -`; - -const UtbetalingerPanelBeta = () => { - const [nyeLogged, setNyeLogged] = useState(false); - - const [tabClicked, setTabClicked] = useState(TAB_VALUE.UTBETALINGER); - - const { t } = useTranslation("utbetalinger"); - const { - data: nye, - isLoading: henterNye, - isError: hentNyeFeilet, - } = useHentNyeUtbetalinger({ - query: { - select: (data) => { - // Legg på en id på hver utbetaling - return data.map((item) => { - return { - ...item, - utbetalingerForManed: item.utbetalingerForManed.map((utbetaling: ManedUtbetaling) => { - return { - ...utbetaling, - id: crypto.randomUUID(), - }; - }), - }; - }); - }, - }, - }); - - useEffect(() => { - if (!nyeLogged && nye && nye.length > 0) { - const sisteManedgruppe = nye[nye.length - 1].utbetalingerForManed; - const sisteDatoVist = - sisteManedgruppe[sisteManedgruppe.length - 1].utbetalingsdato ?? - sisteManedgruppe[sisteManedgruppe.length - 1].forfallsdato; - logAmplitudeEvent("Hentet nye utbetalinger", { sisteDatoVist }); - setNyeLogged(true); - } - logAmplitudeEvent("Lastet utbetalinger", { - antall: nye?.[0]?.utbetalingerForManed.length ? nye?.[0].utbetalingerForManed.length : 0, - }); - }, [nye, nyeLogged]); - - const filtrerteNye = useFiltrerteUtbetalinger(nye ?? []); - - const logTabChange = (tabPath: string) => { - logAmplitudeEvent("Klikket tab", { tab: tabPath }); - }; - const isMobile = useIsMobile(); - return ( - - - - {t("tittel.inne")} - - - {isMobile && } - logTabChange(path)}> - - { - setTabClicked(TAB_VALUE.UTBETALINGER); - }} - className={`${ - tabClicked === TAB_VALUE.UTBETALINGER ? styles.tab_list_blue : styles.tab_list_transparent - }`} - /> - - { - setTabClicked(TAB_VALUE.TIDLIGERE); - }} - className={`${ - tabClicked === TAB_VALUE.TIDLIGERE ? styles.tab_list_blue : styles.tab_list_transparent - }`} - /> - - - {t("utbetalingerIngress")} - - - - - - - - ); -}; - -export default UtbetalingerPanelBeta; diff --git a/src/utbetalinger/beta/filter/FilterContext.tsx b/src/utbetalinger/beta/filter/FilterContext.tsx deleted file mode 100644 index d50add514..000000000 --- a/src/utbetalinger/beta/filter/FilterContext.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PropsWithChildren, useContext, useState } from "react"; - -export enum MottakerFilter { - Alle = "ALLE", - MinKonto = "MIN_KONTO", - AnnenMottaker = "ANNEN_MOTTAKER", -} -export interface FilterKey { - mottaker: MottakerFilter; - fraDato?: Date; - tilDato?: Date; -} -type FilterContextType = { - filter: FilterKey; - oppdaterFilter: (nyttFilter: Partial) => void; - isUsingFilter: boolean; -}; - -const FilterContext = React.createContext(undefined); - -// Egen hook fordi det sjekkes at den blir brukt riktig, og kan ha undefined som defaultValue -export const useFilter = () => { - const context = useContext(FilterContext); - if (context === undefined) { - throw new Error("Kan kun brukes innenfor FilterProvider"); - } - return context; -}; - -const initialState: FilterKey = { - mottaker: MottakerFilter.Alle, - tilDato: undefined, - fraDato: undefined, -}; -export const FilterProvider = (props: PropsWithChildren) => { - const [filter, setFilter] = useState(initialState); - - const oppdaterFilter = (nyttFilter: Partial) => { - const updatedFilter = { ...filter, ...nyttFilter }; - setFilter(updatedFilter); - }; - - return ( - - {props.children} - - ); -}; diff --git a/src/utbetalinger/beta/filter/FilterModal.tsx b/src/utbetalinger/beta/filter/FilterModal.tsx deleted file mode 100644 index b59e5b27a..000000000 --- a/src/utbetalinger/beta/filter/FilterModal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Box, Button, Chips, Modal } from "@navikt/ds-react"; -import React, { useState } from "react"; -import { FilterIcon } from "@navikt/aksel-icons"; -import { useTranslation } from "next-i18next"; - -import UtbetalingerFilter from "./UtbetalingerFilter"; -import { MottakerFilter, useFilter } from "./FilterContext"; -import useChips from "./useChips"; -import styles from "./utbetalingerFilter.module.css"; - -const FilterModal = () => { - const [open, setOpen] = useState(false); - const { oppdaterFilter } = useFilter(); - const { chips, removeChip } = useChips(); - const { t } = useTranslation("utbetalinger"); - - const onCancel = () => { - oppdaterFilter({ mottaker: MottakerFilter.Alle, fraDato: undefined, tilDato: undefined }); - setOpen(false); - }; - - return ( - <> - - {chips.length > 0 ? ( - - {chips.map((c) => ( - removeChip(c.filterType)}> - {c.label} - - ))} - - ) : ( - - )} - setOpen(false)} className={styles.modal}> - - - - - - - - ); -}; - -export default FilterModal; diff --git a/src/utbetalinger/beta/filter/UtbetalingerFilter.tsx b/src/utbetalinger/beta/filter/UtbetalingerFilter.tsx deleted file mode 100644 index 7f0814eea..000000000 --- a/src/utbetalinger/beta/filter/UtbetalingerFilter.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState } from "react"; -import { DatePicker, Fieldset, Radio, RadioGroup, useDatepicker } from "@navikt/ds-react"; -import { useTranslation } from "next-i18next"; - -import useIsMobile from "../../../utils/useIsMobile"; -import { logAmplitudeEvent } from "../../../utils/amplitude"; - -import { MottakerFilter, useFilter } from "./FilterContext"; -import styles from "./utbetalingerFilter.module.css"; - -function subtractMonths(date: Date, months: number) { - date.setMonth(date.getMonth() - months); - return date; -} - -const UtbetalingerFilter = () => { - const { filter, oppdaterFilter } = useFilter(); - const { t, i18n } = useTranslation("utbetalinger"); - - const isMobile = useIsMobile(); - const [fromDateError, setFromDateError] = useState(undefined); - const [toDateError, setToDateError] = useState(undefined); - - const fromDatePicker = useDatepicker({ - fromDate: subtractMonths(new Date(), 15), - toDate: filter.tilDato, - defaultSelected: filter.fraDato, - onDateChange: (dato?) => { - oppdaterFilter({ ...filter, fraDato: dato }); - logAmplitudeEvent("filtervalg", { kategori: "fraDato", filternavn: dato }); - }, - onValidate: (val) => { - if (val.isBefore) setFromDateError(t("filter.tidligstFra")); - else if (val.isAfter) setFromDateError(t("filter.fraEtterTil")); - else if (val.isInvalid) setFromDateError(t("filter.ugylding")); - else if (val.isEmpty) setFromDateError(undefined); - else if (!val.isValidDate) setFromDateError(t("filter.ugylding")); - else setFromDateError(undefined); - }, - }); - const toDatePicker = useDatepicker({ - fromDate: filter.fraDato ? filter.fraDato : subtractMonths(new Date(), 15), - defaultSelected: filter.tilDato, - onDateChange: (dato?) => { - oppdaterFilter({ ...filter, tilDato: dato }); - logAmplitudeEvent("filtervalg", { kategori: "tilDato", filternavn: dato }); - }, - onValidate: (val) => { - if (val.isBefore) setToDateError(filter.fraDato ? t("filter.tilEtterFra") : t("filter.tidligstFra")); - else if (val.isInvalid) setToDateError(t("filter.ugylding")); - else if (val.isEmpty) setToDateError(undefined); - else if (!val.isValidDate) setToDateError(t("filter.ugylding")); - else setToDateError(undefined); - }, - }); - - const onMottakerChanged = (value: MottakerFilter) => { - oppdaterFilter({ ...filter, mottaker: value }); - logAmplitudeEvent("filtervalg", { kategori: "mottaker", filternavn: value }); - }; - return ( -
-
- - - - - - -
- - {t("filter.alle")} - {t("filter.minKonto")} - {t("filter.annen")} - -
- ); -}; -export default UtbetalingerFilter; diff --git a/src/utbetalinger/beta/filter/useChips.tsx b/src/utbetalinger/beta/filter/useChips.tsx deleted file mode 100644 index 227c6d3fa..000000000 --- a/src/utbetalinger/beta/filter/useChips.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "next-i18next"; -import { i18n } from "i18next"; - -import { dateToDDMMYYYY } from "../../../utils/formatting"; - -import { MottakerFilter, useFilter } from "./FilterContext"; - -const mottakerFilterToChip = (value: MottakerFilter, t: (key: string) => string) => { - switch (value) { - case MottakerFilter.Alle: - return undefined; - case MottakerFilter.MinKonto: - return { label: t("utbetalinger:filter.minKonto"), filterType: "mottaker" } as ChipType; - case MottakerFilter.AnnenMottaker: - return { label: t("utbetalinger:filter.annen"), filterType: "mottaker" } as ChipType; - } -}; -const datoFilterToChip = (i18n: i18n, fom?: Date, tom?: Date) => { - if (fom && tom) { - return { - label: `${dateToDDMMYYYY(i18n.language, fom)} - ${dateToDDMMYYYY(i18n.language, tom)}`, - filterType: "dato", - } as ChipType; - } else if (fom) { - return { - label: `${i18n.t("utbetalinger:filter.fra")}: ${dateToDDMMYYYY(i18n.language, fom)}`, - filterType: "dato", - } as ChipType; - } else if (tom) { - return { - label: `${i18n.t("utbetalinger:filter.til")}: ${dateToDDMMYYYY(i18n.language, tom)}`, - filterType: "dato", - } as ChipType; - } - return undefined; -}; - -type FilterType = "mottaker" | "dato"; -interface ChipType { - label: string; - filterType: FilterType; -} -const useChips = () => { - const [chips, setChips] = useState([]); - const { filter, oppdaterFilter } = useFilter(); - const { t, i18n } = useTranslation(); - - const removeChip = useCallback( - (type: FilterType) => { - if (type === "mottaker") { - oppdaterFilter({ ...filter, mottaker: MottakerFilter.Alle }); - } else if (type === "dato") { - oppdaterFilter({ ...filter, tilDato: undefined, fraDato: undefined }); - } - }, - [filter, oppdaterFilter] - ); - useEffect(() => { - const mottaker: ChipType | undefined = mottakerFilterToChip(filter.mottaker, t); - const dato = datoFilterToChip(i18n, filter.fraDato, filter.tilDato); - - // remove empty string - setChips([mottaker, dato].filter(Boolean) as ChipType[]); - }, [filter, i18n, t]); - return { chips, removeChip }; -}; -export default useChips; diff --git a/src/utbetalinger/beta/filter/useFiltrerteUtbetalinger.ts b/src/utbetalinger/beta/filter/useFiltrerteUtbetalinger.ts deleted file mode 100644 index a32cda121..000000000 --- a/src/utbetalinger/beta/filter/useFiltrerteUtbetalinger.ts +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import { isAfter, isBefore, isEqual } from "date-fns"; - -import { ManedUtbetaling } from "../../../generated/model"; -import { UtbetalingerResponseMedId } from "../UtbetalingerPanelBeta"; - -import { FilterKey, MottakerFilter, useFilter } from "./FilterContext"; - -const stringToDateWithoutTimezone = (datoString: string) => { - const dateWithTimesone = new Date(datoString); - return new Date(dateWithTimesone.toISOString().slice(0, -1)); -}; - -export const filterMatch = (utbetaling: ManedUtbetaling, filter: FilterKey) => { - let matchMottaker; - if (filter.mottaker === MottakerFilter.Alle) { - matchMottaker = true; - } else if (filter.mottaker === MottakerFilter.AnnenMottaker) { - matchMottaker = utbetaling.annenMottaker; - } else { - matchMottaker = !utbetaling.annenMottaker; - } - - // Hvis vi ikke har dato-filter eller utbetalingsdato/forfallsdato, trenger vi ikke sjekke datofilteret. - if ((!utbetaling.utbetalingsdato && !utbetaling.forfallsdato) || (!filter.tilDato && !filter.fraDato)) - return matchMottaker; - - const dato = stringToDateWithoutTimezone(utbetaling.utbetalingsdato ?? utbetaling.forfallsdato!); - const matchFra = filter.fraDato ? isAfter(dato, filter.fraDato) || isEqual(dato, filter.fraDato) : true; - const matchTil = filter.tilDato ? isBefore(dato, filter.tilDato) || isEqual(dato, filter.tilDato) : true; - - return matchMottaker && matchTil && matchFra; -}; -const useFiltrerteUtbetalinger = (utbetalinger: UtbetalingerResponseMedId[]) => { - const { filter } = useFilter(); - - return React.useMemo(() => { - return utbetalinger - .map((response) => { - const filtrertPerManed = response.utbetalingerForManed.filter((utbetaling) => { - return filterMatch(utbetaling, filter); - }); - return { ...response, utbetalingerForManed: filtrertPerManed }; - }) - .filter((response) => response.utbetalingerForManed.length > 0); - }, [utbetalinger, filter]); -}; - -export default useFiltrerteUtbetalinger; diff --git a/src/utbetalinger/beta/filter/utbetalingerFilter.module.css b/src/utbetalinger/beta/filter/utbetalingerFilter.module.css deleted file mode 100644 index 861dccde5..000000000 --- a/src/utbetalinger/beta/filter/utbetalingerFilter.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.utbetalinger_filter { - width: fit-content; - margin-bottom: 2rem; -} - -.periodevelger { - margin-bottom: 2rem; - width: min-content; -} - -.chips { - margin: 0.5rem 0; -} - -.modal_content { - padding: 20px 24px; -} diff --git a/src/utbetalinger/beta/filter/utbetalingerFilter.test.tsx b/src/utbetalinger/beta/filter/utbetalingerFilter.test.tsx deleted file mode 100644 index 88ae5b013..000000000 --- a/src/utbetalinger/beta/filter/utbetalingerFilter.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ManedUtbetaling } from "../../../generated/model"; - -import { filterMatch } from "./useFiltrerteUtbetalinger"; -import { MottakerFilter } from "./FilterContext"; - -describe("filtrering på utbetalinger fungerer", () => { - it("skal filtrere dato på fom og tom", () => { - const utbetaling: ManedUtbetaling = { - utbetalingsdato: "2023-04-12", - status: "UTBETALT", - annenMottaker: false, - tittel: "", - belop: 0, - fiksDigisosId: "", - }; - - const filterFra = { - mottaker: MottakerFilter.Alle, - fraDato: new Date(2023, 3, 12), - tilDato: undefined, - }; - const filterFraSenere = { - mottaker: MottakerFilter.Alle, - fraDato: new Date(2023, 3, 20), - tilDato: undefined, - }; - const filterTil = { - mottaker: MottakerFilter.Alle, - fraDato: undefined, - tilDato: new Date(2023, 3, 12), - }; - const filterTilTidligere = { - mottaker: MottakerFilter.Alle, - fraDato: undefined, - tilDato: new Date(2023, 3, 10), - }; - - expect(filterMatch(utbetaling, filterFra)).toBeTruthy(); - expect(filterMatch(utbetaling, filterTil)).toBeTruthy(); - expect(filterMatch(utbetaling, filterFraSenere)).toBeFalsy(); - expect(filterMatch(utbetaling, filterTilTidligere)).toBeFalsy(); - }); - - it("skal filtrere mottaker", () => { - const utbetalingAnnen: ManedUtbetaling = { - utbetalingsdato: "2023-04-12", - status: "UTBETALT", - annenMottaker: true, - tittel: "tilAnnen", - belop: 0, - fiksDigisosId: "", - }; - - const utbetalingMeg: ManedUtbetaling = { - utbetalingsdato: "2023-04-12", - status: "UTBETALT", - annenMottaker: false, - tittel: "tilMeg", - belop: 0, - fiksDigisosId: "", - }; - - const filterMeg = { - mottaker: MottakerFilter.MinKonto, - fraDato: undefined, - tilDato: undefined, - }; - const filterAnnen = { - mottaker: MottakerFilter.AnnenMottaker, - fraDato: undefined, - tilDato: new Date(2023, 3, 12), - }; - expect(filterMatch(utbetalingAnnen, filterAnnen)).toBeTruthy(); - expect(filterMatch(utbetalingAnnen, filterMeg)).toBeFalsy(); - expect(filterMatch(utbetalingMeg, filterMeg)).toBeTruthy(); - expect(filterMatch(utbetalingMeg, filterAnnen)).toBeFalsy(); - }); -}); diff --git a/src/utbetalinger/beta/tabs/ManedGruppe.tsx b/src/utbetalinger/beta/tabs/ManedGruppe.tsx deleted file mode 100644 index e7c4c6c15..000000000 --- a/src/utbetalinger/beta/tabs/ManedGruppe.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import { Accordion, BodyShort } from "@navikt/ds-react"; -import { useTranslation } from "next-i18next"; - -import { UtbetalingerResponseMedId } from "../UtbetalingerPanelBeta"; -import { hentMaanedString } from "../../utbetalingerUtils"; - -import styles from "./manedgruppe.module.css"; -import UtbetalingAccordionItem from "./UtbetalingAccordionItem"; - -interface Props { - utbetalingSak: UtbetalingerResponseMedId; -} -const ManedGruppe = (props: Props) => { - const { utbetalingSak } = props; - const { i18n } = useTranslation(); - - return ( -
- - {hentMaanedString(utbetalingSak.maned, i18n) + " " + utbetalingSak.ar} - - - {utbetalingSak.utbetalingerForManed.map((utbetalingManed) => ( - - ))} - -
- ); -}; -export default ManedGruppe; diff --git a/src/utbetalinger/beta/tabs/NyeUtbetalinger.tsx b/src/utbetalinger/beta/tabs/NyeUtbetalinger.tsx deleted file mode 100644 index ac05097c7..000000000 --- a/src/utbetalinger/beta/tabs/NyeUtbetalinger.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { Alert } from "@navikt/ds-react"; -import { useTranslation } from "next-i18next"; - -import { useFilter } from "../filter/FilterContext"; -import Lastestriper from "../../../components/lastestriper/Lasterstriper"; -import { UtbetalingerResponseMedId } from "../UtbetalingerPanelBeta"; - -import ManedGruppe from "./ManedGruppe"; - -interface Props { - lasterData: boolean; - error: boolean; - utbetalinger: UtbetalingerResponseMedId[]; -} - -const NyeUtbetalinger = (props: Props) => { - const { isUsingFilter } = useFilter(); - const { t } = useTranslation("utbetalinger"); - - if (props.lasterData) { - return ; - } - if (props.error) { - return ( - - {t("feil.fetch")} - - ); - } - if (props.utbetalinger.length === 0) { - return ( - - {`${t("feil.ingen")} ${isUsingFilter ? t("feil.ingen.filter") : t("feil.ingen.default.nye")}`} - - ); - } - - return ( - <> - {props.utbetalinger.map((utbetalingSak: UtbetalingerResponseMedId) => ( - - ))} - - ); -}; -export default NyeUtbetalinger; diff --git a/src/utbetalinger/beta/tabs/TidligereUtbetalinger.tsx b/src/utbetalinger/beta/tabs/TidligereUtbetalinger.tsx deleted file mode 100644 index f0fbae611..000000000 --- a/src/utbetalinger/beta/tabs/TidligereUtbetalinger.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from "react"; -import { Alert, BodyLong } from "@navikt/ds-react"; -import { useTranslation } from "next-i18next"; - -import { useHentTidligereUtbetalinger } from "../../../generated/utbetalinger-controller/utbetalinger-controller"; -import useFiltrerteUtbetalinger from "../filter/useFiltrerteUtbetalinger"; -import { useFilter } from "../filter/FilterContext"; -import Lastestriper from "../../../components/lastestriper/Lasterstriper"; -import { UtbetalingerResponseMedId } from "../UtbetalingerPanelBeta"; -import { ManedUtbetaling } from "../../../generated/model"; - -import ManedGruppe from "./ManedGruppe"; - -const TidligerUtbetalingerInnhold = () => { - const { data, isLoading, isError } = useHentTidligereUtbetalinger({ - query: { - select: (data) => { - // Legg på en id på hver utbetaling - return data.map((item) => { - return { - ...item, - utbetalingerForManed: item.utbetalingerForManed.map((utbetaling: ManedUtbetaling) => { - return { - ...utbetaling, - id: crypto.randomUUID(), - }; - }), - }; - }); - }, - }, - }); - const filtrerteTidligere = useFiltrerteUtbetalinger(data ?? []); - const { isUsingFilter } = useFilter(); - const { t } = useTranslation("utbetalinger"); - - if (isLoading) { - return ; - } - if (isError) { - return ( - - {t("feil.fetch")} - - ); - } - if (filtrerteTidligere.length === 0) { - return ( - - {`${t("feil.ingen")} ${isUsingFilter ? t("feil.ingen.filter") : t("feil.ingen.default.tidligere")}`} - - ); - } - - return ( - <> - {filtrerteTidligere.map((utbetalingSak: UtbetalingerResponseMedId) => ( - - ))} - - ); -}; - -const TidligerUtbetalinger = () => { - const { t } = useTranslation("utbetalinger"); - - return ( - <> - {t("tidligereIngress")} - - - ); -}; -export default TidligerUtbetalinger; diff --git a/src/utbetalinger/beta/tabs/UtbetalingAccordionItem.tsx b/src/utbetalinger/beta/tabs/UtbetalingAccordionItem.tsx deleted file mode 100644 index 276175e87..000000000 --- a/src/utbetalinger/beta/tabs/UtbetalingAccordionItem.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Accordion, BodyShort } from "@navikt/ds-react"; -import { FileTextIcon } from "@navikt/aksel-icons"; -import React, { useState } from "react"; -import { useTranslation } from "next-i18next"; -import Link from "next/link"; -import { logger } from "@navikt/next-logger"; - -import { logAmplitudeEvent, logButtonOrLinkClick } from "../../../utils/amplitude"; -import { formatCurrency, formatDato, getDayAndMonth } from "../../../utils/formatting"; -import { UtbetalingMedId } from "../UtbetalingerPanelBeta"; -import { hentTekstForUtbetalingsmetode, hentUtbetalingTittel } from "../../utbetalingerUtils"; - -import styles from "./manedgruppe.module.css"; - -function statusToTekst(t: (key: string) => string, status?: string) { - switch (status) { - case "STOPPET": - return t("utbetalinger:stoppet") + " "; - case "PLANLAGT_UTBETALING": - return t("utbetalinger:planlagt") + " "; - case "UTBETALT": - return t("utbetalinger:utbetalt") + " "; - default: - if (!status?.toLowerCase) { - logger.error("Status is not a string in statusToTekst? Status: " + status); - } - return status?.toLowerCase?.() + " " ?? "Ingen status"; - } -} -interface Props { - utbetalingManed: UtbetalingMedId; -} - -export const utbetalingsdetaljerDefaultAapnet = (dagensDato: Date, utbetalingsdato?: string) => { - if (utbetalingsdato == "") return false; - const utbetalingsDato: Date = new Date(utbetalingsdato ?? ""); - - const femtenDagerSiden: Date = new Date(dagensDato.getTime() - 15 * 24 * 60 * 60 * 1000); - femtenDagerSiden.setHours(0, 0, 0, 0); - - const femtenDagerTil: Date = new Date(dagensDato.getTime() + 15 * 24 * 60 * 60 * 1000); - femtenDagerTil.setHours(1, 0, 0, 0); - - const erUtbetalingsdatoInnenDeSisteFemtenDagene = - utbetalingsDato >= femtenDagerSiden && utbetalingsDato <= dagensDato; - - const erUtbetalingsdatoInnenDeNesteFemtenDagene = - utbetalingsDato <= femtenDagerTil && utbetalingsDato >= dagensDato; - - return erUtbetalingsdatoInnenDeSisteFemtenDagene || erUtbetalingsdatoInnenDeNesteFemtenDagene; -}; - -const UtbetalingAccordionItem = ({ utbetalingManed }: Props) => { - const { t, i18n } = useTranslation("utbetalinger"); - const [isOpen, setIsOpen] = useState(utbetalingsdetaljerDefaultAapnet(new Date(), utbetalingManed.utbetalingsdato)); - - return ( - <> - - { - logAmplitudeEvent(isOpen ? "accordion lukket" : "accordion åpnet", { - tekst: "Utbetaling", - }); - setIsOpen((isOpen) => !isOpen); - }} - > -
-
- - {hentUtbetalingTittel(utbetalingManed.tittel, t("default_utbetalinger_tittel"))} - - - {utbetalingManed.status === "STOPPET" ? ( - <>{t("utbetalinger:stoppet")} - ) : ( - <> - {statusToTekst(t, utbetalingManed.status)} - {utbetalingManed.utbetalingsdato - ? getDayAndMonth(utbetalingManed.utbetalingsdato, i18n.language) - : utbetalingManed.forfallsdato - ? getDayAndMonth(utbetalingManed.forfallsdato, i18n.language) - : t("ukjentDato")} - - )} - -
- - - {utbetalingManed.status === "STOPPET" ? ( - - {t("opprinneligSum")} - {formatCurrency(utbetalingManed.belop, i18n.language)} kr - - ) : ( - <>{formatCurrency(utbetalingManed.belop, i18n.language)} kr - )} - -
-
- - {utbetalingManed.fom && utbetalingManed.tom && ( - <> - {t("periode")} - - {formatDato(utbetalingManed.fom, i18n.language)} -{" "} - {formatDato(utbetalingManed.tom, i18n.language)} - - - )} - <> - {t("mottaker")} - {utbetalingManed.annenMottaker ? ( - - {utbetalingManed.mottaker} - - ) : ( - - {`${t("tilDeg")} (${hentTekstForUtbetalingsmetode( - utbetalingManed.utbetalingsmetode ?? "", - i18n - )} ${utbetalingManed.kontonummer}) - `} - - )} - - - logButtonOrLinkClick("Åpner søknaden fra utbetalingen")} - > - - {t("soknadLenke")} - - -
- - ); -}; -export default UtbetalingAccordionItem; diff --git a/src/utbetalinger/beta/tabs/manedgruppe.module.css b/src/utbetalinger/beta/tabs/manedgruppe.module.css deleted file mode 100644 index c3312e75f..000000000 --- a/src/utbetalinger/beta/tabs/manedgruppe.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.month_group { - margin-bottom: 40px; -} - -.uthevetTekst { - font-weight: bold; -} -.capitalize { - text-transform: capitalize; -} -.monthYear_header { - margin-bottom: 4px; -} - -.stoppetTekst { - margin-right: 8px; -} - -.accordion_headerContent { - display: flex; - gap: 8px; - flex-direction: row; - align-items: center; -} - -.float_left { - display: flex; - flex-wrap: wrap; - gap: 8px; - max-width: 80%; -} -.accordion_headerContent > .float_right { - margin-left: auto; -} - -.accordion_headerContent s { - color: var(--a-text-subtle); -} - -.accordion_header { - align-items: center; -} -.accordion_header > span:nth-child(2) { - width: 100%; -} - -.accordion_content { - padding-top: 8px; -} -.soknadLenke { - align-items: center; - gap: 8px; - display: flex; -} diff --git a/src/utbetalinger/beta/utbetalinger.module.css b/src/utbetalinger/beta/utbetalinger.module.css deleted file mode 100644 index 617e18f03..000000000 --- a/src/utbetalinger/beta/utbetalinger.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.utbetalinger_side { - padding: 4rem 32px 32px 32px; - display: flex; - flex-direction: row; - justify-content: center; - background-color: #d4e6d8; -} - -.utbetalinger_side_innhold { - display: flex; - flex-direction: row; - justify-content: center; - gap: 2rem; - min-height: 40vh; -} - -.utbetalinger_loader { - margin: 100px; -} - -.utbetalinger_panel { - position: relative; - padding-top: 3rem; - max-width: 40rem; -} - -.utbetalinger_panel h2 { - text-align: center; -} - -.utbetalinger_decoration { - position: absolute; - transform: translate(-50%, -50%); - top: 0; - left: 50%; - background: #9bd0b0; - border-radius: 50%; - height: 4rem; - width: 4rem; -} - -.filter_section { - height: fit-content; - padding: 1.5rem 1.5rem 0; -} - -.tab_list_blue { - background-color: var(--a-blue-500); -} - -.tab_list_blue span { - color: var(--a-text-on-neutral); -} - -.tab_list_transparent { - background-color: transparent; -} - -.tab_panel { - padding: 16px 0; -} diff --git a/src/utbetalinger/filter/FilterChips.tsx b/src/utbetalinger/filter/FilterChips.tsx new file mode 100644 index 000000000..7827a0a24 --- /dev/null +++ b/src/utbetalinger/filter/FilterChips.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from "next-i18next"; +import { Box, Chips } from "@navikt/ds-react"; +import React from "react"; + +import { dateToDDMMYYYY } from "../../utils/formatting"; + +import { useFilter } from "./lib/useFilter"; + +export const FilterChips = () => { + const { filters, setFilter } = useFilter(); + const { t, i18n } = useTranslation("utbetalinger"); + + if (!filters) return ; + + const { fraDato, tilDato, mottaker } = filters; + + return ( + + {fraDato && ( + setFilter({ fraDato: null })}> + {t("filter.fra") + ": " + dateToDDMMYYYY(i18n.language, fraDato)} + + )} + {tilDato && ( + setFilter({ tilDato: null })}> + {t("filter.til") + ": " + dateToDDMMYYYY(i18n.language, tilDato)} + + )} + {mottaker && ( + setFilter({ mottaker: null })}> + {t(`filter.mottaker.${mottaker}` as const)} + + )} + + ); +}; diff --git a/src/utbetalinger/filter/FilterDatePicker.tsx b/src/utbetalinger/filter/FilterDatePicker.tsx new file mode 100644 index 000000000..f74673de4 --- /dev/null +++ b/src/utbetalinger/filter/FilterDatePicker.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "next-i18next"; +import React, { useState } from "react"; +import { DatePicker, DateValidationT, useDatepicker } from "@navikt/ds-react"; +import { subMonths } from "date-fns"; + +import useIsMobile from "../../utils/useIsMobile"; + +const validateFromDate = ( + { isAfter, isBefore, isInvalid, isValidDate }: DateValidationT, + fromDate: Date | null | undefined +) => { + if (isBefore) return fromDate ? "filter.tilEtterFra" : "filter.tidligstFra"; + else if (isAfter) return "filter.fraEtterTil"; + else if (isInvalid || !isValidDate) return "filter.ugylding"; + else return undefined; +}; + +export const FilterDatePicker = ({ + label, + fromDate, + toDate, + defaultSelected, + onDateChange, +}: { + label: string; + fromDate?: Date | null; + toDate?: Date | null; + defaultSelected?: Date | null; + onDateChange: (date?: Date) => void; +}) => { + const { t, i18n } = useTranslation("utbetalinger"); + + const isMobile = useIsMobile(); + const [dateError, setDateError] = useState(undefined); + const { datepickerProps, inputProps } = useDatepicker({ + fromDate: fromDate ?? subMonths(new Date(), 15), + toDate: toDate ?? undefined, + defaultSelected: defaultSelected ?? undefined, + onDateChange, + onValidate: (validation) => setDateError(validateFromDate(validation, fromDate)), + }); + + return ( + + + + ); +}; diff --git a/src/utbetalinger/filter/FilterModal.tsx b/src/utbetalinger/filter/FilterModal.tsx new file mode 100644 index 000000000..00166df45 --- /dev/null +++ b/src/utbetalinger/filter/FilterModal.tsx @@ -0,0 +1,43 @@ +import { Button, Modal } from "@navikt/ds-react"; +import React from "react"; +import { FilterIcon } from "@navikt/aksel-icons"; +import { useTranslation } from "next-i18next"; + +import UtbetalingerFilter from "./UtbetalingerFilter"; +import { FilterChips } from "./FilterChips"; +import { useFilter } from "./lib/useFilter"; + +const FilterModal = () => { + const { clearFilters } = useFilter(); + const { t } = useTranslation("utbetalinger"); + const dialogRef = React.useRef(null); + + const onCancel = () => { + clearFilters(); + dialogRef.current?.close(); + }; + + return ( + <> + + + + + + + + + + + ); +}; + +export default FilterModal; diff --git a/src/utbetalinger/filter/FilterProvider.tsx b/src/utbetalinger/filter/FilterProvider.tsx new file mode 100644 index 000000000..b7ec4909d --- /dev/null +++ b/src/utbetalinger/filter/FilterProvider.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode, useReducer } from "react"; + +import { filterReducer } from "./lib/filterReducer"; +import { filterLogAnalytics } from "./lib/filterLogAnalytics"; +import { FilterContext, FilterCriteria } from "./lib/FilterContext"; + +export const FilterProvider = ({ children }: { children: ReactNode }) => { + const [filters, dispatch] = useReducer(filterReducer, null); + const clearFilters = () => + setFilter({ + mottaker: null, + fraDato: null, + tilDato: null, + }); + + const setFilter = (predicates: FilterCriteria) => { + filterLogAnalytics(predicates); + dispatch(predicates); + }; + + const value = { filters, setFilter, clearFilters }; + + return {children}; +}; diff --git a/src/utbetalinger/filter/UtbetalingerFilter.tsx b/src/utbetalinger/filter/UtbetalingerFilter.tsx new file mode 100644 index 000000000..0e07919f2 --- /dev/null +++ b/src/utbetalinger/filter/UtbetalingerFilter.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Fieldset, Radio, RadioGroup } from "@navikt/ds-react"; +import { useTranslation } from "next-i18next"; + +import { MottakerFilter } from "./lib/FilterContext"; +import { FilterDatePicker } from "./FilterDatePicker"; +import { useFilter } from "./lib/useFilter"; + +const UtbetalingerFilter = () => { + const { t } = useTranslation("utbetalinger"); + const { filters, setFilter } = useFilter(); + const { fraDato, mottaker, tilDato } = filters || {}; + + return ( +
+
+ setFilter({ fraDato })} + /> + setFilter({ tilDato })} + /> +
+ setFilter({ mottaker: mottaker === "" ? null : mottaker })} + > + {t("filter.alle")} + {t("filter.mottaker.minKonto")} + {t("filter.mottaker.annenMottaker")} + +
+ ); +}; +export default UtbetalingerFilter; diff --git a/src/utbetalinger/filter/lib/FilterContext.ts b/src/utbetalinger/filter/lib/FilterContext.ts new file mode 100644 index 000000000..3a6dbbe90 --- /dev/null +++ b/src/utbetalinger/filter/lib/FilterContext.ts @@ -0,0 +1,21 @@ +import { createContext } from "react"; + +type Maybe = T | null; + +export type MottakerFilter = "minKonto" | "annenMottaker"; + +export type FilterCriteria = { + mottaker?: Maybe; + fraDato?: Maybe; + tilDato?: Maybe; +}; + +export type FilterKey = keyof FilterCriteria; + +type FilterContextType = { + filters: FilterCriteria | null; + setFilter: (nyttFilter: FilterCriteria) => void; + clearFilters: () => void; +}; + +export const FilterContext = createContext(undefined); diff --git a/src/utbetalinger/filter/lib/filterLogAnalytics.test.ts b/src/utbetalinger/filter/lib/filterLogAnalytics.test.ts new file mode 100644 index 000000000..8e312c993 --- /dev/null +++ b/src/utbetalinger/filter/lib/filterLogAnalytics.test.ts @@ -0,0 +1,41 @@ +jest.mock("../../../utils/amplitude", () => ({ + logAmplitudeEventTyped: jest.fn(), +})); + +import { logAmplitudeEventTyped } from "../../../utils/amplitude"; + +import { filterLogAnalytics } from "./filterLogAnalytics"; + +describe("filterLogAnalytics", () => { + beforeAll(() => jest.mock("../../../utils/amplitude", () => ({ logAmplitudeEventTyped }))); + beforeEach(() => jest.clearAllMocks()); + + it("logs single filter update", () => { + const fraDato = new Date(); + filterLogAnalytics({ fraDato }); + expect(logAmplitudeEventTyped).toHaveBeenCalledWith({ + eventName: "filtervalg", + eventData: { kategori: "fraDato", filternavn: fraDato }, + }); + }); + + it("logs multiple filter updates", () => { + const fraDato = new Date(); + const tilDato = new Date(); + const action = { fraDato, tilDato }; + filterLogAnalytics(action); + expect(logAmplitudeEventTyped).toHaveBeenCalledWith({ + eventName: "filtervalg", + eventData: { kategori: "fraDato", filternavn: fraDato }, + }); + expect(logAmplitudeEventTyped).toHaveBeenCalledWith({ + eventName: "filtervalg", + eventData: { kategori: "tilDato", filternavn: tilDato }, + }); + }); + + it("logs no events for empty action", () => { + filterLogAnalytics({}); + expect(logAmplitudeEventTyped).not.toHaveBeenCalled(); + }); +}); diff --git a/src/utbetalinger/filter/lib/filterLogAnalytics.ts b/src/utbetalinger/filter/lib/filterLogAnalytics.ts new file mode 100644 index 000000000..5917170dd --- /dev/null +++ b/src/utbetalinger/filter/lib/filterLogAnalytics.ts @@ -0,0 +1,17 @@ +import { AmplitudeFiltervalgEvent, logAmplitudeEventTyped } from "../../../utils/amplitude"; + +import { FilterKey, FilterCriteria } from "./FilterContext"; + +const getFilterUpdates = (action: FilterCriteria): AmplitudeFiltervalgEvent[] => + Object.keys(action) + .filter((key) => action[key as FilterKey] !== undefined) + .map((key) => ({ + eventName: "filtervalg", + eventData: { + kategori: key as FilterKey, + filternavn: action[key as FilterKey], + }, + })); + +export const filterLogAnalytics = (action: FilterCriteria) => + getFilterUpdates(action).map((e) => logAmplitudeEventTyped(e)); diff --git a/src/utbetalinger/filter/lib/filterMatch.test.ts b/src/utbetalinger/filter/lib/filterMatch.test.ts new file mode 100644 index 000000000..b603a63b9 --- /dev/null +++ b/src/utbetalinger/filter/lib/filterMatch.test.ts @@ -0,0 +1,28 @@ +import { FilterCriteria } from "./FilterContext"; +import { filterMatch } from "./filterMatch"; + +describe("filterMatch", () => { + const BASE = { + forfallsdato: "2023-01-01T00:00:00Z", + utbetalingsdato: "2023-01-01T00:00:00Z", + } as const; + + const FILTER_ANNEN_MOTTAKER = { mottaker: "annenMottaker" } as const; + const FILTER_WITHIN_RANGE = { fraDato: new Date("2022-12-31"), tilDato: new Date("2023-01-02") } as const; + const FILTER_BEFORE_FRA = { fraDato: new Date("2023-01-02") }; + const FILTER_AFTER_TIL = { tilDato: new Date("2022-12-31") }; + + const expectMatch = + (filters: FilterCriteria, expected: boolean, annenMottaker: boolean = false) => + () => + expect(filterMatch({ ...BASE, annenMottaker }, filters)).toBe(expected); + + it("true when no filters are applied", expectMatch({}, true)); + it("true for annenMottaker filter when annenMottaker is true", expectMatch(FILTER_ANNEN_MOTTAKER, true, true)); + it("false for annenMottaker filter when annenMottaker is false", expectMatch(FILTER_ANNEN_MOTTAKER, false)); + it("true when utbetalingsdato is within date range", expectMatch(FILTER_WITHIN_RANGE, true)); + it("false when utbetalingsdato is before fraDato", expectMatch(FILTER_BEFORE_FRA, false)); + it("false when utbetalingsdato is after tilDato", expectMatch(FILTER_AFTER_TIL, false)); + it("true for forfallsdato within range", expectMatch(FILTER_WITHIN_RANGE, true)); + it("false for forfallsdato out of range", expectMatch(FILTER_BEFORE_FRA, false)); +}); diff --git a/src/utbetalinger/filter/lib/filterMatch.ts b/src/utbetalinger/filter/lib/filterMatch.ts new file mode 100644 index 000000000..a66dbbbe1 --- /dev/null +++ b/src/utbetalinger/filter/lib/filterMatch.ts @@ -0,0 +1,31 @@ +import { isAfter, isBefore } from "date-fns"; + +import { ManedUtbetaling } from "../../../generated/model"; + +import { FilterCriteria } from "./FilterContext"; + +const stringToDateWithoutTimezone = (datoString: string | undefined) => + !datoString ? undefined : new Date(new Date(datoString).toISOString().slice(0, -1)); + +export const filterMatch = ( + { + annenMottaker, + forfallsdato, + utbetalingsdato, + }: Pick, + filters: FilterCriteria +) => { + const matchMottaker = !filters.mottaker + ? true + : (filters.mottaker === "annenMottaker" && annenMottaker) || + (filters.mottaker === "minKonto" && !annenMottaker); + + const utbetalingDate = stringToDateWithoutTimezone(utbetalingsdato ?? forfallsdato); + + if (!utbetalingDate) return matchMottaker; + + const matchFra = !filters.fraDato ? true : !isBefore(utbetalingDate, filters.fraDato); + const matchTil = !filters.tilDato ? true : !isAfter(utbetalingDate, filters.tilDato); + + return matchMottaker && matchTil && matchFra; +}; diff --git a/src/utbetalinger/filter/lib/filterReducer.test.ts b/src/utbetalinger/filter/lib/filterReducer.test.ts new file mode 100644 index 000000000..f4874f8e6 --- /dev/null +++ b/src/utbetalinger/filter/lib/filterReducer.test.ts @@ -0,0 +1,18 @@ +import { FilterCriteria } from "./FilterContext"; +import { filterReducer } from "./filterReducer"; + +describe("filterReducer", () => { + const predFraDato: FilterCriteria = { fraDato: new Date() } as const; + const predTilDato: FilterCriteria = { tilDato: new Date() } as const; + const predBeggeDatoer: FilterCriteria = { ...predFraDato, ...predTilDato } as const; + + const expectReducer = + (initialState: FilterCriteria | null, action: FilterCriteria, expected: FilterCriteria | null) => () => + expect(filterReducer(initialState, action)).toEqual(expected); + + it("returns state if action is empty", expectReducer(predFraDato, {}, predFraDato)); + it("returns updated state if action has values", expectReducer(predFraDato, predTilDato, predBeggeDatoer)); + it("returns null if both state and action are empty", expectReducer({}, {}, null)); + it("returns action if state is null and action has values", expectReducer(null, predFraDato, predFraDato)); + it("returns null if state is null and action is empty", expectReducer(null, {}, null)); +}); diff --git a/src/utbetalinger/filter/lib/filterReducer.ts b/src/utbetalinger/filter/lib/filterReducer.ts new file mode 100644 index 000000000..f265b5501 --- /dev/null +++ b/src/utbetalinger/filter/lib/filterReducer.ts @@ -0,0 +1,6 @@ +import { FilterCriteria } from "./FilterContext"; + +const nullIfEmpty = (action: FilterCriteria) => (Object.values(action).some((value) => !!value) ? action : null); + +export const filterReducer = (state: FilterCriteria | null, action: FilterCriteria) => + nullIfEmpty({ ...(state ?? {}), ...action }); diff --git a/src/utbetalinger/filter/lib/filterResponses.test.ts b/src/utbetalinger/filter/lib/filterResponses.test.ts new file mode 100644 index 000000000..6a77b5a4f --- /dev/null +++ b/src/utbetalinger/filter/lib/filterResponses.test.ts @@ -0,0 +1,25 @@ +import { NyeOgTidligereUtbetalingerResponse } from "../../../generated/model"; +import { getHentTidligereUtbetalingerResponseMock } from "../../../generated/utbetalinger-controller/utbetalinger-controller.msw"; + +import { filterMatch } from "./filterMatch"; +import { filterResponses } from "./filterResponses"; +jest.mock("./filterMatch"); + +describe("filterResponses", () => { + const UTBETALINGER = [ + { utbetalingerForManed: [{ mottaker: "mottaker1" }] }, + ] as unknown as NyeOgTidligereUtbetalingerResponse[]; + + it("should return undefined when utbetalinger is undefined", () => + expect(filterResponses(undefined, null)).toBeUndefined()); + + it("should return original when filters is null", () => + expect(filterResponses(UTBETALINGER, null)).toBe(UTBETALINGER)); + + it("should invoke filterMatch for each utbetaling", () => { + const response = getHentTidligereUtbetalingerResponseMock(); + const totalUtbetalinger = response.reduce((acc, { utbetalingerForManed: { length } }) => acc + length, 0); + filterResponses(response, { mottaker: "annenMottaker" }); + expect(filterMatch).toHaveBeenCalledTimes(totalUtbetalinger); + }); +}); diff --git a/src/utbetalinger/filter/lib/filterResponses.ts b/src/utbetalinger/filter/lib/filterResponses.ts new file mode 100644 index 000000000..0129b6f5d --- /dev/null +++ b/src/utbetalinger/filter/lib/filterResponses.ts @@ -0,0 +1,17 @@ +import { NyeOgTidligereUtbetalingerResponse } from "../../../generated/model"; + +import { FilterCriteria } from "./FilterContext"; +import { filterMatch } from "./filterMatch"; + +export const filterResponses = ( + utbetalinger: NyeOgTidligereUtbetalingerResponse[] | undefined, + filters: FilterCriteria | null +) => + !filters + ? utbetalinger + : utbetalinger?.map((response: NyeOgTidligereUtbetalingerResponse) => ({ + ...response, + utbetalingerForManed: response.utbetalingerForManed.filter((utbetaling) => + filterMatch(utbetaling, filters) + ), + })); diff --git a/src/utbetalinger/filter/lib/useFilter.ts b/src/utbetalinger/filter/lib/useFilter.ts new file mode 100644 index 000000000..0c179f012 --- /dev/null +++ b/src/utbetalinger/filter/lib/useFilter.ts @@ -0,0 +1,9 @@ +import { useContext } from "react"; + +import { FilterContext } from "./FilterContext"; + +export const useFilter = () => { + const context = useContext(FilterContext); + if (!context) throw new Error("Kan kun brukes innenfor FilterProvider"); + return context; +}; diff --git a/src/utbetalinger/isLessThanTwoWeeksAgo.test.ts b/src/utbetalinger/isLessThanTwoWeeksAgo.test.ts new file mode 100644 index 000000000..8c5d525f9 --- /dev/null +++ b/src/utbetalinger/isLessThanTwoWeeksAgo.test.ts @@ -0,0 +1,31 @@ +import { isLessThanTwoWeeksAgo } from "./isLessThanTwoWeeksAgo"; + +/** + * For en gitt referansedato og måldato, forventer vi at funksjonen returnerer et gitt resultat. + * @param referenceDate Mock-verdi for systemklokka + * @param targetDate Datoen vi vil sjekke + * @param expectedResult Forventet resultat + * @param description Beskrivelse av testen + */ +const expectResult = (referenceDate: string, targetDate: string, expectedResult: boolean, description: string) => + it(`returns ${expectedResult} for ${description}`, () => { + jest.setSystemTime(new Date(referenceDate)); + expect(isLessThanTwoWeeksAgo(targetDate)).toBe(expectedResult); + }); + +describe("isNotMoreThanTwoWeeksAgo", () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + expectResult("2024-01-14", "2023-12-27", false, "a date 18 days before the reference date"); + expectResult("2024-01-20", "2024-01-04", false, "a date 16 days before the reference date in the same month"); + expectResult("2024-01-14", "2023-12-29", false, "a date 16 days before the reference date across months"); + expectResult("2024-01-20", "2024-01-05", true, "a date 15 days before the reference date in the same month"); + expectResult("2024-01-14", "2023-12-30", true, "a date 15 days before the reference date across months"); + expectResult("2024-01-14", "2023-12-31", true, "a date 14 days before the reference date"); + expectResult("2024-01-14", "2024-01-14", true, "the reference date itself"); + expectResult("2024-01-14", "2024-01-28", true, "a date 14 days after the reference date"); + expectResult("2024-01-14", "2024-01-29", true, "a date 15 days after the reference date"); + expectResult("2024-01-14", "2024-01-30", false, "a date 16 days after the reference date"); + expectResult("2024-01-14", "2024-02-02", false, "a date 18 days after the reference date"); + expectResult("2024-01-14", "", false, "an undefined date"); +}); diff --git a/src/utbetalinger/isLessThanTwoWeeksAgo.ts b/src/utbetalinger/isLessThanTwoWeeksAgo.ts new file mode 100644 index 000000000..e6b40b73f --- /dev/null +++ b/src/utbetalinger/isLessThanTwoWeeksAgo.ts @@ -0,0 +1,4 @@ +import { differenceInCalendarDays } from "date-fns"; + +export const isLessThanTwoWeeksAgo = (utbetalingsdato?: string) => + !utbetalingsdato ? false : Math.abs(differenceInCalendarDays(new Date(), utbetalingsdato)) <= 15; diff --git a/src/utbetalinger/tabs/UtbetalingAccordionContent.test.tsx b/src/utbetalinger/tabs/UtbetalingAccordionContent.test.tsx new file mode 100644 index 000000000..f3dcfa1e7 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingAccordionContent.test.tsx @@ -0,0 +1,25 @@ +import { render } from "@testing-library/react"; +import { Accordion } from "@navikt/ds-react"; + +import { UtbetalingAccordionContent } from "./UtbetalingAccordionContent"; + +describe("UtbetalingAccordionContent", () => { + it("matches snapshot with annenMottaker", () => + expect( + render( + + + + + + ).asFragment() + ).toMatchSnapshot()); +}); diff --git a/src/utbetalinger/tabs/UtbetalingAccordionContent.tsx b/src/utbetalinger/tabs/UtbetalingAccordionContent.tsx new file mode 100644 index 000000000..b4acd85a2 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingAccordionContent.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from "next-i18next"; +import { Accordion, BodyShort } from "@navikt/ds-react"; +import Link from "next/link"; +import { FileTextIcon } from "@navikt/aksel-icons"; + +import type { ManedUtbetaling } from "../../generated/model"; +import { formatDato } from "../../utils/formatting"; +import { logButtonOrLinkClick } from "../../utils/amplitude"; + +export const UtbetalingAccordionContent = ({ + fom, + tom, + mottaker, + annenMottaker, + utbetalingsmetode, + kontonummer, + fiksDigisosId, +}: Pick< + ManedUtbetaling, + "fom" | "tom" | "mottaker" | "annenMottaker" | "utbetalingsmetode" | "kontonummer" | "fiksDigisosId" +>) => { + const { t, i18n } = useTranslation("utbetalinger"); + + const utbetalingsmetodeTekst = !utbetalingsmetode + ? null + : i18n.exists(`utbetalingsmetode.${utbetalingsmetode?.toLowerCase()}`) + ? i18n.t(`utbetalingsmetode.${utbetalingsmetode?.toLowerCase()}`) + : utbetalingsmetode; + + return ( + + {fom && tom && ( + <> + {t("periode")} + + {formatDato(fom, i18n.language)} - {formatDato(tom, i18n.language)} + + + )} + {t("mottaker")} + {annenMottaker ? ( + + {mottaker} + + ) : ( + + {t("tilDeg")} {utbetalingsmetodeTekst} {kontonummer} + + )} + + logButtonOrLinkClick("Åpner søknaden fra utbetalingen")} + > + + {t("soknadLenke")} + + + ); +}; diff --git a/src/utbetalinger/tabs/UtbetalingAccordionHeader.tsx b/src/utbetalinger/tabs/UtbetalingAccordionHeader.tsx new file mode 100644 index 000000000..a7fab8408 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingAccordionHeader.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from "next-i18next"; +import { Accordion, BodyShort } from "@navikt/ds-react"; +import cx from "classnames"; + +import { ManedUtbetaling, ManedUtbetalingStatus } from "../../generated/model"; +import { getDayAndMonth } from "../../utils/formatting"; + +export const UtbetalingAccordionHeader = ({ + tittel, + belop, + status, + dato, +}: Pick & { + dato: string | undefined; +}) => { + const { t, i18n } = useTranslation("utbetalinger"); + + const datoStreng = dato ? getDayAndMonth(dato, i18n.language) : t("ukjentDato"); + const erStoppet = status === ManedUtbetalingStatus.STOPPET; + const tittelOrDefault = "default_utbetalinger_tittel" !== tittel ? tittel : t("default_utbetalinger_tittel"); + + return ( + +
+
+ {tittelOrDefault} + + {t(`utbetalingStatus.${status}` as const)} {erStoppet ? null : datoStreng} + +
+ + + {!erStoppet && {t("opprinneligSum")}} + {new Intl.NumberFormat(i18n.language).format(belop)} kr + +
+
+ ); +}; diff --git a/src/utbetalinger/tabs/UtbetalingerLoadingWrapper.tsx b/src/utbetalinger/tabs/UtbetalingerLoadingWrapper.tsx new file mode 100644 index 000000000..7d0913e8e --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingerLoadingWrapper.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useTranslation } from "next-i18next"; +import { Alert } from "@navikt/ds-react"; + +import Lastestriper from "../../components/lastestriper/Lasterstriper"; + +export const UtbetalingerLoadingWrapper = ({ + isLoading, + isError, + children, +}: { + isLoading: boolean; + isError: boolean; + children: React.ReactNode; +}) => { + const { t } = useTranslation("utbetalinger"); + + if (isLoading) return ; + if (isError) + return ( + + {t("feil.fetch")} + + ); + + return children; +}; diff --git a/src/utbetalinger/tabs/UtbetalingerMonthlyList.tsx b/src/utbetalinger/tabs/UtbetalingerMonthlyList.tsx new file mode 100644 index 000000000..d2f60cbc2 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingerMonthlyList.tsx @@ -0,0 +1,70 @@ +import { Accordion, BodyShort } from "@navikt/ds-react"; +import { useTranslation } from "next-i18next"; +import { set } from "date-fns"; +import { useMemo } from "react"; + +import { NyeOgTidligereUtbetalingerResponse } from "../../generated/model"; +import { logAmplitudeEvent } from "../../utils/amplitude"; +import { isLessThanTwoWeeksAgo } from "../isLessThanTwoWeeksAgo"; + +import { UtbetalingAccordionHeader } from "./UtbetalingAccordionHeader"; +import { UtbetalingAccordionContent } from "./UtbetalingAccordionContent"; + +export const onOpenChange = (open: boolean) => + logAmplitudeEvent(open ? "accordion åpnet" : "accordion lukket", { tekst: "Utbetaling" }); + +export const UtbetalingerMonthlyList = ({ + utbetalingSak: { ar, maned, utbetalingerForManed }, +}: { + utbetalingSak: NyeOgTidligereUtbetalingerResponse; +}) => { + const { i18n } = useTranslation(); + + const utbetalingerMedId = useMemo( + () => + utbetalingerForManed.map((utbetaling) => ({ + ...utbetaling, + uuid: crypto.randomUUID(), + })), + [utbetalingerForManed] + ); + + return ( +
+ + {set(new Date(0), { + year: ar, + month: maned - 1, + }).toLocaleString(i18n.language, { + month: "long", + year: "numeric", + })} + + + {utbetalingerMedId.map((utbetaling) => ( + + + + + ))} + +
+ ); +}; diff --git a/src/utbetalinger/tabs/UtbetalingerNye.tsx b/src/utbetalinger/tabs/UtbetalingerNye.tsx new file mode 100644 index 000000000..deee066f3 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingerNye.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Alert } from "@navikt/ds-react"; +import { useTranslation } from "next-i18next"; + +import { useFilter } from "../filter/lib/useFilter"; +import { useHentNyeUtbetalinger } from "../../generated/utbetalinger-controller/utbetalinger-controller"; +import { logAmplitudeEvent } from "../../utils/amplitude"; +import { filterResponses } from "../filter/lib/filterResponses"; + +import { UtbetalingerLoadingWrapper } from "./UtbetalingerLoadingWrapper"; +import { UtbetalingerMonthlyList } from "./UtbetalingerMonthlyList"; + +const UtbetalingerNye = () => { + const [nyeLogged, setNyeLogged] = useState(false); + + const { t } = useTranslation("utbetalinger"); + const { data: nye, isLoading, isError } = useHentNyeUtbetalinger(); + const { filters } = useFilter(); + + useEffect(() => { + logAmplitudeEvent("Lastet utbetalinger", { + antall: nye?.[0]?.utbetalingerForManed.length ? nye?.[0].utbetalingerForManed.length : 0, + }); + + if (nyeLogged && !nye?.length) return; + const sisteManedgruppe = nye?.at(-1)?.utbetalingerForManed; + const sisteDatoVist = sisteManedgruppe?.at(-1)?.utbetalingsdato ?? sisteManedgruppe?.at(-1)?.forfallsdato; + + logAmplitudeEvent("Hentet nye utbetalinger", { sisteDatoVist }); + setNyeLogged(true); + }, [nye, nyeLogged]); + + const filtrerteNye = useMemo( + () => filterResponses(nye, filters)?.filter((nye) => nye.utbetalingerForManed.length), + [nye, filters] + ); + return ( + + {filtrerteNye?.length ? ( + filtrerteNye.map((utbetalingSak) => ( + + )) + ) : ( + + {filters ? t("feil.ingen.filter") : t("feil.ingen.default.nye")} + + )} + + ); +}; +export default UtbetalingerNye; diff --git a/src/utbetalinger/tabs/UtbetalingerTidligere.tsx b/src/utbetalinger/tabs/UtbetalingerTidligere.tsx new file mode 100644 index 000000000..7e7210205 --- /dev/null +++ b/src/utbetalinger/tabs/UtbetalingerTidligere.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from "react"; +import { Alert } from "@navikt/ds-react"; +import { useTranslation } from "next-i18next"; + +import { useHentTidligereUtbetalinger } from "../../generated/utbetalinger-controller/utbetalinger-controller"; +import { filterResponses } from "../filter/lib/filterResponses"; +import { NyeOgTidligereUtbetalingerResponse } from "../../generated/model"; +import { useFilter } from "../filter/lib/useFilter"; + +import { UtbetalingerMonthlyList } from "./UtbetalingerMonthlyList"; +import { UtbetalingerLoadingWrapper } from "./UtbetalingerLoadingWrapper"; + +export const UtbetalingerTidligere = () => { + const { data, isLoading, isError } = useHentTidligereUtbetalinger(); + const { filters } = useFilter(); + const filtrerteTidligere = useMemo( + () => filterResponses(data, filters)?.filter((nye) => nye.utbetalingerForManed.length), + [data, filters] + ); + + const { t } = useTranslation("utbetalinger"); + + return ( + + {filtrerteTidligere?.length ? ( + filtrerteTidligere.map((utbetalingSak: NyeOgTidligereUtbetalingerResponse) => ( + + )) + ) : ( + + {filters ? t("feil.ingen.filter") : t("feil.ingen.default.tidligere")} + + )} + + ); +}; diff --git a/src/utbetalinger/tabs/__snapshots__/UtbetalingAccordionContent.test.tsx.snap b/src/utbetalinger/tabs/__snapshots__/UtbetalingAccordionContent.test.tsx.snap new file mode 100644 index 000000000..a33305e26 --- /dev/null +++ b/src/utbetalinger/tabs/__snapshots__/UtbetalingAccordionContent.test.tsx.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UtbetalingAccordionContent matches snapshot with annenMottaker 1`] = ` + + + +`; diff --git a/src/utbetalinger/utbetalinger.module.css b/src/utbetalinger/utbetalinger.module.css new file mode 100644 index 000000000..e90046859 --- /dev/null +++ b/src/utbetalinger/utbetalinger.module.css @@ -0,0 +1,6 @@ +.utbetalinger_side { + padding: 4rem 32px 32px 32px; + display: flex; + flex-direction: column; + background-color: #d4e6d8; +} diff --git a/src/utbetalinger/utbetalingerUtils.ts b/src/utbetalinger/utbetalingerUtils.ts deleted file mode 100644 index 04bb34263..000000000 --- a/src/utbetalinger/utbetalingerUtils.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { i18n } from "i18next"; - -import { ManedUtbetaling, UtbetalingerResponse } from "../generated/model"; - -const diffInMonths = (d1: Date, d2: Date) => { - const d1Y = d1.getFullYear(); - const d2Y = d2.getFullYear(); - const d1M = d1.getMonth(); - const d2M = d2.getMonth(); - return d2M + 12 * d2Y - (d1M + 12 * d1Y); -}; -const filtrerUtbetalingerForTidsinterval = ( - utbetalinger: UtbetalingerResponse[], - visAntallMnd: number, - now: Date -): UtbetalingerResponse[] => { - return utbetalinger.filter((utbetalingSak: UtbetalingerResponse) => { - const foersteIManeden: Date = new Date(utbetalingSak.foersteIManeden); - return diffInMonths(foersteIManeden, now) < visAntallMnd; - }); -}; - -const filtrerUtbetalingerPaaMottaker = ( - utbetalinger: UtbetalingerResponse[], - visTilBrukersKonto: boolean, - visTilAnnenMottaker: boolean -): UtbetalingerResponse[] => { - return utbetalinger.map((utbetalingSak: UtbetalingerResponse) => { - return { - ...utbetalingSak, - utbetalinger: utbetalingSak.utbetalinger.filter((utbetalingMaaned: ManedUtbetaling) => { - const annenMottaker = utbetalingMaaned.annenMottaker; - if (!annenMottaker) { - return visTilBrukersKonto; - } else { - return visTilAnnenMottaker; - } - }), - }; - }); -}; - -const filtrerMaanederUtenUtbetalinger = (utbetalinger: UtbetalingerResponse[]): UtbetalingerResponse[] => { - return utbetalinger.filter((utbetalingSak: UtbetalingerResponse) => { - return utbetalingSak.utbetalinger.length > 0; - }); -}; - -const hentUtbetalingTittel = (tittel: string, defaultTittel: string) => { - return tittel && tittel !== "default_utbetalinger_tittel" ? tittel : defaultTittel; -}; - -const hentTekstForUtbetalingsmetode = (utbetalingsmetode: string, i18n: i18n) => { - return i18n.exists(`utbetalingsmetode.${utbetalingsmetode.toLowerCase()}`) - ? i18n.t(`utbetalingsmetode.${utbetalingsmetode.toLowerCase()}`) - : utbetalingsmetode; -}; - -const hentMaanedString = (maaned: number, i18n: i18n) => { - const date = new Date(); - date.setMonth(maaned - 1); - return date.toLocaleString(i18n.language, { month: "long" }); -}; - -export { - filtrerUtbetalingerPaaMottaker, - filtrerUtbetalingerForTidsinterval, - filtrerMaanederUtenUtbetalinger, - diffInMonths, - hentUtbetalingTittel, - hentMaanedString, - hentTekstForUtbetalingsmetode, -}; diff --git a/src/utils/amplitude.ts b/src/utils/amplitude.ts index 073e74084..0ac34b3f3 100644 --- a/src/utils/amplitude.ts +++ b/src/utils/amplitude.ts @@ -5,6 +5,8 @@ const origin = "sosialhjelpInnsyn" as const; const skjemaId = "sosialhjelpInnsyn" as const; export async function logAmplitudeEvent(eventName: string, eventData?: Record) { + if (process.env.NODE_ENV === "test") return; + try { if (typeof window !== "undefined") { await logDekoratoren({ origin, eventName, eventData: { ...eventData, skjemaId } }); @@ -16,7 +18,7 @@ export async function logAmplitudeEvent(eventName: string, eventData?: Record + logAmplitudeEvent(eventName, eventData); export function logVeilederBerOmDokumentasjonEvent(vedleggAntallet: number) { logAmplitudeEvent("Veileder ber om dokumentasjon til søknaden", { AntallVedleggForesporsel: vedleggAntallet }); diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index 107e5c169..2fe0b1964 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -9,30 +9,26 @@ function formatBytes(bytes: number, decimals: number = 2): string { return result.replace(".", ","); } -// Eksempel: formatCurrency(12345) => 12.345 -function formatCurrency(amount: number, language: string): string { - return new Intl.NumberFormat(language).format(amount); -} - // Eksempel: "2019-08-01" => "01. august 2019" -function formatDato(isoDate: string, language: string) { - const dato: Date = new Date(isoDate); - const formatter = new Intl.DateTimeFormat(language, { day: "numeric", month: "long", year: "numeric" }); - return formatter.format(dato).replace(/([0-9]) /, "$1. "); -} +export const formatDato = (isoDate: string, language: string) => + new Intl.DateTimeFormat(language, { + day: "numeric", + month: "long", + year: "numeric", + }).format(new Date(isoDate)); + // Eksempel "2022-04-11" => "11. april" -export function getDayAndMonth(isoDate: string, language: string) { - const dato: Date = new Date(isoDate); - const formatter = new Intl.DateTimeFormat(language, { day: "numeric", month: "long" }); - return formatter.format(dato).replace(/([0-9]) /, "$1. "); -} +export const getDayAndMonth = (isoDate: string, language: string) => + new Intl.DateTimeFormat(language, { + day: "numeric", + month: "long", + }).format(new Date(isoDate)); -export function dateToDDMMYYYY(language: string, dato: Date) { - return new Intl.DateTimeFormat(language, { +export const dateToDDMMYYYY = (language: string, dato: Date) => + new Intl.DateTimeFormat(language, { day: "2-digit", month: "2-digit", year: "numeric", }).format(dato); -} -export { formatBytes, formatCurrency, formatDato }; +export { formatBytes };