From 0dc1e81552a7beda30655ff126ee4dd506b21e3a Mon Sep 17 00:00:00 2001 From: Daniel Eriksson Date: Fri, 10 Jan 2025 10:33:59 +0100 Subject: [PATCH] Let all saksbehandlere edit documents in finished behandling, only let creator edit own document in unfinished behandling, add checkbox for forcing own signature instead of assigned saksbehandler --- .../src/components/smart-editor/context.tsx | 13 +- .../smart-editor/history/history-editor.tsx | 2 +- .../hooks/use-can-edit-document.test.ts | 336 ++++++++++++++---- .../hooks/use-can-edit-document.ts | 50 ++- .../smart-editor/tabbed-editors/editor.tsx | 7 +- .../smart-editor/tabbed-editors/tab-panel.tsx | 2 +- .../src/plate/components/signature/hooks.ts | 48 ++- .../signature/individual-signature.tsx | 40 +-- .../plate/components/signature/signature.tsx | 31 +- frontend/src/plate/templates/helpers.ts | 1 + frontend/src/plate/types.ts | 1 + 11 files changed, 387 insertions(+), 144 deletions(-) diff --git a/frontend/src/components/smart-editor/context.tsx b/frontend/src/components/smart-editor/context.tsx index 6e47c7477..8c6631442 100644 --- a/frontend/src/components/smart-editor/context.tsx +++ b/frontend/src/components/smart-editor/context.tsx @@ -1,4 +1,4 @@ -import { useCanManageDocument } from '@app/components/smart-editor/hooks/use-can-edit-document'; +import { useCanEditDocument, useCanManageDocument } from '@app/components/smart-editor/hooks/use-can-edit-document'; import { useSmartEditorAnnotationsAtOrigin, useSmartEditorGodeFormuleringerOpen, @@ -25,6 +25,8 @@ interface ISmartEditorContext extends Pick void; sheetRef: MutableRefObject; canManage: boolean; + canEdit: boolean; + creator: string; } export const SmartEditorContext = createContext({ @@ -43,6 +45,8 @@ export const SmartEditorContext = createContext({ setShowAnnotationsAtOrigin: noop, sheetRef: { current: null }, canManage: false, + canEdit: false, + creator: '', }); interface Props { @@ -51,7 +55,7 @@ interface Props { } export const SmartEditorContextComponent = ({ children, smartDocument }: Props) => { - const { dokumentTypeId, templateId, id } = smartDocument; + const { dokumentTypeId, templateId, id, creator } = smartDocument; const { value: showGodeFormuleringer = false, setValue: setShowGodeFormuleringer } = useSmartEditorGodeFormuleringerOpen(); const { value: showHistory = false, setValue: setShowHistory } = useSmartEditorHistoryOpen(); @@ -61,7 +65,8 @@ export const SmartEditorContextComponent = ({ children, smartDocument }: Props) useSmartEditorAnnotationsAtOrigin(); // const [sheetRef, setSheetRef] = useState(null); const sheetRef = useRef(null); - const canManage = useCanManageDocument(templateId); + const canManage = useCanManageDocument(templateId, creator.employee.navIdent); + const canEdit = useCanEditDocument(templateId, creator.employee.navIdent); return ( {children} diff --git a/frontend/src/components/smart-editor/history/history-editor.tsx b/frontend/src/components/smart-editor/history/history-editor.tsx index fea9b339c..f756208a1 100644 --- a/frontend/src/components/smart-editor/history/history-editor.tsx +++ b/frontend/src/components/smart-editor/history/history-editor.tsx @@ -34,7 +34,7 @@ export const HistoryEditor = memo( ({ smartDocument, version, versionId }: Props) => { const mainEditor = useMyPlateEditorRef(smartDocument.id); const { templateId } = useContext(SmartEditorContext); - const canManage = useCanManageDocument(templateId); + const canManage = useCanManageDocument(templateId, smartDocument.creator.employee.navIdent); const id = `${smartDocument.id}-${versionId}`; diff --git a/frontend/src/components/smart-editor/hooks/use-can-edit-document.test.ts b/frontend/src/components/smart-editor/hooks/use-can-edit-document.test.ts index 051321ee7..96e90d976 100644 --- a/frontend/src/components/smart-editor/hooks/use-can-edit-document.test.ts +++ b/frontend/src/components/smart-editor/hooks/use-can-edit-document.test.ts @@ -1,102 +1,242 @@ import { describe, expect, it } from 'bun:test'; import { canEditDocument } from '@app/components/smart-editor/hooks/use-can-edit-document'; -import type { IUserData } from '@app/types/bruker'; +import { type IUserData, Role } from '@app/types/bruker'; import { SaksTypeEnum } from '@app/types/kodeverk'; import { FlowState } from '@app/types/oppgave-common'; import type { IOppgavebehandling } from '@app/types/oppgavebehandling/oppgavebehandling'; import { TemplateIdEnum } from '@app/types/smart-editor/template-enums'; -const createOppgave = (muFlowState: FlowState, rolFlowState: FlowState): IOppgavebehandling => +const createOppgave = ( + muFlowState: FlowState, + rolFlowState: FlowState, + avsluttetAvSaksbehandlerDate: string | null, +): IOppgavebehandling => ({ + saksbehandler: { navIdent: 'creator' }, typeId: SaksTypeEnum.KLAGE, - saksbehandler: { navIdent: 'saksbehandler' }, rol: { flowState: rolFlowState, employee: { navIdent: 'rol' } }, medunderskriver: { flowState: muFlowState, employee: { navIdent: 'mu' } }, + avsluttetAvSaksbehandlerDate, }) as IOppgavebehandling; -const CASES_WITHOUT_EXPECT = [ - [FlowState.NOT_SENT, FlowState.NOT_SENT], - [FlowState.NOT_SENT, FlowState.SENT], - [FlowState.NOT_SENT, FlowState.RETURNED], - [FlowState.SENT, FlowState.NOT_SENT], - [FlowState.SENT, FlowState.SENT], - [FlowState.SENT, FlowState.RETURNED], - [FlowState.RETURNED, FlowState.NOT_SENT], - [FlowState.RETURNED, FlowState.SENT], - [FlowState.RETURNED, FlowState.RETURNED], +type Cases = [FlowState, FlowState, string, string | null, boolean][]; + +const CANT_EDIT: Cases = [ + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', null, false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', null, false], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.SENT, FlowState.SENT, 'creator', null, false], + [FlowState.SENT, FlowState.RETURNED, 'creator', null, false], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', null, false], + [FlowState.SENT, FlowState.SENT, 'not creator', null, false], + [FlowState.SENT, FlowState.RETURNED, 'not creator', null, false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.RETURNED, FlowState.SENT, 'creator', null, false], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', null, false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', null, false], + [FlowState.RETURNED, FlowState.SENT, 'not creator', null, false], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', null, false], + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', '03.14.1969', false], ]; describe('canEditDocument', () => { describe('Saksbehandler', () => { - const user = { navIdent: 'saksbehandler' } as IUserData; + const user = { navIdent: 'creator', roller: [Role.KABAL_SAKSBEHANDLING] } as IUserData; describe('ROL answers', () => { - it.each(CASES_WITHOUT_EXPECT)('%#. should never allow editing', (mu, rol) => { - expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol), user)).toBe(false); + it.each(CANT_EDIT)('%#. should never allow editing', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); }); }); describe('ROL questions', () => { - const cases = [ - { mu: FlowState.NOT_SENT, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.NOT_SENT, rol: FlowState.SENT, expected: false }, - { mu: FlowState.NOT_SENT, rol: FlowState.RETURNED, expected: true }, - { mu: FlowState.SENT, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.SENT, rol: FlowState.SENT, expected: false }, - { mu: FlowState.SENT, rol: FlowState.RETURNED, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.SENT, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.RETURNED, expected: true }, + const cases: Cases = [ + // Assigned saksbehandler can edit in not-finished behandling when ROL flow state is NOT_SENT/RETURNED + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', null, true], + [FlowState.SENT, FlowState.RETURNED, 'creator', null, true], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', null, true], + + // User is assigned saksbehandler in open behandling, so it doesn't matter if there was another creator + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', null, true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', null, true], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', null, true], + [FlowState.SENT, FlowState.RETURNED, 'not creator', null, true], + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', null, true], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', null, true], + + // Creator can edit in finished behandling when ROL flow state is NOT_SENT/RETURNED + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', '03.14.1969', true], + [FlowState.SENT, FlowState.RETURNED, 'creator', '03.14.1969', true], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', '03.14.1969', true], + + // Only creator can edit in finished behandling + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', '03.14.1969', false], + + // Flow state is sent - only ROL can edit + [FlowState.NOT_SENT, FlowState.SENT, 'creator', null, false], + [FlowState.SENT, FlowState.SENT, 'creator', null, false], + [FlowState.SENT, FlowState.SENT, 'not creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', null, false], + [FlowState.RETURNED, FlowState.SENT, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'not creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'creator', '03.14.1969', false], ]; - it.each(cases)('%#. should not allow editing if ROL flow state is sent', ({ mu, rol, expected }) => { - expect(canEditDocument(TemplateIdEnum.ROL_QUESTIONS, createOppgave(mu, rol), user)).toBe(expected); - }); + it.each(cases)( + '%#. should not allow editing if ROL flow state is sent or if user is not the creator of the document', + (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_QUESTIONS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); + }, + ); }); describe('Other templates', () => { - const cases = [ - { mu: FlowState.NOT_SENT, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.NOT_SENT, rol: FlowState.SENT, expected: true }, - { mu: FlowState.NOT_SENT, rol: FlowState.RETURNED, expected: true }, - { mu: FlowState.SENT, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.SENT, rol: FlowState.SENT, expected: false }, - { mu: FlowState.SENT, rol: FlowState.RETURNED, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.SENT, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.RETURNED, expected: true }, + const cases: Cases = [ + // Assigned saksbehandler can edit in not-finished behandling when MU flow state is NOT_SENT/RETURNED + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', null, true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', null, true], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.RETURNED, FlowState.SENT, 'creator', null, true], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', null, true], + + // Creator can edit in finished behandling when MU flow state is NOT_SENT/RETURNED + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', '03.14.1969', true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', '03.14.1969', true], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.RETURNED, FlowState.SENT, 'creator', '03.14.1969', true], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', '03.14.1969', true], + + // User is assigned saksbehandler in open behandling, so it doesn't matter if there was another creator + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', null, true], + [FlowState.RETURNED, FlowState.SENT, 'not creator', null, true], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', null, true], + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', null, true], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', null, true], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', null, true], + + // Only creator can edit in finished behandling + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'not creator', '03.14.1969', false], + + // Flow state is sent - only MU can edit + [FlowState.SENT, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.SENT, FlowState.SENT, 'creator', null, false], + [FlowState.SENT, FlowState.RETURNED, 'creator', null, false], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', null, false], + [FlowState.SENT, FlowState.SENT, 'not creator', null, false], + [FlowState.SENT, FlowState.RETURNED, 'not creator', null, false], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'creator', '03.14.1969', false], + [FlowState.SENT, FlowState.NOT_SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.SENT, 'not creator', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'not creator', '03.14.1969', false], ]; - it.each(cases)('%#. should not allow editing if MU flow state is sent', ({ mu, rol, expected }) => { - expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol), user)).toBe(expected); - }); + it.each(cases)( + '%#. should not allow editing if MU flow state is sent, or if user is not the creator of the document', + (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); + }, + ); }); }); describe('Medunderskriver', () => { - const user = { navIdent: 'mu' } as IUserData; + const user = { navIdent: 'mu', roller: [Role.KABAL_SAKSBEHANDLING] } as IUserData; describe('ROL answers', () => { - it.each(CASES_WITHOUT_EXPECT)('%#. should never allow editing', (mu, rol) => { - expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol), user)).toBe(false); + it.each(CANT_EDIT)('%#. should never allow editing', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); }); }); describe('Other templates', () => { - const cases = [ - { mu: FlowState.NOT_SENT, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.NOT_SENT, rol: FlowState.SENT, expected: false }, - { mu: FlowState.NOT_SENT, rol: FlowState.RETURNED, expected: false }, - { mu: FlowState.SENT, rol: FlowState.NOT_SENT, expected: true }, - { mu: FlowState.SENT, rol: FlowState.SENT, expected: true }, - { mu: FlowState.SENT, rol: FlowState.RETURNED, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.SENT, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.RETURNED, expected: false }, + const cases: Cases = [ + // User is MU and MU flow state is sent - can edit + [FlowState.SENT, FlowState.NOT_SENT, 'creator', null, true], + [FlowState.SENT, FlowState.SENT, 'creator', null, true], + [FlowState.SENT, FlowState.RETURNED, 'creator', null, true], + [FlowState.SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', true], + [FlowState.SENT, FlowState.SENT, 'creator', '03.14.1969', true], + [FlowState.SENT, FlowState.RETURNED, 'creator', '03.14.1969', true], + + // Behandling is finished, and MU is not creator - can't edit + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.SENT, 'creator', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', '03.14.1969', false], + + // MU Flow state is not sent - MU can't edit + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.SENT, 'creator', null, false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'creator', null, false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'creator', null, false], + [FlowState.RETURNED, FlowState.SENT, 'creator', null, false], + [FlowState.RETURNED, FlowState.RETURNED, 'creator', null, false], ]; - it.each(cases)('%#. should allow editing if MU flow state is sent', ({ mu, rol, expected }) => { - expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol), user)).toBe(expected); + it.each(cases)('%#. should allow editing if MU flow state is sent', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); }); }); }); @@ -105,27 +245,81 @@ describe('canEditDocument', () => { const user = { navIdent: 'rol' } as IUserData; describe('ROL answers', () => { - const cases = [ - { mu: FlowState.NOT_SENT, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.NOT_SENT, rol: FlowState.SENT, expected: true }, - { mu: FlowState.NOT_SENT, rol: FlowState.RETURNED, expected: false }, - { mu: FlowState.SENT, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.SENT, rol: FlowState.SENT, expected: true }, - { mu: FlowState.SENT, rol: FlowState.RETURNED, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.NOT_SENT, expected: false }, - { mu: FlowState.RETURNED, rol: FlowState.SENT, expected: true }, - { mu: FlowState.RETURNED, rol: FlowState.RETURNED, expected: false }, + const cases: Cases = [ + // ROL can edit if ROL flow state is sent + [FlowState.NOT_SENT, FlowState.SENT, 'rol', null, true], + [FlowState.SENT, FlowState.SENT, 'rol', null, true], + [FlowState.RETURNED, FlowState.SENT, 'rol', null, true], + + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.SENT, 'rol', '03.14.1969', true], + [FlowState.SENT, FlowState.SENT, 'rol', '03.14.1969', true], + [FlowState.RETURNED, FlowState.SENT, 'rol', '03.14.1969', true], + + // ROL can't edit if ROL flow state is not SENT + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'rol', null, false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'rol', null, false], + [FlowState.SENT, FlowState.NOT_SENT, 'rol', null, false], + [FlowState.SENT, FlowState.RETURNED, 'rol', null, false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'rol', null, false], + [FlowState.RETURNED, FlowState.RETURNED, 'rol', null, false], + + // Flow state in finished behandling doesn't really make sense. These cases are just for completeness' sake + [FlowState.NOT_SENT, FlowState.NOT_SENT, 'rol', '03.14.1969', false], + [FlowState.NOT_SENT, FlowState.RETURNED, 'rol', '03.14.1969', false], + [FlowState.SENT, FlowState.NOT_SENT, 'rol', '03.14.1969', false], + [FlowState.SENT, FlowState.RETURNED, 'rol', '03.14.1969', false], + [FlowState.RETURNED, FlowState.NOT_SENT, 'rol', '03.14.1969', false], + [FlowState.RETURNED, FlowState.RETURNED, 'rol', '03.14.1969', false], ]; - it.each(cases)('%#. should allow editing if ROL flow state is sent', ({ mu, rol, expected }) => { - expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol), user)).toBe(expected); + it.each(cases)('%#. should allow editing if ROL flow state is sent', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); }); }); describe('Other templates', () => { - it.each(CASES_WITHOUT_EXPECT)('%#. should never allow editing', (mu, rol) => { - expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol), user)).toBe(false); + it.each(CANT_EDIT)('%#. should never allow editing', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); + }); + }); + }); + + describe('Others', () => { + const user = { navIdent: 'other saksbehandler', roller: [Role.KABAL_SAKSBEHANDLING] } as IUserData; + + describe('ROL answers', () => { + it.each(CANT_EDIT)('%#. should never allow editing', (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_ANSWERS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); }); }); + + describe('ROL questions', () => { + it.each(CANT_EDIT)( + '%#. should not allow editing if ROL flow state is sent or if user is not the creator of the document', + (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.ROL_QUESTIONS, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); + }, + ); + }); + + describe('Other templates', () => { + it.each(CANT_EDIT)( + '%#. should not allow editing if MU flow state is sent, or if user is not the creator of the document', + (mu, rol, creator, finished, expected) => { + expect(canEditDocument(TemplateIdEnum.KLAGEVEDTAK_V2, createOppgave(mu, rol, finished), user, creator)).toBe( + expected, + ); + }, + ); + }); }); }); diff --git a/frontend/src/components/smart-editor/hooks/use-can-edit-document.ts b/frontend/src/components/smart-editor/hooks/use-can-edit-document.ts index f50980a8f..a26b0eaa7 100644 --- a/frontend/src/components/smart-editor/hooks/use-can-edit-document.ts +++ b/frontend/src/components/smart-editor/hooks/use-can-edit-document.ts @@ -7,17 +7,22 @@ import type { IOppgavebehandling } from '@app/types/oppgavebehandling/oppgavebeh import { TemplateIdEnum } from '@app/types/smart-editor/template-enums'; import { useContext, useMemo } from 'react'; -export const useCanManageDocument = (templateId: TemplateIdEnum): boolean => { +export const useCanManageDocument = (templateId: TemplateIdEnum, creator: string): boolean => { const { data: oppgave, isSuccess } = useOppgave(); const { user } = useContext(StaticDataContext); return useMemo( - () => isSuccess && canManageDocument(templateId, oppgave, user), - [oppgave, isSuccess, templateId, user], + () => isSuccess && canManageDocument(templateId, oppgave, user, creator), + [oppgave, isSuccess, templateId, user, creator], ); }; -const canManageDocument = (templateId: TemplateIdEnum, oppgave: IOppgavebehandling, user: IUserData): boolean => { +const canManageDocument = ( + templateId: TemplateIdEnum, + oppgave: IOppgavebehandling, + user: IUserData, + creator: string, +): boolean => { if ( (oppgave.typeId === SaksTypeEnum.KLAGE || oppgave.typeId === SaksTypeEnum.ANKE) && oppgave.rol?.flowState === FlowState.SENT && @@ -32,16 +37,23 @@ const canManageDocument = (templateId: TemplateIdEnum, oppgave: IOppgavebehandli } if (isMu(oppgave, user)) { - return false; + return oppgave.avsluttetAvSaksbehandlerDate !== null && saksbehandlerCanEdit(templateId, oppgave, user, creator); } - return saksbehandlerCanEdit(templateId, oppgave, user); + return saksbehandlerCanEdit(templateId, oppgave, user, creator); }; -const saksbehandlerCanEdit = (templateId: TemplateIdEnum, oppgave: IOppgavebehandling, user: IUserData): boolean => { - if (oppgave.saksbehandler?.navIdent !== user.navIdent) { - return false; - } +const saksbehandlerCanEdit = ( + templateId: TemplateIdEnum, + oppgave: IOppgavebehandling, + user: IUserData, + creator: string, +): boolean => { + const isCreator = creator === user.navIdent; + const isFinished = oppgave.avsluttetAvSaksbehandlerDate !== null; + const isAssigned = oppgave.saksbehandler?.navIdent === user.navIdent; + + const canWrite = (!isFinished && isAssigned) || (isFinished && isCreator); if (templateId === TemplateIdEnum.ROL_ANSWERS) { return false; @@ -50,12 +62,13 @@ const saksbehandlerCanEdit = (templateId: TemplateIdEnum, oppgave: IOppgavebehan // When behandling is sent to ROL, saksbehandler can edit everything except questions. if (templateId === TemplateIdEnum.ROL_QUESTIONS) { return ( + canWrite && (oppgave.typeId === SaksTypeEnum.KLAGE || oppgave.typeId === SaksTypeEnum.ANKE) && oppgave.rol?.flowState !== FlowState.SENT ); } - return oppgave.medunderskriver?.flowState !== FlowState.SENT; + return canWrite && oppgave.medunderskriver?.flowState !== FlowState.SENT; }; const isMu = (oppgave: IOppgavebehandling, user: IUserData): boolean => @@ -71,18 +84,23 @@ const rolCanEdit = (templateId: TemplateIdEnum, oppgave: IOppgavebehandling): bo oppgave.rol.flowState === FlowState.SENT && templateId === TemplateIdEnum.ROL_ANSWERS; -export const useCanEditDocument = (templateId: TemplateIdEnum): boolean => { +export const useCanEditDocument = (templateId: TemplateIdEnum, creator: string): boolean => { const { data: oppgave, isSuccess } = useOppgave(); const { user } = useContext(StaticDataContext); return useMemo( - () => isSuccess && canEditDocument(templateId, oppgave, user), - [oppgave, isSuccess, templateId, user], + () => isSuccess && canEditDocument(templateId, oppgave, user, creator), + [oppgave, isSuccess, templateId, user, creator], ); }; const muCanEdit = (templateId: TemplateIdEnum, oppgave: IOppgavebehandling): boolean => oppgave.medunderskriver.flowState === FlowState.SENT && templateId !== TemplateIdEnum.ROL_ANSWERS; -export const canEditDocument = (templateId: TemplateIdEnum, oppgave: IOppgavebehandling, user: IUserData): boolean => - canManageDocument(templateId, oppgave, user) || (isMu(oppgave, user) && muCanEdit(templateId, oppgave)); +export const canEditDocument = ( + templateId: TemplateIdEnum, + oppgave: IOppgavebehandling, + user: IUserData, + creator: string, +): boolean => + canManageDocument(templateId, oppgave, user, creator) || (isMu(oppgave, user) && muCanEdit(templateId, oppgave)); diff --git a/frontend/src/components/smart-editor/tabbed-editors/editor.tsx b/frontend/src/components/smart-editor/tabbed-editors/editor.tsx index 6539eff4c..2d5c05b9a 100644 --- a/frontend/src/components/smart-editor/tabbed-editors/editor.tsx +++ b/frontend/src/components/smart-editor/tabbed-editors/editor.tsx @@ -68,10 +68,10 @@ interface LoadedEditorProps extends EditorProps { } const LoadedEditor = ({ oppgave, smartDocument, scalingGroup }: LoadedEditorProps) => { - const { id, templateId } = smartDocument; + const { id, templateId, creator } = smartDocument; const { newCommentSelection } = useContext(SmartEditorContext); const { user } = useContext(StaticDataContext); - const canEdit = useCanEditDocument(templateId); + const canEdit = useCanEditDocument(templateId, creator.employee.navIdent); const plugins = collaborationSaksbehandlerPlugins(oppgave.id, id, smartDocument, user); const editor = usePlateEditor({ @@ -295,8 +295,7 @@ interface EditorWithNewCommentAndFloatingToolbarProps { } const EditorWithNewCommentAndFloatingToolbar = ({ id, isConnected }: EditorWithNewCommentAndFloatingToolbarProps) => { - const { templateId, sheetRef } = useContext(SmartEditorContext); - const canEdit = useCanEditDocument(templateId); + const { sheetRef, canEdit } = useContext(SmartEditorContext); const [containerElement, setContainerElement] = useState(null); const lang = useSmartEditorSpellCheckLanguage(); diff --git a/frontend/src/components/smart-editor/tabbed-editors/tab-panel.tsx b/frontend/src/components/smart-editor/tabbed-editors/tab-panel.tsx index f81242ed2..7fd2682f0 100644 --- a/frontend/src/components/smart-editor/tabbed-editors/tab-panel.tsx +++ b/frontend/src/components/smart-editor/tabbed-editors/tab-panel.tsx @@ -16,7 +16,7 @@ export const TabPanel = ({ smartDocument }: TabPanelProps) => { const { id } = smartDocument; const smartDocumentRef = useRef(smartDocument); - const canEditDocument = useCanEditDocument(smartDocument.templateId); + const canEditDocument = useCanEditDocument(smartDocument.templateId, smartDocument.creator.employee.navIdent); const canEditDocumentRef = useRef(canEditDocument); // Ensure that smartDocumentRef and canEditDocumentRef are always up to date in order to avoid the unmount debounce triggering on archive/delete/fradeling diff --git a/frontend/src/plate/components/signature/hooks.ts b/frontend/src/plate/components/signature/hooks.ts index f1a0dfd80..f6494afc8 100644 --- a/frontend/src/plate/components/signature/hooks.ts +++ b/frontend/src/plate/components/signature/hooks.ts @@ -1,8 +1,14 @@ +import { SmartEditorContext } from '@app/components/smart-editor/context'; import { useOppgave } from '@app/hooks/oppgavebehandling/use-oppgave'; +import { getName, getTitle } from '@app/plate/components/signature/functions'; +import { MISSING_TITLE } from '@app/plate/components/signature/title'; +import type { ISignature, SignatureElement } from '@app/plate/types'; import { useGetSignatureQuery } from '@app/redux-api/bruker'; +import type { ISignatureResponse } from '@app/types/bruker'; import { SaksTypeEnum } from '@app/types/kodeverk'; import { TemplateIdEnum } from '@app/types/smart-editor/template-enums'; import { skipToken } from '@reduxjs/toolkit/query'; +import { useContext } from 'react'; export const useMedunderskriverSignature = () => { const { data: oppgave } = useOppgave(); @@ -19,31 +25,47 @@ export const useMedunderskriverSignature = () => { return medunderskriverSignature; }; -export const useMainSignature = (template: TemplateIdEnum) => { +export const useMainSignature = (element: SignatureElement): ISignature | undefined => { const { data: oppgave } = useOppgave(); + const { templateId, creator } = useContext(SmartEditorContext); - const isRolAnswers = template === TemplateIdEnum.ROL_ANSWERS; + const isRolAnswers = templateId === TemplateIdEnum.ROL_ANSWERS; const isRolSakstype = oppgave?.typeId === SaksTypeEnum.KLAGE || oppgave?.typeId === SaksTypeEnum.ANKE; - const { data: saksbehandlerSignature } = useGetSignatureQuery( - !isRolAnswers && typeof oppgave?.saksbehandler?.navIdent === 'string' ? oppgave.saksbehandler.navIdent : skipToken, - ); - + const { data: creatorSignature } = useGetSignatureQuery(isRolAnswers ? skipToken : creator); + const { data: overrideSignature } = useGetSignatureQuery(element.overriddenSaksbehandler ?? skipToken); const { data: rolSignature } = useGetSignatureQuery( isRolAnswers && isRolSakstype ? (oppgave.rol.employee?.navIdent ?? skipToken) : skipToken, ); + if (element.anonymous) { + return { name: 'Nav klageinstans' }; + } + + const suffix = templateId !== TemplateIdEnum.ROL_ANSWERS && element.useSuffix ? 'saksbehandler' : undefined; + if (isRolAnswers) { - if (oppgave === undefined || !isRolSakstype || oppgave.rol.employee === null || rolSignature === undefined) { - return null; - } + return toSignature(rolSignature, element.useShortName, suffix); + } - return rolSignature; + if (element.overriddenSaksbehandler !== undefined) { + return toSignature(overrideSignature, element.useShortName, suffix); } - if (oppgave === undefined || oppgave.saksbehandler === null || saksbehandlerSignature === undefined) { - return null; + return toSignature(creatorSignature, element.useShortName, suffix); +}; + +const toSignature = ( + signature: ISignatureResponse | undefined, + useShortName: boolean, + suffix: string | undefined, +): ISignature | undefined => { + if (signature === undefined) { + return undefined; } - return saksbehandlerSignature; + return { + name: getName(signature, useShortName), + title: getTitle(signature.customJobTitle, suffix) ?? MISSING_TITLE, + }; }; diff --git a/frontend/src/plate/components/signature/individual-signature.tsx b/frontend/src/plate/components/signature/individual-signature.tsx index f77e63345..3c9757eac 100644 --- a/frontend/src/plate/components/signature/individual-signature.tsx +++ b/frontend/src/plate/components/signature/individual-signature.tsx @@ -1,7 +1,7 @@ import { SmartEditorContext } from '@app/components/smart-editor/context'; -import { getName, getTitle } from '@app/plate/components/signature/functions'; +import { getName } from '@app/plate/components/signature/functions'; import { useMainSignature, useMedunderskriverSignature } from '@app/plate/components/signature/hooks'; -import { type ISignature, type SignatureElement, useMyPlateEditorRef } from '@app/plate/types'; +import { type SignatureElement, useMyPlateEditorRef } from '@app/plate/types'; import { TemplateIdEnum } from '@app/types/smart-editor/template-enums'; import { setNodes } from '@udecode/plate-common'; import { useContext, useEffect, useMemo } from 'react'; @@ -14,41 +14,17 @@ interface Props { export const SaksbehandlerSignature = ({ element }: Props) => { const editor = useMyPlateEditorRef(); - const { templateId } = useContext(SmartEditorContext); - const saksbehandlerSignature = useMainSignature(templateId); - - const signature: ISignature | undefined = useMemo(() => { - if (saksbehandlerSignature === null) { - return undefined; - } - - const suffix = templateId !== TemplateIdEnum.ROL_ANSWERS && element.useSuffix ? 'saksbehandler' : undefined; - - if (saksbehandlerSignature.anonymous) { - return { name: 'Nav klageinstans' }; - } - - return { - name: getName(saksbehandlerSignature, element.useShortName), - title: getTitle(saksbehandlerSignature.customJobTitle, suffix) ?? MISSING_TITLE, - }; - }, [saksbehandlerSignature, templateId, element.useSuffix, element.useShortName]); + const signature = useMainSignature(element); useEffect(() => { - if (element.saksbehandler?.name === signature?.name && element.saksbehandler?.title === signature?.title) { + if ( + signature === element.saksbehandler || + (signature?.name === element.saksbehandler?.name && signature?.title === element.saksbehandler?.title) + ) { return; } - const data: Partial = { - useShortName: element.useShortName, - medunderskriver: element.medunderskriver, - saksbehandler: signature, - }; - - setNodes(editor, data, { - at: [], - match: (n) => n === element, - }); + setNodes(editor, { saksbehandler: signature }, { at: [], match: (n) => n === element }); }, [editor, element, signature]); if (signature === undefined) { diff --git a/frontend/src/plate/components/signature/signature.tsx b/frontend/src/plate/components/signature/signature.tsx index 66af723c2..fae7cfeca 100644 --- a/frontend/src/plate/components/signature/signature.tsx +++ b/frontend/src/plate/components/signature/signature.tsx @@ -1,3 +1,4 @@ +import { StaticDataContext } from '@app/components/app/static-data-context'; import { SmartEditorContext } from '@app/components/smart-editor/context'; import { useOppgave } from '@app/hooks/oppgavebehandling/use-oppgave'; import { AddNewParagraphs } from '@app/plate/components/common/add-new-paragraph-buttons'; @@ -6,7 +7,7 @@ import { MedunderskriverSignature, SaksbehandlerSignature } from '@app/plate/com import type { SignatureElement } from '@app/plate/types'; import { useGetMySignatureQuery } from '@app/redux-api/bruker'; import { TemplateIdEnum } from '@app/types/smart-editor/template-enums'; -import { setNodes } from '@udecode/plate-common'; +import { type SetNodesOptions, setNodes } from '@udecode/plate-common'; import { PlateElement, type PlateElementProps, useEditorReadOnly } from '@udecode/plate-common/react'; import { type InputHTMLAttributes, useContext } from 'react'; import { styled } from 'styled-components'; @@ -17,7 +18,8 @@ export const Signature = (props: PlateElementProps) => { const isReadOnly = useEditorReadOnly(); const { data: signature } = useGetMySignatureQuery(); const { data: oppgave } = useOppgave(); - const { canManage, templateId } = useContext(SmartEditorContext); + const { canManage, templateId, creator } = useContext(SmartEditorContext); + const { user } = useContext(StaticDataContext); if (oppgave === undefined || signature === undefined) { return null; @@ -33,9 +35,19 @@ export const Signature = (props: PlateElementProps) => { const hideAll = !(showForkortedeNavnCheckbox || showSuffixCheckbox || hasMedunderskriver); const { children, element, editor } = props; + const overriddenWithSelf = element.overriddenSaksbehandler === user.navIdent; + + const options: SetNodesOptions = { at: [], voids: true, mode: 'lowest', match: (n) => n === element }; const setSignatureProp = (prop: Partial) => - setNodes(editor, { ...element, ...prop }, { at: [], voids: true, mode: 'lowest', match: (n) => n === element }); + setNodes( + editor, + { ...prop, overriddenSaksbehandler: overriddenWithSelf ? element.overriddenSaksbehandler : undefined }, + options, + ); + + const setOverriddenSaksbehandler = (overriddenSaksbehandler: string | undefined) => + setNodes(editor, { overriddenSaksbehandler }, options); return ( {...props} asChild contentEditable={false}> @@ -79,6 +91,19 @@ export const Signature = (props: PlateElementProps) => { Bruk «/saksbehandler»-tittel ) : null} + + setOverriddenSaksbehandler(target.checked ? user.navIdent : undefined)} + > + Signer med mitt navn + )} diff --git a/frontend/src/plate/templates/helpers.ts b/frontend/src/plate/templates/helpers.ts index c67ee7c1e..ba2aca07e 100644 --- a/frontend/src/plate/templates/helpers.ts +++ b/frontend/src/plate/templates/helpers.ts @@ -145,6 +145,7 @@ export const createSignature = (): SignatureElement => ({ useShortName: false, includeMedunderskriver: true, useSuffix: true, + overriddenSaksbehandler: undefined, children: [{ text: '' }], threadIds: [], }); diff --git a/frontend/src/plate/types.ts b/frontend/src/plate/types.ts index a299d5d8d..8aed71a40 100644 --- a/frontend/src/plate/types.ts +++ b/frontend/src/plate/types.ts @@ -230,6 +230,7 @@ interface ISignatureContent { useShortName: boolean; useSuffix: boolean; includeMedunderskriver: boolean; + overriddenSaksbehandler?: string; saksbehandler?: ISignature; medunderskriver?: ISignature; }