Skip to content

Commit 50fcab8

Browse files
authored
Nøkkeltall v3 (#3655)
Nye bokser for dagens tall og ferdigstilte per enhet som bruker v3-modellen. Viser de nye panelene bare dersom det finnes data. I prod venter vi litt med å kjøre jobber for å hente data.
1 parent 95833ba commit 50fcab8

12 files changed

+738
-1
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"@types/chai": "5.0.1",
122122
"@types/history": "5.0.0",
123123
"@types/jest": "29.5.14",
124+
"@types/lodash": "^4.17.15",
124125
"@types/react": "19.0.8",
125126
"@types/react-collapse": "5.0.4",
126127
"@types/react-dom": "19.0.3",

src/client/app/api/apiPaths.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const apiPaths = {
1313
hentAlleKoerSaksbehandlerV1: '/api/k9-los-api/saksbehandler/oppgaveko',
1414
hentAlleKoerSaksbehandlerV3: '/api/k9-los-api/ny-oppgavestyring/ko/saksbehandlerskoer',
1515
hentAndreSaksbehandleresKøerV3: '/api/k9-los-api/ny-oppgavestyring/ko/andre-saksbehandleres-koer',
16+
hentDagensTall: '/api/k9-los-api/ny-oppgavestyring/nokkeltall/dagens-tall',
17+
hentFerdigstiltePerEnhet: '/api/k9-los-api/ny-oppgavestyring/nokkeltall/ferdigstilte-per-enhet',
1618
hentFelter: '/api/k9-los-api/ny-oppgavestyring/oppgave/felter',
1719
hentOppgaveFelter: '/api/k9-los-api/ny-oppgavestyring/oppgave/felter',
1820
hentOppgaveFraKoV3: (id: string) => `/api/k9-los-api/ny-oppgavestyring/ko/${id}/fa-oppgave`,

src/client/app/api/queries/avdelingslederQueries.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { DefaultError, UseQueryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
1+
import {
2+
DefaultError,
3+
UseQueryOptions,
4+
keepPreviousData,
5+
useMutation,
6+
useQuery,
7+
useQueryClient,
8+
} from '@tanstack/react-query';
29
import apiPaths from 'api/apiPaths';
310
import { Saksbehandler } from 'avdelingsleder/bemanning/saksbehandlerTsType';
411
import Reservasjon from 'avdelingsleder/reservasjoner/reservasjonTsType';
@@ -149,3 +156,45 @@ export const useKo = (id: string, options?: Omit<UseQueryOptions<OppgavekøV3>,
149156
...options,
150157
queryKey: [apiPaths.hentOppgaveko(id)],
151158
});
159+
160+
export const useHentDagensTall = () =>
161+
useQuery<{
162+
oppdatertTidspunkt?: string;
163+
hovedgrupper: [{ kode: string; navn: string }];
164+
undergrupper: [{ kode: string; navn: string }];
165+
tall: [
166+
{
167+
hovedgruppe: string;
168+
undergruppe: string;
169+
nyeIDag: number;
170+
ferdigstilteIDag: number;
171+
nyeSiste7Dager: number;
172+
ferdigstilteSiste7Dager: number;
173+
},
174+
];
175+
}>({
176+
placeholderData: keepPreviousData,
177+
queryKey: [apiPaths.hentDagensTall],
178+
refetchInterval: 60000,
179+
});
180+
181+
export const useHentFerdigstiltePerEnhet = ({ gruppe, uker }: { gruppe: string; uker: string }) =>
182+
useQuery<{
183+
oppdatertTidspunkt?: string;
184+
grupper: { kode: string; navn: string }[];
185+
kolonner: string[];
186+
serier: [
187+
{
188+
navn: string;
189+
data: number[];
190+
},
191+
];
192+
}>({
193+
placeholderData: keepPreviousData,
194+
queryFn: () =>
195+
axiosInstance
196+
.get(apiPaths.hentFerdigstiltePerEnhet, { params: { gruppe, uker } })
197+
.then((response) => response.data),
198+
queryKey: [apiPaths.hentFerdigstiltePerEnhet, gruppe, uker],
199+
refetchInterval: 60000,
200+
});

src/client/app/avdelingsleder/nokkeltall/NokkeltallIndex.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from 'react';
22
import AksjonspunkterPerEnhetPanel from 'avdelingsleder/nokkeltall/components/aksjonspunkterPerEnhet/AksjonspunkterPerEnhetPanel';
33
import InngangOgFerdigstiltePanel from 'avdelingsleder/nokkeltall/components/dagensTallPanel/InngangOgFerdigstiltePanel';
4+
import DagensTall from 'avdelingsleder/nokkeltall/dagens-tall/DagensTall';
5+
import FerdigstiltePerEnhet from 'avdelingsleder/nokkeltall/ferdigstilte-per-enhet/FerdigstiltePerEnhet';
46
import VerticalSpacer from 'sharedComponents/VerticalSpacer';
57

68
function NokkeltallIndex() {
@@ -9,6 +11,10 @@ function NokkeltallIndex() {
911
<InngangOgFerdigstiltePanel />
1012
<VerticalSpacer twentyPx />
1113
<AksjonspunkterPerEnhetPanel />
14+
<VerticalSpacer twentyPx />
15+
<DagensTall />
16+
<VerticalSpacer twentyPx />
17+
<FerdigstiltePerEnhet />
1218
</div>
1319
);
1420
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useState } from 'react';
2+
import dayjs from 'dayjs';
3+
import { Box, Detail, HGrid, HStack, Heading, Select, ToggleGroup } from '@navikt/ds-react';
4+
import { useHentDagensTall } from 'api/queries/avdelingslederQueries';
5+
import Teller from 'avdelingsleder/nokkeltall/components/dagensTallPanel/Teller';
6+
import VerticalSpacer from 'sharedComponents/VerticalSpacer';
7+
8+
export default function DagensTall() {
9+
const [valgtHovedgruppe, setValgtHovedgruppe] = useState('ALLE');
10+
const { data } = useHentDagensTall();
11+
const [tidsområde, setTidsområde] = useState('I_DAG');
12+
13+
// Henter alltid fra direkte fra cache, så bruker kort tid på loading
14+
if (data === undefined) return null;
15+
16+
// TODO: Fjern denne linja når jobber for å hente data er på plass, gjør det sånn for å slippe å feature toggle
17+
if (!data.oppdatertTidspunkt) return null;
18+
19+
return (
20+
<Box padding="4" borderWidth="1" borderColor="border-default">
21+
<HStack align="center" justify="space-between">
22+
<Heading size="small">Dagens tall (ny)</Heading>
23+
</HStack>
24+
<VerticalSpacer eightPx />
25+
{!data.oppdatertTidspunkt && <p>Ingen data for øyeblikket</p>}
26+
{data.oppdatertTidspunkt && (
27+
<>
28+
<Detail>Oppdatert {dayjs(data.oppdatertTidspunkt).format('DD.MM.YYYY kl. HH:mm:ss')}</Detail>
29+
<HStack className="mt-4 mb-6" gap="4">
30+
<Select
31+
label="Valgt ytelse"
32+
hideLabel
33+
value={valgtHovedgruppe}
34+
onChange={(event) => setValgtHovedgruppe(event.currentTarget.value)}
35+
>
36+
{data.hovedgrupper?.map(({ kode, navn }) => (
37+
<option key={kode} value={kode}>
38+
{navn}
39+
</option>
40+
))}
41+
</Select>
42+
<ToggleGroup value={tidsområde} onChange={(nyttTidsområde) => setTidsområde(nyttTidsområde)}>
43+
<ToggleGroup.Item value="I_DAG" label="I dag" />
44+
<ToggleGroup.Item value="SISTE_7" label="Siste 7 dager" />
45+
</ToggleGroup>
46+
</HStack>
47+
<HGrid gap="2" columns={4}>
48+
{data.tall
49+
.filter(({ hovedgruppe }) => hovedgruppe === valgtHovedgruppe)
50+
.map((value) => {
51+
const venstreTall = tidsområde === 'I_DAG' ? value.nyeIDag : value.nyeSiste7Dager;
52+
const høyreTall = tidsområde === 'I_DAG' ? value.ferdigstilteIDag : value.ferdigstilteSiste7Dager;
53+
return (
54+
<Teller
55+
key={value.undergruppe}
56+
forklaring={data.undergrupper.find(({ kode }) => kode === value.undergruppe).navn}
57+
venstreTall={venstreTall}
58+
hoyreTall={høyreTall}
59+
/>
60+
);
61+
})}
62+
</HGrid>
63+
</>
64+
)}
65+
</Box>
66+
);
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import React, { FunctionComponent, useState } from 'react';
2+
import { FormattedMessage, WrappedComponentProps, injectIntl } from 'react-intl';
3+
import dayjs from 'dayjs';
4+
import Panel from 'nav-frontend-paneler';
5+
import { ToggleKnapp } from 'nav-frontend-toggle';
6+
import { BodyShort, Heading, Label, Loader, Select } from '@navikt/ds-react';
7+
import { K9LosApiKeys, RestApiGlobalStatePathsKeys } from 'api/k9LosApi';
8+
import { useGlobalStateRestApiData } from 'api/rest-api-hooks';
9+
import RestApiState from 'api/rest-api-hooks/src/RestApiState';
10+
import useRestApi from 'api/rest-api-hooks/src/local-data/useRestApi';
11+
import {
12+
ALLE_YTELSETYPER_VALGT,
13+
sjekkOmOppgaveSkalLeggesTil,
14+
ytelseTyper,
15+
} from 'avdelingsleder/nokkeltall/nokkeltallUtils';
16+
import NyeOgFerdigstilteMedStonadstype from 'avdelingsleder/nokkeltall/nyeOgFerdigstilteMedStonadstypeTsType';
17+
import AlleKodeverk from 'kodeverk/alleKodeverkTsType';
18+
import kodeverkTyper from 'kodeverk/kodeverkTyper';
19+
import VerticalSpacer from 'sharedComponents/VerticalSpacer';
20+
import { ISO_DATE_FORMAT } from 'utils/formats';
21+
import { getKodeverknavnFraKode } from 'utils/kodeverkUtils';
22+
import { getValueFromLocalStorage, lagreTilLocalStorageCallback } from 'utils/localStorageHelper';
23+
import Teller from './Teller';
24+
import * as styles from './inngangOgFerdigstiltePanel.css';
25+
26+
export const slaSammenLikeBehandlingstyper = (oppgaver) => {
27+
const sammenslatte = [];
28+
29+
oppgaver.forEach((o) => {
30+
const index = sammenslatte.findIndex((s) => s.behandlingType === o.behandlingType);
31+
if (index === -1) {
32+
sammenslatte.push({
33+
behandlingType: o.behandlingType,
34+
nye: o.nye,
35+
ferdigstilte: o.ferdigstilte,
36+
});
37+
} else {
38+
sammenslatte[index] = {
39+
behandlingType: sammenslatte[index].behandlingType,
40+
nye: sammenslatte[index].nye + o.nye,
41+
ferdigstilte: sammenslatte[index].ferdigstilte + o.ferdigstilte,
42+
};
43+
}
44+
});
45+
46+
return sammenslatte;
47+
};
48+
49+
export const InngangOgFerdigstiltePanel: FunctionComponent<WrappedComponentProps> = () => {
50+
const id = 'inngangOgFerdigstiltePanel';
51+
const [erIdagValgt, setErIdagValgt] = useState(true);
52+
const [valgtYtelseType, setValgtYtelseType] = useState<string>(
53+
getValueFromLocalStorage(`${id}-ytelsestype`) || ALLE_YTELSETYPER_VALGT,
54+
);
55+
56+
const alleKodeverk: AlleKodeverk = useGlobalStateRestApiData(RestApiGlobalStatePathsKeys.KODEVERK);
57+
58+
const { data: nyeOgFerdigstilteOppgaverMedStonadstype = [], state } = useRestApi<NyeOgFerdigstilteMedStonadstype[]>(
59+
K9LosApiKeys.HENT_OPPSUMMERING,
60+
);
61+
62+
const velgYtelsesTypeHandler = (value) => {
63+
lagreTilLocalStorageCallback(`${id}-ytelsestype`, value, setValgtYtelseType);
64+
};
65+
66+
const requestFinished = state === RestApiState.SUCCESS;
67+
68+
const nyeOgFerdigstilteOppgaverIdag = nyeOgFerdigstilteOppgaverMedStonadstype.filter((oppgave) =>
69+
dayjs().isSame(dayjs(oppgave.dato, ISO_DATE_FORMAT), 'day'),
70+
);
71+
const nyeOgFerdigstilteOppgaver7dager = nyeOgFerdigstilteOppgaverMedStonadstype.filter((oppgave) =>
72+
dayjs().startOf('day').isSameOrAfter(dayjs(oppgave.dato, ISO_DATE_FORMAT)),
73+
);
74+
75+
const getNyeTotalt = (oppgaver: NyeOgFerdigstilteMedStonadstype[], ytelseType: string) => {
76+
let nye = 0;
77+
oppgaver
78+
.filter((o) => sjekkOmOppgaveSkalLeggesTil(ytelseType, o))
79+
.forEach((n) => {
80+
nye += n.nye;
81+
});
82+
return nye;
83+
};
84+
85+
const getFerdigstilteTotalt = (oppgaver: NyeOgFerdigstilteMedStonadstype[], ytelseType: string) => {
86+
let ferdigstilte = 0;
87+
oppgaver
88+
.filter((o) => sjekkOmOppgaveSkalLeggesTil(ytelseType, o))
89+
.forEach((n) => {
90+
ferdigstilte += n.ferdigstilte;
91+
});
92+
return ferdigstilte;
93+
};
94+
95+
const getOppgaverStonadstype = (oppgaver: NyeOgFerdigstilteMedStonadstype[], ytelseType: string) =>
96+
slaSammenLikeBehandlingstyper(oppgaver.filter((o) => sjekkOmOppgaveSkalLeggesTil(ytelseType, o)));
97+
98+
return (
99+
<Panel className={styles.panel}>
100+
<Heading spacing level="3" size="small">
101+
<FormattedMessage id="InngangOgFerdigstiltePanel.Header" />
102+
</Heading>
103+
<VerticalSpacer eightPx />
104+
<div className={styles.valgPanel}>
105+
<Select
106+
onChange={(e) => velgYtelsesTypeHandler(e.target.value)}
107+
label="Valgt ytelse"
108+
hideLabel
109+
size="small"
110+
value={valgtYtelseType}
111+
>
112+
{ytelseTyper.map((u) => (
113+
<option key={u.kode} value={u.kode}>
114+
{u.navn}
115+
</option>
116+
))}
117+
</Select>
118+
<div className={styles.toggles}>
119+
<ToggleKnapp
120+
pressed
121+
className={erIdagValgt ? styles.venstreKnappAktiv : styles.venstreKnapp}
122+
onClick={() => setErIdagValgt(true)}
123+
>
124+
<Label size="small">I dag</Label>
125+
</ToggleKnapp>
126+
<ToggleKnapp
127+
className={erIdagValgt ? styles.hoyreKnapp : styles.hoyreKnappAktiv}
128+
onClick={() => setErIdagValgt(false)}
129+
>
130+
<Label size="small">Siste 7 dager</Label>
131+
</ToggleKnapp>
132+
</div>
133+
</div>
134+
{((erIdagValgt && requestFinished && nyeOgFerdigstilteOppgaverIdag.length === 0) ||
135+
(!erIdagValgt && requestFinished && nyeOgFerdigstilteOppgaver7dager.length === 0)) && (
136+
<BodyShort className={styles.ingenTall}>
137+
<FormattedMessage id="InngangOgFerdigstiltePanel.IngenTall" />
138+
</BodyShort>
139+
)}
140+
{nyeOgFerdigstilteOppgaverIdag.length === 0 && !requestFinished && (
141+
<Loader size="2xlarge" className={styles.spinner} />
142+
)}
143+
<div className={styles.container}>
144+
{((erIdagValgt && nyeOgFerdigstilteOppgaverIdag.length > 0) ||
145+
(!erIdagValgt && nyeOgFerdigstilteOppgaver7dager.length > 0)) && (
146+
<Teller
147+
forklaring="Totalt"
148+
venstreTall={
149+
erIdagValgt
150+
? getNyeTotalt(nyeOgFerdigstilteOppgaverIdag, valgtYtelseType)
151+
: getNyeTotalt(nyeOgFerdigstilteOppgaver7dager, valgtYtelseType)
152+
}
153+
hoyreTall={
154+
erIdagValgt
155+
? getFerdigstilteTotalt(nyeOgFerdigstilteOppgaverIdag, valgtYtelseType)
156+
: getFerdigstilteTotalt(nyeOgFerdigstilteOppgaver7dager, valgtYtelseType)
157+
}
158+
/>
159+
)}
160+
{erIdagValgt &&
161+
nyeOgFerdigstilteOppgaverIdag.length > 0 &&
162+
valgtYtelseType !== 'PUNSJ' &&
163+
getOppgaverStonadstype(nyeOgFerdigstilteOppgaverIdag, valgtYtelseType).map((o) => (
164+
<Teller
165+
key={o.behandlingType}
166+
forklaring={getKodeverknavnFraKode(o.behandlingType, kodeverkTyper.BEHANDLING_TYPE, alleKodeverk)}
167+
hoyreTall={o.ferdigstilte}
168+
venstreTall={o.nye}
169+
/>
170+
))}
171+
{!erIdagValgt &&
172+
nyeOgFerdigstilteOppgaver7dager.length > 0 &&
173+
valgtYtelseType !== 'PUNSJ' &&
174+
getOppgaverStonadstype(nyeOgFerdigstilteOppgaver7dager, valgtYtelseType).map((o) => (
175+
<Teller
176+
key={o.behandlingType}
177+
forklaring={getKodeverknavnFraKode(o.behandlingType, kodeverkTyper.BEHANDLING_TYPE, alleKodeverk)}
178+
hoyreTall={o.ferdigstilte}
179+
venstreTall={o.nye}
180+
/>
181+
))}
182+
</div>
183+
</Panel>
184+
);
185+
};
186+
187+
export default injectIntl(InngangOgFerdigstiltePanel);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { FunctionComponent } from 'react';
2+
import * as styles from './teller.css';
3+
4+
interface OwnProps {
5+
forklaring: string;
6+
venstreTall: number;
7+
hoyreTall: number;
8+
}
9+
const Teller: FunctionComponent<OwnProps> = ({ forklaring, venstreTall, hoyreTall }) => (
10+
<div className={styles.frame}>
11+
<div className={styles.container}>
12+
<div>
13+
<p className={styles.beskrivelse}>Inngang</p>
14+
<div className={styles.field}>
15+
<p className={styles.number}>{venstreTall}</p>
16+
</div>
17+
</div>
18+
<div className={styles.vl}> </div>
19+
<div>
20+
<p className={styles.beskrivelse}>Ferdigstilt</p>
21+
<div className={styles.coloredField}>
22+
<p className={styles.number}>{hoyreTall}</p>
23+
</div>
24+
</div>
25+
</div>
26+
<div className={styles.forklaring}>
27+
<p>{forklaring}</p>
28+
</div>
29+
</div>
30+
);
31+
32+
export default Teller;

0 commit comments

Comments
 (0)