Skip to content

Commit 25df141

Browse files
#497 - feat: add status indicator for destruction list co review
1 parent 15c5263 commit 25df141

File tree

10 files changed

+439
-74
lines changed

10 files changed

+439
-74
lines changed

backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_co_review.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async def test_scenario_co_reviewer_select_zaken_visible_to_reviewer(self):
3737
await self.when.user_clicks_button(page, "Destruction list to co-review")
3838
await self.then.page_should_contain_text(page, "Geaccordeerd")
3939

40-
# Co-reviewer rejects see second case.
40+
# Co-reviewer rejects second case.
4141
await self.when.user_clicks_button(page, "Uitzonderen")
4242
await self.when.user_fills_form_field(page, "Reden", "gh-448")
4343
await self.when.user_clicks_button(page, "Zaak uitzonderen")
@@ -50,3 +50,21 @@ async def test_scenario_co_reviewer_select_zaken_visible_to_reviewer(self):
5050
await self.when.reviewer_logs_in(page)
5151
await self.when.user_clicks_button(page, "Destruction list to co-review")
5252
await self.then.page_should_contain_text(page, "Uitgezonderd")
53+
54+
# Log out.
55+
await self.when.user_logs_out(page)
56+
57+
# Co-reviewer finishes co-review
58+
await self.when.co_reviewer_logs_in(page)
59+
await self.when.user_clicks_button(page, "Destruction list to co-review")
60+
await self.when.user_clicks_button(page, "Medebeoordeling afronden")
61+
await self.when.user_fills_form_field(page, "Opmerking", "gh-497")
62+
await self.when.user_clicks_button(page, "Medebeoordeling afronden", 1)
63+
64+
# Log out.
65+
await self.when.user_logs_out(page)
66+
67+
# Reviewer should see review completed.
68+
await self.when.reviewer_logs_in(page)
69+
await self.when.user_clicks_button(page, "Destruction list to co-review")
70+
await self.then.page_should_contain_element_with_title(page, "Medebeoordelaar is klaar met beoordelen")

backend/src/openarchiefbeheer/utils/tests/gherkin.py

+4
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,10 @@ async def page_should_contain_text(self, page, text, timeout=None):
523523
element = page.locator(f"text={text}")
524524
await expect(element.nth(0)).to_be_visible(timeout=timeout)
525525

526+
async def page_should_contain_element_with_title(self, page, title):
527+
element = page.get_by_title(title)
528+
await expect(element).to_be_visible()
529+
526530
async def path_should_be(self, page, path):
527531
await self.url_should_be(page, self.testcase.live_server_url + path)
528532

frontend/.storybook/mockData.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { auditLogFactory } from "../src/fixtures/auditLog";
2+
import { coReviewsFactory } from "../src/fixtures/coReview";
23
import { destructionListAssigneesFactory } from "../src/fixtures/destructionList";
34
import { FIXTURE_SELECTIELIJSTKLASSE_CHOICES } from "../src/fixtures/selectieLijstKlasseChoices";
45
import { userFactory, usersFactory } from "../src/fixtures/user";
@@ -11,6 +12,18 @@ export const MOCKS = {
1112
status: 200,
1213
response: auditLogFactory(),
1314
},
15+
CO_REVIEWS: {
16+
url: "http://localhost:8000/api/v1/destruction-list-co-reviews/?destructionList__uuid=00000000-0000-0000-0000-000000000000",
17+
method: "GET",
18+
status: 200,
19+
response: coReviewsFactory(),
20+
},
21+
CO_REVIEW_CREATE: {
22+
url: "http://localhost:8000/api/v1/destruction-list-co-reviews/?destructionList__uuid=00000000-0000-0000-0000-000000000000",
23+
method: "POST",
24+
status: 201,
25+
response: {},
26+
},
1427
DESTRUCTION_LIST_MAKE_FINAL: {
1528
url: "http://localhost:8000/api/v1/destruction-lists/00000000-0000-0000-0000-000000000000/make_final",
1629
method: "POST",

frontend/src/components/DestructionListReviewer/DestructionListReviewer.stories.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Meta, ReactRenderer, StoryObj } from "@storybook/react";
22
import { expect, userEvent, waitFor, within } from "@storybook/test";
33
import { PlayFunction } from "@storybook/types";
4+
import exp from "node:constants";
45
import { createMock, getMock } from "storybook-addon-module-mock";
56

67
import {
78
ClearSessionStorageDecorator,
89
ReactRouterDecorator,
910
} from "../../../.storybook/decorators";
1011
import { fillForm } from "../../../.storybook/playFunctions";
12+
import { coReviewFactory } from "../../fixtures/coReview";
1113
import { destructionListFactory } from "../../fixtures/destructionList";
1214
import {
1315
beoordelaarFactory,
@@ -262,3 +264,58 @@ export const ReviewerCanReassignCoReviewers: Story = {
262264
await assertEditCoReviewers(context);
263265
},
264266
};
267+
268+
export const CoReviewStatusVisible: Story = {
269+
args: { destructionList: DESTRUCTION_LIST_READY_TO_REVIEW },
270+
parameters: {
271+
moduleMock: {
272+
mock: () => {
273+
const reassignDestructionList = createMock(
274+
libDestructionList,
275+
"reassignDestructionList",
276+
);
277+
reassignDestructionList.mockImplementation(
278+
async () => ({}) as Response,
279+
);
280+
281+
const updateCoReviewers = createMock(
282+
libDestructionList,
283+
"updateCoReviewers",
284+
);
285+
updateCoReviewers.mockImplementation(async () => ({}) as Response);
286+
287+
const useWhoAmI = createMock(hooksUseWhoAmI, "useWhoAmI");
288+
useWhoAmI.mockImplementation(() => beoordelaarFactory());
289+
290+
const useCoReviewers = createMock(hooksUseWhoAmI, "useCoReviewers");
291+
useCoReviewers.mockImplementation(() => [
292+
REVIEWER2,
293+
REVIEWER3,
294+
REVIEWER4,
295+
]);
296+
297+
const useDestructionListCoReviewers = createMock(
298+
hooksUseWhoAmI,
299+
"useDestructionListCoReviewers",
300+
);
301+
useDestructionListCoReviewers.mockImplementation(() => [
302+
{ user: REVIEWER2, role: "co_reviewer" },
303+
]);
304+
305+
const useCoReviews = createMock(hooksUseWhoAmI, "useCoReviews");
306+
useCoReviews.mockImplementation(() => [
307+
coReviewFactory({ author: REVIEWER2 }),
308+
]);
309+
310+
return [reassignDestructionList, updateCoReviewers, useWhoAmI];
311+
},
312+
},
313+
},
314+
play: async (context) => {
315+
const canvas = within(context.canvasElement);
316+
const element = await canvas.getByTitle(
317+
"Medebeoordelaar is klaar met beoordelen",
318+
);
319+
await expect(element).toBeVisible();
320+
},
321+
};

frontend/src/components/DestructionListReviewer/DestructionListReviewer.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
useAlert,
99
useFormDialog,
1010
} from "@maykin-ui/admin-ui";
11-
import { useMemo } from "react";
11+
import React, { useMemo } from "react";
1212
import { useNavigation, useRevalidator } from "react-router-dom";
1313

1414
import {
1515
useCoReviewers,
16+
useCoReviews,
1617
useDestructionListCoReviewers,
1718
useReviewers,
1819
useWhoAmI,
@@ -45,6 +46,7 @@ export function DestructionListReviewer({
4546
const revalidator = useRevalidator();
4647
const alert = useAlert();
4748
const formDialog = useFormDialog();
49+
const coReviews = useCoReviews(destructionList);
4850
const reviewers = useReviewers();
4951
const coReviewers = useCoReviewers();
5052
const assignedCoReviewers = useDestructionListCoReviewers(destructionList);
@@ -169,9 +171,23 @@ export function DestructionListReviewer({
169171
() =>
170172
assignedCoReviewers.reduce((acc, coReviewer, i) => {
171173
const key = `Medebeoordelaar ${1 + i}`;
174+
const hasReview = coReviews.find(
175+
(coReview) => coReview.author?.pk === coReviewer.user.pk,
176+
);
177+
const icon = hasReview && <Solid.CheckCircleIcon />;
178+
172179
return {
173180
...acc,
174-
[key]: { label: key, value: formatUser(coReviewer.user) },
181+
[key]: {
182+
label: key,
183+
value: (
184+
<P title={hasReview && "Medebeoordelaar is klaar met beoordelen"}>
185+
{formatUser(coReviewer.user)}
186+
&nbsp;
187+
{icon}
188+
</P>
189+
),
190+
},
175191
};
176192
}, {}),
177193
[assignedCoReviewers],

frontend/src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./useAuditLog";
22
export * from "./useCombinedSearchParams";
33
export * from "./useCoReviewers";
4+
export * from "./useCoReviews";
45
export * from "./useDestructionListCoReviewers";
56
export * from "./useFields";
67
export * from "./useFilter";

frontend/src/hooks/useCoReviews.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { CoReview, listCoReviews } from "../lib/api/coReview";
4+
import { DestructionList } from "../lib/api/destructionLists";
5+
import { useAlertOnError } from "./useAlertOnError";
6+
7+
/**
8+
* Hook resolving co reviews
9+
*/
10+
export function useCoReviews(destructionList?: DestructionList): CoReview[] {
11+
const alertOnError = useAlertOnError(
12+
"Er is een fout opgetreden bij het ophalen van de mede beoordelingen!",
13+
);
14+
15+
const [valueState, setValueState] = useState<CoReview[]>([]);
16+
useEffect(() => {
17+
if (!destructionList) {
18+
setValueState([]);
19+
return;
20+
}
21+
22+
listCoReviews({ destructionList__uuid: destructionList.uuid })
23+
.then((v) => setValueState(v))
24+
.catch(alertOnError);
25+
}, [destructionList?.uuid]);
26+
27+
return valueState;
28+
}

frontend/src/pages/destructionlist/review/DestructionListReview.action.ts

+46-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ActionFunctionArgs, redirect } from "react-router-dom";
22

33
import { TypedAction } from "../../../hooks";
4+
import { CoReview, createCoReview } from "../../../lib/api/coReview";
45
import {
56
Review,
67
ZaakReview,
@@ -17,22 +18,31 @@ export type DestructionListReviewActionContext = {
1718
};
1819

1920
export type ReviewDestructionListAction =
20-
| TypedAction<"APPROVE_LIST", ReviewDestructionListListApproveActionPayLoad>
21-
| TypedAction<"REJECT_LIST", ReviewDestructionListListRejectActionPayLoad>;
21+
| TypedAction<"APPROVE_LIST", ReviewDestructionListApproveActionPayLoad>
22+
| TypedAction<"REJECT_LIST", ReviewDestructionListRejectActionPayLoad>
23+
| TypedAction<
24+
"COMPLETE_CO_REVIEW",
25+
ReviewDestructionListCompleteCoReviewPayload
26+
>;
2227

23-
export type ReviewDestructionListListApproveActionPayLoad = {
28+
export type ReviewDestructionListApproveActionPayLoad = {
2429
comment: string;
2530
destructionList: string;
2631
status: string;
2732
};
2833

29-
export type ReviewDestructionListListRejectActionPayLoad = {
34+
export type ReviewDestructionListRejectActionPayLoad = {
3035
comment: string;
3136
destructionList: string;
3237
status: string;
3338
zaakReviews?: ZaakReview[];
3439
};
3540

41+
export type ReviewDestructionListCompleteCoReviewPayload = {
42+
comment: string;
43+
destructionList: string;
44+
};
45+
3646
/**
3747
* React Router action.
3848
* @param request
@@ -50,6 +60,8 @@ export const destructionListReviewAction = async ({
5060
return destructionListApproveListAction({ request, params });
5161
case "REJECT_LIST":
5262
return destructionListRejectListAction({ request, params });
63+
case "COMPLETE_CO_REVIEW":
64+
return destructionListCompleteCoReviewAction({ request, params });
5365
}
5466
};
5567

@@ -63,7 +75,7 @@ export async function destructionListApproveListAction({
6375
}: ActionFunctionArgs) {
6476
const { payload } = await request.json();
6577
const { comment, destructionList, status } =
66-
payload as ReviewDestructionListListApproveActionPayLoad;
78+
payload as ReviewDestructionListApproveActionPayLoad;
6779

6880
const data: Review = {
6981
destructionList: destructionList,
@@ -100,7 +112,7 @@ export async function destructionListRejectListAction({
100112
}: ActionFunctionArgs) {
101113
const { payload } = await request.json();
102114
const { comment, destructionList, status, zaakReviews } =
103-
payload as ReviewDestructionListListRejectActionPayLoad;
115+
payload as ReviewDestructionListRejectActionPayLoad;
104116

105117
const data: Review = {
106118
destructionList: destructionList,
@@ -127,3 +139,31 @@ export async function destructionListRejectListAction({
127139
}
128140
return redirect("/");
129141
}
142+
143+
/**
144+
* React Router action, user intends to complete a co-review.
145+
* @param request
146+
* @param params
147+
*/
148+
export async function destructionListCompleteCoReviewAction({
149+
request,
150+
}: ActionFunctionArgs) {
151+
const { payload } = await request.json();
152+
const { comment, destructionList } =
153+
payload as ReviewDestructionListCompleteCoReviewPayload;
154+
155+
const data: CoReview = {
156+
destructionList: destructionList,
157+
listFeedback: comment,
158+
};
159+
160+
try {
161+
await createCoReview(data);
162+
} catch (e: unknown) {
163+
if (e instanceof Response) {
164+
return await (e as Response).json();
165+
}
166+
throw e;
167+
}
168+
return redirect("/");
169+
}

0 commit comments

Comments
 (0)