Skip to content

Commit 6e75fc3

Browse files
committed
feat: multi step poc (behind toggle)
1 parent 5c292c4 commit 6e75fc3

35 files changed

+1552
-56
lines changed

e2e/fhir.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test('can submit 100% sykmelding', async ({ page }) => {
2929
values: {
3030
pasient: '21037712323',
3131
diagnoser: {
32-
hoved: { code: 'P74', system: 'ICPC2', text: 'Angstlidelse' },
32+
hoved: { code: 'P74', system: 'ICPC2' },
3333
},
3434
aktivitet: {
3535
type: 'AKTIVITET_IKKE_MULIG',
@@ -55,7 +55,7 @@ test('can submit 100% sykmelding and use week picker', async ({ page }) => {
5555
values: {
5656
pasient: '21037712323',
5757
diagnoser: {
58-
hoved: { code: 'P74', system: 'ICPC2', text: 'Angstlidelse' },
58+
hoved: { code: 'P74', system: 'ICPC2' },
5959
},
6060
aktivitet: {
6161
type: 'AKTIVITET_IKKE_MULIG',
@@ -87,7 +87,7 @@ test('shall be able to edit diagnose', async ({ page }) => {
8787
behandlerHpr: '9144889',
8888
values: {
8989
pasient: '21037712323',
90-
diagnoser: { hoved: { code: 'D290', system: 'ICD10', text: 'Godartet svulst i penis' } },
90+
diagnoser: { hoved: { code: 'D290', system: 'ICD10' } },
9191
aktivitet: {
9292
type: 'AKTIVITET_IKKE_MULIG',
9393
fom: '2024-02-15',
@@ -116,7 +116,7 @@ test('can submit gradert sykmelding', async ({ page }) => {
116116
values: {
117117
pasient: '21037712323',
118118
diagnoser: {
119-
hoved: { system: 'ICPC2', code: 'P74', text: 'Angstlidelse' },
119+
hoved: { system: 'ICPC2', code: 'P74' },
120120
},
121121
aktivitet: {
122122
type: 'GRADERT',

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@navikt/next-logger": "^2.0.0",
3131
"@navikt/oasis": "^3.7.0",
3232
"@redis/client": "^1.6.0",
33+
"@reduxjs/toolkit": "^2.5.1",
3334
"@tanstack/react-query": "^5.66.9",
3435
"@tanstack/react-query-devtools": "^5.66.9",
3536
"@unleash/nextjs": "^1.6.1",
@@ -39,6 +40,7 @@
3940
"fuse.js": "^7.1.0",
4041
"html-react-parser": "^5.2.2",
4142
"jose": "^6.0.8",
43+
"motion": "^12.4.9",
4244
"next": "15.2.0",
4345
"next-logger": "^5.0.1",
4446
"nextleton": "^0.6.1",
@@ -52,6 +54,7 @@
5254
"react-dom": "^19.0.0",
5355
"react-error-boundary": "^5.0.0",
5456
"react-hook-form": "^7.54.2",
57+
"react-redux": "^9.2.0",
5558
"remeda": "^2.21.0",
5659
"server-only": "^0.0.1",
5760
"tailwind-merge": "^2.6.0",

src/app/(fhir)/fhir/page.tsx

+32-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import Link from 'next/link'
66

77
import { isLocalOrDemo } from '@utils/env'
88
import NySykmeldingForm from '@components/ny-sykmelding-form/NySykmeldingForm'
9+
import NySykmeldingFormMultiStep from '@components/ny-sykmelding-form-multi-step/NySykmeldingFormMultiStep'
910
import TidligereSykmeldingerTimeline from '@components/tidligere-sykmeldinger/TidligereSykmeldingerTimeline'
1011
import FhirDataProvider from '@fhir/components/FhirDataProvider'
1112
import { serverFhirResources } from '@fhir/fhir-data/fhir-data-server'
1213
import { getFlag, getToggles } from '@toggles/unleash'
1314

15+
import { BehandlerInfo } from '../../../data-fetcher/data-service'
16+
1417
async function Page(): Promise<ReactElement> {
1518
const [behandler, toggles] = await Promise.all([serverFhirResources.getBehandlerInfo(), getToggles()])
1619
const tidligereSykmeldingerToggle = getFlag('SYK_INN_TIDLIGERE_SYKMELDINGER', toggles)
20+
const multistepToggle = getFlag('SYK_INN_MULTISTEP_FORM_V1', toggles)
1721

1822
return (
1923
<PageBlock as="main" width="xl" gutters className="pt-4">
@@ -22,6 +26,24 @@ async function Page(): Promise<ReactElement> {
2226
<Link href="/">← Back to development page</Link>
2327
</div>
2428
)}
29+
{multistepToggle.enabled ? (
30+
<MultistepFhir behandler={behandler} />
31+
) : (
32+
<NormalFhir behandler={behandler} tidligereSykmeldingerEnabled={tidligereSykmeldingerToggle.enabled} />
33+
)}
34+
</PageBlock>
35+
)
36+
}
37+
38+
function NormalFhir({
39+
behandler,
40+
tidligereSykmeldingerEnabled,
41+
}: {
42+
behandler: BehandlerInfo
43+
tidligereSykmeldingerEnabled: boolean
44+
}): ReactElement {
45+
return (
46+
<>
2547
<section className="max-w-prose mb-8">
2648
<Heading level="2" size="medium" spacing>
2749
Opprett ny sykmelding
@@ -41,10 +63,18 @@ async function Page(): Promise<ReactElement> {
4163
</List>
4264
</section>
4365
<FhirDataProvider behandler={behandler}>
44-
{tidligereSykmeldingerToggle.enabled && <TidligereSykmeldingerTimeline />}
66+
{tidligereSykmeldingerEnabled && <TidligereSykmeldingerTimeline />}
4567
<NySykmeldingForm />
4668
</FhirDataProvider>
47-
</PageBlock>
69+
</>
70+
)
71+
}
72+
73+
function MultistepFhir({ behandler }: { behandler: BehandlerInfo }): ReactElement {
74+
return (
75+
<FhirDataProvider behandler={behandler}>
76+
<NySykmeldingFormMultiStep />
77+
</FhirDataProvider>
4878
)
4979
}
5080

src/app/(fhir)/fhir/resources/sykmelding/submit/route.ts

+1-31
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,13 @@
11
import { z } from 'zod'
22
import { logger } from '@navikt/next-logger'
33

4-
import { DateOnly } from '@utils/zod'
54
import { isE2E, isLocalOrDemo } from '@utils/env'
65
import { wait } from '@utils/wait'
76
import { sykInnApiService } from '@services/syk-inn-api/SykInnApiService'
8-
import { NySykmelding } from '@services/syk-inn-api/SykInnApiSchema'
7+
import { NySykmelding, SubmitSykmeldingFormValuesSchema } from '@services/syk-inn-api/SykInnApiSchema'
98
import { raise } from '@utils/ts'
109
import { ensureValidFhirAuth } from '@fhir/auth/verify'
1110

12-
/**
13-
* TODO: Payload will be identical for standalone and FHIR
14-
*/
15-
const SubmitSykmeldingFormValuesSchema = z.object({
16-
pasient: z.string().optional(),
17-
diagnoser: z.object({
18-
hoved: z.object({
19-
system: z.union([z.literal('ICD10'), z.literal('ICPC2')]),
20-
code: z.string(),
21-
}),
22-
}),
23-
aktivitet: z.discriminatedUnion('type', [
24-
z.object({
25-
type: z.literal('AKTIVITET_IKKE_MULIG'),
26-
fom: DateOnly,
27-
tom: DateOnly,
28-
}),
29-
z.object({
30-
type: z.literal('GRADERT'),
31-
fom: DateOnly,
32-
tom: DateOnly,
33-
grad: z
34-
.string()
35-
.transform((it) => +it)
36-
.pipe(z.number().min(1).max(99)),
37-
}),
38-
]),
39-
})
40-
4111
const SubmitSykmeldingPayloadSchema = z.object({
4212
values: SubmitSykmeldingFormValuesSchema,
4313
// TODO: Should be retrieved from context/session?

src/app/(fhir)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getToggles } from '@toggles/unleash'
1010
import FhirHeader from '@fhir/components/FhirHeader'
1111

1212
import { LazyDevTools } from '../../devtools/LazyDevTools'
13-
import Providers from '../providers'
13+
import Providers from '../../providers/Providers'
1414
import Preload from '../preload'
1515

1616
export const metadata: Metadata = {

src/app/(standalone)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import DemoWarning from '@components/demo-warning'
99

1010
import { Autorisasjoner } from '../../data-fetcher/data-service'
1111
import { LazyDevTools } from '../../devtools/LazyDevTools'
12-
import Providers from '../providers'
12+
import Providers from '../../providers/Providers'
1313
import Preload from '../preload'
1414
import HelseIdDataProvider from '../../helseid/components/HelseIdDataProvider'
1515
import { getHelseIdUserInfo, HprDetails } from '../../helseid/helseid-userinfo'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import React, { ReactElement } from 'react'
4+
import { Heading, Skeleton } from '@navikt/ds-react'
5+
6+
import { useContextPasient } from '../../data-fetcher/hooks/use-context-pasient'
7+
import { useContextKonsultasjon } from '../../data-fetcher/hooks/use-context-konsultasjon'
8+
9+
import StepsSummary from './steps/StepsSummary'
10+
import NySykmeldingFormSections from './NySykmeldingFormSections'
11+
12+
function NySykmeldingFormMultiStep(): ReactElement {
13+
const { isLoading, data, error } = useContextPasient()
14+
15+
// Preload data for next steps
16+
useContextKonsultasjon()
17+
18+
if (error) {
19+
// Defer to global error handling
20+
throw error
21+
}
22+
23+
return (
24+
<div className="mt-8">
25+
<Heading size="xlarge" level="2" spacing className="flex gap-3">
26+
Sykmelding for{' '}
27+
{!isLoading && data ? <span>{data.navn}</span> : <Skeleton width={240} className="-my-4" />}
28+
</Heading>
29+
<div className="flex">
30+
<div className="w-full">
31+
<NySykmeldingFormSections />
32+
</div>
33+
<div className="w-full ml-8">
34+
<StepsSummary />
35+
</div>
36+
</div>
37+
</div>
38+
)
39+
}
40+
41+
export default NySykmeldingFormMultiStep
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use client'
2+
3+
import React, { ReactElement, useState } from 'react'
4+
import { AnimatePresence, motion } from 'motion/react'
5+
6+
import { StepSection, useFormStep } from './steps/useFormStep'
7+
import AktivitetSection from './pasient/AktivitetSection'
8+
import FormStart from './start/FormStart'
9+
import DiagnoseSection from './diagnose/DiagnoseSection'
10+
import SummarySection from './summary/SummarySection'
11+
12+
function NySykmeldingFormSections(): ReactElement {
13+
const [step] = useFormStep()
14+
const [prevStep, setPrevStep] = useState(step)
15+
const [direction, setDirection] = useState<-1 | 1>(1)
16+
17+
if (step !== prevStep) {
18+
setPrevStep(step)
19+
setDirection(step > prevStep ? 1 : -1)
20+
}
21+
22+
const goingLeft = direction === 1
23+
24+
return (
25+
<div className="max-w-prose relative">
26+
<AnimatePresence initial={false} custom={goingLeft}>
27+
<motion.div
28+
className="absolute w-full pb-16"
29+
key={step}
30+
initial={{ opacity: 0, x: !goingLeft ? -100 : 100 }}
31+
animate={{ opacity: 1, x: 0 }}
32+
exit={{ opacity: 0, x: !goingLeft ? 100 : -100 }}
33+
transition={{ duration: 0.1 }}
34+
layout="size"
35+
>
36+
<Sections section={step} />
37+
</motion.div>
38+
</AnimatePresence>
39+
</div>
40+
)
41+
}
42+
43+
function Sections({ section }: { section: StepSection }): ReactElement {
44+
switch (section) {
45+
case 1:
46+
return <FormStart />
47+
case 2:
48+
return <AktivitetSection />
49+
case 3:
50+
return <DiagnoseSection />
51+
case 4:
52+
return <SummarySection />
53+
}
54+
}
55+
56+
export default NySykmeldingFormSections
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { ReactElement } from 'react'
2+
import { Control, useController } from 'react-hook-form'
3+
4+
import DiagnoseCombobox from '@components/ny-sykmelding-form/diagnose/combobox/DiagnoseCombobox'
5+
6+
import { KonsultasjonInfo } from '../../../data-fetcher/data-service'
7+
8+
import { DiagnoseFormValues } from './DiagnoseSection'
9+
10+
type Props = {
11+
control: Control<DiagnoseFormValues>
12+
suggestedDiagnoser: KonsultasjonInfo['diagnoser'] | null
13+
}
14+
15+
function DiagnosePicker({ control }: Props): ReactElement {
16+
const { field, fieldState } = useController({
17+
control,
18+
name: 'hoved',
19+
rules: {
20+
validate: (value) => {
21+
if (value?.code == null) return `Du må velge en diagnosekode`
22+
},
23+
},
24+
})
25+
26+
return (
27+
<>
28+
<DiagnoseCombobox
29+
id="diagnoser.hoved"
30+
label="Hoveddiagnose"
31+
description="Diagnosekoder fra både ICPC-2 og ICD-10."
32+
value={field.value}
33+
onBlur={field.onBlur}
34+
error={fieldState.error?.message}
35+
onSelect={(suggestion) => {
36+
field.onChange(suggestion)
37+
}}
38+
onChange={() => {
39+
if (field.value != null) {
40+
field.onChange(null)
41+
}
42+
}}
43+
/>
44+
</>
45+
)
46+
}
47+
48+
export default DiagnosePicker

0 commit comments

Comments
 (0)