diff --git a/.deploy/fp-avdelingsleder/dev-gcp-teamforeldrepenger.json b/.deploy/fp-avdelingsleder/dev-gcp-teamforeldrepenger.json
new file mode 100644
index 00000000000..dbbee1588dd
--- /dev/null
+++ b/.deploy/fp-avdelingsleder/dev-gcp-teamforeldrepenger.json
@@ -0,0 +1,25 @@
+{
+ "app": "fp-avdelingsleder",
+ "ingresses": ["https://fp-avdelingsleder.intern.dev.nav.no"],
+ "minReplicas": "1",
+ "maxReplicas": "2",
+ "env": "development",
+ "groups": [
+ "27e77109-fef2-48ce-a174-269074490353",
+ "8cddda87-0a22-4d35-9186-a2c32a6ab450",
+ "e6508a2a-2e74-450e-ad24-eb1b2b4625c6"
+ ],
+ "externals": ["fpsak-api.dev-fss-pub.nais.io", "fplos.dev-fss-pub.nais.io"],
+ "proxyRedirects": [
+ {
+ "path": "/fpsak/api",
+ "url": "https://fpsak-api.dev-fss-pub.nais.io",
+ "scope": "api://dev-fss.teamforeldrepenger.fpsak/.default"
+ },
+ {
+ "path": "/fplos/api",
+ "url": "https://fplos.dev-fss-pub.nais.io",
+ "scope": "api://dev-fss.teamforeldrepenger.fplos/.default"
+ }
+ ]
+}
diff --git a/.deploy/fp-avdelingsleder/prod-gcp-teamforeldrepenger.json b/.deploy/fp-avdelingsleder/prod-gcp-teamforeldrepenger.json
new file mode 100644
index 00000000000..88176d24b03
--- /dev/null
+++ b/.deploy/fp-avdelingsleder/prod-gcp-teamforeldrepenger.json
@@ -0,0 +1,25 @@
+{
+ "app": "fp-avdelingsleder",
+ "ingresses": ["https://fp-avdelingsleder.intern.nav.no"],
+ "minReplicas": "2",
+ "maxReplicas": "4",
+ "env": "production",
+ "groups": [
+ "73107205-17ec-4a07-a56e-e0a8542f90c9",
+ "77f05833-ebfd-45fb-8be7-88eca8e7418f",
+ "1a59da27-4c55-4a9d-8480-6abd1a856cd2"
+ ],
+ "externals": ["fpsak-api.prod-fss-pub.nais.io", "fplos.prod-fss-pub.nais.io"],
+ "proxyRedirects": [
+ {
+ "path": "/fpsak/api",
+ "url": "https://fpsak-api.prod-fss-pub.nais.io",
+ "scope": "api://prod-fss.teamforeldrepenger.fpsak/.default"
+ },
+ {
+ "path": "/fplos/api",
+ "url": "https://fplos.prod-fss-pub.nais.io",
+ "scope": "api://prod-fss.teamforeldrepenger.fplos/.default"
+ }
+ ]
+}
diff --git a/.deploy/dev-gcp-teamforeldrepenger.json b/.deploy/fp-frontend-app/dev-gcp-teamforeldrepenger.json
similarity index 83%
rename from .deploy/dev-gcp-teamforeldrepenger.json
rename to .deploy/fp-frontend-app/dev-gcp-teamforeldrepenger.json
index 118b6631e69..0c2d439fd94 100644
--- a/.deploy/dev-gcp-teamforeldrepenger.json
+++ b/.deploy/fp-frontend-app/dev-gcp-teamforeldrepenger.json
@@ -1,4 +1,5 @@
{
+ "app": "fp-frontend",
"ingresses": ["https://fpsak.intern.dev.nav.no"],
"minReplicas": "1",
"maxReplicas": "2",
@@ -8,7 +9,12 @@
"8cddda87-0a22-4d35-9186-a2c32a6ab450",
"e6508a2a-2e74-450e-ad24-eb1b2b4625c6"
],
- "externals": ["fpsak-api.dev-fss-pub.nais.io", "fplos.dev-fss-pub.nais.io", "fptilbake.dev-fss-pub.nais.io", "fpfordel.dev-fss-pub.nais.io"],
+ "externals": [
+ "fpsak-api.dev-fss-pub.nais.io",
+ "fplos.dev-fss-pub.nais.io",
+ "fptilbake.dev-fss-pub.nais.io",
+ "fpfordel.dev-fss-pub.nais.io"
+ ],
"proxyRedirects": [
{
"path": "/fpsak/api",
diff --git a/.deploy/prod-gcp-teamforeldrepenger.json b/.deploy/fp-frontend-app/prod-gcp-teamforeldrepenger.json
similarity index 83%
rename from .deploy/prod-gcp-teamforeldrepenger.json
rename to .deploy/fp-frontend-app/prod-gcp-teamforeldrepenger.json
index 2cf84bbe83d..b5a251e8bca 100644
--- a/.deploy/prod-gcp-teamforeldrepenger.json
+++ b/.deploy/fp-frontend-app/prod-gcp-teamforeldrepenger.json
@@ -1,4 +1,5 @@
{
+ "app": "fp-frontend",
"ingresses": ["https://fpsak.intern.nav.no"],
"minReplicas": "2",
"maxReplicas": "4",
@@ -8,7 +9,12 @@
"77f05833-ebfd-45fb-8be7-88eca8e7418f",
"1a59da27-4c55-4a9d-8480-6abd1a856cd2"
],
- "externals": ["fpsak-api.prod-fss-pub.nais.io", "fplos.prod-fss-pub.nais.io", "fptilbake.prod-fss-pub.nais.io", "fpfordel.prod-fss-pub.nais.io"],
+ "externals": [
+ "fpsak-api.prod-fss-pub.nais.io",
+ "fplos.prod-fss-pub.nais.io",
+ "fptilbake.prod-fss-pub.nais.io",
+ "fpfordel.prod-fss-pub.nais.io"
+ ],
"proxyRedirects": [
{
"path": "/fpsak/api",
diff --git a/.deploy/naiserator.yaml b/.deploy/naiserator.yaml
index bb9505cffce..c912cb6c28c 100644
--- a/.deploy/naiserator.yaml
+++ b/.deploy/naiserator.yaml
@@ -1,7 +1,7 @@
apiVersion: "nais.io/v1alpha1"
kind: "Application"
metadata:
- name: fp-frontend
+ name: {{app}}
namespace: teamforeldrepenger
labels:
team: teamforeldrepenger
diff --git a/.github/workflows/build-deploy-branch.yml b/.github/workflows/build-deploy-branch.yml
index e49202133be..a7e894f3f02 100644
--- a/.github/workflows/build-deploy-branch.yml
+++ b/.github/workflows/build-deploy-branch.yml
@@ -6,6 +6,13 @@ on:
description: 'Branch name'
type: string
required: true
+ app:
+ required: true
+ type: choice
+ description: 'Applikasjon som skal deployes fra branch'
+ options:
+ - fp-frontend
+ - fp-avdelingsleder
jobs:
build-app:
@@ -20,6 +27,7 @@ jobs:
node-version: '22.17.1'
push-image: true
branch: ${{ inputs.branch }}
+ bake-target: ${{ inputs.app }}
run-knip: true
secrets: inherit
@@ -31,6 +39,9 @@ jobs:
uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
with:
gar: true
+ branch: ${{ inputs.branch }}
image: ${{ needs.build-app.outputs.build-version }}
cluster: dev-gcp
+ image_suffix: '/${{ inputs.app }}'
+ deploy_context: '/${{ inputs.app }}'
secrets: inherit
diff --git a/.github/workflows/build-fp-avdelingsleder.yml b/.github/workflows/build-fp-avdelingsleder.yml
new file mode 100644
index 00000000000..f57b681e202
--- /dev/null
+++ b/.github/workflows/build-fp-avdelingsleder.yml
@@ -0,0 +1,56 @@
+name: Bygg og deploy fp-avdelingsleder
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - 'apps/fp-avdelingsleder/**'
+ - 'packages/**'
+ - 'server/**'
+ - '.deploy/fp-avdelingsleder/**'
+ - '.deploy/naiserator.yaml'
+ - '.github/workflows/build-fp-avdelingsleder.yml'
+ - 'Dockerfile'
+ - 'yarn.lock'
+
+jobs:
+ build-app:
+ name: Build
+ permissions:
+ contents: read
+ packages: write
+ id-token: write
+ uses: navikt/fp-frontend/.github/workflows/build.yml@los-app # midlertidig til vi er på master
+ with:
+ runs-on: 'ubuntu-latest-8-cores'
+ node-version: '22.17.1'
+ build-image: ${{ github.ref_name == 'master' }} # default: true
+ push-image: ${{ github.ref_name == 'master' }} # default: false
+ run-knip: true
+ app: fp-avdelingsleder
+ secrets: inherit
+
+ deploy-dev:
+ name: Deploy dev
+ permissions:
+ id-token: write
+ if: github.ref_name == 'master'
+ needs: build-app
+ uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
+ with:
+ gar: true
+ image: ${{ needs.build-app.outputs.build-version }}
+ cluster: dev-gcp
+ secrets: inherit
+
+ deploy-prod:
+ name: Deploy prod
+ permissions:
+ id-token: write
+ if: github.ref_name == 'master'
+ needs: [build-app, deploy-dev]
+ uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
+ with:
+ gar: true
+ image: ${{ needs.build-app.outputs.build-version }}
+ cluster: prod-gcp
+ secrets: inherit
diff --git a/.github/workflows/build-fp-frontend.yml b/.github/workflows/build-fp-frontend.yml
new file mode 100644
index 00000000000..01908f959b1
--- /dev/null
+++ b/.github/workflows/build-fp-frontend.yml
@@ -0,0 +1,54 @@
+name: Bygg og deploy fp-frontend
+on:
+ push:
+ branches:
+ - '**'
+ paths-ignore:
+ - '**.md'
+ - '**.MD'
+ - '.gitignore'
+ - '.editorconfig'
+ - 'LICENCE'
+ - 'CODEOWNERS'
+
+jobs:
+ build-app:
+ name: Build
+ permissions:
+ contents: read
+ packages: write
+ id-token: write
+ uses: navikt/fp-gha-workflows/.github/workflows/build-app-frontend-yarn.yml@main
+ with:
+ runs-on: 'ubuntu-latest-8-cores'
+ node-version: '22.17.1'
+ build-image: ${{ github.ref_name == 'master' }} # default: true
+ push-image: ${{ github.ref_name == 'master' }} # default: false
+ run-knip: true
+ secrets: inherit
+
+ deploy-dev:
+ name: Deploy dev
+ permissions:
+ id-token: write
+ if: github.ref_name == 'master'
+ needs: build-app
+ uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
+ with:
+ gar: true
+ image: ${{ needs.build-app.outputs.build-version }}
+ cluster: dev-gcp
+ secrets: inherit
+
+ deploy-prod:
+ name: Deploy prod
+ permissions:
+ id-token: write
+ if: github.ref_name == 'master'
+ needs: [build-app, deploy-dev]
+ uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
+ with:
+ gar: true
+ image: ${{ needs.build-app.outputs.build-version }}
+ cluster: prod-gcp
+ secrets: inherit
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2aa6198ba08..e235ad00781 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,54 +1,109 @@
-name: Bygg og deploy
+name: Build app
on:
- push:
- branches:
- - '**'
- paths-ignore:
- - '**.md'
- - '**.MD'
- - '.gitignore'
- - '.editorconfig'
- - 'LICENCE'
- - 'CODEOWNERS'
-
+ workflow_call:
+ inputs:
+ runs-on:
+ required: false
+ type: string
+ description: Miljø versjon som skal brukes til bygg.
+ default: 'ubuntu-latest'
+ node-version:
+ required: false
+ type: string
+ description: Node version som brukes.
+ default: '22'
+ build-image:
+ required: false
+ type: boolean
+ description: Skal docker image bygges?
+ default: true
+ run-knip:
+ required: false
+ type: boolean
+ description: Skal knip kjøres?
+ default: false
+ push-image:
+ required: false
+ type: boolean
+ description: Skal docker image pushes?
+ default: false
+ branch:
+ required: false
+ type: string
+ description: Alternativ branch å bygge fra
+ app:
+ required: false
+ type: string
+ description: App som skal bygges
+ outputs:
+ build-version:
+ description: 'Build version'
+ value: ${{ jobs.build-and-test.outputs.build-version }}
jobs:
- build-app:
- name: Build
+ build-and-test:
+ name: Build and test
permissions:
- contents: read
packages: write
id-token: write
- uses: navikt/fp-gha-workflows/.github/workflows/build-app-frontend-yarn.yml@main
- with:
- runs-on: 'ubuntu-latest-8-cores'
- node-version: '22.17.1'
- build-image: ${{ github.ref_name == 'master' }} # default: true
- push-image: ${{ github.ref_name == 'master' }} # default: false
- run-knip: true
- secrets: inherit
-
- deploy-dev:
- name: Deploy dev
- permissions:
- id-token: write
- if: github.ref_name == 'master'
- needs: build-app
- uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
- with:
- gar: true
- image: ${{ needs.build-app.outputs.build-version }}
- cluster: dev-gcp
- secrets: inherit
-
- deploy-prod:
- name: Deploy prod
- permissions:
- id-token: write
- if: github.ref_name == 'master'
- needs: [build-app, deploy-dev]
- uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
- with:
- gar: true
- image: ${{ needs.build-app.outputs.build-version }}
- cluster: prod-gcp
- secrets: inherit
+ runs-on: ${{ (github.event.pull_request.user.login == 'dependabot[bot]' && 'ubuntu-latest') || inputs.runs-on }}
+ env:
+ TZ: 'Europe/Oslo'
+ outputs:
+ build-version: ${{ steps.generate-build-version.outputs.build-version }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # ratchet:actions/checkout@v4
+ with:
+ ref: ${{ (inputs.branch != '' && inputs.branch) || '' }}
+ fetch-depth: 0
+ - name: Sette yarn-config
+ run: |
+ yarn config set npmScopes.navikt.npmRegistryServer "https://npm.pkg.github.com"
+ yarn config set npmScopes.navikt.npmAlwaysAuth true
+ yarn config set npmScopes.navikt.npmAuthToken $NPM_AUTH_TOKEN
+ env:
+ NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
+ - name: Set up NODE med yarn
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # ratchet:actions/setup-node@v3
+ with:
+ node-version: ${{ inputs.node-version }}
+ cache: 'yarn'
+ cache-dependency-path: |
+ node_modules/.cache/nx
+ yarn.lock
+ - name: Installere dependencies
+ run: yarn install --immutable
+ - name: Build
+ run: yarn build-${{ inputs.app }}
+ - name: Run linting
+ run: yarn stylelint && yarn eslint:changed && yarn tsc:changed
+ - name: Run tests
+ id: run-tests
+ run: yarn test:changed
+ - name: Knip it
+ if: inputs.run-knip
+ run: yarn knip --no-exit-code --reporter=markdown >> "$GITHUB_STEP_SUMMARY"
+ - name: Opprett release med Sentry
+ if: github.ref_name == 'master'
+ run: yarn sentry-release
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ - name: Bygg server
+ run: cd ./server && yarn install --immutable && yarn build
+ - name: Generate build version
+ id: generate-build-version
+ run: |
+ echo "build-version=$(date +%Y.%m.%d.%H%M%S)-$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_OUTPUT
+ - name: Print build version
+ run: echo "Generated build-version is ${{ steps.generate-build-version.outputs.build-version }}"
+ - name: Bygg og push docker image
+ if: inputs.build-image
+ uses: navikt/fp-gha-workflows/.github/actions/build-push-docker-image@main # ratchet:exclude
+ with:
+ build-version: ${{ steps.generate-build-version.outputs.build-version }}
+ push-image: ${{ inputs.push-image }}
+ - name: Archive code coverage results
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # ratchet:actions/upload-artifact@v4.6.2
+ with:
+ name: code-coverage-report
+ path: coverage
diff --git a/.github/workflows/deploy-manuelt.yml b/.github/workflows/deploy-manuelt.yml
index 8d9ab5e60d0..1d4cd7a912e 100644
--- a/.github/workflows/deploy-manuelt.yml
+++ b/.github/workflows/deploy-manuelt.yml
@@ -13,10 +13,17 @@ on:
options:
- dev
- prod
+ app:
+ required: true
+ type: choice
+ description: 'Application to deploy ()'
+ options:
+ - fp-frontend
+ - los-avdelingsleder
jobs:
deploy:
- name: Deploy dev
+ name: Deploy
permissions:
id-token: write
uses: navikt/fp-gha-workflows/.github/workflows/deploy.yml@main
@@ -24,4 +31,6 @@ jobs:
gar: true
image: ${{ inputs.image }}
cluster: ${{ inputs.environment }}-gcp
+ image_suffix: '/${{ inputs.app }}'
+ deploy_context: '/${{ inputs.app }}'
secrets: inherit
diff --git a/apps/fp-avdelingsleder/.storybook/main.ts b/apps/fp-avdelingsleder/.storybook/main.ts
new file mode 100644
index 00000000000..29f16ba63bd
--- /dev/null
+++ b/apps/fp-avdelingsleder/.storybook/main.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/no-default-export
+export { config as default } from '../../../.storybook/main-storybook';
diff --git a/apps/fp-avdelingsleder/.storybook/preview.tsx b/apps/fp-avdelingsleder/.storybook/preview.tsx
new file mode 100644
index 00000000000..1bcddfcd447
--- /dev/null
+++ b/apps/fp-avdelingsleder/.storybook/preview.tsx
@@ -0,0 +1 @@
+export { default } from '../../../.storybook/preview-storybook';
diff --git a/apps/fp-avdelingsleder/eslint.config.mjs b/apps/fp-avdelingsleder/eslint.config.mjs
new file mode 100644
index 00000000000..ceb7e1709ba
--- /dev/null
+++ b/apps/fp-avdelingsleder/eslint.config.mjs
@@ -0,0 +1 @@
+export { default } from '@navikt/fp-config-eslint';
diff --git a/apps/fp-avdelingsleder/i18n/nb_NO.json b/apps/fp-avdelingsleder/i18n/nb_NO.json
new file mode 100644
index 00000000000..233170d80e6
--- /dev/null
+++ b/apps/fp-avdelingsleder/i18n/nb_NO.json
@@ -0,0 +1,159 @@
+{
+ "Dekorator.Foreldrepenger": "LOS - Avdelingsleder",
+ "Dekorator.Rettskilde": "Rettskildene",
+ "Dekorator.Systemrutine": "Systemrutine",
+
+ "AvdelingslederIndex.Behandlingskoer": "Behandlingskøer",
+ "AvdelingslederIndex.Saksbehandlere": "Saksbehandlere",
+ "AvdelingslederIndex.Grupper": "Grupper",
+ "AvdelingslederIndex.Nokkeltall": "Nøkkeltall",
+ "AvdelingslederIndex.Reservasjoner": "Reservasjoner",
+
+ "IkkeTilgangTilKode6AvdelingPanel.HarIkkeTilgang": "Du har ikke tilgang til denne avdelingen",
+ "IkkeTilgangTilAvdelingslederPanel.HarIkkeTilgang": "Du har ikke tilgang til å bruke dette programmet",
+
+ "LeggTilSaksbehandlerForm.LeggTil": "Legg til saksbehandler",
+ "LeggTilSaksbehandlerForm.Brukerident": "Brukerident",
+ "LeggTilSaksbehandlerForm.Sok": "Søk",
+ "LeggTilSaksbehandlerForm.LeggTilIListen": "Legg til i listen",
+ "LeggTilSaksbehandlerForm.Nullstill": "Nullstill",
+ "LeggTilSaksbehandlerForm.FinnesAllerede": "Saksbehandler finnes allerede i listen",
+ "LeggTilSaksbehandlerForm.FinnesIkke": "Kan ikke finne brukerident",
+
+ "EndreSakslisterPanel.KnyttetMotSaksbehandlere": "Behandlingskøen benyttes av disse saksbehandlerne",
+
+ "Avdelingsvelger.Avdeling": "Avdeling",
+
+ "SorteringVelger.Sortering": "Sortering",
+ "SorteringVelger.FiltrerPaTidsintervall": "Ta kun med behandlinger med dato",
+ "SorteringVelger.Fom": "F.o.m.",
+ "SorteringVelger.Tom": "T.o.m.",
+ "SorteringVelger.DagerMedBindestrek": "dager frem -",
+ "SorteringVelger.Bindestrek": "-",
+ "SorteringVelger.Dager": "dager frem",
+ "SorteringVelger.DynamiskPeriode": "Dynamisk periode",
+ "SorteringVelger.FiltrerPaHeltall": "Ta kun med behandlinger mellom",
+ "SorteringVelger.Valuta": "kr",
+ "SorteringVelger.Fra": "Fra",
+ "SorteringVelger.Til": "Til",
+
+ "UtvalgskriterierForSakslisteForm.Utvalgskriterier": "Utvalgskriterier",
+ "UtvalgskriterierForSakslisteForm.Navn": "Navn",
+ "UtvalgskriterierForSakslisteForm.NyListe": "Ny behandlingskø",
+
+ "AndreKriterierVelger.TilBeslutter": "Til beslutter",
+ "AndreKriterierVelger.RegistrerPapirsoknad": "Registrer papirsøknad",
+ "AndreKriterierVelger.AndreKriterier": "Andre kriterier",
+ "AndreKriterierVelger.TaMed": "Ta med i køen",
+ "AndreKriterierVelger.Fjern": "Fjern fra køen",
+
+ "GjeldendeSakslisterTabell.GjeldendeLister": "Gjeldende behandlingskøer",
+ "GjeldendeSakslisterTabell.IngenLister": "Ingen behandlingskøer er laget",
+ "GjeldendeSakslisterTabell.Listenavn": "Navn",
+ "GjeldendeSakslisterTabell.LeggTilListe": "Legg til behandlingskø",
+ "GjeldendeSakslisterTabell.Behandlingtype": "Behandlingstype",
+ "GjeldendeSakslisterTabell.Stonadstype": "Stønadstype",
+ "GjeldendeSakslisterTabell.Alle": "Alle",
+ "GjeldendeSakslisterTabell.AntallSaksbehandlere": "Antall saksbehandlere",
+ "GjeldendeSakslisterTabell.AntallBehandlinger": "Antall behandlinger",
+ "GjeldendeSakslisterTabell.OppgaverForAvdeling": "Antall åpne behandlinger totalt",
+
+ "AntallOppgaverForSaksliste.HentingAvAntallOppgaverFeilet": "Henting av antall feilet",
+ "AntallOppgaverForSaksliste.HentingAvAntallOppgaverHentes": "Henter antall oppgaver for behandlingskøen",
+
+ "FagsakYtelseTypeVelger.Stonadstype": "Stønadstype",
+
+ "BehandlingstypeVelger.Behandlingstype": "Behandlingstype",
+
+ "SaksbehandlereForSakslisteForm.Saksbehandlere": "Saksbehandlere",
+ "SaksbehandlereForSakslisteForm.IngenSaksbehandlere": "Avdelingen har ingen saksbehandlere tilknyttet",
+ "SaksbehandlereForSakslisteForm.Gruppenavn": "Gruppenavn",
+ "SaksbehandlereForSakslisteForm.AntallSaksbehandlere": "Antall tilknyttet køen",
+ "SaksbehandlereForSakslisteForm.VisAlle": "Alle saksbehandlere",
+
+ "SletteSaksbehandlerModal.SletteModal": "Ønsker du å slette saksbehandler?",
+ "SletteSaksbehandlerModal.SletteSaksbehandler": "Ønsker du å slette {saksbehandlerNavn}?",
+ "SletteSaksbehandlerModal.Ja": "Ja",
+ "SletteSaksbehandlerModal.Nei": "Nei",
+
+ "FordelingAvBehandlingstypePanel.Fordeling": "Antall åpne oppgaver nå",
+ "FordelingAvBehandlingstypePanel.Alle": "Alle",
+ "FordelingAvBehandlingstypeGraf.TilBehandling": "Til behandling",
+ "FordelingAvBehandlingstypeGraf.TilBeslutter": "Til beslutter",
+
+ "TilBehandlingPanel.TilBehandling": "Antall åpne oppgaver pr dato",
+ "TilBehandlingPanel.ToSisteUker": "2 siste uker",
+ "TilBehandlingPanel.FireSisteUker": "4 siste uker",
+ "TilBehandlingPanel.Alle": "Alle",
+
+ "VentefristUtløperPanel.SattPaVent": "Førstegangsbehandlinger på vent fordelt på utløp av ventefrist",
+ "VentefristUtløperPanel.Alle": "Alle",
+
+ "OppgaverSomErApneEllerPaVentPanel.Apne": "Åpne behandlinger foreldrepenger fordelt på første uttaksdag fra søknad",
+ "OppgaverSomErApneEllerPaVentGraf.PaVent": "På vent",
+ "OppgaverSomErApneEllerPaVentGraf.IkkePaVent": "Ikke på vent",
+ "OppgaverSomErApneEllerPaVentGraf.AntallPaVent": "På vent: {antall}",
+ "OppgaverSomErApneEllerPaVentGraf.AntallIkkePaVent": "Ikke på vent: {antall}",
+ "OppgaverSomErApneEllerPaVentGraf.AntallGraf": "Antall",
+ "OppgaverSomErApneEllerPaVentGraf.Ukjent": "Ukjent",
+ "OppgaverSomErApneEllerPaVentGraf.Dato": "dato",
+ "OppgaverSomErApneEllerPaVentGraf.0": "Jan",
+ "OppgaverSomErApneEllerPaVentGraf.1": "Feb",
+ "OppgaverSomErApneEllerPaVentGraf.2": "Mars",
+ "OppgaverSomErApneEllerPaVentGraf.3": "April",
+ "OppgaverSomErApneEllerPaVentGraf.4": "Mai",
+ "OppgaverSomErApneEllerPaVentGraf.5": "Juni",
+ "OppgaverSomErApneEllerPaVentGraf.6": "Juli",
+ "OppgaverSomErApneEllerPaVentGraf.7": "Aug",
+ "OppgaverSomErApneEllerPaVentGraf.8": "Sept",
+ "OppgaverSomErApneEllerPaVentGraf.9": "Okt",
+ "OppgaverSomErApneEllerPaVentGraf.10": "Nov",
+ "OppgaverSomErApneEllerPaVentGraf.11": "Des",
+
+ "ReservasjonerTabell.Navn": "Navn",
+ "ReservasjonerTabell.Saksnr": "Saksnummer",
+ "ReservasjonerTabell.BehandlingType": "Type",
+ "ReservasjonerTabell.ReservertTil": "Reservert til",
+ "ReservasjonerTabell.ReservertTilFormat": "{time} - {date}",
+ "ReservasjonerTabell.Endre": "Endre",
+ "ReservasjonerTabell.Flytt": "Flytt",
+ "ReservasjonerTabell.Slett": "Slett",
+ "ReservasjonerTabell.Reservasjoner": "Reservasjoner for avdelingen",
+ "ReservasjonerTabell.IngenReservasjoner": "Ingen reservasjoner funnet",
+
+ "OppgaveReservasjonEndringDatoModal.Header": "Velg dato som reservasjonen avsluttes",
+ "OppgaveReservasjonEndringDatoModal.Ok": "OK",
+ "OppgaveReservasjonEndringDatoModal.Avbryt": "Avbryt",
+
+ "FlyttReservasjonModal.FlyttReservasjon": "Flytt reservasjonen til annen saksbehandler",
+ "FlyttReservasjonModal.Brukerident": "Brukerident",
+ "FlyttReservasjonModal.Sok": "Søk",
+ "FlyttReservasjonModal.Begrunn": "Begrunn flytting av reservasjonen",
+ "FlyttReservasjonModal.Ok": "OK",
+ "FlyttReservasjonModal.Avbryt": "Avbryt",
+
+ "SaksbehandlereTabell.AnsattVed": "Ansatt ved avdeling",
+ "SaksbehandlereTabell.Navn": "Navn",
+ "SaksbehandlereTabell.Brukerident": "Brukerident",
+ "SaksbehandlereTabell.IngenSaksbehandlere": "Ingen saksbehandlere lagt til",
+
+ "OppgaverPerForsteStonadsdagPanel.FordeltPaForsteStonadsdag": "Antall åpne oppgaver for førstegangsbehandlinger fordelt på første stønadsdag - alle ytelser",
+
+ "SletteSakslisteModal.SletteModal": "Ønsker du å slette behandlingskøen?",
+ "SletteSakslisteModal.SletteSaksliste": "Ønsker du å slette {sakslisteNavn}?",
+ "SletteSakslisteModal.Ja": "Ja",
+ "SletteSakslisteModal.Nei": "Nei",
+
+ "GrupperPanel.OpprettGruppe": "Opprett gruppe",
+
+ "GrupperTabell.Grupper": "Grupper",
+ "GrupperTabell.Id": "Id",
+ "GrupperTabell.Navn": "Navn",
+ "GrupperTabell.AntallSaksbehandlere": "Antall saksbehandlere",
+ "GrupperTabell.IngenGrupper": "Ingen grupper",
+
+ "GruppeSaksbehandlere.ValgteSaksbehandlere": "Valgte saksbehandlere",
+ "GruppeSaksbehandlere.Ingen": "Ingen valgte saksbehandlere",
+ "GruppeSaksbehandlere.Navn": "Navn på gruppe",
+ "GruppeSaksbehandlere.VelgSaksbehandlere": "Velg saksbehandlere"
+}
diff --git a/apps/fp-avdelingsleder/index.html b/apps/fp-avdelingsleder/index.html
new file mode 100644
index 00000000000..cbfe3ca829f
--- /dev/null
+++ b/apps/fp-avdelingsleder/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Los Avdelingsleder
+
+
+
+
+
+
diff --git a/apps/fp-avdelingsleder/package.json b/apps/fp-avdelingsleder/package.json
new file mode 100644
index 00000000000..804c16ab484
--- /dev/null
+++ b/apps/fp-avdelingsleder/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@navikt/fp-los-avdelingsleder",
+ "license": "MIT",
+ "private": true,
+ "module": "src/main.tsx",
+ "type": "module",
+ "exports": {
+ ".": "./index.html"
+ },
+ "scripts": {
+ "test": "vitest",
+ "test:watch": "vitest --watch=true",
+ "tsc": "tsc --pretty",
+ "eslint": "eslint \"src/**/*.ts*\" --color",
+ "eslint:fix": "eslint --fix \"src/**/*.ts*\" --color",
+ "stylelint": "stylelint \"src/**/*.module.css\"",
+ "prettier": "prettier --write src",
+ "dev": "vite serve",
+ "build": "vite build",
+ "preview": "vite preview",
+ "clean": "rm -rf ./dist ./node_modules ./coverage ./.storybook-static-build",
+ "build-storybook": "storybook build -o .storybook-static-build",
+ "storybook": "storybook dev --quiet -p 7020"
+ },
+ "dependencies": {
+ "@navikt/aksel-icons": "7.26.0",
+ "@navikt/ds-css": "7.26.0",
+ "@navikt/ds-react": "7.26.0",
+ "@navikt/fp-kodeverk": "workspace:*",
+ "@navikt/fp-los-felles": "workspace:*",
+ "@navikt/fp-types": "workspace:*",
+ "@navikt/fp-utils": "workspace:*",
+ "@navikt/ft-form-hooks": "9.0.1",
+ "@navikt/ft-form-validators": "4.1.1",
+ "@navikt/ft-ui-komponenter": "6.0.1",
+ "@navikt/ft-utils": "3.7.1",
+ "@tanstack/react-query": "5.84.1",
+ "dayjs": "1.11.13",
+ "history": "5.3.0",
+ "ky": "1.8.2",
+ "lodash.debounce": "4.0.8",
+ "react": "19.1.1",
+ "react-hook-form": "7.62.0",
+ "react-intl": "7.1.11",
+ "react-router-dom": "7.8.0"
+ },
+ "devDependencies": {
+ "@navikt/fp-config-eslint": "workspace:*",
+ "@navikt/fp-config-typescript": "workspace:*",
+ "@navikt/fp-config-vite": "workspace:*",
+ "@navikt/fp-storybook-utils": "workspace:*",
+ "@storybook/react": "9.1.1",
+ "@storybook/react-vite": "9.1.1",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/react": "16.3.0",
+ "@testing-library/user-event": "14.6.1",
+ "@types/lodash.debounce": "4.0.9",
+ "eslint": "9.32.0",
+ "msw": "2.10.4",
+ "msw-storybook-addon": "2.0.5",
+ "storybook": "9.1.1",
+ "stylelint": "16.23.1",
+ "typescript": "5.9.2",
+ "vite": "7.1.1",
+ "vitest": "3.2.4"
+ }
+}
diff --git a/apps/fp-avdelingsleder/public/favicon.ico b/apps/fp-avdelingsleder/public/favicon.ico
new file mode 100644
index 00000000000..4e741d3f6d5
Binary files /dev/null and b/apps/fp-avdelingsleder/public/favicon.ico differ
diff --git a/apps/fp-avdelingsleder/src/app/LosAppIndex.tsx b/apps/fp-avdelingsleder/src/app/LosAppIndex.tsx
new file mode 100644
index 00000000000..150f1691fc5
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/LosAppIndex.tsx
@@ -0,0 +1,170 @@
+import { type ComponentProps, useMemo, useState } from 'react';
+import { RawIntlProvider } from 'react-intl';
+import { Link, useLocation } from 'react-router-dom';
+
+import { Theme } from '@navikt/ds-react';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { createIntl, parseQueryString } from '@navikt/ft-utils';
+import { MutationCache, QueryCache, QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { HTTPError } from 'ky';
+
+import { ForbiddenPage, UnauthorizedPage } from '@navikt/fp-sak-infosider';
+
+import { ErrorType, type FpError } from '../data/error/errorType';
+import { useRestApiError, useRestApiErrorDispatcher } from '../data/error/RestApiErrorContext';
+import { initFetchOptions } from '../data/fplosAvdelingslederApi';
+import { Dekorator } from './components/Dekorator';
+import { ErrorBoundary } from './components/ErrorBoundary';
+import { Home } from './components/Home';
+
+import '../globalCss/global.module.css';
+
+import messages from '../../i18n/nb_NO.json';
+
+import '@navikt/ds-css/darkside';
+import '@navikt/ds-css-internal';
+import '@navikt/ft-form-hooks/dist/style.css';
+import '@navikt/ft-plattform-komponenter/dist/style.css';
+import '@navikt/ft-ui-komponenter/dist/style.css';
+
+const EMPTY_ARRAY = new Array();
+
+const intl = createIntl(messages);
+
+export const LosAppIndexWrapper = () => {
+ const { addErrorMessage } = useRestApiErrorDispatcher();
+ const queryClient = useMemo(() => createQueryClient(getErrorHandler(addErrorMessage)), []);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const LosAppIndex = () => {
+ const [headerHeight, setHeaderHeight] = useState(0);
+ const [crashMessage, setCrashMessage] = useState();
+ const [theme, setTheme] = useState['theme']>('light');
+
+ const initFetchQuery = useQuery(initFetchOptions());
+
+ const location = useLocation();
+
+ const setSiteHeight = (newHeaderHeight: number): void => {
+ document.documentElement.setAttribute('style', `height: calc(100% - ${newHeaderHeight}px)`);
+ setHeaderHeight(newHeaderHeight);
+ };
+
+ const addErrorMessageAndSetAsCrashed = (error: FpError) => {
+ setCrashMessage(
+ error.type === ErrorType.GENERAL_ERROR
+ ? error.message
+ : 'Det oppstod en feilsituasjon som ikke blir håndtert korrekt',
+ );
+ };
+
+ const errorMessages = useRestApiError() ?? EMPTY_ARRAY;
+ const queryStrings = parseQueryString(location.search);
+ const hasForbiddenErrors = errorMessages.some(o => o.type === ErrorType.REQUEST_FORBIDDEN);
+ const hasUnauthorizedErrors = errorMessages.some(o => o.type === ErrorType.REQUEST_UNAUTHORIZED);
+ const hasForbiddenOrUnauthorizedErrors = hasForbiddenErrors || hasUnauthorizedErrors;
+ const shouldRenderHome = !crashMessage && !hasForbiddenOrUnauthorizedErrors;
+
+ if (initFetchQuery.isPending || !initFetchQuery.data) {
+ return ;
+ }
+
+ const navAnsatt = initFetchQuery.data.innloggetBruker;
+
+ return (
+
+
+
+
+ {shouldRenderHome && }
+
+ {hasForbiddenErrors && {tekst}} />}
+ {hasUnauthorizedErrors && {tekst}} />}
+
+
+ );
+};
+
+const createQueryClient = (errorHandler: (error: Error) => void) =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: retryHandler(),
+ },
+ mutations: {
+ retry: retryHandler(),
+ },
+ },
+ queryCache: new QueryCache({
+ onError: errorHandler,
+ }),
+ mutationCache: new MutationCache({
+ onError: errorHandler,
+ }),
+ });
+
+const ZERO_RETRIES = false;
+
+const retryHandler = () => {
+ if (import.meta.env.MODE === 'test') {
+ return ZERO_RETRIES;
+ }
+
+ return (failureCount: number, error: Error) => {
+ if (error instanceof HTTPError) {
+ if (error.response.status === 401 || error.response.status === 403) {
+ return ZERO_RETRIES;
+ }
+ if (error.response.status === 500) {
+ return failureCount < 1;
+ }
+ }
+ return failureCount < 3;
+ };
+};
+
+const getErrorHandler = (addErrorMessage: (data: FpError) => void) => async (error: Error) => {
+ // eslint-disable-next-line no-console
+ console.log(error);
+
+ if (error instanceof HTTPError) {
+ if (error.response.status === 403) {
+ addErrorMessage({ type: ErrorType.REQUEST_FORBIDDEN, message: error.message });
+ } else if (error.response.status === 401) {
+ addErrorMessage({ type: ErrorType.REQUEST_UNAUTHORIZED, message: error.message });
+ } else if (error.response.status === 504 || error.response.status === 404) {
+ addErrorMessage({
+ type: ErrorType.REQUEST_GATEWAY_TIMEOUT_OR_NOT_FOUND,
+ //@ts-expect-error Fiks
+ location: error.response?.config?.url,
+ });
+ } else {
+ try {
+ const feildataJson = await error.response.json();
+ addErrorMessage({ type: ErrorType.GENERAL_ERROR, message: feildataJson.feilmelding ?? error.message });
+ } catch {
+ addErrorMessage({ type: ErrorType.GENERAL_ERROR, message: error.message });
+ }
+ }
+ } else {
+ addErrorMessage({ type: ErrorType.GENERAL_ERROR, message: error.message });
+ }
+};
diff --git a/apps/fp-avdelingsleder/src/app/components/AvdelingslederIndex.tsx b/apps/fp-avdelingsleder/src/app/components/AvdelingslederIndex.tsx
new file mode 100644
index 00000000000..4e8088238f0
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/AvdelingslederIndex.tsx
@@ -0,0 +1,218 @@
+import { useEffect, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useNavigate } from 'react-router-dom';
+
+import { Box, Heading, HStack, Select, Tabs } from '@navikt/ds-react';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { formatQueryString, parseQueryString } from '@navikt/ft-utils';
+import { useQuery } from '@tanstack/react-query';
+import { type Location } from 'history';
+
+import type { NavAnsatt } from '@navikt/fp-types';
+import { useTrackRouteParam } from '@navikt/fp-utils';
+
+import { EndreSakslisterPanel } from '../../behandlingskoer/EndreSakslisterPanel';
+import { IkkeTilgangTilAvdelingslederPanel } from '../../components/IkkeTilgangTilAvdelingslederPanel';
+import {
+ avdelingerOptions,
+ losKodeverkOptions,
+ saksbehandlareForAvdelingOptions,
+} from '../../data/fplosAvdelingslederApi';
+import {
+ getValueFromLocalStorage,
+ removeValueFromLocalStorage,
+ setValueInLocalStorage,
+} from '../../data/localStorageHelper';
+import { GrupperPanel } from '../../grupper/GrupperPanel';
+import { NokkeltallPanel } from '../../nokkeltall/NokkeltallPanel';
+import { ReservasjonerTabell } from '../../reservasjoner/ReservasjonerTabell';
+import { SaksbehandlerePanel } from '../../saksbehandlere/SaksbehandlerePanel';
+import type { Avdeling } from '../../typer/avdelingTsType';
+import { AvdelingslederPanels } from './avdelingslederPanels';
+
+import styles from './avdelingslederIndex.module.css';
+
+interface Props {
+ navAnsatt?: NavAnsatt;
+}
+
+export const AvdelingslederIndex = ({ navAnsatt }: Props) => {
+ const navigate = useNavigate();
+ const [valgtAvdelingEnhet, setValgtAvdelingEnhet] = useState();
+
+ const { selected: activeAvdelingslederPanelTemp, location } = useTrackRouteParam({
+ paramName: 'fane',
+ isQueryParam: true,
+ });
+
+ const alleKodeverkQuery = useQuery(losKodeverkOptions());
+
+ useEffect(() => {
+ if (alleKodeverkQuery.isError) {
+ setLosErIkkeTilgjengelig();
+ }
+ }, [alleKodeverkQuery.isError]);
+
+ const { data: filtrerteAvdelinger, status: avdelingerStatus } = useQuery({
+ ...avdelingerOptions(!!navAnsatt?.kanOppgavestyre),
+ select: avdelinger => avdelinger.filter(a => !!navAnsatt?.kanBehandleKode6 || !a.kreverKode6),
+ });
+
+ const { data: avdelingensSaksbehandlere } = useQuery(saksbehandlareForAvdelingOptions(valgtAvdelingEnhet));
+
+ useEffect(() => {
+ if (avdelingerStatus === 'success') {
+ setAvdeling(setValgtAvdelingEnhet, filtrerteAvdelinger, valgtAvdelingEnhet);
+ }
+ }, [avdelingerStatus]);
+
+ const getAvdelingslederPanelLocation = (avdelingslederPanel: string) => ({
+ ...location,
+ search: updateQueryParams(location.search, { fane: avdelingslederPanel }),
+ });
+ const activeAvdelingslederPanel = activeAvdelingslederPanelTemp || getPanelFromUrlOrDefault(location);
+
+ if (!navAnsatt?.kanOppgavestyre) {
+ return ;
+ }
+ if (alleKodeverkQuery.isPending || avdelingerStatus !== 'success' || valgtAvdelingEnhet === undefined) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {
+ navigate(getAvdelingslederPanelLocation(avdelingslederPanel));
+ }}
+ className={styles.paddingHeader}
+ >
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ {activeAvdelingslederPanel === AvdelingslederPanels.BEHANDLINGSKOER && (
+
+ )}
+ {activeAvdelingslederPanel === AvdelingslederPanels.SAKSBEHANDLERE && (
+
+ )}
+ {activeAvdelingslederPanel === AvdelingslederPanels.GRUPPER && (
+
+ )}
+ {activeAvdelingslederPanel === AvdelingslederPanels.NOKKELTALL && (
+
+ )}
+ {activeAvdelingslederPanel === AvdelingslederPanels.RESERVASJONER && (
+
+ )}
+
+
+ );
+};
+
+const nasjonalEnhet = '4867';
+
+const setAvdeling = (
+ setValgtAvdeling: (avdelingEnhet: string) => void,
+ avdelinger: Avdeling[],
+ valgtAvdelingEnhet?: string,
+) => {
+ if (avdelinger.length > 0 && !valgtAvdelingEnhet) {
+ let valgtEnhet = avdelinger.some(a => a.avdelingEnhet === nasjonalEnhet)
+ ? nasjonalEnhet
+ : avdelinger[0].avdelingEnhet;
+ const lagretAvdelingEnhet = getValueFromLocalStorage('avdelingEnhet');
+ if (lagretAvdelingEnhet) {
+ if (avdelinger.some(a => a.avdelingEnhet === lagretAvdelingEnhet)) {
+ valgtEnhet = lagretAvdelingEnhet;
+ } else {
+ removeValueFromLocalStorage('avdelingEnhet');
+ }
+ }
+ setValgtAvdeling(valgtEnhet);
+ }
+};
+
+const emptyQueryString = (queryString: string) => queryString === '?' || !queryString;
+
+const updateQueryParams = (queryString: string, nextParams: Record) => {
+ const prevParams = emptyQueryString(queryString) ? {} : parseQueryString(queryString);
+ return formatQueryString({
+ ...prevParams,
+ ...nextParams,
+ });
+};
+
+const getPanelFromUrlOrDefault = (location: Location) => {
+ const panelFromUrl = parseQueryString(location.search);
+ return panelFromUrl['avdelingsleder'] ?? AvdelingslederPanels.BEHANDLINGSKOER;
+};
diff --git a/apps/fp-avdelingsleder/src/app/components/Dekorator.tsx b/apps/fp-avdelingsleder/src/app/components/Dekorator.tsx
new file mode 100644
index 00000000000..abc8c0c938a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/Dekorator.tsx
@@ -0,0 +1,168 @@
+import React, { type ComponentProps } from 'react';
+import { type IntlShape, useIntl } from 'react-intl';
+import { useNavigate } from 'react-router-dom';
+
+import type { Theme } from '@navikt/ds-react';
+import { dateFormat, decodeHtmlEntity, timeFormat } from '@navikt/ft-utils';
+
+import { ApiPollingStatus, RETTSKILDE_URL, SYSTEMRUTINE_URL } from '@navikt/fp-konstanter';
+import { DekoratorMedFeilviserSakIndex, type Feilmelding } from '@navikt/fp-sak-dekorator';
+import type { NavAnsatt } from '@navikt/fp-types';
+
+import { ErrorType, type FpError } from '../../data/error/errorType';
+import { useRestApiError, useRestApiErrorDispatcher } from '../../data/error/RestApiErrorContext';
+
+type QueryStrings = {
+ errorcode?: string;
+ errormessage?: string;
+};
+
+interface Props {
+ queryStrings: QueryStrings;
+ setSiteHeight: (headerHeight: number) => void;
+ crashMessage?: string;
+ hideErrorMessages?: boolean;
+ theme: ComponentProps['theme'];
+ setTheme: (theme: ComponentProps['theme']) => void;
+ navAnsatt: NavAnsatt;
+}
+
+export const Dekorator = ({
+ queryStrings,
+ setSiteHeight,
+ crashMessage,
+ hideErrorMessages = false,
+ theme,
+ setTheme,
+ navAnsatt,
+}: Props) => {
+ const intl = useIntl();
+
+ const errorMessages = useRestApiError();
+ const { removeErrorMessages } = useRestApiErrorDispatcher();
+
+ const navigate = useNavigate();
+ const visLos = (e: React.SyntheticEvent) => {
+ if (e.type === 'click') {
+ navigate('/');
+ }
+ if (e.type === 'contextmenu') {
+ window.open('/', '_newtab');
+ }
+ e.preventDefault();
+ };
+
+ const eksterneLenker = [
+ {
+ tekst: intl.formatMessage({ id: 'Dekorator.Rettskilde' }),
+ href: RETTSKILDE_URL,
+ },
+ {
+ tekst: intl.formatMessage({ id: 'Dekorator.Systemrutine' }),
+ href: SYSTEMRUTINE_URL,
+ },
+ ];
+
+ return (
+
+ );
+};
+
+const addIfNotExists = (feilmeldinger: Feilmelding[], nyFeilmelding: Feilmelding) => {
+ if (
+ !feilmeldinger.some(
+ feil => feil.melding === nyFeilmelding.melding && feil.tilleggsInfo === nyFeilmelding.tilleggsInfo,
+ )
+ ) {
+ feilmeldinger.push(nyFeilmelding);
+ }
+};
+
+const formaterFeilmeldinger = (
+ intl: IntlShape,
+ alleFeilmeldinger: FpError[],
+ queryStringFeilmeldinger: QueryStrings,
+ crashMessage?: string,
+): Feilmelding[] => {
+ const feilmeldinger: Feilmelding[] = [];
+
+ if (queryStringFeilmeldinger.errorcode) {
+ addIfNotExists(feilmeldinger, { melding: intl.formatMessage({ id: queryStringFeilmeldinger.errorcode }) });
+ }
+ if (queryStringFeilmeldinger.errormessage) {
+ addIfNotExists(feilmeldinger, { melding: queryStringFeilmeldinger.errormessage });
+ }
+ if (crashMessage) {
+ addIfNotExists(feilmeldinger, { melding: crashMessage });
+ }
+
+ alleFeilmeldinger.forEach(feilmelding => {
+ switch (feilmelding.type) {
+ case ErrorType.POLLING_HALTED_OR_DELAYED:
+ if (feilmelding.status === ApiPollingStatus.HALTED) {
+ const decoded = decodeHtmlEntity(feilmelding.message);
+ addIfNotExists(feilmeldinger, {
+ melding: intl.formatMessage({ id: 'Rest.ErrorMessage.General' }),
+ tilleggsInfo: decoded ? parseErrorDetails(decoded) : undefined,
+ });
+ }
+ if (feilmelding.status === ApiPollingStatus.DELAYED) {
+ addIfNotExists(feilmeldinger, {
+ melding: intl.formatMessage(
+ { id: 'Rest.ErrorMessage.DownTime' },
+ {
+ date: dateFormat(feilmelding.eta),
+ time: timeFormat(feilmelding.eta),
+ message: feilmelding.message,
+ },
+ ),
+ });
+ }
+ break;
+ case ErrorType.POLLING_TIMEOUT:
+ addIfNotExists(feilmeldinger, {
+ melding: intl.formatMessage({ id: 'Rest.ErrorMessage.PollingTimeout' }, { location: feilmelding.location }),
+ });
+ break;
+ case ErrorType.REQUEST_GATEWAY_TIMEOUT_OR_NOT_FOUND:
+ addIfNotExists(feilmeldinger, {
+ melding: intl.formatMessage(
+ { id: 'Rest.ErrorMessage.GatewayTimeoutOrNotFound' },
+ {
+ contextPath: feilmelding.location ? feilmelding.location.split('/')[1]?.toUpperCase() : '',
+ location: feilmelding.location,
+ },
+ ),
+ });
+ break;
+ case ErrorType.REQUEST_FORBIDDEN:
+ case ErrorType.REQUEST_UNAUTHORIZED:
+ case ErrorType.GENERAL_ERROR:
+ default:
+ addIfNotExists(feilmeldinger, {
+ melding: feilmelding.message,
+ });
+ }
+ });
+
+ return feilmeldinger;
+};
+
+const parseErrorDetails = (details: string) => {
+ try {
+ return JSON.parse(details);
+ } catch {
+ return 'Kunne ikke tolke feildetaljer';
+ }
+};
diff --git a/apps/fp-avdelingsleder/src/app/components/ErrorBoundary.tsx b/apps/fp-avdelingsleder/src/app/components/ErrorBoundary.tsx
new file mode 100644
index 00000000000..95cb099bcf1
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/ErrorBoundary.tsx
@@ -0,0 +1,88 @@
+import { Component, type ErrorInfo, type ReactNode } from 'react';
+
+import { ErrorMessage } from '@navikt/ds-react';
+import { captureException, withScope } from '@sentry/browser';
+
+import { ErrorPage } from '@navikt/fp-sak-infosider';
+
+import { ErrorType, type FpError } from '../../data/error/errorType';
+
+const isDevelopment = import.meta.env.MODE === 'development';
+
+interface OwnProps {
+ errorMessageCallback: (error: FpError) => void;
+ children: ReactNode;
+ errorMessage?: string;
+ showChild?: boolean;
+}
+
+interface State {
+ hasError: boolean;
+}
+
+export class ErrorBoundary extends Component {
+ static defaultProps = {
+ showChild: false,
+ };
+
+ constructor(props: OwnProps) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError() {
+ // Update state so the next render will show the fallback UI.
+ return { hasError: true };
+ }
+
+ override componentDidCatch(error: Error, info: ErrorInfo): void {
+ const { errorMessageCallback } = this.props;
+
+ if (!isDevelopment) {
+ withScope(scope => {
+ Object.entries(info).forEach(entry => {
+ scope.setExtra(entry[0], entry[1]);
+ captureException(error);
+ });
+ });
+ }
+
+ const errorStrings = info.componentStack
+ ? [
+ error.toString(),
+ info.componentStack
+ .split('\n')
+ .map(line => line.trim())
+ .find(line => !!line),
+ ].join(' ')
+ : error.toString();
+
+ errorMessageCallback({ type: ErrorType.GENERAL_ERROR, message: errorStrings });
+
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+
+ override render(): ReactNode {
+ const { children, showChild, errorMessage } = this.props;
+ const { hasError } = this.state;
+
+ if (hasError) {
+ if (errorMessage) {
+ return (
+
+ {errorMessage}
+
+ );
+ }
+ return (
+ <>
+ {showChild && {children}
}
+
+ >
+ );
+ }
+
+ return children;
+ }
+}
diff --git a/apps/fp-avdelingsleder/src/app/components/Home.tsx b/apps/fp-avdelingsleder/src/app/components/Home.tsx
new file mode 100644
index 00000000000..94c3cea0c4c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/Home.tsx
@@ -0,0 +1,29 @@
+import { Link, Route, Routes } from 'react-router-dom';
+
+import { NotFoundPage } from '@navikt/fp-sak-infosider';
+import type { NavAnsatt } from '@navikt/fp-types';
+
+import { AvdelingslederIndex } from './AvdelingslederIndex';
+
+import styles from './home.module.css';
+
+interface Props {
+ headerHeight: number;
+ navAnsatt?: NavAnsatt;
+}
+
+/**
+ * Home
+ *
+ * Wrapper for sideinnholdet som vises under header.
+ */
+export const Home = ({ headerHeight, navAnsatt }: Props) => {
+ return (
+
+
+ } />
+ {tekst}} />} />
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/app/components/avdelingslederIndex.module.css b/apps/fp-avdelingsleder/src/app/components/avdelingslederIndex.module.css
new file mode 100644
index 00000000000..c91cb59baf9
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/avdelingslederIndex.module.css
@@ -0,0 +1,9 @@
+.container {
+ background-color: var(--ax-bg-default);
+ padding-top: 15px;
+}
+
+.paddingHeader {
+ box-shadow: inset 0 -1px 0 0 var(--ax-neutral-400);
+ padding-left: 100px;
+}
diff --git a/apps/fp-avdelingsleder/src/app/components/avdelingslederPanels.ts b/apps/fp-avdelingsleder/src/app/components/avdelingslederPanels.ts
new file mode 100644
index 00000000000..5ca76cf543b
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/avdelingslederPanels.ts
@@ -0,0 +1,7 @@
+export const AvdelingslederPanels = {
+ BEHANDLINGSKOER: 'behandlingskoer',
+ SAKSBEHANDLERE: 'saksbehandlere',
+ GRUPPER: 'grupper',
+ NOKKELTALL: 'nokkeltall',
+ RESERVASJONER: 'reservasjoner',
+};
diff --git a/apps/fp-avdelingsleder/src/app/components/home.module.css b/apps/fp-avdelingsleder/src/app/components/home.module.css
new file mode 100644
index 00000000000..0c30906d181
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/app/components/home.module.css
@@ -0,0 +1,4 @@
+.content {
+ font-family: 'Source Sans Pro', Arial, sans-serif;
+ height: 100%;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.stories.tsx
new file mode 100644
index 00000000000..11427d23494
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.stories.tsx
@@ -0,0 +1,78 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { AndreKriterierType, BehandlingType, FagsakYtelseType, KøSortering } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../data/fplosAvdelingslederApi';
+import type { SakslisteAvdeling } from '../typer/sakslisteAvdelingTsType';
+import { EndreSakslisterPanel } from './EndreSakslisterPanel';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const SAKSLISTER = [
+ {
+ sakslisteId: 1,
+ navn: 'test',
+ saksbehandlerIdenter: [],
+ sortering: {
+ sorteringType: KøSortering.BEHANDLINGSFRIST,
+ fra: 1,
+ til: 4,
+ erDynamiskPeriode: true,
+ },
+ behandlingTyper: [BehandlingType.FORSTEGANGSSOKNAD],
+ fagsakYtelseTyper: [FagsakYtelseType.FORELDREPENGER],
+ andreKriterier: [
+ {
+ andreKriterierType: AndreKriterierType.TIL_BESLUTTER,
+ inkluder: true,
+ },
+ {
+ andreKriterierType: AndreKriterierType.PAPIRSOKNAD,
+ inkluder: false,
+ },
+ ],
+ },
+] satisfies SakslisteAvdeling[];
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/EndreSakslisterPanel',
+ component: EndreSakslisterPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.OPPGAVE_ANTALL, () => HttpResponse.json(1)),
+ http.get(LosUrl.SAKSLISTER_FOR_AVDELING, () => HttpResponse.json(SAKSLISTER)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_NAVN, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_INTERVALL, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_DYNAMISK_PERIDE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_FAGSAK_YTELSE_TYPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_BEHANDLINGSTYPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_ANDRE_KRITERIER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ render: args => {
+ const { data: kodeverkLos } = useQuery(losKodeverkOptions());
+
+ return kodeverkLos ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ valgtAvdelingEnhet: 'NAV Oslo',
+ avdelingensSaksbehandlere: [],
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.tsx
new file mode 100644
index 00000000000..eb7658e24cb
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/EndreSakslisterPanel.tsx
@@ -0,0 +1,83 @@
+import React, { useState } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { ArrowDownIcon } from '@navikt/aksel-icons';
+import { HStack } from '@navikt/ds-react';
+import { useMutation, useQuery } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import {
+ oppgaverForAvdelingAntallOptions,
+ opprettNySaksliste,
+ sakslisterForAvdelingOptions,
+} from '../data/fplosAvdelingslederApi';
+import { GjeldendeSakslisterTabell } from './GjeldendeSakslisterTabell';
+import { SaksbehandlereForSakslisteForm } from './saksbehandlerForm/SaksbehandlereForSakslisteForm';
+import { UtvalgskriterierForSakslisteForm } from './sakslisteForm/UtvalgskriterierForSakslisteForm';
+
+import styles from './endreSakslisterPanel.module.css';
+
+interface Props {
+ valgtAvdelingEnhet: string;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+}
+
+export const EndreSakslisterPanel = ({ valgtAvdelingEnhet, avdelingensSaksbehandlere }: Props) => {
+ const intl = useIntl();
+ const [valgtSakslisteId, setValgtSakslisteId] = useState();
+
+ const { data: oppgaverForAvdelingAntall } = useQuery(oppgaverForAvdelingAntallOptions(valgtAvdelingEnhet));
+ const { data: sakslister, refetch: refetchSakslister } = useQuery(sakslisterForAvdelingOptions(valgtAvdelingEnhet));
+
+ const { mutate: lagNySakslisteOgHentAvdelingensSakslisterPåNytt, data: nySakslisteObject } = useMutation({
+ mutationFn: () => opprettNySaksliste(valgtAvdelingEnhet),
+ onSuccess: () => {
+ setValgtSakslisteId(undefined);
+ refetchSakslister();
+ },
+ });
+
+ const nyId = nySakslisteObject ? parseInt(nySakslisteObject.sakslisteId, 10) : undefined;
+ const valgtSakId = valgtSakslisteId !== undefined ? valgtSakslisteId : nyId;
+
+ const valgtSaksliste = sakslister.find(s => s.sakslisteId === valgtSakId);
+
+ return (
+ setValgtSakslisteId(undefined)}
+ >
+
+ {valgtSakId && valgtSaksliste && (
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.spec.tsx
new file mode 100644
index 00000000000..7f7375a9faf
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.spec.tsx
@@ -0,0 +1,53 @@
+import { composeStories } from '@storybook/react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './GjeldendeSakslisterTabell.stories';
+
+const { TabellNårDetIkkeFinnesBehandlingskøer, TabellNårDetFinnesEnBehandlingskø } = composeStories(stories);
+
+describe('GjeldendeSakslisterTabell', () => {
+ it('skal vise at ingen behandlingskøer er laget og så legge til en ny kø', async () => {
+ await applyRequestHandlers(TabellNårDetIkkeFinnesBehandlingskøer.parameters['msw']);
+ render();
+ expect(await screen.findByText('Ingen behandlingskøer er laget')).toBeInTheDocument();
+ expect(screen.queryByText('Navn')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Legg til behandlingskø'));
+
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+ expect(await screen.findByText('Ny liste')).toBeInTheDocument();
+ });
+
+ it('skal vise slette kø ved å trykke på ikon for sletting', async () => {
+ await applyRequestHandlers(TabellNårDetFinnesEnBehandlingskø.parameters['msw']);
+ render();
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+
+ await userEvent.click(screen.getAllByRole('img')[1]);
+
+ expect(await screen.findByText('Ønsker du å slette Saksliste 1?')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Ja'));
+
+ expect(screen.queryByText('Ønsker du å slette Saksliste 1?')).not.toBeInTheDocument();
+ });
+
+ it('skal legge til en ny kø ved bruk av tastaturet (enter)', async () => {
+ await applyRequestHandlers(TabellNårDetIkkeFinnesBehandlingskøer.parameters['msw']);
+ render();
+ expect(await screen.findByText('Ingen behandlingskøer er laget')).toBeInTheDocument();
+ expect(screen.queryByText('Navn')).not.toBeInTheDocument();
+
+ await fireEvent.keyDown(screen.getByText('Legg til behandlingskø'), {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ charCode: 13,
+ });
+
+ expect(screen.getByText('Navn')).toBeInTheDocument();
+ expect(screen.getByText('Ny liste')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.stories.tsx
new file mode 100644
index 00000000000..ee5da192d37
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.stories.tsx
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+import { action } from 'storybook/actions';
+
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../data/fplosAvdelingslederApi';
+import { GjeldendeSakslisterTabell } from './GjeldendeSakslisterTabell';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/GjeldendeSakslisterTabell',
+ component: GjeldendeSakslisterTabell,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.post(LosUrl.SLETT_SAKSLISTE, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ setValgtSakslisteId: action('button-click'),
+ resetValgtSakslisteId: action('button-click'),
+ lagNySaksliste: action('button-click'),
+ valgtAvdelingEnhet: '1',
+ children: test
,
+ },
+ render: storyArgs => {
+ const [args, setArgs] = useState(storyArgs);
+
+ const { data: kodeverkLos } = useQuery(losKodeverkOptions());
+
+ const lagNySaksliste = () => {
+ args.lagNySaksliste?.();
+ setArgs(oldArgs => ({
+ ...oldArgs,
+ sakslister: oldArgs.sakslister.concat({
+ sakslisteId: oldArgs.sakslister.length === 1 ? 1 : 2,
+ navn: 'Ny liste',
+ saksbehandlerIdenter: [],
+ }),
+ }));
+ };
+
+ return kodeverkLos ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const TabellNårDetIkkeFinnesBehandlingskøer: Story = {
+ args: {
+ sakslister: [],
+ },
+};
+
+export const TabellNårDetFinnesEnBehandlingskø: Story = {
+ args: {
+ sakslister: [
+ {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: ['R23233'],
+ },
+ ],
+ oppgaverForAvdelingAntall: 1,
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.tsx
new file mode 100644
index 00000000000..b617466e2b0
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/GjeldendeSakslisterTabell.tsx
@@ -0,0 +1,243 @@
+import { type KeyboardEvent, type ReactElement, useEffect, useRef, useState } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { PlusCircleIcon, XMarkIcon } from '@navikt/aksel-icons';
+import { BodyShort, Detail, HStack, Label, Link, Loader, Table, VStack } from '@navikt/ds-react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+import { LosUrl, oppgaveAntallOptions, slettSaksliste } from '../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../data/useLosKodeverk';
+import type { SakslisteAvdeling } from '../typer/sakslisteAvdelingTsType';
+import { SletteSakslisteModal } from './SletteSakslisteModal';
+
+import styles from './gjeldendeSakslisterTabell.module.css';
+
+const headerTextCodes = [
+ 'GjeldendeSakslisterTabell.Listenavn',
+ 'GjeldendeSakslisterTabell.Stonadstype',
+ 'GjeldendeSakslisterTabell.Behandlingtype',
+ 'GjeldendeSakslisterTabell.AntallSaksbehandlere',
+ 'GjeldendeSakslisterTabell.AntallBehandlinger',
+];
+
+const formatStonadstyper = (
+ fagsakYtelseTyper: LosKodeverkMedNavn<'FagsakYtelseType'>[],
+ valgteFagsakYtelseTyper?: string[],
+) => {
+ if (!valgteFagsakYtelseTyper || valgteFagsakYtelseTyper.length === 0) {
+ return ;
+ }
+
+ return valgteFagsakYtelseTyper
+ .map(fyt => {
+ const type = fagsakYtelseTyper.find(def => def.kode === fyt);
+ return type ? type.navn : '';
+ })
+ .sort((a, b) => a.localeCompare(b))
+ .join(', ');
+};
+
+const formatBehandlingstyper = (
+ behandlingTyper: LosKodeverkMedNavn<'BehandlingType'>[],
+ valgteBehandlingTyper?: string[],
+) => {
+ if (
+ !valgteBehandlingTyper ||
+ valgteBehandlingTyper.length === 0 ||
+ valgteBehandlingTyper.length === behandlingTyper.length
+ ) {
+ return ;
+ }
+
+ return valgteBehandlingTyper
+ .map(bt => {
+ const type = behandlingTyper.find(def => def.kode === bt);
+ return type ? type.navn : '';
+ })
+ .sort((a, b) => a.localeCompare(b))
+ .join(', ');
+};
+
+interface Props {
+ sakslister: SakslisteAvdeling[];
+ setValgtSakslisteId: (sakslisteId?: number) => void;
+ valgtSakslisteId?: number;
+ valgtAvdelingEnhet: string;
+ oppgaverForAvdelingAntall?: number;
+ lagNySaksliste: () => void;
+ resetValgtSakslisteId: () => void;
+ children: ReactElement;
+}
+
+const wait = (ms: number): Promise =>
+ new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+
+export const GjeldendeSakslisterTabell = ({
+ sakslister,
+ valgtAvdelingEnhet,
+ setValgtSakslisteId,
+ valgtSakslisteId,
+ oppgaverForAvdelingAntall,
+ lagNySaksliste,
+ resetValgtSakslisteId,
+ children,
+}: Props) => {
+ const queryClient = useQueryClient();
+
+ const [valgtSakslisteForSletting, setValgtSakslisteForSletting] = useState();
+ const tabRef = useRef<(HTMLDivElement | null)[]>([]);
+
+ const behandlingTyper = useLosKodeverk('BehandlingType');
+ const fagsakYtelseTyper = useLosKodeverk('FagsakYtelseType');
+
+ const { mutate: fjernSaksliste } = useMutation({
+ mutationFn: (values: { sakslisteId: number }) => slettSaksliste(values.sakslisteId, valgtAvdelingEnhet),
+ onSuccess: () => {
+ resetValgtSakslisteId();
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ useEffect(() => {
+ tabRef.current = tabRef.current.slice(0, sakslister.length);
+ }, [sakslister]);
+
+ const setValgtSaksliste = async (isOpen: boolean, id: number): Promise => {
+ // Må vente 100 ms før en byttar behandlingskø i tabell. Dette fordi lagring av navn skjer som blur-event. Så i tilfellet
+ // der en endrer navn og så trykker direkte på en annen behandlingskø vil ikke lagringen skje før etter at ny kø er valgt.
+ await wait(100);
+
+ if (id) {
+ setValgtSakslisteId(isOpen ? id : undefined);
+ }
+ return Promise.resolve();
+ };
+
+ const lagNySakslisteFn = (event: KeyboardEvent): void => {
+ if (event.keyCode === 13) {
+ lagNySaksliste();
+ }
+ };
+
+ const fjernSakslisteOgSkjulModal = (saksliste: SakslisteAvdeling): void => {
+ setValgtSakslisteForSletting(undefined);
+ fjernSaksliste({ sakslisteId: saksliste.sakslisteId });
+ };
+
+ return (
+
+
+
+
+
+
+
+ {oppgaverForAvdelingAntall ?? '0'}
+
+
+ {sakslister.length === 0 && (
+
+
+
+ )}
+ {sakslister.length > 0 && (
+
+
+
+
+ {headerTextCodes.map(code => (
+
+
+
+ ))}
+
+
+
+
+ {sakslister.map((saksliste, index) => (
+ setValgtSaksliste(isOpen, saksliste.sakslisteId)}
+ content={saksliste.sakslisteId === valgtSakslisteId ? children : undefined}
+ open={saksliste.sakslisteId === valgtSakslisteId}
+ expandOnRowClick
+ >
+ {saksliste.navn}
+ {formatStonadstyper(fagsakYtelseTyper, saksliste.fagsakYtelseTyper)}
+ {formatBehandlingstyper(behandlingTyper, saksliste.behandlingTyper)}
+
+ {saksliste.saksbehandlerIdenter.length > 0 ? saksliste.saksbehandlerIdenter.length : ''}
+
+
+
+
+
+ {
+ tabRef.current[index] = el;
+ }}
+ >
+ setValgtSakslisteForSletting(saksliste)}
+ onKeyDown={() => setValgtSakslisteForSletting(saksliste)}
+ />
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ {valgtSakslisteForSletting && (
+ setValgtSakslisteForSletting(undefined)}
+ submit={fjernSakslisteOgSkjulModal}
+ />
+ )}
+
+ );
+};
+
+const AntallOppgaverForSaksliste = ({
+ valgtAvdelingEnhet,
+ sakslisteId,
+}: {
+ valgtAvdelingEnhet: string;
+ sakslisteId: number;
+}) => {
+ const intl = useIntl();
+ const { data: antallOppgaver, isFetching, isError } = useQuery(oppgaveAntallOptions(sakslisteId, valgtAvdelingEnhet));
+
+ if (isError) {
+ return ;
+ }
+
+ return isFetching ? (
+
+ ) : (
+ antallOppgaver
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.spec.tsx
new file mode 100644
index 00000000000..49c2e7da079
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.spec.tsx
@@ -0,0 +1,34 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import * as stories from './SletteSakslisteModal.stories';
+
+const { Default } = composeStories(stories);
+
+describe('SletteSakslisteModal', () => {
+ it('skal vise modal for sletting av saksliste og så svare ja', async () => {
+ const submit = vi.fn();
+ render();
+ expect(await screen.findByText('Ønsker du å slette Saksliste 1?')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Ja'));
+
+ await waitFor(() => expect(submit).toHaveBeenCalledTimes(1));
+ expect(submit).toHaveBeenNthCalledWith(1, {
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: [],
+ sakslisteId: 1,
+ });
+ });
+
+ it('skal vise modal for sletting av saksliste og så svare nei', async () => {
+ const cancel = vi.fn();
+ render();
+ expect(await screen.findByText('Ønsker du å slette Saksliste 1?')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Nei'));
+
+ await waitFor(() => expect(cancel).toHaveBeenCalledTimes(1));
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.stories.tsx
new file mode 100644
index 00000000000..f50412606dd
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { action } from 'storybook/actions';
+
+import { getIntlDecorator } from '@navikt/fp-storybook-utils';
+
+import { SletteSakslisteModal } from './SletteSakslisteModal';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/SletteSakslisteModal',
+ component: SletteSakslisteModal,
+ decorators: [withIntl],
+ args: {
+ cancel: action('button-click'),
+ submit: action('button-click'),
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: [],
+ },
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.tsx
new file mode 100644
index 00000000000..9817422baf7
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/SletteSakslisteModal.tsx
@@ -0,0 +1,51 @@
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { Button, Heading, Modal } from '@navikt/ds-react';
+
+import type { SakslisteAvdeling } from '../typer/sakslisteAvdelingTsType';
+
+import styles from './sletteSakslisteModal.module.css';
+
+interface Props {
+ valgtSaksliste: SakslisteAvdeling;
+ cancel: () => void;
+ submit: (saksliste: SakslisteAvdeling) => void;
+}
+
+/**
+ * SletteSakslisteModal
+ *
+ * Modal som lar en avdelingsleder fjerne sakslister.
+ */
+export const SletteSakslisteModal = ({ valgtSaksliste, cancel, submit }: Props) => {
+ const intl = useIntl();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/endreSakslisterPanel.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/endreSakslisterPanel.module.css
new file mode 100644
index 00000000000..4ba4a77f3ad
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/endreSakslisterPanel.module.css
@@ -0,0 +1,13 @@
+.text {
+ padding-top: 10px;
+}
+
+.leftCol {
+ width: 40%;
+}
+
+.arrow {
+ color: var(--ax-neutral-500);
+ height: 35px;
+ width: 35px;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/gjeldendeSakslisterTabell.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/gjeldendeSakslisterTabell.module.css
new file mode 100644
index 00000000000..481e5e6bf84
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/gjeldendeSakslisterTabell.module.css
@@ -0,0 +1,29 @@
+.isSelected {
+ background-color: var(--ax-neutral-200);
+}
+
+.addCircleIcon,
+.imageText {
+ display: inline-block;
+ margin-right: 1ex;
+ vertical-align: text-bottom;
+}
+
+.grayBox {
+ background-color: var(--ax-neutral-200);
+ padding: 5px 15px 5px 15px;
+}
+
+.margin {
+ margin-left: auto;
+}
+
+.addPeriode {
+ cursor: pointer;
+ width: 200px;
+}
+
+.removeImage {
+ color: var(--ax-danger-500);
+ margin-top: 5px;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.spec.tsx
new file mode 100644
index 00000000000..e142122b184
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.spec.tsx
@@ -0,0 +1,38 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './SaksbehandlereForSakslisteForm.stories';
+
+const { IngenSaksbehandlere, ToSaksbehandlere, SaksbehandlereSomErGruppert } = composeStories(stories);
+
+describe('SaksbehandlereForSakslisteForm', () => {
+ it('skal vise tekst som viser at ingen saksbehandlere er tilknyttet', async () => {
+ await applyRequestHandlers(IngenSaksbehandlere.parameters['msw']);
+ render();
+ expect(await screen.findByText('Saksbehandlere')).toBeInTheDocument();
+ expect(await screen.findByText('Avdelingen har ingen saksbehandlere tilknyttet')).toBeInTheDocument();
+ });
+
+ it('skal vise to saksbehandlere i listen', async () => {
+ await applyRequestHandlers(ToSaksbehandlere.parameters['msw']);
+ render();
+ expect(await screen.findByText('Saksbehandlere')).toBeInTheDocument();
+ expect(await screen.findByText('Espen Utvikler')).toBeInTheDocument();
+ expect(screen.getByText('Steffen')).toBeInTheDocument();
+ });
+
+ it.skip('skal vise gruppe og liste med alle saksbehandlere', async () => {
+ await applyRequestHandlers(SaksbehandlereSomErGruppert.parameters['msw']);
+ render();
+ expect(await screen.findByText('Saksbehandlere')).toBeInTheDocument();
+ expect(screen.getByText('Gruppenavn')).toBeInTheDocument();
+ expect(screen.getByText('Gruppe 1')).toBeInTheDocument();
+ expect(screen.getByText('Antall tilknyttet køen')).toBeInTheDocument();
+ expect(screen.getByText('1')).toBeInTheDocument();
+
+ expect(screen.getAllByText('Steffen')).toHaveLength(2);
+ expect(screen.getAllByText('Espen Utvikler')).toHaveLength(2);
+ expect(screen.getByText('Eirik')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.stories.tsx
new file mode 100644
index 00000000000..e7d644a5a73
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.stories.tsx
@@ -0,0 +1,163 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { http, HttpResponse } from 'msw';
+
+import { getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { LosUrl } from '../../data/fplosAvdelingslederApi';
+import { SaksbehandlereForSakslisteForm } from './SaksbehandlereForSakslisteForm';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/SaksbehandlereForSakslisteForm',
+ component: SaksbehandlereForSakslisteForm,
+ decorators: [withIntl, withQueryClient],
+ args: {
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const IngenSaksbehandlere: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_GRUPPER, () => HttpResponse.json()),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: ['S34354'],
+ },
+ avdelingensSaksbehandlere: [],
+ },
+};
+
+export const ToSaksbehandlere: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_GRUPPER, () => HttpResponse.json()),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: ['S34354'],
+ },
+ avdelingensSaksbehandlere: [
+ {
+ brukerIdent: 'E23232',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'S34354',
+ navn: 'Steffen',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ ],
+ },
+};
+
+export const TreSaksbehandlere: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_GRUPPER, () => HttpResponse.json()),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: ['S34354'],
+ },
+ avdelingensSaksbehandlere: [
+ {
+ brukerIdent: 'E23232',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'S34354',
+ navn: 'Steffen',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'E24353',
+ navn: 'Eirik',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ ],
+ },
+};
+
+export const SaksbehandlereSomErGruppert: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_GRUPPER, () =>
+ HttpResponse.json({
+ saksbehandlerGrupper: [
+ {
+ gruppeId: 1,
+ gruppeNavn: 'Gruppe 1',
+ saksbehandlere: [
+ {
+ brukerIdent: 'E23232',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'S34354',
+ navn: 'Steffen',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ ],
+ },
+ ],
+ }),
+ ),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'Saksliste 1',
+ saksbehandlerIdenter: ['S34354'],
+ },
+ avdelingensSaksbehandlere: [
+ {
+ brukerIdent: 'E23232',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'S34354',
+ navn: 'Steffen',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'E24353',
+ navn: 'Eirik',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ ],
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.tsx
new file mode 100644
index 00000000000..24e644ae788
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/SaksbehandlereForSakslisteForm.tsx
@@ -0,0 +1,146 @@
+import { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { BodyShort, Box, ExpansionCard, Label, Table, VStack } from '@navikt/ds-react';
+import { RhfForm } from '@navikt/ft-form-hooks';
+import { useQuery } from '@tanstack/react-query';
+
+import { type SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { grupperOptions } from '../../data/fplosAvdelingslederApi';
+import type { SaksbehandlerGruppe } from '../../typer/saksbehandlereOgSaksbehandlerGrupper';
+import type { SakslisteAvdeling } from '../../typer/sakslisteAvdelingTsType';
+import { ValgAvSaksbehandlere } from './ValgAvSaksbehandlere';
+
+type FormValues = {
+ reserverTil: string;
+};
+
+interface Props {
+ valgtSaksliste: SakslisteAvdeling;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+ valgtAvdelingEnhet: string;
+}
+
+export const SaksbehandlereForSakslisteForm = ({
+ avdelingensSaksbehandlere = [],
+ valgtSaksliste,
+ valgtAvdelingEnhet,
+}: Props) => {
+ const intl = useIntl();
+
+ const sorterteAvdelingensSaksbehandlere = avdelingensSaksbehandlere.toSorted((saksbehandler1, saksbehandler2) =>
+ saksbehandler1.navn.localeCompare(saksbehandler2.navn),
+ );
+
+ const { data: grupper } = useQuery(grupperOptions(valgtAvdelingEnhet));
+
+ const defaultValues = valgtSaksliste.saksbehandlerIdenter.reduce(
+ (acc, brukerIdent) => ({ ...acc, [brukerIdent]: true }),
+ {},
+ );
+
+ const formMethods = useForm({
+ defaultValues,
+ });
+
+ useEffect(() => {
+ formMethods.reset(defaultValues);
+ }, [valgtSaksliste.sakslisteId]);
+
+ const harGrupper =
+ grupper &&
+ grupper.saksbehandlerGrupper.length > 0 &&
+ !grupper.saksbehandlerGrupper.every(sg => sg.saksbehandlere.length === 0);
+
+ return (
+ formMethods={formMethods}>
+
+
+
+ {sorterteAvdelingensSaksbehandlere.length === 0 && (
+
+
+
+ )}
+
+ {harGrupper && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {grupper.saksbehandlerGrupper.map(sg => (
+ ({
+ brukerIdent: sb.brukerIdent,
+ navn: sb.navn,
+ }))}
+ valgtSaksliste={valgtSaksliste}
+ valgtAvdelingEnhet={valgtAvdelingEnhet}
+ />
+ }
+ expandOnRowClick
+ >
+ {sg.gruppeNavn}
+ {antallTilknyttetSaksliste(valgtSaksliste, sg)}
+
+ ))}
+
+
+ )}
+
+ {sorterteAvdelingensSaksbehandlere.length > 0 && !harGrupper && (
+
+ )}
+ {harGrupper && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+const antallTilknyttetSaksliste = (saksliste: SakslisteAvdeling, gruppe: SaksbehandlerGruppe) => {
+ let matchCount = 0;
+ saksliste.saksbehandlerIdenter.forEach(ident => {
+ const matches = gruppe.saksbehandlere.filter(saksbehandler => saksbehandler.brukerIdent === ident);
+ matchCount += matches.length;
+ });
+ return matchCount;
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/ValgAvSaksbehandlere.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/ValgAvSaksbehandlere.tsx
new file mode 100644
index 00000000000..95b5b0367ea
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/saksbehandlerForm/ValgAvSaksbehandlere.tsx
@@ -0,0 +1,75 @@
+import { useFormContext } from 'react-hook-form';
+
+import { HStack, VStack } from '@navikt/ds-react';
+import { RhfCheckbox } from '@navikt/ft-form-hooks';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { lagreSakslisteSaksbehandler, LosUrl } from '../../data/fplosAvdelingslederApi';
+import type { SakslisteAvdeling } from '../../typer/sakslisteAvdelingTsType';
+
+interface Props {
+ valgtSaksliste: SakslisteAvdeling;
+ valgtAvdelingEnhet: string;
+ saksbehandlere: {
+ brukerIdent: string;
+ navn: string;
+ }[];
+}
+
+export const ValgAvSaksbehandlere = ({ valgtSaksliste, valgtAvdelingEnhet, saksbehandlere }: Props) => {
+ const queryClient = useQueryClient();
+
+ // TODO (TOR) Manglar type
+ const { control } = useFormContext();
+
+ const { mutate: knyttSaksbehandlerTilSaksliste } = useMutation({
+ mutationFn: (values: { brukerIdent: string; checked: boolean }) =>
+ lagreSakslisteSaksbehandler(valgtSaksliste.sakslisteId, values.brukerIdent, values.checked, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const pos = Math.ceil(saksbehandlere.length / 2);
+ const avdelingensSaksbehandlereVenstreListe = saksbehandlere.slice(0, pos);
+ const avdelingensSaksbehandlereHoyreListe = saksbehandlere.slice(pos);
+
+ return (
+
+
+ {avdelingensSaksbehandlereVenstreListe.map(s => (
+
+ knyttSaksbehandlerTilSaksliste({
+ brukerIdent: s.brukerIdent,
+ checked: isChecked,
+ })
+ }
+ />
+ ))}
+
+
+ {avdelingensSaksbehandlereHoyreListe.map(s => (
+
+ knyttSaksbehandlerTilSaksliste({
+ brukerIdent: s.brukerIdent,
+ checked: isChecked,
+ })
+ }
+ />
+ ))}
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.spec.tsx
new file mode 100644
index 00000000000..e720654a0e0
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.spec.tsx
@@ -0,0 +1,37 @@
+import { composeStories } from '@storybook/react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './UtvalgskriterierForSakslisteForm.stories';
+
+const { MedGittNavn, MedDefaultNavn } = composeStories(stories);
+
+describe('UtvalgskriterierForSakslisteForm', () => {
+ it('skal vise sakslistenavn som saksbehandler har skrive inn', async () => {
+ await applyRequestHandlers(MedGittNavn.parameters['msw']);
+ render();
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+ expect(await screen.findByLabelText('Navn')).toHaveValue('liste');
+ });
+
+ it('skal vise default sakslistenavn', async () => {
+ await applyRequestHandlers(MedDefaultNavn.parameters['msw']);
+ render();
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+ expect(await screen.findByLabelText('Navn')).toHaveValue('Ny behandlingskø');
+ });
+
+ it('skal vise feilmelding når en fjerner nok tegn til at navnet blir færre enn 3 tegn langt', async () => {
+ await applyRequestHandlers(MedGittNavn.parameters['msw']);
+ const { getByLabelText } = render();
+
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+
+ const navnInput = getByLabelText('Navn');
+ await userEvent.type(navnInput, '{Backspace}{Backspace}{Backspace}');
+ await fireEvent.blur(navnInput);
+
+ expect(await screen.findByText('Du må skrive minst 3 tegn')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.stories.tsx
new file mode 100644
index 00000000000..ab309e8ff3f
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.stories.tsx
@@ -0,0 +1,103 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { AndreKriterierType, BehandlingType, FagsakYtelseType, KøSortering } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../data/fplosAvdelingslederApi';
+import { UtvalgskriterierForSakslisteForm } from './UtvalgskriterierForSakslisteForm';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/UtvalgskriterierForSakslisteForm',
+ component: UtvalgskriterierForSakslisteForm,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.OPPGAVE_ANTALL, () => HttpResponse.json(1)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_NAVN, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_INTERVALL, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_DYNAMISK_PERIDE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_TIDSINTERVALL_DATO, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_FAGSAK_YTELSE_TYPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_BEHANDLINGSTYPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_ANDRE_KRITERIER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtAvdelingEnhet: '',
+ },
+ render: args => {
+ const { data: kodeverkLos } = useQuery(losKodeverkOptions());
+
+ return kodeverkLos ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const MedGittNavn: Story = {
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: 'liste',
+ saksbehandlerIdenter: [],
+ sortering: {
+ sorteringType: KøSortering.BEHANDLINGSFRIST,
+ fra: 1,
+ til: 4,
+ erDynamiskPeriode: true,
+ },
+ behandlingTyper: [BehandlingType.FORSTEGANGSSOKNAD],
+ fagsakYtelseTyper: [FagsakYtelseType.FORELDREPENGER],
+ andreKriterier: [
+ {
+ andreKriterierType: AndreKriterierType.TIL_BESLUTTER,
+ inkluder: true,
+ },
+ {
+ andreKriterierType: AndreKriterierType.PAPIRSOKNAD,
+ inkluder: false,
+ },
+ ],
+ },
+ },
+};
+
+export const MedDefaultNavn: Story = {
+ args: {
+ valgtSaksliste: {
+ sakslisteId: 1,
+ navn: undefined,
+ saksbehandlerIdenter: [],
+ sortering: {
+ sorteringType: KøSortering.BEHANDLINGSFRIST,
+ fra: 1,
+ til: 4,
+ erDynamiskPeriode: true,
+ },
+ behandlingTyper: [BehandlingType.FORSTEGANGSSOKNAD],
+ fagsakYtelseTyper: [FagsakYtelseType.FORELDREPENGER],
+ andreKriterier: [
+ {
+ andreKriterierType: AndreKriterierType.TIL_BESLUTTER,
+ inkluder: true,
+ },
+ {
+ andreKriterierType: AndreKriterierType.PAPIRSOKNAD,
+ inkluder: false,
+ },
+ ],
+ },
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.tsx
new file mode 100644
index 00000000000..83380cd0879
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/UtvalgskriterierForSakslisteForm.tsx
@@ -0,0 +1,148 @@
+import { useForm } from 'react-hook-form';
+import { FormattedMessage, type IntlShape, useIntl } from 'react-intl';
+
+import { Box, Heading, HStack, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfTextField } from '@navikt/ft-form-hooks';
+import { hasValidName, maxLength, minLength, required } from '@navikt/ft-form-validators';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { lagreSakslisteNavn, LosUrl } from '../../data/fplosAvdelingslederApi';
+import type { SakslisteAvdeling } from '../../typer/sakslisteAvdelingTsType';
+import { AndreKriterierVelger } from './filtrering/AndreKriterierVelger';
+import { BehandlingstypeVelger } from './filtrering/BehandlingstypeVelger';
+import { FagsakYtelseTypeVelger } from './filtrering/FagsakYtelseTypeVelger';
+import { SorteringVelger } from './sortering/SorteringVelger';
+import { useDebounce } from './useDebounce';
+
+import styles from './utvalgskriterierForSakslisteForm.module.css';
+
+const minLength3 = minLength(3);
+const maxLength100 = maxLength(100);
+
+type FormValues = {
+ sakslisteId: number;
+ navn: string;
+ sortering?: string;
+ erDynamiskPeriode?: boolean;
+ fra?: string;
+ til?: string;
+ fomDato?: string;
+ tomDato?: string;
+};
+
+const buildDefaultValues = (intl: IntlShape, valgtSaksliste: SakslisteAvdeling): FormValues => {
+ const behandlingTypes = valgtSaksliste.behandlingTyper
+ ? valgtSaksliste.behandlingTyper.reduce((acc, bt) => ({ ...acc, [bt]: true }), {})
+ : {};
+ const fagsakYtelseTypes = valgtSaksliste.fagsakYtelseTyper
+ ? valgtSaksliste.fagsakYtelseTyper.reduce((acc, fyt) => ({ ...acc, [fyt]: true }), {})
+ : {};
+
+ const andreKriterierTyper = valgtSaksliste.andreKriterier
+ ? valgtSaksliste.andreKriterier.reduce((acc, ak) => ({ ...acc, [ak.andreKriterierType]: true }), {})
+ : {};
+ const andreKriterierInkluder = valgtSaksliste.andreKriterier
+ ? valgtSaksliste.andreKriterier.reduce(
+ (acc, ak) => ({ ...acc, [`${ak.andreKriterierType}_inkluder`]: ak.inkluder }),
+ {},
+ )
+ : {};
+
+ return {
+ sakslisteId: valgtSaksliste.sakslisteId,
+ navn: valgtSaksliste.navn ?? intl.formatMessage({ id: 'UtvalgskriterierForSakslisteForm.NyListe' }),
+ sortering: valgtSaksliste.sortering ? valgtSaksliste.sortering.sorteringType : undefined,
+ fomDato: valgtSaksliste.sortering ? valgtSaksliste.sortering.fomDato : undefined,
+ tomDato: valgtSaksliste.sortering ? valgtSaksliste.sortering.tomDato : undefined,
+ fra: valgtSaksliste.sortering ? valgtSaksliste.sortering.fra?.toString() : undefined,
+ til: valgtSaksliste.sortering ? valgtSaksliste.sortering.til?.toString() : undefined,
+ erDynamiskPeriode: valgtSaksliste.sortering ? valgtSaksliste.sortering.erDynamiskPeriode : undefined,
+ ...andreKriterierTyper,
+ ...andreKriterierInkluder,
+ ...behandlingTypes,
+ ...fagsakYtelseTypes,
+ };
+};
+
+interface Props {
+ valgtSaksliste: SakslisteAvdeling;
+ valgtAvdelingEnhet: string;
+}
+
+export const UtvalgskriterierForSakslisteForm = ({ valgtSaksliste, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+ const intl = useIntl();
+
+ const { mutate: lagreSakslistensNavn } = useMutation({
+ mutationFn: (values: { sakslisteId: number; navn: string; avdelingEnhet: string }) =>
+ lagreSakslisteNavn(values.sakslisteId, values.navn, values.avdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const formMethods = useForm({
+ defaultValues: buildDefaultValues(intl, valgtSaksliste),
+ });
+
+ const values = formMethods.watch();
+
+ const tranformValues = (nyttNavn: string): void => {
+ lagreSakslistensNavn({
+ sakslisteId: valgtSaksliste.sakslisteId,
+ navn: nyttNavn,
+ avdelingEnhet: valgtAvdelingEnhet,
+ });
+ };
+ const lagreNavn = useDebounce('navn', tranformValues, formMethods.trigger);
+
+ return (
+
+
+
+
+
+
+
+ lagreNavn(value)}
+ className={styles.bredde}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.spec.tsx
new file mode 100644
index 00000000000..d869772106e
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.spec.tsx
@@ -0,0 +1,35 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './AndreKriterierVelger.stories';
+
+const { Default } = composeStories(stories);
+
+describe('AndreKriterierVelger', () => {
+ it('skal vise checkboxer for andre kriterier der Til beslutter er valgt fra før', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Til beslutter')).toBeInTheDocument();
+ expect(getByLabelText('Til beslutter')).toBeChecked();
+ expect(getByLabelText('Ta med i køen')).toBeChecked();
+ expect(getByLabelText('Fjern fra køen')).not.toBeChecked();
+ });
+
+ it('skal velge Registrer papirsøknad og fjerne dette fra køen', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getAllByLabelText } = render();
+ expect(await screen.findByText('Registrer papirsøknad')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Registrer papirsøknad'));
+
+ expect(getAllByLabelText('Ta med i køen')[1]).toBeChecked();
+ expect(getAllByLabelText('Fjern fra køen')[1]).not.toBeChecked();
+
+ await userEvent.click(getAllByLabelText('Fjern fra køen')[1]);
+
+ await waitFor(() => expect(getAllByLabelText('Fjern fra køen')[1]).toBeChecked());
+ expect(getAllByLabelText('Ta med i køen')[1]).not.toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.stories.tsx
new file mode 100644
index 00000000000..d65402225bf
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.stories.tsx
@@ -0,0 +1,59 @@
+import { useForm } from 'react-hook-form';
+
+import { RhfForm } from '@navikt/ft-form-hooks';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { AndreKriterierType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { AndreKriterierVelger } from './AndreKriterierVelger';
+
+import messages from '../../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/AndreKriterierVelger',
+ component: AndreKriterierVelger,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_ANDRE_KRITERIER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSakslisteId: 1,
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+ render: args => {
+ const formMethods = useForm({
+ defaultValues: {
+ [AndreKriterierType.TIL_BESLUTTER]: true,
+ [`${AndreKriterierType.TIL_BESLUTTER}_inkluder`]: true,
+ },
+ });
+
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+
+ return alleKodeverk ? (
+
+
+
+ ) : (
+
+ );
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.tsx
new file mode 100644
index 00000000000..f2ca3bcf635
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/AndreKriterierVelger.tsx
@@ -0,0 +1,103 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { Label, VStack } from '@navikt/ds-react';
+import { RhfCheckbox, RhfRadioGroup } from '@navikt/ft-form-hooks';
+import { ArrowBox } from '@navikt/ft-ui-komponenter';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { lagreSakslisteAndreKriterier, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../../../data/useLosKodeverk';
+
+import styles from './andreKriterierVelger.module.css';
+
+interface Props {
+ valgtSakslisteId: number;
+ valgtAvdelingEnhet: string;
+}
+
+export const AndreKriterierVelger = ({ valgtSakslisteId, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+ const { setValue, watch, control } = useFormContext();
+
+ const values = watch();
+
+ const andreKriterierTyper = useLosKodeverk('AndreKriterierType');
+
+ const { mutate: lagreAndreKriterier } = useMutation({
+ mutationFn: (valuesToStore: { andreKriterierType: string; checked: boolean; inkluder: boolean }) =>
+ lagreSakslisteAndreKriterier(
+ valgtSakslisteId,
+ valgtAvdelingEnhet,
+ valuesToStore.andreKriterierType,
+ valuesToStore.checked,
+ valuesToStore.inkluder,
+ ),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ return (
+
+
+ {andreKriterierTyper.map(akt => (
+
+
{
+ setValue(`${akt.kode}_inkluder`, true);
+ return lagreAndreKriterier({
+ andreKriterierType: akt.kode,
+ checked: isChecked,
+ inkluder: true,
+ });
+ }}
+ />
+ {values[akt.kode] && (
+
+
+
+ lagreAndreKriterier({
+ andreKriterierType: akt.kode,
+ checked: true,
+ inkluder: skalInkludere,
+ })
+ }
+ radios={[
+ {
+ value: 'true',
+ label: ,
+ },
+ {
+ value: 'false',
+ label: ,
+ },
+ ]}
+ />
+
+
+ )}
+
+ ))}
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.spec.tsx
new file mode 100644
index 00000000000..3c512e13928
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.spec.tsx
@@ -0,0 +1,29 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './BehandlingstypeVelger.stories';
+
+const { Default } = composeStories(stories);
+
+describe('BehandlingstypeVelger', () => {
+ it('skal vise checkboxer for behandlingstyper', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Behandlingstype')).toBeInTheDocument();
+ expect(getByLabelText('Førstegangsbehandling')).toBeChecked();
+ expect(getByLabelText('Klage')).not.toBeChecked();
+ });
+
+ it('skal velge klage', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Behandlingstype')).toBeInTheDocument();
+ expect(getByLabelText('Klage')).not.toBeChecked();
+
+ await userEvent.click(screen.getByText('Klage'));
+
+ await waitFor(() => expect(getByLabelText('Klage')).toBeChecked());
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.stories.tsx
new file mode 100644
index 00000000000..ee628bf43b8
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.stories.tsx
@@ -0,0 +1,58 @@
+import { useForm } from 'react-hook-form';
+
+import { RhfForm } from '@navikt/ft-form-hooks';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { BehandlingstypeVelger } from './BehandlingstypeVelger';
+
+import messages from '../../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/BehandlingstypeVelger',
+ component: BehandlingstypeVelger,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_BEHANDLINGSTYPE, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSakslisteId: 1,
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+ render: args => {
+ const formMethods = useForm({
+ defaultValues: {
+ [BehandlingType.FORSTEGANGSSOKNAD]: true,
+ },
+ });
+
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+
+ return alleKodeverk ? (
+
+
+
+ ) : (
+
+ );
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.tsx
new file mode 100644
index 00000000000..7622f11f2f2
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/BehandlingstypeVelger.tsx
@@ -0,0 +1,79 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { Label, VStack } from '@navikt/ds-react';
+import { RhfCheckbox } from '@navikt/ft-form-hooks';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+
+import { lagreSakslisteBehandlingstype, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../../../data/useLosKodeverk';
+
+const behandlingstypeOrder = Object.values(BehandlingType);
+
+interface Props {
+ valgtSakslisteId: number;
+ valgtAvdelingEnhet: string;
+}
+
+export const BehandlingstypeVelger = ({ valgtSakslisteId, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+
+ // TODO (TOR) Manglar type
+ const { control } = useFormContext();
+
+ const { mutate: lagreBehandlingstype } = useMutation({
+ mutationFn: (valuesToStore: { behandlingType: string; checked: boolean }) =>
+ lagreSakslisteBehandlingstype(
+ valgtSakslisteId,
+ valgtAvdelingEnhet,
+ valuesToStore.behandlingType,
+ valuesToStore.checked,
+ ),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const alleBehandlingTyper = useLosKodeverk('BehandlingType');
+
+ const behandlingTyper = behandlingstypeOrder.map(kode => alleBehandlingTyper.find(bt => bt.kode === kode));
+
+ return (
+
+
+ {behandlingTyper
+ .map(bt => {
+ if (!bt) {
+ return null;
+ }
+ return (
+
+ lagreBehandlingstype({
+ behandlingType: bt.kode,
+ checked: isChecked,
+ })
+ }
+ />
+ );
+ })
+ .filter(bt => !!bt)}
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.spec.tsx
new file mode 100644
index 00000000000..29ea169afb1
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.spec.tsx
@@ -0,0 +1,23 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './FagsakYtelseTypeVelger.stories';
+
+const { Default } = composeStories(stories);
+
+describe('FagsakYtelseTypeVelger', () => {
+ it('skal vise checkboxer for stønadstyper og så velge engangsstønad', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Stønadstype')).toBeInTheDocument();
+ expect(getByLabelText('Foreldrepenger')).toBeChecked();
+ expect(getByLabelText('Engangsstønad')).toBeChecked();
+
+ await userEvent.click(screen.getByText('Engangsstønad'));
+
+ await waitFor(() => expect(getByLabelText('Engangsstønad')).not.toBeChecked());
+ expect(getByLabelText('Foreldrepenger')).toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.stories.tsx
new file mode 100644
index 00000000000..064c4c5934e
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.stories.tsx
@@ -0,0 +1,59 @@
+import { useForm } from 'react-hook-form';
+
+import { RhfForm } from '@navikt/ft-form-hooks';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { FagsakYtelseType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { FagsakYtelseTypeVelger } from './FagsakYtelseTypeVelger';
+
+import messages from '../../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/FagsakYtelseTypeVelger',
+ component: FagsakYtelseTypeVelger,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_FAGSAK_YTELSE_TYPE, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSakslisteId: 1,
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+ render: args => {
+ const formMethods = useForm({
+ defaultValues: {
+ [FagsakYtelseType.FORELDREPENGER]: true,
+ [FagsakYtelseType.ENGANGSSTONAD]: true,
+ },
+ });
+
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+
+ return alleKodeverk ? (
+
+
+
+ ) : (
+
+ );
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.tsx
new file mode 100644
index 00000000000..e42b3f892b1
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/FagsakYtelseTypeVelger.tsx
@@ -0,0 +1,63 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { Label, VStack } from '@navikt/ds-react';
+import { RhfCheckbox } from '@navikt/ft-form-hooks';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { lagreSakslisteFagsakYtelseType, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../../../data/useLosKodeverk';
+
+interface Props {
+ valgtSakslisteId: number;
+ valgtAvdelingEnhet: string;
+}
+
+export const FagsakYtelseTypeVelger = ({ valgtSakslisteId, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+
+ // TODO (TOR) Manglar type
+ const { control } = useFormContext();
+
+ const { mutate: lagreFagsakYtelseType } = useMutation({
+ mutationFn: (values: { sakslisteId: number; avdelingEnhet: string; fagsakYtelseType: string; checked: boolean }) =>
+ lagreSakslisteFagsakYtelseType(values.sakslisteId, values.avdelingEnhet, values.fagsakYtelseType, values.checked),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const alleFagsakYtelseTyper = useLosKodeverk('FagsakYtelseType');
+
+ return (
+
+
+ {alleFagsakYtelseTyper.map(fyt => (
+ type.kode === fyt.kode)?.navn ?? ''}
+ onChange={isChecked =>
+ lagreFagsakYtelseType({
+ sakslisteId: valgtSakslisteId,
+ avdelingEnhet: valgtAvdelingEnhet,
+ fagsakYtelseType: fyt.kode,
+ checked: isChecked,
+ })
+ }
+ />
+ ))}
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/andreKriterierVelger.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/andreKriterierVelger.module.css
new file mode 100644
index 00000000000..68215e72238
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/filtrering/andreKriterierVelger.module.css
@@ -0,0 +1,4 @@
+.arrowbox {
+ padding-top: 10px;
+ width: 75%;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/BelopSorteringValg.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/BelopSorteringValg.tsx
new file mode 100644
index 00000000000..7e4cfe38c5c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/BelopSorteringValg.tsx
@@ -0,0 +1,87 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { Detail, HStack } from '@navikt/ds-react';
+import { RhfTextField } from '@navikt/ft-form-hooks';
+import { hasValidPosOrNegInteger } from '@navikt/ft-form-validators';
+import { ArrowBox } from '@navikt/ft-ui-komponenter';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { lagreSakslisteSorteringIntervall, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { useDebounce } from '../useDebounce';
+
+import styles from './sorteringVelger.module.css';
+
+interface Props {
+ valgtSakslisteId: number;
+ valgtAvdelingEnhet: string;
+}
+
+export const BelopSorteringValg = ({ valgtSakslisteId, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+
+ // TODO (TOR) Manglar type
+ const { watch, trigger, control } = useFormContext();
+ const fraVerdi = watch('fra');
+ const tilVerdi = watch('til');
+
+ const { mutate: lagreSakslisteSorteringTidsintervallDager } = useMutation({
+ mutationFn: (valuesToStore: { fra: number; til: number }) =>
+ lagreSakslisteSorteringIntervall(valgtSakslisteId, valuesToStore.fra, valuesToStore.til, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const lagreFra = (nyFraVerdi: number) =>
+ lagreSakslisteSorteringTidsintervallDager({
+ fra: nyFraVerdi,
+ til: tilVerdi,
+ });
+ const lagreTil = (nyTilVerdi: number) =>
+ lagreSakslisteSorteringTidsintervallDager({
+ fra: fraVerdi,
+ til: nyTilVerdi,
+ });
+
+ const lagreFraDebounce = useDebounce('fra', lagreFra, trigger);
+ const lagreTilDebounce = useDebounce('til', lagreTil, trigger);
+
+ return (
+
+
+
+
+
+ lagreFraDebounce(value)}
+ />
+
+
+
+ lagreTilDebounce(value)}
+ />
+
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/DatoSorteringValg.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/DatoSorteringValg.tsx
new file mode 100644
index 00000000000..778744ca097
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/DatoSorteringValg.tsx
@@ -0,0 +1,220 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { Detail, HStack, VStack } from '@navikt/ds-react';
+import { RhfCheckbox, RhfDatepicker, RhfTextField } from '@navikt/ft-form-hooks';
+import { hasValidDate, hasValidPosOrNegInteger } from '@navikt/ft-form-validators';
+import { ArrowBox, DateLabel } from '@navikt/ft-ui-komponenter';
+import { ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import customParseFormat from 'dayjs/plugin/customParseFormat';
+
+import {
+ lagreSakslisteSorteringDynamiskPeriode,
+ lagreSakslisteSorteringIntervall,
+ lagreSakslisteSorteringTidsintervallDato,
+ LosUrl,
+} from '../../../data/fplosAvdelingslederApi';
+import { useDebounce } from '../useDebounce';
+
+import styles from './sorteringVelger.module.css';
+
+dayjs.extend(customParseFormat);
+
+interface Props {
+ valgtSakslisteId: number;
+ valgtAvdelingEnhet: string;
+ erDynamiskPeriode: boolean;
+}
+
+export const DatoSorteringValg = ({ valgtSakslisteId, valgtAvdelingEnhet, erDynamiskPeriode }: Props) => {
+ const queryClient = useQueryClient();
+ const intl = useIntl();
+
+ const { mutate: lagreSakslisteSorteringTidsintervallDager } = useMutation({
+ mutationFn: (valuesToStore: { fra: number; til: number }) =>
+ lagreSakslisteSorteringIntervall(valgtSakslisteId, valuesToStore.fra, valuesToStore.til, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const { mutate: lagreSakslisteSorteringErDynamiskPeriode } = useMutation({
+ mutationFn: () => lagreSakslisteSorteringDynamiskPeriode(valgtSakslisteId, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const { mutate: lagreSorteringTidsintervallDato } = useMutation({
+ mutationFn: (valuesToStore: { fomDato?: string; tomDato?: string }) =>
+ lagreSakslisteSorteringTidsintervallDato(
+ valgtSakslisteId,
+ valgtAvdelingEnhet,
+ valuesToStore.fomDato,
+ valuesToStore.tomDato,
+ ),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ // TODO (TOR) Manglar typing for useFormContext
+ const { watch, control } = useFormContext();
+ const fraVerdi = watch('fra');
+ const tilVerdi = watch('til');
+ const fomDatoVerdi = watch('fomDato');
+ const tomDatoVerdi = watch('tomDato');
+
+ const lagreFra = (nyFraVerdi: number) =>
+ lagreSakslisteSorteringTidsintervallDager({
+ fra: nyFraVerdi,
+ til: tilVerdi,
+ });
+ const lagreTil = (nyTilVerdi: number) =>
+ lagreSakslisteSorteringTidsintervallDager({
+ fra: fraVerdi,
+ til: nyTilVerdi,
+ });
+
+ const lagreFomDato = getLagreDatoFn(lagreSorteringTidsintervallDato, true, tomDatoVerdi);
+ const lagreTomDato = getLagreDatoFn(lagreSorteringTidsintervallDato, false, fomDatoVerdi);
+
+ const lagreFomDatoDebounce = useDebounce('fomDato', lagreFomDato);
+ const lagreTomDatoDebounce = useDebounce('tomDato', lagreTomDato);
+
+ return (
+
+
+
+
+
+
+ {erDynamiskPeriode && (
+
+
+ lagreFra(value)}
+ />
+ {(fraVerdi || fraVerdi === 0) && (
+
+
+
+ )}
+
+
+
+
+
+ lagreTil(value)}
+ />
+ {(tilVerdi || tilVerdi === 0) && (
+
+
+
+ )}
+
+
+
+
+
+ )}
+ {!erDynamiskPeriode && (
+
+
+
+
+
+
+
+ )}
+ lagreSakslisteSorteringErDynamiskPeriode()}
+ />
+
+
+
+ );
+};
+
+const finnDato = (antallDager: number) => dayjs().add(antallDager, 'd').format();
+
+const getLagreDatoFn =
+ (
+ lagreSorteringTidsintervallDato: (valuesToStore: { fomDato?: string; tomDato?: string }) => void,
+ erFomDato: boolean,
+ annenDato?: string,
+ ) =>
+ (inputdato: string) => {
+ let dato;
+ if (inputdato) {
+ dato = dayjs(inputdato);
+ }
+ if (!dato || dato.isValid()) {
+ const d = dato ? dato.format(ISO_DATE_FORMAT) : dato;
+
+ const params = erFomDato
+ ? {
+ fomDato: d,
+ tomDato: annenDato,
+ }
+ : {
+ fomDato: annenDato,
+ tomDato: d,
+ };
+
+ return lagreSorteringTidsintervallDato(params);
+ }
+ return undefined;
+ };
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.spec.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.spec.tsx
new file mode 100644
index 00000000000..0d71bbd4ef4
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.spec.tsx
@@ -0,0 +1,100 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './SorteringVelger.stories';
+
+const {
+ SorteringsvelgerNårMangeBehandlingstyperErValgt,
+ SorteringsvelgerNårKunTilbakekrevingErValgt,
+ SorteringsvelgerNårDynamiskPeriodeErValgt,
+} = composeStories(stories);
+
+describe('SorteringVelger', () => {
+ it('skal vise tre sorteringsvalg når mange behandlingstyper er valgt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårMangeBehandlingstyperErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+ expect(await screen.findByLabelText('Dato for behandlingsfrist')).toBeChecked();
+ expect(screen.getByLabelText('Dato for opprettelse av behandling')).not.toBeChecked();
+ expect(screen.getByLabelText('Dato for første stønadsdag')).not.toBeChecked();
+ expect(screen.queryByText('Feilutbetalt beløp')).not.toBeInTheDocument();
+ expect(screen.queryByText('Dato for første feilutbetaling')).not.toBeInTheDocument();
+ });
+
+ it('skal vise datovelger der dynamisk periode ikke er valgt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårMangeBehandlingstyperErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+ expect(await screen.findByText('Ta kun med behandlinger med dato')).toBeInTheDocument();
+ expect(screen.getByText('F.o.m.')).toBeInTheDocument();
+ expect(screen.getByText('T.o.m.')).toBeInTheDocument();
+
+ expect(screen.getByLabelText('Dynamisk periode')).not.toBeChecked();
+ });
+
+ it('skal vise datovelger der dynamisk periode er valgt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårDynamiskPeriodeErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+ expect(await screen.findByText('Ta kun med behandlinger med dato')).toBeInTheDocument();
+ expect(screen.getByText('F.o.m.')).toBeInTheDocument();
+ expect(screen.getByText('T.o.m.')).toBeInTheDocument();
+
+ expect(screen.getByLabelText('Dynamisk periode')).toBeChecked();
+ });
+
+ it('skal vise vis beløpvelger når Feilutbetalt beløp er valgt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårKunTilbakekrevingErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Feilutbetalt beløp'));
+
+ expect(await screen.findByText('Ta kun med behandlinger mellom')).toBeInTheDocument();
+ expect(screen.getAllByText('kr')[0]).toBeInTheDocument();
+ expect(screen.getAllByText('kr')[1]).toBeInTheDocument();
+ });
+
+ it('skal vise feilmelding når en skriver inn bokstaver i fra-beløpfelt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårKunTilbakekrevingErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Feilutbetalt beløp'));
+
+ expect(await screen.findByText('Ta kun med behandlinger mellom')).toBeInTheDocument();
+
+ const fraInput = screen.getAllByRole('textbox')[0];
+ await userEvent.type(fraInput, 'bokstaver');
+
+ expect(await screen.findByText('Feltet kan kun inneholde tall')).toBeInTheDocument();
+ });
+
+ it('skal vise feilmelding når en skriver inn bokstaver i til-beløpfelt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårKunTilbakekrevingErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Feilutbetalt beløp'));
+
+ expect(await screen.findByText('Ta kun med behandlinger mellom')).toBeInTheDocument();
+
+ const tilInput = screen.getAllByRole('textbox')[1];
+ await userEvent.type(tilInput, 'bokstaver');
+
+ expect(await screen.findByText('Feltet kan kun inneholde tall')).toBeInTheDocument();
+ });
+
+ it('skal vise fem sorteringsvalg når kun tilbakekreving er valgt', async () => {
+ await applyRequestHandlers(SorteringsvelgerNårKunTilbakekrevingErValgt.parameters['msw']);
+ render();
+ expect(await screen.findByText('Dato for behandlingsfrist')).toBeInTheDocument();
+ expect(await screen.findByLabelText('Dato for behandlingsfrist')).toBeChecked();
+ expect(screen.getByLabelText('Dato for opprettelse av behandling')).not.toBeChecked();
+ expect(screen.getByLabelText('Dato for første stønadsdag')).not.toBeChecked();
+ expect(screen.getByLabelText('Feilutbetalt beløp')).not.toBeChecked();
+ expect(screen.getByLabelText('Dato for første feilutbetaling')).not.toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.stories.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.stories.tsx
new file mode 100644
index 00000000000..308a9b13b05
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.stories.tsx
@@ -0,0 +1,84 @@
+import { useForm } from 'react-hook-form';
+
+import { RhfForm } from '@navikt/ft-form-hooks';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType, KøSortering } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { SorteringVelger } from './SorteringVelger';
+
+import messages from '../../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/behandlingskoer/SorteringVelger',
+ component: SorteringVelger,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_INTERVALL, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_DYNAMISK_PERIDE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LAGRE_SAKSLISTE_SORTERING_TIDSINTERVALL_DATO, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtSakslisteId: 1,
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+ render: args => {
+ const formMethods = useForm({
+ defaultValues: {
+ sortering: KøSortering.BEHANDLINGSFRIST,
+ fra: 2,
+ til: 3,
+ fomDato: '2020-01-10',
+ tomDato: '2020-10-01',
+ erDynamiskPeriode: args.erDynamiskPeriode,
+ },
+ });
+
+ const { data: kodeverkLos } = useQuery(losKodeverkOptions());
+
+ return kodeverkLos ? (
+
+
+
+ ) : (
+
+ );
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const SorteringsvelgerNårMangeBehandlingstyperErValgt: Story = {
+ args: {
+ valgteBehandlingtyper: [BehandlingType.FORSTEGANGSSOKNAD, BehandlingType.DOKUMENTINNSYN],
+ erDynamiskPeriode: false,
+ },
+};
+
+export const SorteringsvelgerNårDynamiskPeriodeErValgt: Story = {
+ args: {
+ valgteBehandlingtyper: [BehandlingType.FORSTEGANGSSOKNAD, BehandlingType.DOKUMENTINNSYN],
+ erDynamiskPeriode: true,
+ },
+};
+
+export const SorteringsvelgerNårKunTilbakekrevingErValgt: Story = {
+ args: {
+ valgteBehandlingtyper: [BehandlingType.TILBAKEKREVING],
+ erDynamiskPeriode: false,
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.tsx b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.tsx
new file mode 100644
index 00000000000..ae863c1838d
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/SorteringVelger.tsx
@@ -0,0 +1,100 @@
+import { useFormContext } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { RhfRadioGroup } from '@navikt/ft-form-hooks';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+
+import { lagreSakslisteSortering, LosUrl } from '../../../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../../../data/useLosKodeverk';
+import { BelopSorteringValg } from './BelopSorteringValg';
+import { DatoSorteringValg } from './DatoSorteringValg';
+
+const bareTilbakekrevingValgt = (valgteBehandlingtyper?: string[]) =>
+ valgteBehandlingtyper &&
+ valgteBehandlingtyper.some(
+ type => type === BehandlingType.TILBAKEKREVING || type === BehandlingType.TILBAKEKREVING_REVURDERING,
+ ) &&
+ !valgteBehandlingtyper.some(
+ type => type !== BehandlingType.TILBAKEKREVING && type !== BehandlingType.TILBAKEKREVING_REVURDERING,
+ );
+
+interface Props {
+ valgtSakslisteId: number;
+ valgteBehandlingtyper?: string[];
+ valgtAvdelingEnhet: string;
+ erDynamiskPeriode: boolean;
+}
+
+export const SorteringVelger = ({
+ valgtSakslisteId,
+ valgteBehandlingtyper,
+ valgtAvdelingEnhet,
+ erDynamiskPeriode,
+}: Props) => {
+ const queryClient = useQueryClient();
+
+ // TODO (TOR) typing på useFormContext
+ const { resetField, control } = useFormContext();
+
+ const { mutate: lagreSortering } = useMutation({
+ mutationFn: (valuesToStore: { sorteringType: string }) =>
+ lagreSakslisteSortering(valgtSakslisteId, valuesToStore.sorteringType, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, valgtSakslisteId, valgtAvdelingEnhet],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING],
+ });
+ },
+ });
+
+ const koSorteringer = useLosKodeverk('KøSortering');
+
+ return (
+ }
+ onChange={sorteringType => {
+ resetField('fra', { defaultValue: '' });
+ resetField('til', { defaultValue: '' });
+ resetField('fomDato', { defaultValue: '' });
+ resetField('tomDato', { defaultValue: '' });
+ resetField('erDynamiskPeriode', { defaultValue: '' });
+
+ return lagreSortering({
+ sorteringType,
+ });
+ }}
+ radios={koSorteringer
+ .filter(
+ koSortering =>
+ koSortering.feltkategori !== 'TILBAKEKREVING' || bareTilbakekrevingValgt(valgteBehandlingtyper),
+ )
+ .map(koSortering => ({
+ value: koSortering.kode as string,
+ label: koSortering.navn,
+ element: (
+ <>
+ {koSortering.felttype === 'DATO' && (
+
+ )}
+ {koSortering.felttype === 'HELTALL' && (
+
+ )}
+ >
+ ),
+ }))}
+ />
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/sorteringVelger.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/sorteringVelger.module.css
new file mode 100644
index 00000000000..99bdbad0a2f
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/sortering/sorteringVelger.module.css
@@ -0,0 +1,27 @@
+.dager {
+ padding-top: 40px;
+}
+
+.dagerMedBindestrek {
+ padding-right: 20px;
+ padding-top: 40px;
+}
+
+.beløp {
+ padding-right: 20px;
+ padding-top: 20px;
+}
+
+.tomDato {
+ padding-right: 20px;
+}
+
+.dato {
+ margin-bottom: 5px;
+ width: 60px;
+}
+
+.arrowBoxWidth {
+ margin-top: 10px;
+ width: 420px;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/useDebounce.ts b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/useDebounce.ts
new file mode 100644
index 00000000000..d25f64a7fbf
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/useDebounce.ts
@@ -0,0 +1,26 @@
+import { useCallback, useEffect } from 'react';
+import { type FieldValues, type Path, useFormContext, type UseFormTrigger } from 'react-hook-form';
+
+import debounce from 'lodash.debounce';
+
+const getTimeoutValue = () => (import.meta.env.MODE === 'test' ? 0 : 1000);
+
+export const useDebounce = (
+ feltNavn: Path,
+ funksjon: (verdier: VALUE) => void,
+ trigger?: UseFormTrigger,
+) => {
+ const context = useFormContext();
+ const validationTrigger = trigger || context.trigger;
+
+ const lagre = useCallback(
+ debounce((verdi: VALUE) => {
+ validationTrigger(feltNavn).then(isValid => isValid && funksjon(verdi));
+ }, getTimeoutValue()),
+ [funksjon],
+ );
+
+ useEffect(() => () => lagre.cancel(), []);
+
+ return lagre;
+};
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/utvalgskriterierForSakslisteForm.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/utvalgskriterierForSakslisteForm.module.css
new file mode 100644
index 00000000000..b8a78791842
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sakslisteForm/utvalgskriterierForSakslisteForm.module.css
@@ -0,0 +1,3 @@
+.bredde {
+ width: 300px;
+}
diff --git a/apps/fp-avdelingsleder/src/behandlingskoer/sletteSakslisteModal.module.css b/apps/fp-avdelingsleder/src/behandlingskoer/sletteSakslisteModal.module.css
new file mode 100644
index 00000000000..687cc358010
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/behandlingskoer/sletteSakslisteModal.module.css
@@ -0,0 +1,8 @@
+.submitButton {
+ margin-right: 10px;
+ margin-top: 10px;
+}
+
+.cancelButton {
+ margin-top: 10px;
+}
diff --git a/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.spec.tsx b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.spec.tsx
new file mode 100644
index 00000000000..f2c0dcd5f06
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.spec.tsx
@@ -0,0 +1,13 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+
+import * as stories from './IkkeTilgangTilAvdelingslederPanel.stories';
+
+const { IkkeTilgangTilAvdelingsleder } = composeStories(stories);
+
+describe('IkkeTilgangTilAvdelingslederPanel', () => {
+ it('skal vise side for ikke tilgang til avdelingsleder', async () => {
+ render();
+ expect(await screen.findByText('Du har ikke tilgang til å bruke dette programmet')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.stories.tsx b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.stories.tsx
new file mode 100644
index 00000000000..d904764418a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { getIntlDecorator } from '@navikt/fp-storybook-utils';
+
+import { IkkeTilgangTilAvdelingslederPanel } from './IkkeTilgangTilAvdelingslederPanel';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/IkkeTilgangTilAvdelingslederPanel',
+ component: IkkeTilgangTilAvdelingslederPanel,
+ decorators: [withIntl],
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const IkkeTilgangTilAvdelingsleder: Story = {};
diff --git a/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.tsx b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.tsx
new file mode 100644
index 00000000000..925868bed3e
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/components/IkkeTilgangTilAvdelingslederPanel.tsx
@@ -0,0 +1,13 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Box, Heading } from '@navikt/ds-react';
+
+import styles from './ikkeTilgangTilAvdelingslederPanel.module.css';
+
+export const IkkeTilgangTilAvdelingslederPanel = () => (
+
+
+
+
+
+);
diff --git a/apps/fp-avdelingsleder/src/components/ikkeTilgangTilAvdelingslederPanel.module.css b/apps/fp-avdelingsleder/src/components/ikkeTilgangTilAvdelingslederPanel.module.css
new file mode 100644
index 00000000000..d2a4fa1591f
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/components/ikkeTilgangTilAvdelingslederPanel.module.css
@@ -0,0 +1,3 @@
+.container {
+ text-align: center;
+}
diff --git a/apps/fp-avdelingsleder/src/data/StoreValuesInLocalStorage.tsx b/apps/fp-avdelingsleder/src/data/StoreValuesInLocalStorage.tsx
new file mode 100644
index 00000000000..805bd11daad
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/StoreValuesInLocalStorage.tsx
@@ -0,0 +1,24 @@
+import { useEffect } from 'react';
+
+import { setValueInLocalStorage } from './localStorageHelper';
+
+//TODO Skriv om til hook
+
+interface Props {
+ stateKey: string;
+ values: unknown;
+}
+
+/**
+ * StoreValuesInLocalStorage
+ *
+ * Lagrer verdier i localstorage når komponenten blir kastet. Brukt for å mellomlagre form-state
+ * ved navigering fra og til komponenter som har en final-form.
+ */
+export const StoreValuesInLocalStorage = ({ stateKey, values }: Props): null => {
+ useEffect(() => {
+ setValueInLocalStorage(stateKey, JSON.stringify(values));
+ }, [values]);
+
+ return null;
+};
diff --git a/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.spec.tsx b/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.spec.tsx
new file mode 100644
index 00000000000..df17cbc239b
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.spec.tsx
@@ -0,0 +1,53 @@
+import { useEffect } from 'react';
+
+import { render, screen } from '@testing-library/react';
+
+import { ErrorType } from './errorType';
+import { RestApiErrorProvider, useRestApiError, useRestApiErrorDispatcher } from './RestApiErrorContext';
+
+const TestErrorMessage = ({ skalFjerne = false }) => {
+ const { addErrorMessage, removeErrorMessages } = useRestApiErrorDispatcher();
+ useEffect(() => {
+ addErrorMessage({ type: ErrorType.GENERAL_ERROR, message: 'Feilmeldingstest 1' });
+ addErrorMessage({ type: ErrorType.GENERAL_ERROR, message: 'Feilmeldingstest 2' });
+
+ if (skalFjerne) {
+ removeErrorMessages();
+ }
+ }, []);
+
+ const feilmeldinger = useRestApiError();
+ return (
+ <>
+ Feilmeldinger:
+ {feilmeldinger.map(feil => (
+ {feil.type === ErrorType.GENERAL_ERROR ? feil.message : ''}
+ ))}
+ >
+ );
+};
+
+describe('RestApiErrorContext', () => {
+ it('skal legge til feilmelding og så hente alle i kontekst', async () => {
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.findByText('Feilmeldingstest 1')).toBeInTheDocument();
+ expect(screen.getByText('Feilmeldingstest 2')).toBeInTheDocument();
+ });
+
+ it('skal legge til feilmelding og så fjerne alle i kontekst', async () => {
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.findByText('Feilmeldinger:')).toBeInTheDocument();
+ expect(screen.queryByText('Feilmeldingstest 1')).not.toBeInTheDocument();
+ expect(screen.queryByText('Feilmeldingstest 2')).not.toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.tsx b/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.tsx
new file mode 100644
index 00000000000..a9af2959da5
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/error/RestApiErrorContext.tsx
@@ -0,0 +1,74 @@
+import { createContext, type JSX, type ReactNode, useContext, useReducer } from 'react';
+
+import type { FpError } from './errorType';
+
+const defaultInitialState = {
+ errors: [],
+};
+
+type Action = { type: 'add'; data: FpError } | { type: 'remove' };
+type Dispatch = (action: Action) => void;
+type State = { errors: FpError[] };
+
+const RestApiErrorStateContext = createContext(defaultInitialState);
+const RestApiErrorDispatchContext = createContext(undefined);
+
+interface Props {
+ children: ReactNode;
+ initialState?: State;
+}
+
+/**
+ * Tilbyr kontekst for lagring av feilmeldinger.
+ */
+export const RestApiErrorProvider = ({ children, initialState }: Props): JSX.Element => {
+ const [state, dispatch] = useReducer((oldState: State, action: Action) => {
+ switch (action.type) {
+ case 'add':
+ return {
+ errors: oldState.errors.concat(action.data),
+ };
+ case 'remove':
+ return defaultInitialState;
+ default:
+ throw new Error();
+ }
+ }, initialState || defaultInitialState);
+
+ return (
+
+ {children}
+
+ );
+};
+
+/**
+ * Hook som tilbyr funksjoner for å legge til eller fjerne feil i kontekst.
+ */
+export const useRestApiErrorDispatcher = () => {
+ const dispatch = useContext(RestApiErrorDispatchContext);
+
+ const addErrorMessage = (data: FpError) => {
+ if (dispatch) {
+ dispatch({ type: 'add', data });
+ }
+ };
+ const removeErrorMessages = () => {
+ if (dispatch) {
+ dispatch({ type: 'remove' });
+ }
+ };
+
+ return {
+ addErrorMessage,
+ removeErrorMessages,
+ };
+};
+
+/**
+ * Hook som henter alle feilmeldinger registrert i kontekst.
+ */
+export const useRestApiError = () => {
+ const state = useContext(RestApiErrorStateContext);
+ return state.errors;
+};
diff --git a/apps/fp-avdelingsleder/src/data/error/errorType.ts b/apps/fp-avdelingsleder/src/data/error/errorType.ts
new file mode 100644
index 00000000000..482c3c645f7
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/error/errorType.ts
@@ -0,0 +1,58 @@
+import { ApiPollingStatus } from '@navikt/fp-konstanter';
+
+export enum ErrorType {
+ GENERAL_ERROR = 'GENERAL_ERROR',
+ POLLING_TIMEOUT = 'POLLING_TIMEOUT',
+ POLLING_HALTED_OR_DELAYED = 'POLLING_HALTED_OR_DELAYED',
+ REQUEST_GATEWAY_TIMEOUT_OR_NOT_FOUND = 'REQUEST_GATEWAY_TIMEOUT_OR_NOT_FOUND',
+ REQUEST_FORBIDDEN = 'REQUEST_FORBIDDEN',
+ REQUEST_UNAUTHORIZED = 'REQUEST_UNAUTHORIZED',
+}
+
+type GeneralError = {
+ type: ErrorType.GENERAL_ERROR;
+ message: string;
+};
+
+type PollingTimeoutError = {
+ type: ErrorType.POLLING_TIMEOUT;
+ message: string;
+ location: string;
+};
+
+type RequestForbiddenError = {
+ type: ErrorType.REQUEST_FORBIDDEN;
+ message: string;
+};
+
+type RequestUnauthorizedError = {
+ type: ErrorType.REQUEST_UNAUTHORIZED;
+ message: string;
+};
+
+type RequestGatewayTimeoutOrNotFoundError = {
+ type: ErrorType.REQUEST_GATEWAY_TIMEOUT_OR_NOT_FOUND;
+ location: string;
+};
+
+type PollingHaltedError = {
+ type: ErrorType.POLLING_HALTED_OR_DELAYED;
+ status: ApiPollingStatus.HALTED;
+ message: string;
+};
+
+type PollingDelayedError = {
+ type: ErrorType.POLLING_HALTED_OR_DELAYED;
+ status: ApiPollingStatus.DELAYED;
+ message: string;
+ eta: string;
+};
+
+export type FpError =
+ | GeneralError
+ | PollingTimeoutError
+ | RequestForbiddenError
+ | RequestUnauthorizedError
+ | RequestGatewayTimeoutOrNotFoundError
+ | PollingHaltedError
+ | PollingDelayedError;
diff --git a/apps/fp-avdelingsleder/src/data/fplosAvdelingslederApi.ts b/apps/fp-avdelingsleder/src/data/fplosAvdelingslederApi.ts
new file mode 100644
index 00000000000..b0dce35dd3c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/fplosAvdelingslederApi.ts
@@ -0,0 +1,387 @@
+import { queryOptions } from '@tanstack/react-query';
+import ky from 'ky';
+
+import type { Oppgave, SaksbehandlerProfil } from '@navikt/fp-los-felles';
+import type { AlleKodeverkLos, ApiLink, NavAnsatt } from '@navikt/fp-types';
+
+import type { Avdeling } from '../typer/avdelingTsType';
+import type { BehandlingVentefrist } from '../typer/behandlingVentefristTsType';
+import type { OppgaverForAvdeling } from '../typer/oppgaverForAvdelingTsType';
+import type { OppgaveForDato } from '../typer/oppgaverForDatoTsType';
+import type { OppgaverForForsteStonadsdag } from '../typer/oppgaverForForsteStonadsdagTsType';
+import type { OppgaverSomErApneEllerPaVent } from '../typer/oppgaverSomErApneEllerPaVentTsType';
+import type { Reservasjon } from '../typer/reservasjonTsType';
+import type { SaksbehandlereOgSaksbehandlerGrupper } from '../typer/saksbehandlereOgSaksbehandlerGrupper';
+import type { SakslisteAvdeling } from '../typer/sakslisteAvdelingTsType';
+
+type BehandlendeEnheter = {
+ enhetId: string;
+ enhetNavn: string;
+}[];
+
+export type InitDataFpSak = {
+ behandlendeEnheter: BehandlendeEnheter;
+ innloggetBruker: NavAnsatt;
+ links: ApiLink[];
+ sakLinks: ApiLink[];
+};
+
+const kyExtended = ky.extend({
+ retry: 0,
+ timeout: 15000,
+ hooks: {
+ beforeRequest: [
+ request => {
+ const navCallId = `CallId_${new Date().getTime()}_${Math.floor(Math.random() * 1000000000)}`;
+ request.headers.set('Nav-Callid', navCallId);
+ },
+ ],
+ },
+});
+
+//MÅ være en gyldig URL for at KY skal fungere i test
+const isTest = import.meta.env.MODE === 'test';
+const wrapUrl = (url: string) => (isTest ? `https://www.test.com${url}` : url);
+
+export const FagsakUrl = {
+ INIT_FETCH: wrapUrl('/fpsak/api/init-fetch'),
+};
+
+export const LosUrl = {
+ KODEVERK_LOS: wrapUrl('/fplos/api/kodeverk'),
+ AVDELINGER: wrapUrl('/fplos/api/avdelingsleder/avdelinger'),
+ SAKSBEHANDLERE_FOR_AVDELING: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere'),
+ OPPGAVE_AVDELING_ANTALL: wrapUrl('/fplos/api/avdelingsleder/oppgaver/avdelingantall'),
+ SAKSLISTER_FOR_AVDELING: wrapUrl('/fplos/api/avdelingsleder/sakslister'),
+ OPPRETT_NY_SAKSLISTE: wrapUrl('/fplos/api/avdelingsleder/sakslister'),
+ OPPGAVE_ANTALL: wrapUrl('/fplos/api/avdelingsleder/oppgaver/antall'),
+ LAGRE_SAKSLISTE_NAVN: wrapUrl('/fplos/api/avdelingsleder/sakslister/navn'),
+ LAGRE_SAKSLISTE_SAKSBEHANDLER: wrapUrl('/fplos/api/avdelingsleder/sakslister/saksbehandler'),
+ LAGRE_SAKSLISTE_SORTERING: wrapUrl('/fplos/api/avdelingsleder/sakslister/sortering'),
+ LAGRE_SAKSLISTE_SORTERING_INTERVALL: wrapUrl('/fplos/api/avdelingsleder/sakslister/sortering-numerisk-intervall'),
+ LAGRE_SAKSLISTE_SORTERING_DYNAMISK_PERIDE: wrapUrl(
+ '/fplos/api/avdelingsleder/sakslister/sortering-tidsintervall-type',
+ ),
+ LAGRE_SAKSLISTE_SORTERING_TIDSINTERVALL_DATO: wrapUrl(
+ '/fplos/api/avdelingsleder/sakslister/sortering-tidsintervall-dato',
+ ),
+ LAGRE_SAKSLISTE_FAGSAK_YTELSE_TYPE: wrapUrl('/fplos/api/avdelingsleder/sakslister/ytelsetyper'),
+ LAGRE_SAKSLISTE_BEHANDLINGSTYPE: wrapUrl('/fplos/api/avdelingsleder/sakslister/behandlingstype'),
+ LAGRE_SAKSLISTE_ANDRE_KRITERIER: wrapUrl('/fplos/api/avdelingsleder/sakslister/andre-kriterier'),
+ HENT_OPPGAVER_FOR_AVDELING: wrapUrl('/fplos/api/avdelingsleder/nøkkeltall/behandlinger-under-arbeid'),
+ HENT_OPPGAVER_PER_DATO: wrapUrl('/fplos/api/avdelingsleder/nøkkeltall/behandlinger-under-arbeid-historikk'),
+ HENT_OPPGAVER_APNE_ELLER_PA_VENT: wrapUrl('/fplos/api/avdelingsleder/nøkkeltall/åpne-behandlinger'),
+ HENT_BEHANDLINGER_FRISTUTLOP: wrapUrl('/fplos/api/avdelingsleder/nøkkeltall/frist-utløp'),
+ HENT_OPPGAVER_PER_FORSTE_STONADSDAG: wrapUrl('/fplos/api/avdelingsleder/nøkkeltall/behandlinger-første-stønadsdag'),
+ RESERVASJONER_FOR_AVDELING: wrapUrl('/fplos/api/avdelingsleder/reservasjoner'),
+ SLETT_SAKSLISTE: wrapUrl('/fplos/api/avdelingsleder/sakslister/slett'),
+ HENT_GRUPPER: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper'),
+ OPPRETT_GRUPPE: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper/opprett-gruppe'),
+ LEGG_SAKSBEHANDLER_TIL_GRUPPE: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper/legg-til-saksbehandler'),
+ FJERN_SAKSBEHANDLER_FRA_GRUPPE: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper/fjern-saksbehandler'),
+ ENDRE_GRUPPENAVN: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper/endre-gruppenavn'),
+ SLETT_GRUPPE: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/grupper/slett-saksbehandlergruppe'),
+ AVDELINGSLEDER_OPPHEVER_RESERVASJON: wrapUrl('/fplos/api/avdelingsleder/reservasjoner/opphev'),
+ FLYTT_RESERVASJON: wrapUrl('/fplos/api/reservasjon/flytt-reservasjon'),
+ ENDRE_OPPGAVERESERVASJON: wrapUrl('/fplos/api/reservasjon/endre-varighet'),
+ FLYTT_RESERVASJON_SAKSBEHANDLER_SOK: wrapUrl('/fplos/api/reservasjon/flytt-reservasjon/søk'),
+ SLETT_SAKSBEHANDLER: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/slett'),
+ SAKSBEHANDLER_SOK: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere/søk'),
+ OPPRETT_NY_SAKSBEHANDLER: wrapUrl('/fplos/api/avdelingsleder/saksbehandlere'),
+};
+
+export const initFetchOptions = () =>
+ queryOptions({
+ queryKey: [FagsakUrl.INIT_FETCH],
+ queryFn: () => kyExtended.get(FagsakUrl.INIT_FETCH).json(),
+ staleTime: Infinity,
+ });
+
+export const oppgaverForAvdelingAntallOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.OPPGAVE_AVDELING_ANTALL, avdelingEnhet],
+ queryFn: () => kyExtended.get(LosUrl.OPPGAVE_AVDELING_ANTALL, { searchParams: { avdelingEnhet } }).json(),
+ });
+
+export const sakslisterForAvdelingOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.SAKSLISTER_FOR_AVDELING, avdelingEnhet],
+ queryFn: () =>
+ kyExtended.get(LosUrl.SAKSLISTER_FOR_AVDELING, { searchParams: { avdelingEnhet } }).json(),
+ initialData: [],
+ });
+
+export const saksbehandlareForAvdelingOptions = (avdelingEnhet?: string) =>
+ queryOptions({
+ queryKey: [LosUrl.SAKSBEHANDLERE_FOR_AVDELING, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.SAKSBEHANDLERE_FOR_AVDELING, { searchParams: { avdelingEnhet: avdelingEnhet ?? '' } })
+ .json(),
+ initialData: [],
+ enabled: !!avdelingEnhet,
+ });
+
+export const oppgaveAntallOptions = (sakslisteId: number, avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.OPPGAVE_ANTALL, sakslisteId, avdelingEnhet],
+ queryFn: () =>
+ kyExtended.get(LosUrl.OPPGAVE_ANTALL, { searchParams: { sakslisteId, avdelingEnhet } }).json(),
+ });
+
+export const losKodeverkOptions = () =>
+ queryOptions({
+ queryKey: [LosUrl.KODEVERK_LOS],
+ queryFn: () => kyExtended.get(LosUrl.KODEVERK_LOS).json(),
+ staleTime: Infinity,
+ });
+
+export const grupperOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_GRUPPER, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.HENT_GRUPPER, { searchParams: { avdelingEnhet } })
+ .json(),
+ });
+
+export const avdelingerOptions = (isEnabled: boolean) =>
+ queryOptions({
+ queryKey: [LosUrl.AVDELINGER],
+ queryFn: () => kyExtended.get(LosUrl.AVDELINGER).json(),
+ enabled: isEnabled,
+ });
+
+export const oppgaverPerFørsteStønadsdagOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_OPPGAVER_PER_FORSTE_STONADSDAG, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.HENT_OPPGAVER_PER_FORSTE_STONADSDAG, { searchParams: { avdelingEnhet } })
+ .json(),
+ initialData: [],
+ });
+
+export const oppgaverÅpneEllerPåVentOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_OPPGAVER_APNE_ELLER_PA_VENT, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.HENT_OPPGAVER_APNE_ELLER_PA_VENT, { searchParams: { avdelingEnhet } })
+ .json(),
+ initialData: [],
+ });
+
+export const oppgaverForAvdelingOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_OPPGAVER_FOR_AVDELING, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.HENT_OPPGAVER_FOR_AVDELING, { searchParams: { avdelingEnhet } })
+ .json(),
+ initialData: [],
+ });
+
+export const oppgaverPerDatoOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_OPPGAVER_PER_DATO, avdelingEnhet],
+ queryFn: () =>
+ kyExtended.get(LosUrl.HENT_OPPGAVER_PER_DATO, { searchParams: { avdelingEnhet } }).json(),
+ initialData: [],
+ });
+
+export const behandlingerFristUtløptOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.HENT_BEHANDLINGER_FRISTUTLOP, avdelingEnhet],
+ queryFn: () =>
+ kyExtended
+ .get(LosUrl.HENT_BEHANDLINGER_FRISTUTLOP, { searchParams: { avdelingEnhet } })
+ .json(),
+ initialData: [],
+ });
+
+export const reservasjonerForAvdelingOptions = (avdelingEnhet: string) =>
+ queryOptions({
+ queryKey: [LosUrl.RESERVASJONER_FOR_AVDELING, avdelingEnhet],
+ queryFn: () =>
+ kyExtended.get(LosUrl.RESERVASJONER_FOR_AVDELING, { searchParams: { avdelingEnhet } }).json(),
+ initialData: [],
+ });
+
+export const opprettNySaksliste = (avdelingEnhet: string) =>
+ kyExtended.post(LosUrl.OPPRETT_NY_SAKSLISTE, { json: { avdelingEnhet } }).json<{ sakslisteId: string }>();
+
+export const lagreSakslisteNavn = (sakslisteId: number, navn: string, avdelingEnhet: string) =>
+ kyExtended.post(LosUrl.LAGRE_SAKSLISTE_NAVN, { json: { sakslisteId, navn, avdelingEnhet } }).json();
+
+export const lagreSakslisteSaksbehandler = (
+ sakslisteId: number,
+ brukerIdent: string,
+ checked: boolean,
+ avdelingEnhet: string,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_SAKSBEHANDLER, { json: { sakslisteId, brukerIdent, checked, avdelingEnhet } })
+ .json();
+
+export const lagreSakslisteSortering = (sakslisteId: number, sakslisteSorteringValg: string, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_SORTERING, { json: { sakslisteId, sakslisteSorteringValg, avdelingEnhet } })
+ .json();
+
+export const lagreSakslisteSorteringIntervall = (
+ sakslisteId: number,
+ fra: number,
+ til: number,
+ avdelingEnhet: string,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_SORTERING_INTERVALL, { json: { sakslisteId, fra, til, avdelingEnhet } })
+ .json();
+
+export const lagreSakslisteSorteringDynamiskPeriode = (sakslisteId: number, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_SORTERING_DYNAMISK_PERIDE, {
+ json: { sakslisteId, avdelingEnhet },
+ })
+ .json();
+
+export const lagreSakslisteSorteringTidsintervallDato = (
+ sakslisteId: number,
+ avdelingEnhet: string,
+ fomDato?: string,
+ tomDato?: string,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_SORTERING_TIDSINTERVALL_DATO, {
+ json: { sakslisteId, avdelingEnhet, fomDato, tomDato },
+ })
+ .json();
+
+export const lagreSakslisteFagsakYtelseType = (
+ sakslisteId: number,
+ avdelingEnhet: string,
+ fagsakYtelseType: string,
+ checked: boolean,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_FAGSAK_YTELSE_TYPE, {
+ json: { sakslisteId, avdelingEnhet, fagsakYtelseType, checked },
+ })
+ .json();
+
+export const lagreSakslisteBehandlingstype = (
+ sakslisteId: number,
+ avdelingEnhet: string,
+ behandlingType: string,
+ checked: boolean,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_BEHANDLINGSTYPE, {
+ json: { sakslisteId, avdelingEnhet, behandlingType, checked },
+ })
+ .json();
+
+export const lagreSakslisteAndreKriterier = (
+ sakslisteId: number,
+ avdelingEnhet: string,
+ andreKriterierType: string,
+ checked: boolean,
+ inkluder: boolean,
+) =>
+ kyExtended
+ .post(LosUrl.LAGRE_SAKSLISTE_ANDRE_KRITERIER, {
+ json: { sakslisteId, avdelingEnhet, andreKriterierType, checked, inkluder },
+ })
+ .json();
+
+export const slettSaksliste = (sakslisteId: number, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.SLETT_SAKSLISTE, {
+ json: { sakslisteId, avdelingEnhet },
+ })
+ .json();
+
+export const opprettGruppe = (avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.OPPRETT_GRUPPE, {
+ json: { avdelingEnhet },
+ })
+ .json();
+
+export const leggSaksbehandlerTilGruppe = (brukerIdent: string, avdelingEnhet: string, gruppeId: number) =>
+ kyExtended
+ .post(LosUrl.LEGG_SAKSBEHANDLER_TIL_GRUPPE, {
+ json: { brukerIdent, avdelingEnhet, gruppeId },
+ })
+ .json();
+
+export const fjernSaksbehandlerFraGruppe = (brukerIdent: string, avdelingEnhet: string, gruppeId: number) =>
+ kyExtended
+ .post(LosUrl.FJERN_SAKSBEHANDLER_FRA_GRUPPE, {
+ json: { brukerIdent, avdelingEnhet, gruppeId },
+ })
+ .json();
+
+export const endreGruppenavn = (gruppeId: number, gruppeNavn: string, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.ENDRE_GRUPPENAVN, {
+ json: { gruppeId, gruppeNavn, avdelingEnhet },
+ })
+ .json();
+
+export const slettGruppe = (gruppeId: number, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.SLETT_GRUPPE, {
+ json: { gruppeId, avdelingEnhet },
+ })
+ .json();
+
+export const opphevReservasjon = (oppgaveId: number) =>
+ kyExtended
+ .post(LosUrl.AVDELINGSLEDER_OPPHEVER_RESERVASJON, {
+ json: { oppgaveId },
+ })
+ .json();
+
+export const flyttReservasjon = (oppgaveId: number, brukerIdent: string, begrunnelse: string) =>
+ kyExtended
+ .post(LosUrl.FLYTT_RESERVASJON, {
+ json: { oppgaveId, brukerIdent, begrunnelse },
+ })
+ .json();
+
+export const endreReservasjon = (oppgaveId: number, reserverTil: string) =>
+ kyExtended
+ .post(LosUrl.ENDRE_OPPGAVERESERVASJON, {
+ json: { oppgaveId, reserverTil },
+ })
+ .json();
+
+export const flyttReservasjonSaksbehandlerSøk = (brukerIdent: string) =>
+ kyExtended
+ .post(LosUrl.FLYTT_RESERVASJON_SAKSBEHANDLER_SOK, {
+ json: { brukerIdent },
+ })
+ .json();
+
+export const slettSaksbehandler = (brukerIdent: string, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.SLETT_SAKSBEHANDLER, {
+ json: { brukerIdent, avdelingEnhet },
+ })
+ .json();
+
+export const saksbehandlgerSøk = (brukerIdent: string) =>
+ kyExtended
+ .post(LosUrl.SAKSBEHANDLER_SOK, {
+ json: { brukerIdent },
+ })
+ .json();
+
+export const opprettNySaksbehandler = (brukerIdent: string, avdelingEnhet: string) =>
+ kyExtended
+ .post(LosUrl.OPPRETT_NY_SAKSBEHANDLER, {
+ json: { brukerIdent, avdelingEnhet },
+ })
+ .json();
diff --git a/apps/fp-avdelingsleder/src/data/localStorageHelper.ts b/apps/fp-avdelingsleder/src/data/localStorageHelper.ts
new file mode 100644
index 00000000000..d7e201aaf83
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/localStorageHelper.ts
@@ -0,0 +1,12 @@
+export const getValueFromLocalStorage = (key: string): string | undefined => {
+ const value = window.localStorage.getItem(key);
+ return value !== 'undefined' && value !== null ? value : undefined;
+};
+
+export const setValueInLocalStorage = (key: string, value: string): void => {
+ window.localStorage.setItem(key, value);
+};
+
+export const removeValueFromLocalStorage = (key: string): void => {
+ window.localStorage.removeItem(key);
+};
diff --git a/apps/fp-avdelingsleder/src/data/useLosKodeverk.ts b/apps/fp-avdelingsleder/src/data/useLosKodeverk.ts
new file mode 100644
index 00000000000..2d022cd05e9
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/data/useLosKodeverk.ts
@@ -0,0 +1,21 @@
+import { useQuery } from '@tanstack/react-query';
+
+import type { AlleKodeverkLos, LosKodeverkType } from '@navikt/fp-types';
+
+import { losKodeverkOptions } from './fplosAvdelingslederApi';
+
+/**
+ * Hook som henter et gitt kodeverk fra respons som allerede er hentet fra backend.
+ */
+export const useLosKodeverk = (kodeverkType: T): AlleKodeverkLos[T] => {
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+
+ if (!alleKodeverk) {
+ throw new Error('Kodeverk for LOS er ikke lastet inn');
+ }
+ if (!alleKodeverk[kodeverkType]) {
+ throw new Error(`Kodeverk ${kodeverkType} for LOS finnes ikke`);
+ }
+
+ return alleKodeverk[kodeverkType];
+};
diff --git a/apps/fp-avdelingsleder/src/globalCss/global.module.css b/apps/fp-avdelingsleder/src/globalCss/global.module.css
new file mode 100644
index 00000000000..65ab3092c60
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/globalCss/global.module.css
@@ -0,0 +1,15 @@
+html {
+ height: 100%;
+}
+
+body {
+ background-color: var(--ax-bg-default);
+ color: var(--ax-text-neutral);
+ height: 100%;
+ line-height: 1.42857143;
+ margin: 0;
+}
+
+:global(#app) {
+ height: 100%;
+}
diff --git a/apps/fp-avdelingsleder/src/grupper/GruppeSaksbehandlere.tsx b/apps/fp-avdelingsleder/src/grupper/GruppeSaksbehandlere.tsx
new file mode 100644
index 00000000000..09e8357b6e6
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/GruppeSaksbehandlere.tsx
@@ -0,0 +1,182 @@
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { XMarkIcon } from '@navikt/aksel-icons';
+import { BodyShort, HStack, Label, UNSAFE_Combobox, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfTextField } from '@navikt/ft-form-hooks';
+import { hasValidName, maxLength, minLength, required } from '@navikt/ft-form-validators';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { useDebounce } from '../behandlingskoer/sakslisteForm/useDebounce';
+import {
+ endreGruppenavn,
+ fjernSaksbehandlerFraGruppe,
+ leggSaksbehandlerTilGruppe,
+ LosUrl,
+} from '../data/fplosAvdelingslederApi';
+import type { SaksbehandlerGruppe } from '../typer/saksbehandlereOgSaksbehandlerGrupper';
+
+import styles from './gruppeSaksbehandlere.module.css';
+
+const minLength3 = minLength(3);
+const maxLength100 = maxLength(100);
+
+const sortGrupperteSaksbehandlere = (saksbehandlere: SaksbehandlerProfil[]) =>
+ [...saksbehandlere].sort((saksbehandler1, saksbehandler2) => saksbehandler1.navn.localeCompare(saksbehandler2.navn));
+
+const sortAvdelingensSaksbehandlere = (
+ saksbehandlere: SaksbehandlerProfil[],
+ grupperteSaksbehandlere: SaksbehandlerProfil[],
+) =>
+ saksbehandlere
+ .filter(s => !grupperteSaksbehandlere.some(gs => gs.brukerIdent === s.brukerIdent))
+ .sort((saksbehandler1, saksbehandler2) => saksbehandler1.navn.localeCompare(saksbehandler2.navn));
+
+interface Props {
+ valgAvdeldingEnhet: string;
+ saksbehandlerGruppe: SaksbehandlerGruppe;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+}
+
+export const GruppeSaksbehandlere = ({ valgAvdeldingEnhet, saksbehandlerGruppe, avdelingensSaksbehandlere }: Props) => {
+ const queryClient = useQueryClient();
+ const intl = useIntl();
+
+ const { mutate: leggTilSaksbehandler } = useMutation({
+ mutationFn: (valuesToStore: { brukerIdent: string; gruppeId: number }) =>
+ leggSaksbehandlerTilGruppe(valuesToStore.brukerIdent, valgAvdeldingEnhet, valuesToStore.gruppeId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.HENT_GRUPPER],
+ });
+ },
+ });
+
+ const { mutate: fjernSaksbehandler } = useMutation({
+ mutationFn: (valuesToStore: { brukerIdent: string; gruppeId: number }) =>
+ fjernSaksbehandlerFraGruppe(valuesToStore.brukerIdent, valgAvdeldingEnhet, valuesToStore.gruppeId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.HENT_GRUPPER],
+ });
+ },
+ });
+
+ const { mutate: endreNavnPåGruppe } = useMutation({
+ mutationFn: (valuesToStore: { gruppeId: number; gruppeNavn: string }) =>
+ endreGruppenavn(valuesToStore.gruppeId, valuesToStore.gruppeNavn, valgAvdeldingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.HENT_GRUPPER],
+ });
+ },
+ });
+
+ const formMethods = useForm({
+ defaultValues: {
+ navn: saksbehandlerGruppe.gruppeNavn,
+ },
+ });
+
+ const sorterteGrupperteSaksbehandlere = sortGrupperteSaksbehandlere(saksbehandlerGruppe.saksbehandlere);
+ const sorterteSaksbehandlereForAvdeling = sortAvdelingensSaksbehandlere(
+ avdelingensSaksbehandlere,
+ sorterteGrupperteSaksbehandlere,
+ );
+
+ const options = sorterteSaksbehandlereForAvdeling.map(sb => `${sb.navn} (${sb.brukerIdent})`);
+
+ const lagreNavnDebounce = useDebounce(
+ 'navn',
+ (navn: string) => endreNavnPåGruppe({ gruppeId: saksbehandlerGruppe.gruppeId, gruppeNavn: navn }),
+ formMethods.trigger,
+ );
+
+ const [filteredOptions, setFilteredOptions] = useState([]);
+ const [filterValue, setFilterValue] = useState('');
+
+ const filterOptions = (searchTerm: string | undefined) => {
+ if (searchTerm?.trim()) {
+ setFilteredOptions(options.filter(option => option.toLowerCase().includes(searchTerm.toLowerCase())));
+ } else {
+ setFilteredOptions(options);
+ }
+ };
+
+ const toggleSelected = (option: string, isSelected: boolean) => {
+ const selectedOption = filteredOptions.find(o => o.toLowerCase().includes(option?.toLowerCase()));
+ const navnOgBrukerIdent = selectedOption?.replace(')', '').split(' (');
+ const alreadySelected = sorterteGrupperteSaksbehandlere.some(
+ gs => navnOgBrukerIdent && gs.brukerIdent === navnOgBrukerIdent[1],
+ );
+
+ if (selectedOption && isSelected && !alreadySelected && navnOgBrukerIdent) {
+ leggTilSaksbehandler({ brukerIdent: navnOgBrukerIdent[1], gruppeId: saksbehandlerGruppe.gruppeId });
+ }
+ };
+
+ useEffect(() => filterOptions(filterValue), [saksbehandlerGruppe, filterValue]);
+
+ return (
+
+
+ lagreNavnDebounce(value)}
+ className={styles.navn}
+ />
+
+
+
+
+
+ {sorterteGrupperteSaksbehandlere.length === 0 && (
+
+
+
+ )}
+ {sorterteGrupperteSaksbehandlere.map(saksbehandler => (
+
+ {`${saksbehandler.navn} (${saksbehandler.brukerIdent})`}
+
+
+ fjernSaksbehandler({
+ brukerIdent: saksbehandler.brukerIdent,
+ gruppeId: saksbehandlerGruppe.gruppeId,
+ })
+ }
+ onKeyDown={() =>
+ fjernSaksbehandler({
+ brukerIdent: saksbehandler.brukerIdent,
+ gruppeId: saksbehandlerGruppe.gruppeId,
+ })
+ }
+ />
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/grupper/GrupperPanel.spec.tsx b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.spec.tsx
new file mode 100644
index 00000000000..b14706f79e1
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.spec.tsx
@@ -0,0 +1,18 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './GrupperPanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('GrupperPanel', () => {
+ it('skal vise gruppe i tabell', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ render();
+ expect(await screen.findByText('Grupper')).toBeInTheDocument();
+ expect(screen.getByText('Id')).toBeInTheDocument();
+ expect(screen.getByText('Navn')).toBeInTheDocument();
+ expect(screen.getByText('Antall saksbehandlere')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/grupper/GrupperPanel.stories.tsx b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.stories.tsx
new file mode 100644
index 00000000000..fbe5df2ee74
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.stories.tsx
@@ -0,0 +1,76 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { http, HttpResponse } from 'msw';
+
+import { getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { LosUrl } from '../data/fplosAvdelingslederApi';
+import type { SaksbehandlereOgSaksbehandlerGrupper } from '../typer/saksbehandlereOgSaksbehandlerGrupper';
+import { GrupperPanel } from './GrupperPanel';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const AVDELING_SAKSBEHANDLERE = [
+ {
+ brukerIdent: 'ident1',
+ navn: 'Anders Utvikler',
+ ansattAvdeling: null,
+ },
+ {
+ brukerIdent: 'ident12',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'ident4',
+ navn: 'Olga Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+ {
+ brukerIdent: 'ident3',
+ navn: 'Klara Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+ },
+];
+
+const SAKSBEHANDLERE_OG_SAKSBEHANDLER_GRUPPER = {
+ saksbehandlerGrupper: [
+ {
+ gruppeId: 1,
+ gruppeNavn: 'Dette er navnet på en gruppe',
+ saksbehandlere: [
+ {
+ brukerIdent: 'ident1',
+ navn: 'Anders Utvikler',
+ },
+ ],
+ },
+ ],
+} as SaksbehandlereOgSaksbehandlerGrupper;
+
+const meta = {
+ title: 'los/avdelingsleder/grupper/GrupperPanel',
+ component: GrupperPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_GRUPPER, () => HttpResponse.json(SAKSBEHANDLERE_OG_SAKSBEHANDLER_GRUPPER)),
+ http.post(LosUrl.OPPRETT_GRUPPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.LEGG_SAKSBEHANDLER_TIL_GRUPPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.SLETT_GRUPPE, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.FJERN_SAKSBEHANDLER_FRA_GRUPPE, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtAvdelingEnhet: '1',
+ avdelingensSaksbehandlere: AVDELING_SAKSBEHANDLERE,
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/grupper/GrupperPanel.tsx b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.tsx
new file mode 100644
index 00000000000..2798a2c5e89
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/GrupperPanel.tsx
@@ -0,0 +1,50 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Button, VStack } from '@navikt/ds-react';
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { useMutation, useQuery } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { grupperOptions, opprettGruppe } from '../data/fplosAvdelingslederApi';
+import { GrupperTabell } from './GrupperTabell';
+
+interface Props {
+ valgtAvdelingEnhet: string;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+}
+
+export const GrupperPanel = ({ valgtAvdelingEnhet, avdelingensSaksbehandlere }: Props) => {
+ const { data: grupper, refetch: hentGrupperPåNytt } = useQuery(grupperOptions(valgtAvdelingEnhet));
+
+ const { mutate: lagGruppe, status } = useMutation({
+ mutationFn: () => opprettGruppe(valgtAvdelingEnhet),
+ onSuccess: () => {
+ hentGrupperPåNytt();
+ },
+ });
+
+ if (!grupper) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/grupper/GrupperTabell.tsx b/apps/fp-avdelingsleder/src/grupper/GrupperTabell.tsx
new file mode 100644
index 00000000000..65d09261093
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/GrupperTabell.tsx
@@ -0,0 +1,90 @@
+import { FormattedMessage } from 'react-intl';
+
+import { XMarkIcon } from '@navikt/aksel-icons';
+import { BodyShort, Label, Table } from '@navikt/ds-react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { LosUrl, slettGruppe } from '../data/fplosAvdelingslederApi';
+import type { SaksbehandlereOgSaksbehandlerGrupper } from '../typer/saksbehandlereOgSaksbehandlerGrupper';
+import { GruppeSaksbehandlere } from './GruppeSaksbehandlere';
+
+import styles from './grupperTabell.module.css';
+
+interface Props {
+ valgAvdeldingEnhet: string;
+ grupper: SaksbehandlereOgSaksbehandlerGrupper;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+}
+
+export const GrupperTabell = ({ valgAvdeldingEnhet, grupper, avdelingensSaksbehandlere }: Props) => {
+ const queryClient = useQueryClient();
+
+ const { mutate: fjernGruppe } = useMutation({
+ mutationFn: (valuesToStore: { gruppeId: number }) => slettGruppe(valuesToStore.gruppeId, valgAvdeldingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.HENT_GRUPPER],
+ });
+ },
+ });
+
+ return (
+ <>
+
+ {grupper.saksbehandlerGrupper.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {grupper.saksbehandlerGrupper?.map(saksbehandlerGruppe => (
+
+ }
+ >
+ {saksbehandlerGruppe.gruppeId}
+ {saksbehandlerGruppe.gruppeNavn ?? '-'}
+ {saksbehandlerGruppe.saksbehandlere.length}
+
+ fjernGruppe({ gruppeId: saksbehandlerGruppe.gruppeId })}
+ onKeyDown={() => fjernGruppe({ gruppeId: saksbehandlerGruppe.gruppeId })}
+ />
+
+
+ ))}
+
+
+ )}
+ {grupper.saksbehandlerGrupper.length === 0 && (
+
+
+
+ )}
+ >
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/grupper/gruppeSaksbehandlere.module.css b/apps/fp-avdelingsleder/src/grupper/gruppeSaksbehandlere.module.css
new file mode 100644
index 00000000000..643a143c89e
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/gruppeSaksbehandlere.module.css
@@ -0,0 +1,14 @@
+.navn {
+ width: 400px;
+}
+
+.saksbehandlerCombo {
+ width: 400px;
+}
+
+.removeIcon {
+ color: var(--ax-danger-500);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
diff --git a/apps/fp-avdelingsleder/src/grupper/grupperTabell.module.css b/apps/fp-avdelingsleder/src/grupper/grupperTabell.module.css
new file mode 100644
index 00000000000..ce49c3658c5
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/grupper/grupperTabell.module.css
@@ -0,0 +1,6 @@
+.removeIcon {
+ color: var(--ax-danger-500);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
diff --git a/apps/fp-avdelingsleder/src/kodeverk/behandlingVenteStatus.ts b/apps/fp-avdelingsleder/src/kodeverk/behandlingVenteStatus.ts
new file mode 100644
index 00000000000..1a9df193005
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/kodeverk/behandlingVenteStatus.ts
@@ -0,0 +1,4 @@
+export enum BehandlingVenteStatus {
+ PA_VENT = 'PÅ_VENT',
+ IKKE_PA_VENT = 'IKKE_PÅ_VENT',
+}
diff --git a/apps/fp-avdelingsleder/src/main.tsx b/apps/fp-avdelingsleder/src/main.tsx
new file mode 100644
index 00000000000..f440a0aa076
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/main.tsx
@@ -0,0 +1,42 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+
+import { breadcrumbsIntegration, init } from '@sentry/browser';
+import dayjs from 'dayjs';
+
+import { LosAppIndexWrapper } from './app/LosAppIndex';
+import { RestApiErrorProvider } from './data/error/RestApiErrorContext';
+
+import 'dayjs/locale/nb.js';
+
+dayjs.locale('nb');
+
+const app = document.getElementById('root');
+if (app === null) {
+ throw new Error('No app element');
+}
+
+const environment = window.location.hostname;
+const isDevelopment = import.meta.env.MODE === 'development';
+
+if (!isDevelopment) {
+ init({
+ dsn: 'https://d1b7de8cc42949569da03849b47d3ea1@sentry.gc.nav.no/17',
+ release: import.meta.env['SENTRY_RELEASE'] ?? 'unknown',
+ environment,
+ integrations: [breadcrumbsIntegration({ console: false })],
+ });
+}
+
+const root = createRoot(app);
+
+root.render(
+
+
+
+
+
+
+ ,
+);
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/NokkeltallPanel.tsx b/apps/fp-avdelingsleder/src/nokkeltall/NokkeltallPanel.tsx
new file mode 100644
index 00000000000..1c0291cac9e
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/NokkeltallPanel.tsx
@@ -0,0 +1,42 @@
+import { VStack } from '@navikt/ds-react';
+
+import { getValueFromLocalStorage } from '../data/localStorageHelper';
+import { OppgaverPerForsteStonadsdagPanel } from './antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel';
+import { OppgaverSomErApneEllerPaVentPanel } from './apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel';
+import { FordelingAvBehandlingstypePanel } from './fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel';
+import { TilBehandlingPanel } from './tilBehandling/TilBehandlingPanel';
+import { VentefristUtløperPanel } from './ventefristUtløper/VentefristUtløperPanel';
+
+interface Props {
+ valgtAvdelingEnhet: string;
+}
+
+export const NokkeltallPanel = ({ valgtAvdelingEnhet }: Props) => {
+ const height = 300;
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagGraf.tsx b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagGraf.tsx
new file mode 100644
index 00000000000..acfc6ea339a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagGraf.tsx
@@ -0,0 +1,94 @@
+import { DDMMYYYY_DATE_FORMAT } from '@navikt/ft-utils';
+import dayjs from 'dayjs';
+import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
+import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
+
+import { ReactECharts } from '@navikt/fp-los-felles';
+
+import type { OppgaverForForsteStonadsdag } from '../../typer/oppgaverForForsteStonadsdagTsType';
+
+dayjs.extend(isSameOrBefore);
+dayjs.extend(isSameOrAfter);
+
+interface Koordinat {
+ x: number;
+ y: number;
+}
+
+interface Props {
+ height: number;
+ oppgaverPerForsteStonadsdag: OppgaverForForsteStonadsdag[];
+}
+
+export const OppgaverPerForsteStonadsdagGraf = ({ height, oppgaverPerForsteStonadsdag }: Props) => {
+ const koordinater = lagKoordinater(oppgaverPerForsteStonadsdag);
+ const data = lagDatastruktur(koordinater);
+ return (
+ dayjs(params.value).format(DDMMYYYY_DATE_FORMAT),
+ },
+ },
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: {
+ title: 'Lagre ',
+ name: 'Antall_førstegangsbehandlinger_fordelt_på_første_stønadsdag',
+ },
+ },
+ },
+ xAxis: {
+ type: 'time',
+ axisLabel: {
+ formatter: '{dd}.{MM}.{yyyy}',
+ },
+ },
+ yAxis: {
+ type: 'value',
+ },
+ series: [
+ {
+ data,
+ type: 'line',
+ areaStyle: {},
+ },
+ ],
+ color: ['#337c9b'],
+ }}
+ />
+ );
+};
+
+const lagKoordinater = (oppgaverPerForsteStonadsdag: OppgaverForForsteStonadsdag[]): Koordinat[] =>
+ oppgaverPerForsteStonadsdag.map(o => ({
+ x: dayjs(o.forsteStonadsdag).startOf('day').toDate().getTime(),
+ y: o.antall,
+ }));
+
+const lagDatastruktur = (koordinater: Koordinat[]): number[][] => {
+ const nyeKoordinater = [];
+ const periodeStart = koordinater
+ .map(koordinat => dayjs(koordinat.x))
+ .reduce(
+ (tidligesteDato, dato) => (tidligesteDato.isSameOrBefore(dato) ? tidligesteDato : dato),
+ dayjs().startOf('day'),
+ )
+ .toDate();
+ const periodeSlutt = koordinater
+ .map(koordinat => dayjs(koordinat.x))
+ .reduce((senesteDato, dato) => (senesteDato.isSameOrAfter(dato) ? senesteDato : dato), dayjs().startOf('day'))
+ .toDate();
+
+ for (let dato = dayjs(periodeStart); dato.isSameOrBefore(periodeSlutt); dato = dato.add(1, 'days')) {
+ const funnetKoordinat = koordinater.find(k => dayjs(k.x).isSame(dato));
+ nyeKoordinater.push([dato.toDate().getTime(), funnetKoordinat ? funnetKoordinat.y : 0]);
+ }
+ return nyeKoordinater;
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.spec.tsx b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.spec.tsx
new file mode 100644
index 00000000000..896f9f6022a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.spec.tsx
@@ -0,0 +1,18 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+
+import * as stories from './OppgaverPerForsteStonadsdagPanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('OppgaverPerForsteStonadsdagPanel', () => {
+ // TODO echarts-testing
+ it.skip('skal rendre graf', async () => {
+ render();
+ expect(
+ await screen.findByText(
+ 'Antall åpne oppgaver for førstegangsbehandlinger fordelt på første stønadsdag - alle ytelser',
+ ),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.stories.tsx b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.stories.tsx
new file mode 100644
index 00000000000..454791bd429
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.stories.tsx
@@ -0,0 +1,66 @@
+import { ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import type { Meta, StoryObj } from '@storybook/react';
+import dayjs from 'dayjs';
+import { http, HttpResponse } from 'msw';
+
+import { getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { LosUrl } from '../../data/fplosAvdelingslederApi';
+import { OppgaverPerForsteStonadsdagPanel } from './OppgaverPerForsteStonadsdagPanel';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/nokkeltall/OppgaverPerForsteStonadsdagPanel',
+ component: OppgaverPerForsteStonadsdagPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.HENT_OPPGAVER_PER_FORSTE_STONADSDAG.replaceAll('ø', '%C3%B8'), () =>
+ HttpResponse.json([
+ {
+ forsteStonadsdag: dayjs().subtract(14, 'd').format(ISO_DATE_FORMAT),
+ antall: 10,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(13, 'd').format(ISO_DATE_FORMAT),
+ antall: 9,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(12, 'd').format(ISO_DATE_FORMAT),
+ antall: 6,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(11, 'd').format(ISO_DATE_FORMAT),
+ antall: 11,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(10, 'd').format(ISO_DATE_FORMAT),
+ antall: 15,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(9, 'd').format(ISO_DATE_FORMAT),
+ antall: 20,
+ },
+ {
+ forsteStonadsdag: dayjs().subtract(8, 'd').format(ISO_DATE_FORMAT),
+ antall: 13,
+ },
+ ]),
+ ),
+ ],
+ },
+ },
+ args: {
+ height: 300,
+ valgtAvdelingEnhet: '1',
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.tsx b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.tsx
new file mode 100644
index 00000000000..f358fca12e1
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/antallBehandlingerPerForsteStonadsdag/OppgaverPerForsteStonadsdagPanel.tsx
@@ -0,0 +1,25 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Label, VStack } from '@navikt/ds-react';
+import { useQuery } from '@tanstack/react-query';
+
+import { oppgaverPerFørsteStønadsdagOptions } from '../../data/fplosAvdelingslederApi';
+import { OppgaverPerForsteStonadsdagGraf } from './OppgaverPerForsteStonadsdagGraf';
+
+interface Props {
+ height: number;
+ valgtAvdelingEnhet: string;
+}
+
+export const OppgaverPerForsteStonadsdagPanel = ({ height, valgtAvdelingEnhet }: Props) => {
+ const { data: oppgaverPerForsteStonadsdag } = useQuery(oppgaverPerFørsteStønadsdagOptions(valgtAvdelingEnhet));
+
+ return (
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentGraf.tsx b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentGraf.tsx
new file mode 100644
index 00000000000..b55dc02d5b4
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentGraf.tsx
@@ -0,0 +1,211 @@
+import { type IntlShape, useIntl } from 'react-intl';
+
+import dayjs from 'dayjs';
+
+import { ReactECharts } from '@navikt/fp-los-felles';
+
+import { BehandlingVenteStatus } from '../../kodeverk/behandlingVenteStatus';
+import type { OppgaverSomErApneEllerPaVent } from '../../typer/oppgaverSomErApneEllerPaVentTsType';
+
+const UKJENT_DATO = 'UKJENT_DATO';
+
+const getYearText = (month: number, intl: IntlShape): string =>
+ intl.formatMessage({ id: `OppgaverSomErApneEllerPaVentGraf.${month}` });
+
+interface KoordinatDatoEllerUkjent {
+ x: string;
+ y: number;
+}
+
+interface Props {
+ height: number;
+ oppgaverApneEllerPaVent: OppgaverSomErApneEllerPaVent[];
+}
+
+export const OppgaverSomErApneEllerPaVentGraf = ({ height, oppgaverApneEllerPaVent }: Props) => {
+ const intl = useIntl();
+ const paVentTekst = intl.formatMessage({ id: 'OppgaverSomErApneEllerPaVentGraf.PaVent' });
+ const ikkePaVentTekst = intl.formatMessage({ id: 'OppgaverSomErApneEllerPaVentGraf.IkkePaVent' });
+ const ukjentTekst = intl.formatMessage({ id: 'OppgaverSomErApneEllerPaVentGraf.Ukjent' });
+ const datoTekst = intl.formatMessage({ id: 'OppgaverSomErApneEllerPaVentGraf.Dato' });
+
+ const oppgaverPaVentPerDato = finnAntallPerDato(
+ oppgaverApneEllerPaVent.filter(o => o.behandlingVenteStatus === BehandlingVenteStatus.PA_VENT),
+ );
+ const oppgaverIkkePaVentPerDato = finnAntallPerDato(
+ oppgaverApneEllerPaVent.filter(o => o.behandlingVenteStatus === BehandlingVenteStatus.IKKE_PA_VENT),
+ );
+
+ const [periodeStart, periodeSlutt] = finnGrafPeriode(oppgaverApneEllerPaVent);
+
+ const { koordinaterPaVent, koordinaterIkkePaVent } = fyllInnManglendeDatoerOgSorterEtterDato(
+ oppgaverPaVentPerDato,
+ oppgaverIkkePaVentPerDato,
+ periodeStart,
+ periodeSlutt,
+ );
+
+ return (
+ {
+ const dato = dayjs(params.value);
+ if (dato.isSame(periodeSlutt)) {
+ return `${ukjentTekst} ${datoTekst}`;
+ }
+ return `${getYearText(dato.month(), intl)} - ${dato.year()}`;
+ },
+ },
+ },
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: {
+ title: 'Lagre ',
+ name: 'Status_åpne_behandlinger',
+ },
+ },
+ },
+ legend: {
+ data: [paVentTekst, ikkePaVentTekst],
+ top: 'top',
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true,
+ },
+ xAxis: [
+ {
+ type: 'category',
+ boundaryGap: false,
+ axisLabel: {
+ formatter: value => {
+ const dato = dayjs(value);
+ const erSiste = dato.isSame(periodeSlutt);
+ const maned = erSiste ? ukjentTekst : getYearText(dato.month(), intl);
+ const ar = erSiste ? datoTekst : dato.year();
+
+ return `${maned}\n${ar}`;
+ },
+ },
+ },
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ name: intl.formatMessage({ id: 'OppgaverSomErApneEllerPaVentGraf.AntallGraf' }),
+ },
+ ],
+ series: [
+ {
+ name: paVentTekst,
+ type: 'bar',
+ stack: 'total',
+ label: {
+ show: true,
+ },
+ emphasis: {
+ focus: 'series',
+ },
+ data: koordinaterPaVent,
+ },
+ {
+ name: ikkePaVentTekst,
+ type: 'bar',
+ stack: 'total',
+ label: {
+ show: true,
+ },
+ emphasis: {
+ focus: 'series',
+ },
+ data: koordinaterIkkePaVent,
+ },
+ ],
+ color: ['#85d5f0', '#38a161'],
+ }}
+ />
+ );
+};
+
+const finnGrafPeriode = (oppgaverSomErApneEllerPaVent: OppgaverSomErApneEllerPaVent[]): dayjs.Dayjs[] => {
+ // Data er garantert å gå inntil 6 mnd tilbake og 10 mnd fram i tid. Utliggerne havner på første/siste mnd
+ let periodeStart = dayjs().subtract(6, 'M');
+ let periodeSlutt = dayjs().add(1, 'M');
+
+ oppgaverSomErApneEllerPaVent
+ .filter(oppgave => !!oppgave.førsteUttakMåned)
+ .forEach(oppgave => {
+ const dato = dayjs(oppgave.førsteUttakMåned);
+ if (dato.isBefore(periodeStart)) {
+ periodeStart = dato;
+ }
+ if (dato.isAfter(periodeSlutt)) {
+ periodeSlutt = dato;
+ }
+ });
+
+ // Ekstra kolonne for data med ukjent dato
+ return [dayjs(periodeStart.startOf('month')), dayjs(periodeSlutt.add(1, 'months').startOf('month'))];
+};
+
+const finnAntallPerDato = (
+ oppgaverSomErApneEllerPaVent: OppgaverSomErApneEllerPaVent[],
+): KoordinatDatoEllerUkjent[] => {
+ const antallPerDatoOgUkjent = oppgaverSomErApneEllerPaVent.reduce(
+ (acc, oppgave) => {
+ const { førsteUttakMåned, antall } = oppgave;
+ const key = førsteUttakMåned ?? UKJENT_DATO;
+ return {
+ ...acc,
+ [key]: acc[key] ? acc[key] + antall : antall,
+ };
+ },
+ {} as Record,
+ );
+
+ return Object.keys(antallPerDatoOgUkjent).map(k => ({ x: k, y: antallPerDatoOgUkjent[k] }));
+};
+
+const lagKoordinatForDato = (dato: dayjs.Dayjs, oppgaver: KoordinatDatoEllerUkjent[]): (number | Date)[] => {
+ const eksisterendeDato = oppgaver.filter(o => o.x !== UKJENT_DATO).find(o => dayjs(o.x).isSame(dato));
+ return [
+ eksisterendeDato ? dayjs(eksisterendeDato.x).toDate() : dato.toDate(),
+ eksisterendeDato ? eksisterendeDato.y : 0,
+ ];
+};
+
+const fyllInnManglendeDatoerOgSorterEtterDato = (
+ oppgaverPaVent: KoordinatDatoEllerUkjent[],
+ oppgaverIkkePaVent: KoordinatDatoEllerUkjent[],
+ periodeStart: dayjs.Dayjs,
+ periodeSlutt: dayjs.Dayjs,
+): { koordinaterPaVent: (number | Date)[][]; koordinaterIkkePaVent: (number | Date)[][] } => {
+ const koordinaterPaVent: (number | Date)[][] = [];
+ const koordinaterIkkePaVent: (number | Date)[][] = [];
+
+ // For å unngå kollisjon med Y-akse
+ koordinaterPaVent.push([periodeStart.subtract(1, 'months').startOf('month').toDate(), 0]);
+ let dato = dayjs(periodeStart);
+ do {
+ koordinaterPaVent.push(lagKoordinatForDato(dato, oppgaverPaVent));
+ koordinaterIkkePaVent.push(lagKoordinatForDato(dato, oppgaverIkkePaVent));
+ dato = dayjs(dato.add(1, 'month'));
+ } while (dato.isBefore(periodeSlutt));
+
+ koordinaterPaVent.push([periodeSlutt.toDate(), oppgaverPaVent.find(d => d.x === UKJENT_DATO)?.y ?? 0]);
+ koordinaterIkkePaVent.push([periodeSlutt.toDate(), oppgaverIkkePaVent.find(d => d.x === UKJENT_DATO)?.y ?? 0]);
+
+ return {
+ koordinaterPaVent,
+ koordinaterIkkePaVent,
+ };
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.spec.tsx b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.spec.tsx
new file mode 100644
index 00000000000..66d28c92bd4
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.spec.tsx
@@ -0,0 +1,24 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './OppgaverSomErApneEllerPaVentPanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('OppgaverSomErApneEllerPaVentPanel', () => {
+ // TODO echarts-testing
+ it.skip('skal vise graffilter', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(
+ await screen.findByText('Åpne behandlinger foreldrepenger fordelt på første uttaksdag fra søknad'),
+ ).toBeInTheDocument();
+
+ expect(getByLabelText('Førstegangsbehandling')).toBeChecked();
+ expect(getByLabelText('Klage')).toBeChecked();
+ expect(getByLabelText('Revurdering')).toBeChecked();
+ expect(getByLabelText('Innsyn')).toBeChecked();
+ expect(getByLabelText('Anke')).toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.stories.tsx b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.stories.tsx
new file mode 100644
index 00000000000..ec6b30dd87a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.stories.tsx
@@ -0,0 +1,97 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../data/fplosAvdelingslederApi';
+import { BehandlingVenteStatus } from '../../kodeverk/behandlingVenteStatus';
+import { OppgaverSomErApneEllerPaVentPanel } from './OppgaverSomErApneEllerPaVentPanel';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const OPPGAVER_ÅPNE_ELLER_PÅ_VENT = [
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.PA_VENT,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ førsteUttakMåned: dayjs().startOf('month').format(ISO_DATE_FORMAT),
+ antall: 2,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.IKKE_PA_VENT,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ førsteUttakMåned: dayjs().startOf('month').format(ISO_DATE_FORMAT),
+ antall: 5,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.IKKE_PA_VENT,
+ behandlingType: BehandlingType.REVURDERING,
+ førsteUttakMåned: dayjs().startOf('month').subtract(4, 'M').format(ISO_DATE_FORMAT),
+ antall: 2,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.IKKE_PA_VENT,
+ behandlingType: BehandlingType.KLAGE,
+ antall: 2,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.PA_VENT,
+ behandlingType: BehandlingType.KLAGE,
+ antall: 6,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.PA_VENT,
+ behandlingType: BehandlingType.REVURDERING,
+ førsteUttakMåned: dayjs().startOf('month').subtract(4, 'M').format(ISO_DATE_FORMAT),
+ antall: 6,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.PA_VENT,
+ behandlingType: BehandlingType.DOKUMENTINNSYN,
+ førsteUttakMåned: dayjs().startOf('month').subtract(10, 'M').format(ISO_DATE_FORMAT),
+ antall: 3,
+ },
+ {
+ behandlingVenteStatus: BehandlingVenteStatus.IKKE_PA_VENT,
+ behandlingType: BehandlingType.DOKUMENTINNSYN,
+ førsteUttakMåned: dayjs().startOf('month').subtract(10, 'M').format(ISO_DATE_FORMAT),
+ antall: 5,
+ },
+];
+
+const meta = {
+ title: 'los/avdelingsleder/nokkeltall/OppgaverSomErApneEllerPaVentPanel',
+ component: OppgaverSomErApneEllerPaVentPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.HENT_OPPGAVER_APNE_ELLER_PA_VENT.replaceAll('ø', '%C3%B8').replaceAll('å', '%C3%A5'), () =>
+ HttpResponse.json(OPPGAVER_ÅPNE_ELLER_PÅ_VENT),
+ ),
+ ],
+ },
+ },
+ args: {
+ height: 300,
+ valgtAvdelingEnhet: '1',
+ getValueFromLocalStorage: () => '',
+ },
+ render: props => {
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+ return alleKodeverk ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.tsx b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.tsx
new file mode 100644
index 00000000000..c9e8a509acd
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/apneOgPaVentBehandlinger/OppgaverSomErApneEllerPaVentPanel.tsx
@@ -0,0 +1,68 @@
+import { useForm } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { HStack, Label, VStack } from '@navikt/ds-react';
+import { RhfCheckbox, RhfForm } from '@navikt/ft-form-hooks';
+import { useQuery } from '@tanstack/react-query';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+
+import { oppgaverÅpneEllerPåVentOptions } from '../../data/fplosAvdelingslederApi';
+import { StoreValuesInLocalStorage } from '../../data/StoreValuesInLocalStorage';
+import { useLosKodeverk } from '../../data/useLosKodeverk';
+import { OppgaverSomErApneEllerPaVentGraf } from './OppgaverSomErApneEllerPaVentGraf';
+
+const formName = 'oppgaverSomErApneEllerPaVent';
+
+interface Props {
+ height: number;
+ valgtAvdelingEnhet: string;
+ getValueFromLocalStorage: (key: string) => string | undefined;
+}
+
+export const OppgaverSomErApneEllerPaVentPanel = ({ height, valgtAvdelingEnhet, getValueFromLocalStorage }: Props) => {
+ const { data: oppgaverApneEllerPaVent } = useQuery(oppgaverÅpneEllerPåVentOptions(valgtAvdelingEnhet));
+
+ const behandlingTyper = useLosKodeverk('BehandlingType');
+ const stringFromStorage = getValueFromLocalStorage(formName);
+ const lagredeVerdier = stringFromStorage ? JSON.parse(stringFromStorage) : undefined;
+
+ const filtrerteBehandlingstyper = behandlingTyper.filter(
+ type => type.kode !== BehandlingType.TILBAKEKREVING && type.kode !== BehandlingType.TILBAKEKREVING_REVURDERING,
+ );
+
+ const formDefaultValues = Object.values(filtrerteBehandlingstyper).reduce(
+ (app, type) => ({
+ ...app,
+ [type.kode]: true,
+ }),
+ {},
+ );
+
+ // TODO (TOR) Mangler typing for useForm
+ const formMethods = useForm({
+ defaultValues: lagredeVerdier ?? formDefaultValues,
+ });
+
+ const values = formMethods.watch();
+
+ return (
+
+
+
+
+
+ {filtrerteBehandlingstyper.map(type => (
+
+ ))}
+
+ values[oav.behandlingType])}
+ />
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypeGraf.tsx b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypeGraf.tsx
new file mode 100644
index 00000000000..a19a9e9b04c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypeGraf.tsx
@@ -0,0 +1,117 @@
+import { useIntl } from 'react-intl';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+import { ReactECharts } from '@navikt/fp-los-felles';
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+import type { OppgaverForAvdeling } from '../../typer/oppgaverForAvdelingTsType';
+
+const behandlingstypeOrder = [
+ BehandlingType.TILBAKEKREVING_REVURDERING,
+ BehandlingType.TILBAKEKREVING,
+ BehandlingType.DOKUMENTINNSYN,
+ BehandlingType.KLAGE,
+ BehandlingType.REVURDERING,
+ BehandlingType.FORSTEGANGSSOKNAD,
+];
+
+interface Props {
+ height: number;
+ behandlingTyper: LosKodeverkMedNavn<'BehandlingType'>[];
+ oppgaverForAvdeling: OppgaverForAvdeling[];
+}
+
+export const FordelingAvBehandlingstypeGraf = ({ height, oppgaverForAvdeling, behandlingTyper }: Props) => {
+ const intl = useIntl();
+ const tilBehandlingTekst = intl.formatMessage({ id: 'FordelingAvBehandlingstypeGraf.TilBehandling' });
+ const tilBeslutterTekst = intl.formatMessage({ id: 'FordelingAvBehandlingstypeGraf.TilBeslutter' });
+
+ const finnBehandlingTypeNavn = behandlingstypeOrder.map(t => {
+ const type = behandlingTyper.find(bt => bt.kode === t);
+ return type ? type.navn : '';
+ });
+
+ const tilBehandlingData = slåSammen(oppgaverForAvdeling.filter(o => o.tilBehandling));
+ const tilBeslutterData = slåSammen(oppgaverForAvdeling.filter(o => !o.tilBehandling));
+
+ return (
+
+ );
+};
+
+const slåSammen = (oppgaverForAvdeling: OppgaverForAvdeling[]): number[] => {
+ const test = oppgaverForAvdeling.reduce(
+ (acc, o) => {
+ const index = behandlingstypeOrder.findIndex(bo => bo === o.behandlingType) + 1;
+ return {
+ ...acc,
+ [index]: acc[index] ? acc[index] + o.antall : o.antall,
+ };
+ },
+ {} as Record,
+ );
+
+ return behandlingstypeOrder.map((_b, index) => test[index + 1]);
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.spec.tsx b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.spec.tsx
new file mode 100644
index 00000000000..89f041fe494
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.spec.tsx
@@ -0,0 +1,21 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './FordelingAvBehandlingstypePanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('FordelingAvBehandlingstypePanel', () => {
+ // TODO echarts-testing
+ it.skip('skal vise graffilter', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Antall åpne oppgaver nå')).toBeInTheDocument();
+
+ expect(getByLabelText('Foreldrepenger')).not.toBeChecked();
+ expect(getByLabelText('Engangsstønad')).not.toBeChecked();
+ expect(getByLabelText('Svangerskapspenger')).not.toBeChecked();
+ expect(getByLabelText('Alle')).toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.stories.tsx b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.stories.tsx
new file mode 100644
index 00000000000..cd928b27835
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.stories.tsx
@@ -0,0 +1,78 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType, FagsakYtelseType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../data/fplosAvdelingslederApi';
+import { FordelingAvBehandlingstypePanel } from './FordelingAvBehandlingstypePanel';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const OPPGAVER_FOR_AVDELING = [
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ tilBehandling: true,
+ antall: 10,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.ENGANGSSTONAD,
+ behandlingType: BehandlingType.KLAGE,
+ tilBehandling: true,
+ antall: 4,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.ENGANGSSTONAD,
+ behandlingType: BehandlingType.REVURDERING,
+ tilBehandling: true,
+ antall: 14,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.ENGANGSSTONAD,
+ behandlingType: BehandlingType.REVURDERING,
+ tilBehandling: false,
+ antall: 4,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.TILBAKEKREVING,
+ tilBehandling: false,
+ antall: 6,
+ },
+];
+
+const meta = {
+ title: 'los/avdelingsleder/nokkeltall/FordelingAvBehandlingstypePanel',
+ component: FordelingAvBehandlingstypePanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.HENT_OPPGAVER_FOR_AVDELING.replace('ø', '%C3%B8'), () =>
+ HttpResponse.json(OPPGAVER_FOR_AVDELING),
+ ),
+ ],
+ },
+ },
+ args: {
+ height: 300,
+ valgtAvdelingEnhet: '1',
+ getValueFromLocalStorage: () => '',
+ },
+ render: props => {
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+ return alleKodeverk ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.tsx b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.tsx
new file mode 100644
index 00000000000..6196094795c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/fordelingAvBehandlingstype/FordelingAvBehandlingstypePanel.tsx
@@ -0,0 +1,103 @@
+import { useForm } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { Label, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfRadioGroup } from '@navikt/ft-form-hooks';
+import { useQuery } from '@tanstack/react-query';
+
+import { FagsakYtelseType } from '@navikt/fp-kodeverk';
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+import { oppgaverForAvdelingOptions } from '../../data/fplosAvdelingslederApi';
+import { StoreValuesInLocalStorage } from '../../data/StoreValuesInLocalStorage';
+import { useLosKodeverk } from '../../data/useLosKodeverk';
+import { FordelingAvBehandlingstypeGraf } from './FordelingAvBehandlingstypeGraf';
+
+const finnFagsakYtelseTypeNavn = (
+ fagsakYtelseTyper: LosKodeverkMedNavn<'FagsakYtelseType'>[],
+ valgtFagsakYtelseType: string,
+) => {
+ const type = fagsakYtelseTyper.find(fyt => fyt.kode === valgtFagsakYtelseType);
+ return type ? type.navn : '';
+};
+
+const ALLE_YTELSETYPER_VALGT = 'ALLE';
+
+interface InitialValues {
+ valgtYtelseType: string;
+}
+
+type FormValues = {
+ valgtYtelseType: string;
+};
+
+interface Props {
+ height: number;
+ valgtAvdelingEnhet: string;
+ getValueFromLocalStorage: (key: string) => string | undefined;
+}
+
+const formName = 'fordelingAvBehandlingstype';
+const formDefaultValues: InitialValues = { valgtYtelseType: ALLE_YTELSETYPER_VALGT };
+
+export const FordelingAvBehandlingstypePanel = ({ height, valgtAvdelingEnhet, getValueFromLocalStorage }: Props) => {
+ const { data: oppgaverForAvdeling } = useQuery(oppgaverForAvdelingOptions(valgtAvdelingEnhet));
+
+ const fagsakYtelseTyper = useLosKodeverk('FagsakYtelseType');
+ const behandlingTyper = useLosKodeverk('BehandlingType');
+ const stringFromStorage = getValueFromLocalStorage(formName);
+ const lagredeVerdier = stringFromStorage ? JSON.parse(stringFromStorage) : undefined;
+
+ const formMethods = useForm({
+ defaultValues: lagredeVerdier ?? formDefaultValues,
+ });
+
+ const values = formMethods.watch();
+
+ return (
+
+
+
+
+ ,
+ },
+ ]}
+ />
+
+ values.valgtYtelseType === ALLE_YTELSETYPER_VALGT
+ ? true
+ : values.valgtYtelseType === ofa.fagsakYtelseType,
+ )
+ : []
+ }
+ />
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingGraf.tsx b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingGraf.tsx
new file mode 100644
index 00000000000..276682f33b6
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingGraf.tsx
@@ -0,0 +1,187 @@
+import { DDMMYYYY_DATE_FORMAT, ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import dayjs from 'dayjs';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+import { ReactECharts } from '@navikt/fp-los-felles';
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+const behandlingstypeOrder = [
+ BehandlingType.TILBAKEKREVING_REVURDERING,
+ BehandlingType.TILBAKEKREVING,
+ BehandlingType.DOKUMENTINNSYN,
+ BehandlingType.KLAGE,
+ BehandlingType.REVURDERING,
+ BehandlingType.FORSTEGANGSSOKNAD,
+];
+
+const behandlingstypeFarger = {
+ [BehandlingType.TILBAKEKREVING_REVURDERING]: '#ef5d28',
+ [BehandlingType.TILBAKEKREVING]: '#ff842f',
+ [BehandlingType.DOKUMENTINNSYN]: '#ffd23b',
+ [BehandlingType.KLAGE]: '#826ba1',
+ [BehandlingType.REVURDERING]: '#3385d1',
+ [BehandlingType.FORSTEGANGSSOKNAD]: '#85d5f0',
+ [BehandlingType.ANKE]: '#85d5f0',
+};
+
+export interface OppgaveForDatoGraf {
+ behandlingType: string;
+ opprettetDato: string;
+ antall: number;
+}
+
+type Koordinat = {
+ x: Date;
+ y: number;
+};
+
+const keysFromObject = (object: T): (keyof T)[] => {
+ return Object.keys(object) as (keyof T)[];
+};
+
+interface Props {
+ height: number;
+ behandlingTyper: LosKodeverkMedNavn<'BehandlingType'>[];
+ oppgaverPerDato: OppgaveForDatoGraf[];
+ isToUkerValgt: boolean;
+}
+
+export const TilBehandlingGraf = ({ height, oppgaverPerDato, isToUkerValgt, behandlingTyper }: Props) => {
+ const periodeStart = dayjs()
+ .subtract(isToUkerValgt ? 2 : 4, 'w')
+ .add(1, 'd');
+ const periodeSlutt = dayjs();
+
+ const koordinater = konverterTilKoordinaterGruppertPaBehandlingstype(oppgaverPerDato);
+ const data = fyllInnManglendeDatoerOgSorterEtterDato(koordinater, periodeStart, periodeSlutt);
+
+ const alleBehandlingstyperSortert = behandlingTyper.map(bt => bt.kode).sort(sorterBehandlingtyper);
+ const sorterteBehandlingstyper = keysFromObject(data).sort(sorterBehandlingtyper);
+ const reversertSorterteBehandlingstyper = sorterteBehandlingstyper.slice().reverse();
+ const farger = alleBehandlingstyperSortert.map(bt => behandlingstypeFarger[bt]);
+
+ return (
+ {
+ if (params.axisDimension === 'y') {
+ return parseInt(params.value as string, 10).toString();
+ }
+ return dayjs(params.value).format(DDMMYYYY_DATE_FORMAT);
+ },
+ },
+ },
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: {
+ title: 'Lagre ',
+ name: 'Antall_til_behandling',
+ },
+ },
+ },
+ legend: {
+ data: reversertSorterteBehandlingstyper.map(type => finnBehandlingTypeNavn(behandlingTyper, type)),
+ top: 'top',
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true,
+ },
+ xAxis: [
+ {
+ type: 'time',
+ axisLabel: {
+ formatter: '{dd}.{MM}.{yyyy}',
+ },
+ },
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ },
+ ],
+ series: alleBehandlingstyperSortert.map(type => ({
+ name: finnBehandlingTypeNavn(behandlingTyper, type),
+ type: 'line',
+ stack: 'stackname',
+ areaStyle: {},
+ emphasis: {
+ focus: 'series',
+ },
+ data: data[type],
+ })),
+ color: farger,
+ }}
+ />
+ );
+};
+
+const sorterBehandlingtyper = (b1: BehandlingType, b2: BehandlingType): number => {
+ const index1 = behandlingstypeOrder.findIndex(bo => bo === b1);
+ const index2 = behandlingstypeOrder.findIndex(bo => bo === b2);
+ if (index1 === index2) {
+ return 0;
+ }
+ return index1 > index2 ? -1 : 1;
+};
+
+const finnBehandlingTypeNavn = (
+ behandlingTyper: LosKodeverkMedNavn<'BehandlingType'>[],
+ behandlingTypeKode: BehandlingType,
+): string => {
+ const type = behandlingTyper.find(bt => bt.kode === behandlingTypeKode);
+ return type ? type.navn : '';
+};
+
+const konverterTilKoordinaterGruppertPaBehandlingstype = (
+ oppgaverForAvdeling: OppgaveForDatoGraf[],
+): Record =>
+ oppgaverForAvdeling.reduce(
+ (acc, o) => {
+ const nyKoordinat = {
+ x: dayjs(o.opprettetDato).startOf('day').toDate(),
+ y: o.antall,
+ };
+
+ const eksisterendeKoordinater = acc[o.behandlingType];
+ return {
+ ...acc,
+ [o.behandlingType]: eksisterendeKoordinater ? eksisterendeKoordinater.concat(nyKoordinat) : [nyKoordinat],
+ };
+ },
+ {} as Record,
+ );
+
+const fyllInnManglendeDatoerOgSorterEtterDato = (
+ data: Record,
+ periodeStart: dayjs.Dayjs,
+ periodeSlutt: dayjs.Dayjs,
+): Record =>
+ keysFromObject(data).reduce(
+ (acc, behandlingstype) => {
+ const behandlingstypeData = data[behandlingstype];
+ const koordinater = [];
+
+ for (let dato = dayjs(periodeStart); dato.isSameOrBefore(periodeSlutt); dato = dato.add(1, 'days')) {
+ const funnetDato = behandlingstypeData.find(d => dayjs(d.x).startOf('day').isSame(dato.startOf('day')));
+ koordinater.push(
+ funnetDato ? [dayjs(funnetDato.x).format(ISO_DATE_FORMAT), funnetDato.y] : [dato.format(ISO_DATE_FORMAT), 0],
+ );
+ }
+
+ return {
+ ...acc,
+ [behandlingstype]: koordinater,
+ };
+ },
+ {} as Record,
+ );
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.spec.tsx b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.spec.tsx
new file mode 100644
index 00000000000..473c323fd93
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.spec.tsx
@@ -0,0 +1,24 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './TilBehandlingPanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('TilBehandlingPanel', () => {
+ // TODO echarts-testing
+ it.skip('skal vise graffilter', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const { getByLabelText } = render();
+ expect(await screen.findByText('Antall åpne oppgaver pr dato')).toBeInTheDocument();
+
+ expect((screen.getByText('2 siste uker') as HTMLOptionElement).selected).toBeTruthy();
+ expect((screen.getByText('4 siste uker') as HTMLOptionElement).selected).toBeFalsy();
+
+ expect(getByLabelText('Foreldrepenger')).not.toBeChecked();
+ expect(getByLabelText('Engangsstønad')).not.toBeChecked();
+ expect(getByLabelText('Svangerskapspenger')).not.toBeChecked();
+ expect(getByLabelText('Alle')).toBeChecked();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.stories.tsx b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.stories.tsx
new file mode 100644
index 00000000000..b782c5d700c
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.stories.tsx
@@ -0,0 +1,84 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType, FagsakYtelseType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../data/fplosAvdelingslederApi';
+import { TilBehandlingPanel } from './TilBehandlingPanel';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const OPPGAVER_PER_DATO = [
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ opprettetDato: dayjs().format(ISO_DATE_FORMAT),
+ antall: 1,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ opprettetDato: dayjs().subtract(3, 'd').format(ISO_DATE_FORMAT),
+ antall: 2,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.KLAGE,
+ opprettetDato: dayjs().subtract(4, 'd').format(ISO_DATE_FORMAT),
+ antall: 2,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ opprettetDato: dayjs().subtract(4, 'd').format(ISO_DATE_FORMAT),
+ antall: 6,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.DOKUMENTINNSYN,
+ opprettetDato: dayjs().subtract(10, 'd').format(ISO_DATE_FORMAT),
+ antall: 3,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingType: BehandlingType.DOKUMENTINNSYN,
+ opprettetDato: dayjs().subtract(16, 'd').format(ISO_DATE_FORMAT),
+ antall: 3,
+ },
+];
+
+const meta = {
+ title: 'los/avdelingsleder/nokkeltall/TilBehandlingPanel',
+ component: TilBehandlingPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.HENT_OPPGAVER_PER_DATO.replace('ø', '%C3%B8'), () => HttpResponse.json(OPPGAVER_PER_DATO)),
+ ],
+ },
+ },
+ args: {
+ height: 300,
+ valgtAvdelingEnhet: '1',
+ getValueFromLocalStorage: () => '',
+ },
+ render: props => {
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+ return alleKodeverk ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.tsx b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.tsx
new file mode 100644
index 00000000000..5276097a263
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/nokkeltall/tilBehandling/TilBehandlingPanel.tsx
@@ -0,0 +1,166 @@
+import { useForm } from 'react-hook-form';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { HStack, Label, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfRadioGroup, RhfSelect } from '@navikt/ft-form-hooks';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
+import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
+
+import { FagsakYtelseType } from '@navikt/fp-kodeverk';
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+import { oppgaverPerDatoOptions } from '../../data/fplosAvdelingslederApi';
+import { StoreValuesInLocalStorage } from '../../data/StoreValuesInLocalStorage';
+import { useLosKodeverk } from '../../data/useLosKodeverk';
+import type { OppgaveForDato } from '../../typer/oppgaverForDatoTsType';
+import { type OppgaveForDatoGraf, TilBehandlingGraf } from './TilBehandlingGraf';
+
+dayjs.extend(isSameOrAfter);
+dayjs.extend(isSameOrBefore);
+
+const ALLE_YTELSETYPER_VALGT = 'ALLE';
+const UKE_2 = '2';
+
+type FormValues = {
+ ukevalg: string;
+ ytelseType: string;
+};
+
+const formName = 'tilBehandlingForm';
+const formDefaultValues = { ytelseType: ALLE_YTELSETYPER_VALGT, ukevalg: UKE_2 };
+
+interface Props {
+ height: number;
+ valgtAvdelingEnhet: string;
+ getValueFromLocalStorage: (key: string) => string | undefined;
+}
+
+export const TilBehandlingPanel = ({ height, valgtAvdelingEnhet, getValueFromLocalStorage }: Props) => {
+ const intl = useIntl();
+
+ const { data: oppgaverPerDato } = useQuery(oppgaverPerDatoOptions(valgtAvdelingEnhet));
+
+ const behandlingTyper = useLosKodeverk('BehandlingType');
+ const fagsakYtelseTyper = useLosKodeverk('FagsakYtelseType');
+ const stringFromStorage = getValueFromLocalStorage(formName);
+
+ const lagredeVerdier = stringFromStorage ? JSON.parse(stringFromStorage) : undefined;
+
+ const formMethods = useForm({
+ defaultValues: lagredeVerdier ?? formDefaultValues,
+ });
+
+ const values = formMethods.watch();
+
+ return (
+ formMethods={formMethods}>
+
+
+
+
+ (
+
+ ))}
+ />
+ ,
+ },
+ ]}
+ />
+
+
+ values.ytelseType === ALLE_YTELSETYPER_VALGT ? true : values.ytelseType === ofa.fagsakYtelseType,
+ )
+ .filter(ofa => erDatoInnenforPeriode(ofa, values.ukevalg)),
+ )
+ : []
+ }
+ />
+
+
+ );
+};
+
+const uker = [
+ {
+ kode: UKE_2,
+ tekstKode: 'TilBehandlingPanel.ToSisteUker',
+ },
+ {
+ kode: '4',
+ tekstKode: 'TilBehandlingPanel.FireSisteUker',
+ },
+];
+
+const erDatoInnenforPeriode = (oppgaveForAvdeling: OppgaveForDato, ukevalg: string): boolean => {
+ if (ukevalg === uker[1].kode) {
+ return true;
+ }
+ const toUkerSiden = dayjs().subtract(2, 'w');
+ return dayjs(oppgaveForAvdeling.opprettetDato).isSameOrAfter(toUkerSiden);
+};
+
+const finnFagsakYtelseTypeNavn = (
+ fagsakYtelseTyper: LosKodeverkMedNavn<'FagsakYtelseType'>[],
+ valgtFagsakYtelseType: string,
+): string => {
+ const type = fagsakYtelseTyper.find(fyt => fyt.kode === valgtFagsakYtelseType);
+ return type ? type.navn : '';
+};
+
+const slaSammenLikeBehandlingstyperOgDatoer = (oppgaverForAvdeling: OppgaveForDato[]): OppgaveForDatoGraf[] => {
+ const sammenslatte: OppgaveForDatoGraf[] = [];
+
+ oppgaverForAvdeling.forEach(o => {
+ const index = sammenslatte.findIndex(
+ s => s.behandlingType === o.behandlingType && s.opprettetDato === o.opprettetDato,
+ );
+ if (index === -1) {
+ sammenslatte.push(o);
+ } else {
+ sammenslatte[index] = {
+ behandlingType: sammenslatte[index].behandlingType,
+ opprettetDato: sammenslatte[index].opprettetDato,
+ antall: sammenslatte[index].antall + o.antall,
+ };
+ }
+ });
+
+ return sammenslatte;
+};
diff --git "a/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perGraf.tsx" "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perGraf.tsx"
new file mode 100644
index 00000000000..97d8b8af3b6
--- /dev/null
+++ "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perGraf.tsx"
@@ -0,0 +1,101 @@
+import { DDMMYYYY_DATE_FORMAT } from '@navikt/ft-utils';
+import dayjs from 'dayjs';
+import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
+
+import { ReactECharts } from '@navikt/fp-los-felles';
+
+import type { BehandlingVentefrist } from '../../typer/behandlingVentefristTsType';
+
+dayjs.extend(isSameOrBefore);
+
+interface Koordinat {
+ x: number;
+ y: number;
+}
+
+interface Props {
+ height: number;
+ behandlingerPaVent: BehandlingVentefrist[];
+}
+
+export const VentefristUtløperGraf = ({ height, behandlingerPaVent }: Props) => {
+ const koordinater = lagKoordinater(behandlingerPaVent);
+ const data = lagDatastruktur(koordinater);
+ return (
+ {
+ if (params.axisDimension === 'y') {
+ return parseInt(params.value as string, 10).toString();
+ }
+ return dayjs(params.value).format(DDMMYYYY_DATE_FORMAT);
+ },
+ },
+ },
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: {
+ title: 'Lagre ',
+ name: 'Antall_forstegangsbehandlinger_der_frist_utloper',
+ },
+ },
+ },
+ xAxis: {
+ type: 'time',
+ axisLabel: {
+ formatter: '{dd}.{MM}.{yyyy}',
+ },
+ },
+ yAxis: {
+ type: 'value',
+ },
+ series: [
+ {
+ data,
+ type: 'line',
+ areaStyle: {},
+ },
+ ],
+ color: ['#337c9b'],
+ }}
+ />
+ );
+};
+
+const lagKoordinater = (oppgaverManueltPaVent: BehandlingVentefrist[]): Koordinat[] =>
+ oppgaverManueltPaVent.map(o => ({
+ x: dayjs(o.behandlingFrist).startOf('day').toDate().getTime(),
+ y: o.antall,
+ }));
+
+const lagDatastruktur = (koordinater: Koordinat[]): (number | Date)[][] => {
+ const nyeKoordinater = [];
+ const periodeStart = koordinater
+ .map(koordinat => dayjs(koordinat.x))
+ .reduce(
+ (tidligesteDato, dato) => (tidligesteDato.isSameOrBefore(dato) ? tidligesteDato : dato),
+ dayjs().startOf('day'),
+ )
+ .toDate();
+ const periodeSlutt = koordinater
+ .map(koordinat => dayjs(koordinat.x))
+ .reduce((senesteDato, dato) => (senesteDato.isSameOrAfter(dato) ? senesteDato : dato), dayjs().startOf('day'))
+ .toDate();
+
+ for (let dato = dayjs(periodeStart); dato.isSameOrBefore(periodeSlutt); dato = dato.add(1, 'days')) {
+ const sumY = koordinater
+ .filter(k => dayjs(k.x).isSame(dato))
+ .map(k => k.y)
+ .reduce((sum, y) => sum + y, 0);
+ nyeKoordinater.push([dato.toDate(), sumY]);
+ }
+
+ return nyeKoordinater;
+};
diff --git "a/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.spec.tsx" "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.spec.tsx"
new file mode 100644
index 00000000000..75c7959f56d
--- /dev/null
+++ "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.spec.tsx"
@@ -0,0 +1,21 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+
+import * as stories from './VentefristUtløperPanel.stories';
+
+const { Default } = composeStories(stories);
+
+describe('VentefristUtløperPanel', () => {
+ // TODO echarts-testing
+ it.skip('skal vise graffilter', async () => {
+ const { getByLabelText } = render();
+ expect(
+ await screen.findByText('Førstegangsbehandlinger på vent fordelt på utløp av ventefrist'),
+ ).toBeInTheDocument();
+
+ expect(getByLabelText('Foreldrepenger')).not.toBeChecked();
+ expect(getByLabelText('Engangsstønad')).not.toBeChecked();
+ expect(getByLabelText('Svangerskapspenger')).not.toBeChecked();
+ expect(getByLabelText('Alle')).toBeChecked();
+ });
+});
diff --git "a/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.stories.tsx" "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.stories.tsx"
new file mode 100644
index 00000000000..f8375fd4f94
--- /dev/null
+++ "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.stories.tsx"
@@ -0,0 +1,65 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import { ISO_DATE_FORMAT } from '@navikt/ft-utils';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { http, HttpResponse } from 'msw';
+
+import { FagsakYtelseType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../../data/fplosAvdelingslederApi';
+import { VentefristUtløperPanel } from './VentefristUtløperPanel';
+
+import messages from '../../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const BEHANDLINGER_PÅ_VENT = [
+ {
+ fagsakYtelseType: FagsakYtelseType.FORELDREPENGER,
+ behandlingFrist: dayjs().format(ISO_DATE_FORMAT),
+ antall: 10,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.ENGANGSSTONAD,
+ behandlingFrist: dayjs().add(5, 'd').format(ISO_DATE_FORMAT),
+ antall: 4,
+ },
+ {
+ fagsakYtelseType: FagsakYtelseType.ENGANGSSTONAD,
+ behandlingFrist: dayjs().add(5, 'w').format(ISO_DATE_FORMAT),
+ antall: 14,
+ },
+];
+
+const meta = {
+ title: 'los/avdelingsleder/nokkeltall/VentefristUtløperPanel',
+ component: VentefristUtløperPanel,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.HENT_BEHANDLINGER_FRISTUTLOP.replaceAll('ø', '%C3%B8'), () =>
+ HttpResponse.json(BEHANDLINGER_PÅ_VENT),
+ ),
+ ],
+ },
+ },
+ args: {
+ height: 300,
+ valgtAvdelingEnhet: '1',
+ getValueFromLocalStorage: () => '',
+ },
+ render: props => {
+ //Må hente data til cache før testa komponent blir kalla
+ const alleKodeverk = useQuery(losKodeverkOptions()).data;
+ return alleKodeverk ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git "a/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.tsx" "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.tsx"
new file mode 100644
index 00000000000..c3cf9f77cc2
--- /dev/null
+++ "b/apps/fp-avdelingsleder/src/nokkeltall/ventefristUtl\303\270per/VentefristUtl\303\270perPanel.tsx"
@@ -0,0 +1,94 @@
+import { useForm } from 'react-hook-form';
+import { FormattedMessage } from 'react-intl';
+
+import { HStack, Label, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfRadioGroup } from '@navikt/ft-form-hooks';
+import { useQuery } from '@tanstack/react-query';
+
+import { FagsakYtelseType } from '@navikt/fp-kodeverk';
+import type { LosKodeverkMedNavn } from '@navikt/fp-types';
+
+import { behandlingerFristUtløptOptions } from '../../data/fplosAvdelingslederApi';
+import { StoreValuesInLocalStorage } from '../../data/StoreValuesInLocalStorage';
+import { useLosKodeverk } from '../../data/useLosKodeverk';
+import { VentefristUtløperGraf } from './VentefristUtløperGraf';
+
+const finnFagsakYtelseTypeNavn = (
+ fagsakYtelseTyper: LosKodeverkMedNavn<'FagsakYtelseType'>[],
+ valgtFagsakYtelseType: string,
+): string => {
+ const type = fagsakYtelseTyper.find(fyt => fyt.kode === valgtFagsakYtelseType);
+ return type ? type.navn : '';
+};
+
+const ALLE_YTELSETYPER_VALGT = 'ALLE';
+
+const formName = 'ventefristUtløperForm';
+const formDefaultValues = { valgtYtelsetype: ALLE_YTELSETYPER_VALGT };
+
+type FormValues = {
+ valgtYtelsetype: string;
+};
+
+interface Props {
+ height: number;
+ valgtAvdelingEnhet: string;
+ getValueFromLocalStorage: (key: string) => string | undefined;
+}
+
+export const VentefristUtløperPanel = ({ height, valgtAvdelingEnhet, getValueFromLocalStorage }: Props) => {
+ const { data: behandlingerPaVent } = useQuery(behandlingerFristUtløptOptions(valgtAvdelingEnhet));
+
+ const fagsakYtelseTyper = useLosKodeverk('FagsakYtelseType');
+
+ const stringFromStorage = getValueFromLocalStorage(formName);
+ const lagredeVerdier = stringFromStorage ? JSON.parse(stringFromStorage) : undefined;
+
+ const formMethods = useForm({
+ defaultValues: lagredeVerdier ?? formDefaultValues,
+ });
+
+ const values = formMethods.watch();
+
+ return (
+ formMethods={formMethods}>
+
+
+
+
+ ,
+ },
+ ]}
+ />
+
+
+ values.valgtYtelsetype === ALLE_YTELSETYPER_VALGT ? true : values.valgtYtelsetype === ompv.fagsakYtelseType,
+ )}
+ />
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.spec.tsx b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.spec.tsx
new file mode 100644
index 00000000000..83cf97b7756
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.spec.tsx
@@ -0,0 +1,26 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './ReservasjonerTabell.stories';
+
+const { ViseAtIngenReservasjonerBleFunnet, VisTabellMedReservasjoner } = composeStories(stories);
+
+describe('ReservasjonerTabell', () => {
+ it('skal vise tekst som viser at ingen reservasjoner er lagt til', async () => {
+ await applyRequestHandlers(ViseAtIngenReservasjonerBleFunnet.parameters['msw']);
+ render();
+
+ expect(await screen.findByText('Reservasjoner for avdelingen')).toBeInTheDocument();
+ expect(await screen.findByText('Ingen reservasjoner funnet')).toBeInTheDocument();
+ });
+
+ it('skal vise to reservasjoner i tabell', async () => {
+ await applyRequestHandlers(VisTabellMedReservasjoner.parameters['msw']);
+ render();
+
+ expect(await screen.findByText('Reservasjoner for avdelingen')).toBeInTheDocument();
+ expect(await screen.findByText('Eirik Utvikler')).toBeInTheDocument();
+ expect(screen.getByText('Espen Utvikler')).toBeInTheDocument();
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.stories.tsx b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.stories.tsx
new file mode 100644
index 00000000000..718f8b61633
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.stories.tsx
@@ -0,0 +1,80 @@
+import { LoadingPanel } from '@navikt/ft-ui-komponenter';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useQuery } from '@tanstack/react-query';
+import { http, HttpResponse } from 'msw';
+
+import { BehandlingType } from '@navikt/fp-kodeverk';
+import { alleKodeverkLos, getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { losKodeverkOptions, LosUrl } from '../data/fplosAvdelingslederApi';
+import { ReservasjonerTabell } from './ReservasjonerTabell';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/reservasjoner/ReservasjonerTabell',
+ component: ReservasjonerTabell,
+ decorators: [withIntl, withQueryClient],
+
+ args: {
+ valgtAvdelingEnhet: '1',
+ },
+ render: args => {
+ const { data: kodeverkLos } = useQuery(losKodeverkOptions());
+ return kodeverkLos ? : ;
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const ViseAtIngenReservasjonerBleFunnet: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.RESERVASJONER_FOR_AVDELING, () => HttpResponse.json([])),
+ http.post(LosUrl.AVDELINGSLEDER_OPPHEVER_RESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.ENDRE_OPPGAVERESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.FLYTT_RESERVASJON_SAKSBEHANDLER_SOK, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.FLYTT_RESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+};
+
+export const VisTabellMedReservasjoner: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.get(LosUrl.KODEVERK_LOS, () => HttpResponse.json(alleKodeverkLos)),
+ http.get(LosUrl.RESERVASJONER_FOR_AVDELING, () =>
+ HttpResponse.json([
+ {
+ reservertAvUid: 'wsfwer-sdsfd',
+ reservertAvNavn: 'Espen Utvikler',
+ reservertTilTidspunkt: '2020-01-10',
+ oppgaveId: 1,
+ oppgaveSaksNr: '122234',
+ behandlingType: BehandlingType.FORSTEGANGSSOKNAD,
+ },
+ {
+ reservertAvUid: 'gtfbrt-tbrtb',
+ reservertAvNavn: 'Eirik Utvikler',
+ reservertTilTidspunkt: '2020-01-01',
+ oppgaveId: 2,
+ oppgaveSaksNr: '23454',
+ behandlingType: BehandlingType.KLAGE,
+ },
+ ]),
+ ),
+ http.post(LosUrl.AVDELINGSLEDER_OPPHEVER_RESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.ENDRE_OPPGAVERESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.FLYTT_RESERVASJON_SAKSBEHANDLER_SOK, () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.FLYTT_RESERVASJON, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.tsx b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.tsx
new file mode 100644
index 00000000000..608d4e37d29
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/reservasjoner/ReservasjonerTabell.tsx
@@ -0,0 +1,184 @@
+import { useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+
+import { CalendarIcon, PersonGroupIcon, XMarkIcon } from '@navikt/aksel-icons';
+import { BodyShort, Label, Table, VStack } from '@navikt/ds-react';
+import { getDateAndTime } from '@navikt/ft-utils';
+import { useMutation, useQuery } from '@tanstack/react-query';
+
+import { FlyttReservasjonModal, OppgaveReservasjonEndringDatoModal } from '@navikt/fp-los-felles';
+
+import {
+ endreReservasjon,
+ flyttReservasjon,
+ flyttReservasjonSaksbehandlerSøk,
+ opphevReservasjon,
+ reservasjonerForAvdelingOptions,
+} from '../data/fplosAvdelingslederApi';
+import { useLosKodeverk } from '../data/useLosKodeverk';
+import type { Reservasjon } from '../typer/reservasjonTsType';
+
+import styles from './reservasjonerTabell.module.css';
+
+interface Props {
+ valgtAvdelingEnhet: string;
+}
+
+export const ReservasjonerTabell = ({ valgtAvdelingEnhet }: Props) => {
+ const [showReservasjonEndringDatoModal, setShowReservasjonEndringDatoModal] = useState(false);
+ const [showFlyttReservasjonModal, setShowFlyttReservasjonModal] = useState(false);
+ const [valgtReservasjon, setValgtReservasjon] = useState(undefined);
+
+ const behandlingTyper = useLosKodeverk('BehandlingType');
+
+ const { data: reservasjoner, refetch: hentAvdelingensReservasjoner } = useQuery(
+ reservasjonerForAvdelingOptions(valgtAvdelingEnhet),
+ );
+
+ const sorterteReservasjoner = reservasjoner.toSorted((reservasjon1, reservasjon2) =>
+ reservasjon1.reservertAvNavn.localeCompare(reservasjon2.reservertAvNavn),
+ );
+
+ const { mutate: opphevOppgaveReservasjon } = useMutation({
+ mutationFn: (valuesToStore: { oppgaveId: number }) => opphevReservasjon(valuesToStore.oppgaveId),
+ onSuccess: () => hentAvdelingensReservasjoner(),
+ });
+
+ const {
+ mutateAsync: hentSaksbehandler,
+ data: saksbehandler,
+ status: hentSaksbehandlerStatus,
+ reset: resetHentSaksbehandler,
+ } = useMutation({
+ mutationFn: (valuesToStore: { brukerIdent: string }) => flyttReservasjonSaksbehandlerSøk(valuesToStore.brukerIdent),
+ });
+
+ const showReservasjonEndringDato = (reservasjon: Reservasjon): void => {
+ setShowReservasjonEndringDatoModal(true);
+ setValgtReservasjon(reservasjon);
+ };
+
+ const showFlytteModal = (reservasjon: Reservasjon): void => {
+ setShowFlyttReservasjonModal(true);
+ setValgtReservasjon(reservasjon);
+ };
+
+ const { mutateAsync: endreOppgavereservasjonRequest } = useMutation({
+ mutationFn: (valuesToStore: { oppgaveId: number; reserverTil: string }) =>
+ endreReservasjon(valuesToStore.oppgaveId, valuesToStore.reserverTil),
+ });
+
+ const endreOppgavereservasjon = async (reserverTil: string) => {
+ if (!valgtReservasjon) {
+ throw new Error('Reservasjon må være valgt');
+ }
+ await endreOppgavereservasjonRequest({ oppgaveId: valgtReservasjon.oppgaveId, reserverTil });
+ setShowReservasjonEndringDatoModal(false);
+ hentAvdelingensReservasjoner();
+ };
+
+ const { mutateAsync: flyttOppgavereservasjonRequest } = useMutation({
+ mutationFn: (valuesToStore: { oppgaveId: number; brukerIdent: string; begrunnelse: string }) =>
+ flyttReservasjon(valuesToStore.oppgaveId, valuesToStore.brukerIdent, valuesToStore.begrunnelse),
+ });
+
+ const flyttOppgavereservasjon = async (params: { brukerIdent: string; begrunnelse: string }) => {
+ if (!valgtReservasjon) {
+ throw new Error('Reservasjon må være valgt');
+ }
+ await flyttOppgavereservasjonRequest({ oppgaveId: valgtReservasjon.oppgaveId, ...params });
+ hentAvdelingensReservasjoner();
+ };
+
+ return (
+
+
+ {sorterteReservasjoner.length === 0 && (
+
+
+
+ )}
+ {sorterteReservasjoner.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {sorterteReservasjoner.map(reservasjon => (
+
+ {reservasjon.reservertAvNavn}
+ {reservasjon.oppgaveSaksNr}
+
+ {behandlingTyper.find(t => t.kode === reservasjon.behandlingType)?.navn}
+
+
+
+
+
+ showReservasjonEndringDato(reservasjon)}
+ />
+
+
+ showFlytteModal(reservasjon)} />
+
+
+ opphevOppgaveReservasjon({ oppgaveId: reservasjon.oppgaveId })}
+ />
+
+
+ ))}
+
+
+ )}
+ {valgtReservasjon && showReservasjonEndringDatoModal && (
+ setShowReservasjonEndringDatoModal(false)}
+ reserverTilDefault={valgtReservasjon.reservertTilTidspunkt}
+ endreOppgavereservasjon={endreOppgavereservasjon}
+ />
+ )}
+ {valgtReservasjon && showFlyttReservasjonModal && (
+ setShowFlyttReservasjonModal(false)}
+ flyttOppgavereservasjon={flyttOppgavereservasjon}
+ hentSaksbehandler={(brukerIdent: string) => hentSaksbehandler({ brukerIdent })}
+ hentSaksbehandlerIsPending={hentSaksbehandlerStatus === 'pending'}
+ hentSaksbehandlerIsSuccess={hentSaksbehandlerStatus === 'success'}
+ saksbehandler={saksbehandler}
+ resetHentSaksbehandler={resetHentSaksbehandler}
+ />
+ )}
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/reservasjoner/reservasjonerTabell.module.css b/apps/fp-avdelingsleder/src/reservasjoner/reservasjonerTabell.module.css
new file mode 100644
index 00000000000..4378fd1a8b2
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/reservasjoner/reservasjonerTabell.module.css
@@ -0,0 +1,19 @@
+.removeIcon {
+ color: var(--ax-danger-500);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
+.calendarIcon {
+ color: var(--ax-accent-700);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
+
+.flyttIcon {
+ color: var(--ax-accent-700);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.spec.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.spec.tsx
new file mode 100644
index 00000000000..ad95a0c6615
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.spec.tsx
@@ -0,0 +1,47 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './LeggTilSaksbehandlerForm.stories';
+
+const { Default, SaksbehandlerFinnesIkke } = composeStories(stories);
+
+describe('LeggTilSaksbehandlerForm', () => {
+ it('skal vise at oppgitt brukerident ikke finnes', async () => {
+ await applyRequestHandlers(SaksbehandlerFinnesIkke.parameters['msw']);
+ const utils = render();
+
+ expect(await screen.findByText('Legg til saksbehandler')).toBeInTheDocument();
+
+ const brukerIdentInput = utils.getByLabelText('Brukerident');
+ await userEvent.type(brukerIdentInput, 'TESTIDENT');
+
+ expect(await screen.findByText('Søk')).toBeInTheDocument();
+ expect(screen.getByText('Søk')).toBeEnabled();
+
+ await userEvent.click(screen.getByText('Søk'));
+
+ expect(await screen.findByText('Kan ikke finne brukerident')).toBeInTheDocument();
+ expect(screen.getByText('Legg til i listen').closest('button')).toBeDisabled();
+ });
+
+ it('skal finne brukerident og så legge saksbehandler til listen', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ const utils = render();
+
+ expect(await screen.findByText('Legg til saksbehandler')).toBeInTheDocument();
+
+ const brukerIdentInput = utils.getByLabelText('Brukerident');
+ await userEvent.type(brukerIdentInput, 'TESTIDENT');
+
+ expect(await screen.findByText('Søk')).toBeInTheDocument();
+ expect(screen.getByText('Søk')).toBeEnabled();
+
+ await userEvent.click(screen.getByText('Søk'));
+
+ expect(await screen.findByText('Espen Utvikler')).toBeInTheDocument();
+
+ await waitFor(() => expect(screen.getByText('Legg til i listen')).toBeEnabled());
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.stories.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.stories.tsx
new file mode 100644
index 00000000000..97fa3a46664
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.stories.tsx
@@ -0,0 +1,71 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { http, HttpResponse } from 'msw';
+
+import { getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { LosUrl } from '../data/fplosAvdelingslederApi';
+import { LeggTilSaksbehandlerForm } from './LeggTilSaksbehandlerForm';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const saksbehandler = {
+ brukerIdent: 'R232323',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Avdeling Å',
+};
+
+const meta = {
+ title: 'los/avdelingsleder/saksbehandlere/LeggTilSaksbehandlerForm',
+ component: LeggTilSaksbehandlerForm,
+ decorators: [withIntl, withQueryClient],
+ args: {
+ valgtAvdelingEnhet: '1',
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(LosUrl.SAKSBEHANDLER_SOK.replace('søk', 's%C3%B8k'), () => HttpResponse.json(saksbehandler)),
+ http.post(LosUrl.OPPRETT_NY_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ avdelingensSaksbehandlere: [],
+ },
+};
+
+export const AlleredeLagtTil: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(LosUrl.SAKSBEHANDLER_SOK.replace('søk', 's%C3%B8k'), () => HttpResponse.json(saksbehandler)),
+ http.post(LosUrl.OPPRETT_NY_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ avdelingensSaksbehandlere: [saksbehandler],
+ },
+};
+
+export const SaksbehandlerFinnesIkke: Story = {
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(LosUrl.SAKSBEHANDLER_SOK.replace('søk', 's%C3%B8k'), () => new HttpResponse(null, { status: 200 })),
+ http.post(LosUrl.OPPRETT_NY_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ avdelingensSaksbehandlere: [saksbehandler],
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.tsx
new file mode 100644
index 00000000000..57407700917
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/LeggTilSaksbehandlerForm.tsx
@@ -0,0 +1,137 @@
+import { useMemo } from 'react';
+import { useForm } from 'react-hook-form';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { BodyShort, Button, HStack, Label, VStack } from '@navikt/ds-react';
+import { RhfForm, RhfTextField } from '@navikt/ft-form-hooks';
+import { required } from '@navikt/ft-form-validators';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { LosUrl, opprettNySaksbehandler, saksbehandlgerSøk } from '../data/fplosAvdelingslederApi';
+
+import styles from './leggTilSaksbehandlerForm.module.css';
+
+type FormValues = {
+ brukerIdent: string;
+};
+
+interface Props {
+ valgtAvdelingEnhet: string;
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+}
+
+export const LeggTilSaksbehandlerForm = ({ valgtAvdelingEnhet, avdelingensSaksbehandlere }: Props) => {
+ const queryClient = useQueryClient();
+ const intl = useIntl();
+
+ const formMethods = useForm();
+
+ const {
+ mutate: finnSaksbehandler,
+ data: saksbehandler,
+ status: saksbehandlerStatus,
+ reset: resetSaksbehandlerSøk,
+ } = useMutation({
+ mutationFn: (valuesToStore: { brukerIdent: string }) => saksbehandlgerSøk(valuesToStore.brukerIdent),
+ });
+
+ const { mutate: leggTilSaksbehandler, isPending } = useMutation({
+ mutationFn: (valuesToStore: { brukerIdent: string; avdelingEnhet: string }) =>
+ opprettNySaksbehandler(valuesToStore.brukerIdent, valuesToStore.avdelingEnhet),
+ onSuccess: () => {
+ resetSaksbehandlerSøk();
+ formMethods.reset();
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSBEHANDLERE_FOR_AVDELING],
+ });
+ },
+ });
+
+ const erLagtTilAllerede = avdelingensSaksbehandlere.some(
+ s => saksbehandler && s.brukerIdent.toLowerCase() === saksbehandler.brukerIdent.toLowerCase(),
+ );
+
+ const leggTilSaksbehandlerFn = () => {
+ if (saksbehandler) {
+ leggTilSaksbehandler({
+ brukerIdent: saksbehandler.brukerIdent,
+ avdelingEnhet: valgtAvdelingEnhet,
+ });
+ }
+ };
+
+ const formattedText = useMemo((): string => {
+ if (saksbehandlerStatus === 'success' && !saksbehandler) {
+ return intl.formatMessage({ id: 'LeggTilSaksbehandlerForm.FinnesIkke' });
+ }
+ if (!saksbehandler) {
+ return '';
+ }
+
+ return erLagtTilAllerede
+ ? `${saksbehandler.navn} (${intl.formatMessage({ id: 'LeggTilSaksbehandlerForm.FinnesAllerede' })})`
+ : saksbehandler.navn;
+ }, [saksbehandlerStatus, saksbehandler, erLagtTilAllerede]);
+
+ return (
+ finnSaksbehandler({ brukerIdent: values.brukerIdent })}>
+
+
+
+
+
+
+
+
+ {saksbehandlerStatus === 'success' && (
+
+ {formattedText}
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlerePanel.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlerePanel.tsx
new file mode 100644
index 00000000000..321a44f126b
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlerePanel.tsx
@@ -0,0 +1,21 @@
+import { VStack } from '@navikt/ds-react';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { LeggTilSaksbehandlerForm } from './LeggTilSaksbehandlerForm';
+import { SaksbehandlereTabell } from './SaksbehandlereTabell';
+
+interface Props {
+ avdelingensSaksbehandlere: SaksbehandlerProfil[];
+ valgtAvdelingEnhet: string;
+}
+
+export const SaksbehandlerePanel = ({ avdelingensSaksbehandlere, valgtAvdelingEnhet }: Props) => (
+
+
+
+
+);
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.spec.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.spec.tsx
new file mode 100644
index 00000000000..d55181fae26
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.spec.tsx
@@ -0,0 +1,66 @@
+import { composeStories } from '@storybook/react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { applyRequestHandlers } from 'msw-storybook-addon';
+
+import * as stories from './SaksbehandlereTabell.stories';
+
+const { Default, TomTabell, MedSaksbehandlerUtenAnsattAvdeling } = composeStories(stories);
+
+describe('SaksbehandlereTabell', () => {
+ it('skal vise to saksbehandlere i tabell', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ render();
+
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+
+ expect(screen.getByText('Navn')).toBeInTheDocument();
+ expect(screen.getByText('Espen Utvikler')).toBeInTheDocument();
+ expect(screen.getByText('Steffen')).toBeInTheDocument();
+
+ expect(screen.getByText('Brukerident')).toBeInTheDocument();
+ expect(screen.getByText('R12122')).toBeInTheDocument();
+ expect(screen.getByText('S53343')).toBeInTheDocument();
+ });
+
+ it('skal vise tekst som viser at ingen saksbehandlere er lagt til', async () => {
+ render();
+ expect(await screen.findByText('Ingen saksbehandlere lagt til')).toBeInTheDocument();
+ });
+
+ it('skal fjerne en saksbehandler ved å trykk på fjern-knappen', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ render();
+
+ expect(await screen.findByText('Navn')).toBeInTheDocument();
+
+ await userEvent.click(screen.getAllByRole('img')[1]);
+
+ expect(await screen.findByText('Ønsker du å slette Espen Utvikler?')).toBeInTheDocument();
+ });
+
+ it('skal sortere saksbehandlere etter ansattAvdeling og navn', async () => {
+ await applyRequestHandlers(Default.parameters['msw']);
+ render();
+
+ const sortedNames = ['Hildegunn', 'Espen Utvikler', 'Steffen'];
+
+ const rows = await screen.findAllByRole('row');
+
+ rows.slice(1).forEach((row, index) => {
+ expect(row).toHaveTextContent(sortedNames[index]);
+ });
+ });
+
+ it('skal sortere saksbehandlere med ansattAvdeling null sist', async () => {
+ await applyRequestHandlers(MedSaksbehandlerUtenAnsattAvdeling.parameters['msw']);
+ render();
+ const sortedNames = ['Hildegunn', 'Ukjent saksbehandler (X11111)'];
+
+ const rows = await screen.findAllByRole('row');
+
+ rows.slice(1).forEach((row, index) => {
+ expect(row).toHaveTextContent(sortedNames[index]);
+ });
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.stories.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.stories.tsx
new file mode 100644
index 00000000000..d2e0ecf1c20
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.stories.tsx
@@ -0,0 +1,76 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { http, HttpResponse } from 'msw';
+
+import { getIntlDecorator, withQueryClient } from '@navikt/fp-storybook-utils';
+
+import { LosUrl } from '../data/fplosAvdelingslederApi';
+import { SaksbehandlereTabell } from './SaksbehandlereTabell';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/saksbehandlere/SaksbehandlereTabell',
+ component: SaksbehandlereTabell,
+ decorators: [withIntl, withQueryClient],
+ parameters: {
+ msw: {
+ handlers: [
+ http.post(LosUrl.SLETT_SAKSBEHANDLER, () => new HttpResponse(null, { status: 200 })),
+ http.get(LosUrl.SAKSBEHANDLERE_FOR_AVDELING, () => new HttpResponse(null, { status: 200 })),
+ ],
+ },
+ },
+ args: {
+ valgtAvdelingEnhet: 'Nav Vikafossen',
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ saksbehandlere: [
+ {
+ brukerIdent: 'R12122',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: 'Oslo',
+ },
+ {
+ brukerIdent: 'S53343',
+ navn: 'Steffen',
+ ansattAvdeling: 'Oslo',
+ },
+ {
+ brukerIdent: 'H11111',
+ navn: 'Hildegunn',
+ ansattAvdeling: 'Drammen',
+ },
+ ],
+ },
+};
+
+export const TomTabell: Story = {
+ args: {
+ saksbehandlere: [],
+ },
+};
+
+export const MedSaksbehandlerUtenAnsattAvdeling: Story = {
+ args: {
+ saksbehandlere: [
+ {
+ brukerIdent: 'X1111',
+ navn: 'Ukjent saksbehandler (X11111)',
+ ansattAvdeling: null,
+ },
+ {
+ brukerIdent: 'H11111',
+ navn: 'Hildegunn',
+ ansattAvdeling: 'Drammen',
+ },
+ ],
+ },
+};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.tsx
new file mode 100644
index 00000000000..fc4158e324a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SaksbehandlereTabell.tsx
@@ -0,0 +1,99 @@
+import { useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+
+import { XMarkIcon } from '@navikt/aksel-icons';
+import { BodyShort, Table, VStack } from '@navikt/ds-react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import { LosUrl, slettSaksbehandler } from '../data/fplosAvdelingslederApi';
+import { SletteSaksbehandlerModal } from './SletteSaksbehandlerModal';
+
+import styles from './saksbehandlereTabell.module.css';
+
+interface Props {
+ saksbehandlere: SaksbehandlerProfil[];
+ valgtAvdelingEnhet: string;
+}
+
+export const SaksbehandlereTabell = ({ saksbehandlere, valgtAvdelingEnhet }: Props) => {
+ const queryClient = useQueryClient();
+ const [valgtSaksbehandler, setValgtSaksbehandler] = useState();
+
+ const { mutate: fjernSaksbehandler } = useMutation({
+ mutationFn: (valuesToStore: SaksbehandlerProfil) =>
+ slettSaksbehandler(valuesToStore.brukerIdent, valgtAvdelingEnhet),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [LosUrl.SAKSBEHANDLERE_FOR_AVDELING],
+ });
+ setValgtSaksbehandler(undefined);
+ },
+ });
+
+ const sorterteSaksbehandlere = saksbehandlere.toSorted((saksbehandler1, saksbehandler2) => {
+ const compareWithNullsLast = (a: string | null, b: string | null) => {
+ if (a != null && b != null) return a.localeCompare(b);
+ if (a == null && b == null) return 0;
+ return a == null ? 1 : -1;
+ };
+
+ const enhetComparison = compareWithNullsLast(saksbehandler1.ansattAvdeling, saksbehandler2.ansattAvdeling);
+ if (enhetComparison !== 0) {
+ return enhetComparison;
+ }
+ return compareWithNullsLast(saksbehandler1.navn, saksbehandler2.navn);
+ });
+
+ return (
+
+ {sorterteSaksbehandlere.length === 0 && (
+
+
+
+ )}
+ {sorterteSaksbehandlere.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {sorterteSaksbehandlere.map(saksbehandler => (
+
+ {saksbehandler.navn}
+ {saksbehandler.brukerIdent}
+ {saksbehandler.ansattAvdeling}
+
+ setValgtSaksbehandler(saksbehandler)}
+ onKeyDown={() => setValgtSaksbehandler(saksbehandler)}
+ />
+
+
+ ))}
+
+
+ )}
+ {valgtSaksbehandler && (
+ setValgtSaksbehandler(undefined)}
+ fjernSaksbehandler={fjernSaksbehandler}
+ />
+ )}
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.spec.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.spec.tsx
new file mode 100644
index 00000000000..4b6de7357cf
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.spec.tsx
@@ -0,0 +1,19 @@
+import { composeStories } from '@storybook/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import * as stories from './SletteSaksbehandlerModal.stories';
+
+const { Default } = composeStories(stories);
+
+describe('SletteSaksbehandlerModal', () => {
+ it('skal vise modal og slette ved å trykk ja', async () => {
+ const fjernSaksbehandler = vi.fn();
+ render();
+ expect(await screen.findByText('Ønsker du å slette Espen Utvikler?')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Ja'));
+
+ await waitFor(() => expect(fjernSaksbehandler).toHaveBeenCalledTimes(1));
+ });
+});
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.stories.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.stories.tsx
new file mode 100644
index 00000000000..a252a3a570b
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { action } from 'storybook/actions';
+
+import { getIntlDecorator } from '@navikt/fp-storybook-utils';
+
+import { SletteSaksbehandlerModal } from './SletteSaksbehandlerModal';
+
+import messages from '../../i18n/nb_NO.json';
+
+const withIntl = getIntlDecorator(messages);
+
+const meta = {
+ title: 'los/avdelingsleder/saksbehandlere/SletteSaksbehandlerModal',
+ component: SletteSaksbehandlerModal,
+ decorators: [withIntl],
+ args: {
+ closeSletteModal: action('button-click'),
+ fjernSaksbehandler: action('button-click'),
+ valgtSaksbehandler: {
+ brukerIdent: 'R12122',
+ navn: 'Espen Utvikler',
+ ansattAvdeling: null,
+ },
+ },
+} satisfies Meta;
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.tsx b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.tsx
new file mode 100644
index 00000000000..4b02870e4ed
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/SletteSaksbehandlerModal.tsx
@@ -0,0 +1,61 @@
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { Button, Heading, Modal as NavModal } from '@navikt/ds-react';
+
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+import styles from './sletteSaksbehandlerModal.module.css';
+
+type Props = Readonly<{
+ valgtSaksbehandler: SaksbehandlerProfil;
+ closeSletteModal: () => void;
+ fjernSaksbehandler: (saksbehandler: SaksbehandlerProfil) => void;
+}>;
+
+/**
+ * SletteSaksbehandlerModal
+ *
+ * Presentasjonskomponent. Modal som lar en avdelingsleder fjerne tilgjengelige saksbehandlere.
+ */
+export const SletteSaksbehandlerModal = ({ valgtSaksbehandler, closeSletteModal, fjernSaksbehandler }: Props) => {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/leggTilSaksbehandlerForm.module.css b/apps/fp-avdelingsleder/src/saksbehandlere/leggTilSaksbehandlerForm.module.css
new file mode 100644
index 00000000000..84b25250931
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/leggTilSaksbehandlerForm.module.css
@@ -0,0 +1,3 @@
+.button {
+ margin-top: 30px;
+}
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/saksbehandlereTabell.module.css b/apps/fp-avdelingsleder/src/saksbehandlere/saksbehandlereTabell.module.css
new file mode 100644
index 00000000000..ce49c3658c5
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/saksbehandlereTabell.module.css
@@ -0,0 +1,6 @@
+.removeIcon {
+ color: var(--ax-danger-500);
+ cursor: pointer;
+ height: 25px;
+ width: 25px;
+}
diff --git a/apps/fp-avdelingsleder/src/saksbehandlere/sletteSaksbehandlerModal.module.css b/apps/fp-avdelingsleder/src/saksbehandlere/sletteSaksbehandlerModal.module.css
new file mode 100644
index 00000000000..687cc358010
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/saksbehandlere/sletteSaksbehandlerModal.module.css
@@ -0,0 +1,8 @@
+.submitButton {
+ margin-right: 10px;
+ margin-top: 10px;
+}
+
+.cancelButton {
+ margin-top: 10px;
+}
diff --git a/apps/fp-avdelingsleder/src/typer/avdelingTsType.ts b/apps/fp-avdelingsleder/src/typer/avdelingTsType.ts
new file mode 100644
index 00000000000..6d59ac63ca2
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/avdelingTsType.ts
@@ -0,0 +1,5 @@
+export type Avdeling = Readonly<{
+ avdelingEnhet: string;
+ navn: string;
+ kreverKode6: boolean;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/behandlingVentefristTsType.ts b/apps/fp-avdelingsleder/src/typer/behandlingVentefristTsType.ts
new file mode 100644
index 00000000000..a6da9d63810
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/behandlingVentefristTsType.ts
@@ -0,0 +1,5 @@
+export type BehandlingVentefrist = Readonly<{
+ fagsakYtelseType: string;
+ behandlingFrist: string;
+ antall: number;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/oppgaverForAvdelingTsType.ts b/apps/fp-avdelingsleder/src/typer/oppgaverForAvdelingTsType.ts
new file mode 100644
index 00000000000..318b339fb9a
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/oppgaverForAvdelingTsType.ts
@@ -0,0 +1,6 @@
+export type OppgaverForAvdeling = Readonly<{
+ fagsakYtelseType: string;
+ behandlingType: string;
+ tilBehandling: boolean;
+ antall: number;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/oppgaverForDatoTsType.ts b/apps/fp-avdelingsleder/src/typer/oppgaverForDatoTsType.ts
new file mode 100644
index 00000000000..17b0f84d0ed
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/oppgaverForDatoTsType.ts
@@ -0,0 +1,6 @@
+export type OppgaveForDato = Readonly<{
+ fagsakYtelseType: string;
+ behandlingType: string;
+ opprettetDato: string;
+ antall: number;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/oppgaverForForsteStonadsdagTsType.ts b/apps/fp-avdelingsleder/src/typer/oppgaverForForsteStonadsdagTsType.ts
new file mode 100644
index 00000000000..b72ec5288f7
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/oppgaverForForsteStonadsdagTsType.ts
@@ -0,0 +1,4 @@
+export type OppgaverForForsteStonadsdag = Readonly<{
+ forsteStonadsdag: string;
+ antall: number;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/oppgaverSomErApneEllerPaVentTsType.ts b/apps/fp-avdelingsleder/src/typer/oppgaverSomErApneEllerPaVentTsType.ts
new file mode 100644
index 00000000000..9bf20f9027b
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/oppgaverSomErApneEllerPaVentTsType.ts
@@ -0,0 +1,6 @@
+export type OppgaverSomErApneEllerPaVent = Readonly<{
+ antall: number;
+ behandlingType: string;
+ behandlingVenteStatus: string;
+ førsteUttakMåned?: string;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/reservasjonTsType.ts b/apps/fp-avdelingsleder/src/typer/reservasjonTsType.ts
new file mode 100644
index 00000000000..9c0348f3a27
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/reservasjonTsType.ts
@@ -0,0 +1,8 @@
+export type Reservasjon = Readonly<{
+ reservertAvUid: string;
+ reservertAvNavn: string;
+ reservertTilTidspunkt: string;
+ oppgaveId: number;
+ oppgaveSaksNr: string;
+ behandlingType: string;
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/saksbehandlereOgSaksbehandlerGrupper.ts b/apps/fp-avdelingsleder/src/typer/saksbehandlereOgSaksbehandlerGrupper.ts
new file mode 100644
index 00000000000..d7499c67010
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/saksbehandlereOgSaksbehandlerGrupper.ts
@@ -0,0 +1,11 @@
+import type { SaksbehandlerProfil } from '@navikt/fp-los-felles';
+
+export type SaksbehandlerGruppe = Readonly<{
+ gruppeId: number;
+ gruppeNavn?: string;
+ saksbehandlere: SaksbehandlerProfil[];
+}>;
+
+export type SaksbehandlereOgSaksbehandlerGrupper = Readonly<{
+ saksbehandlerGrupper: SaksbehandlerGruppe[];
+}>;
diff --git a/apps/fp-avdelingsleder/src/typer/sakslisteAvdelingTsType.ts b/apps/fp-avdelingsleder/src/typer/sakslisteAvdelingTsType.ts
new file mode 100644
index 00000000000..9749ba30af2
--- /dev/null
+++ b/apps/fp-avdelingsleder/src/typer/sakslisteAvdelingTsType.ts
@@ -0,0 +1,21 @@
+type AnnetKriterie = Readonly<{
+ andreKriterierType: string;
+ inkluder: boolean;
+}>;
+
+export type SakslisteAvdeling = Readonly<{
+ sakslisteId: number;
+ navn?: string;
+ behandlingTyper?: string[];
+ fagsakYtelseTyper?: string[];
+ sortering?: {
+ sorteringType: string;
+ fra?: number;
+ til?: number;
+ fomDato?: string;
+ tomDato?: string;
+ erDynamiskPeriode: boolean;
+ };
+ andreKriterier?: AnnetKriterie[];
+ saksbehandlerIdenter: string[];
+}>;
diff --git a/apps/fp-avdelingsleder/tsconfig.json b/apps/fp-avdelingsleder/tsconfig.json
new file mode 100644
index 00000000000..ed23d31ec90
--- /dev/null
+++ b/apps/fp-avdelingsleder/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@navikt/fp-config-typescript",
+ "include": ["./", "../../@types/externals.d.ts"]
+}
diff --git a/apps/fp-avdelingsleder/vite.config.ts b/apps/fp-avdelingsleder/vite.config.ts
new file mode 100644
index 00000000000..66a0588f4ed
--- /dev/null
+++ b/apps/fp-avdelingsleder/vite.config.ts
@@ -0,0 +1,26 @@
+///
+import { mergeConfig } from 'vite';
+
+import { createSharedAppConfig } from '@navikt/fp-config-vite';
+
+// eslint-disable-next-line import/no-default-export
+export default mergeConfig(createSharedAppConfig(), {
+ server: {
+ port: 9010,
+ cors: {
+ origin: ['https://fpsak.intern.dev.nav.no', 'https://fpsak.intern.nav.no', 'http://localhost:9000'],
+ },
+ proxy: {
+ '/fpsak/api': {
+ target: 'http://127.0.0.1:9000',
+ changeOrigin: false,
+ secure: false,
+ },
+ '/fplos/api': {
+ target: 'http://127.0.0.1:9000',
+ changeOrigin: false,
+ secure: false,
+ },
+ },
+ },
+});
diff --git a/apps/fp-frontend-app/package.json b/apps/fp-frontend-app/package.json
index 2ea1021e91c..162eb85633a 100644
--- a/apps/fp-frontend-app/package.json
+++ b/apps/fp-frontend-app/package.json
@@ -49,7 +49,7 @@
"@navikt/fp-journalforing": "workspace:*",
"@navikt/fp-kodeverk": "workspace:*",
"@navikt/fp-konstanter": "workspace:*",
- "@navikt/fp-los-avdelingsleder": "workspace:*",
+ "@navikt/fp-los-avdelingsleder-old": "workspace:*",
"@navikt/fp-los-saksbehandler": "workspace:*",
"@navikt/fp-modal-sett-pa-vent": "workspace:*",
"@navikt/fp-papirsoknad": "workspace:*",
diff --git a/apps/fp-frontend-app/src/app/components/Home.tsx b/apps/fp-frontend-app/src/app/components/Home.tsx
index 87f49c2c174..a8b43e07939 100644
--- a/apps/fp-frontend-app/src/app/components/Home.tsx
+++ b/apps/fp-frontend-app/src/app/components/Home.tsx
@@ -6,7 +6,7 @@ import { Heading } from '@navikt/ds-react';
import { useMutation } from '@tanstack/react-query';
import { OppgaveJournalføringIndex } from '@navikt/fp-journalforing';
-import { AvdelingslederIndex } from '@navikt/fp-los-avdelingsleder';
+import { AvdelingslederIndex } from '@navikt/fp-los-avdelingsleder-old';
import { SaksbehandlerIndex } from '@navikt/fp-los-saksbehandler';
import { NotFoundPage } from '@navikt/fp-sak-infosider';
import type { NavAnsatt } from '@navikt/fp-types';
diff --git a/package.json b/package.json
index 07f6fd99717..662f780f95b 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"clean": "lerna run clean --stream",
"dev": "yarn lerna run dev --scope=@navikt/fp-frontend-app",
"build": "lerna run build --stream",
+ "build-fp-avdelingsleder": "lerna run build --stream --scope=@navikt/fp-avdelingsleder",
"up": "docker-compose up -d --remove-orphans --build",
"down": "docker-compose down",
"remove-node-modules": "find . -name \"node_modules\" -exec rm -rf '{}' +",
diff --git a/packages/los/avdelingsleder/package.json b/packages/los/avdelingsleder/package.json
index 12760723cd0..d395642dc89 100644
--- a/packages/los/avdelingsleder/package.json
+++ b/packages/los/avdelingsleder/package.json
@@ -1,5 +1,5 @@
{
- "name": "@navikt/fp-los-avdelingsleder",
+ "name": "@navikt/fp-los-avdelingsleder-old",
"license": "MIT",
"private": true,
"type": "module",
diff --git a/yarn.lock b/yarn.lock
index e15ad30ebfb..0bb21c5452b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2411,7 +2411,7 @@ __metadata:
"@navikt/fp-journalforing": "workspace:*"
"@navikt/fp-kodeverk": "workspace:*"
"@navikt/fp-konstanter": "workspace:*"
- "@navikt/fp-los-avdelingsleder": "workspace:*"
+ "@navikt/fp-los-avdelingsleder-old": "workspace:*"
"@navikt/fp-los-saksbehandler": "workspace:*"
"@navikt/fp-modal-sett-pa-vent": "workspace:*"
"@navikt/fp-papirsoknad": "workspace:*"
@@ -2572,9 +2572,54 @@ __metadata:
languageName: unknown
linkType: soft
-"@navikt/fp-los-avdelingsleder@workspace:*, @navikt/fp-los-avdelingsleder@workspace:packages/los/avdelingsleder":
+"@navikt/fp-los-avdelingsleder-old@workspace:*, @navikt/fp-los-avdelingsleder-old@workspace:packages/los/avdelingsleder":
version: 0.0.0-use.local
- resolution: "@navikt/fp-los-avdelingsleder@workspace:packages/los/avdelingsleder"
+ resolution: "@navikt/fp-los-avdelingsleder-old@workspace:packages/los/avdelingsleder"
+ dependencies:
+ "@navikt/aksel-icons": 7.26.0
+ "@navikt/ds-css": 7.26.0
+ "@navikt/ds-react": 7.26.0
+ "@navikt/fp-config-eslint": "workspace:*"
+ "@navikt/fp-config-typescript": "workspace:*"
+ "@navikt/fp-config-vite": "workspace:*"
+ "@navikt/fp-kodeverk": "workspace:*"
+ "@navikt/fp-los-felles": "workspace:*"
+ "@navikt/fp-storybook-utils": "workspace:*"
+ "@navikt/fp-types": "workspace:*"
+ "@navikt/fp-utils": "workspace:*"
+ "@navikt/ft-form-hooks": 9.0.1
+ "@navikt/ft-form-validators": 4.1.1
+ "@navikt/ft-ui-komponenter": 6.0.1
+ "@navikt/ft-utils": 3.7.1
+ "@storybook/react": 9.1.1
+ "@storybook/react-vite": 9.1.1
+ "@tanstack/react-query": 5.84.1
+ "@testing-library/dom": 10.4.1
+ "@testing-library/react": 16.3.0
+ "@testing-library/user-event": 14.6.1
+ "@types/lodash.debounce": 4.0.9
+ dayjs: 1.11.13
+ eslint: 9.32.0
+ history: 5.3.0
+ ky: 1.8.2
+ lodash.debounce: 4.0.8
+ msw: 2.10.4
+ msw-storybook-addon: 2.0.5
+ react: 19.1.1
+ react-hook-form: 7.62.0
+ react-intl: 7.1.11
+ react-router-dom: 7.8.0
+ storybook: 9.1.1
+ stylelint: 16.23.1
+ typescript: 5.9.2
+ vite: 7.1.1
+ vitest: 3.2.4
+ languageName: unknown
+ linkType: soft
+
+"@navikt/fp-los-avdelingsleder@workspace:apps/fp-los-avdelingsleder":
+ version: 0.0.0-use.local
+ resolution: "@navikt/fp-los-avdelingsleder@workspace:apps/fp-los-avdelingsleder"
dependencies:
"@navikt/aksel-icons": 7.26.0
"@navikt/ds-css": 7.26.0