Skip to content

Commit a37da0b

Browse files
authored
Merge pull request #385 from maykinmedia/issue/#94-watchperiodes
Issue/#94 wachtperiodes
2 parents 17687ca + 47b5ca4 commit a37da0b

File tree

13 files changed

+362
-52
lines changed

13 files changed

+362
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Badge, field2Title } from "@maykin-ui/admin-ui";
1+
import { Badge, Outline, field2Title } from "@maykin-ui/admin-ui";
22
import React from "react";
33

44
import { ProcessingStatus } from "../../lib/api/processingStatus";
5+
import { timeAgo } from "../../lib/format/date";
56
import {
67
PROCESSING_STATUS_ICON_MAPPING,
78
PROCESSING_STATUS_LEVEL_MAPPING,
@@ -10,17 +11,45 @@ import {
1011

1112
type ProcessingStatusBadgeProps = {
1213
processingStatus: ProcessingStatus;
14+
plannedDestructionDate?: string | null;
1315
};
1416

1517
export const ProcessingStatusBadge: React.FC<ProcessingStatusBadgeProps> = ({
1618
processingStatus,
19+
plannedDestructionDate,
1720
}) => {
21+
const getLevel = () => {
22+
if (processingStatus === "new" && plannedDestructionDate) {
23+
return "warning";
24+
}
25+
return PROCESSING_STATUS_LEVEL_MAPPING[processingStatus];
26+
};
27+
28+
const getStatusIcon = () => {
29+
if (processingStatus === "new" && plannedDestructionDate) {
30+
return <Outline.ClockIcon />;
31+
}
32+
return PROCESSING_STATUS_ICON_MAPPING[processingStatus];
33+
};
34+
35+
const getStatusText = () => {
36+
if (processingStatus === "new" && plannedDestructionDate) {
37+
const isPlannedDestructionDateInPast =
38+
new Date(plannedDestructionDate) < new Date();
39+
if (isPlannedDestructionDateInPast) {
40+
return `Wordt vernietigd`;
41+
}
42+
return `Wordt vernietigd ${timeAgo(plannedDestructionDate, { shortFormat: true })}`;
43+
}
44+
return field2Title(PROCESSING_STATUS_MAPPING[processingStatus], {
45+
unHyphen: false,
46+
});
47+
};
48+
1849
return (
19-
<Badge level={PROCESSING_STATUS_LEVEL_MAPPING[processingStatus]}>
20-
{PROCESSING_STATUS_ICON_MAPPING[processingStatus]}
21-
{field2Title(PROCESSING_STATUS_MAPPING[processingStatus], {
22-
unHyphen: false,
23-
})}
50+
<Badge level={getLevel()}>
51+
{getStatusIcon()}
52+
{getStatusText()}
2453
</Badge>
2554
);
2655
};

frontend/src/fixtures/destructionList.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const FIXTURE_DESTRUCTION_LIST: DestructionList = {
1111
containsSensitiveInfo: false,
1212
status: "changes_requested",
1313
processingStatus: "new",
14+
plannedDestructionDate: null,
1415
assignees: defaultAssignees,
1516
assignee: defaultAssignees[0].user,
1617
created: "2024-07-11T16:57",

frontend/src/fixtures/destructionListItem.ts

+3
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,23 @@ export const FIXTURE_DESTRUCTION_LIST_ITEM: DestructionListItem = {
1111
extraZaakData: null,
1212
zaak: zaakFactory(),
1313
processingStatus: "new",
14+
plannedDestructionDate: null,
1415
};
1516
export const FIXTURE_DESTRUCTION_LIST_ITEM_DELETED: DestructionListItem = {
1617
pk: 2,
1718
status: "suggested",
1819
extraZaakData: null,
1920
zaak: null,
2021
processingStatus: "succeeded",
22+
plannedDestructionDate: "2026-01-01T00:00:00Z",
2123
};
2224
export const FIXTURE_DESTRUCTION_LIST_ITEM_FAILED: DestructionListItem = {
2325
pk: 3,
2426
status: "suggested",
2527
extraZaakData: null,
2628
zaak: zaakFactory(),
2729
processingStatus: "failed",
30+
plannedDestructionDate: "2026-01-01T00:00:00Z",
2831
};
2932

3033
export const destructionListItemFactory = createObjectFactory(

frontend/src/lib/api/destructionLists.ts

+17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type DestructionList = {
1212
assignees: DestructionListAssignee[];
1313
author: User;
1414
containsSensitiveInfo: boolean;
15+
plannedDestructionDate: string | null;
1516
created: string;
1617
name: string;
1718
status: DestructionListStatus;
@@ -220,3 +221,19 @@ export async function reassignDestructionList(
220221
) {
221222
return request("POST", `/destruction-lists/${uuid}/reassign/`, {}, data);
222223
}
224+
225+
/**
226+
* Abort the destruction of a destruction list.
227+
* @param uuid
228+
*/
229+
export async function abortPlannedDestruction(
230+
uuid: string,
231+
data: { comment: string },
232+
) {
233+
return request(
234+
"POST",
235+
`/destruction-lists/${uuid}/abort_destruction/`,
236+
{},
237+
data,
238+
);
239+
}

frontend/src/lib/api/destructionListsItem.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type DestructionListItem = {
99
extraZaakData?: Record<string, unknown> | null;
1010
zaak: Zaak | null;
1111
processingStatus: ProcessingStatus;
12+
plannedDestructionDate: string | null;
1213
};
1314

1415
export interface ZaakItem extends Zaak {

frontend/src/lib/auth/permissions.ts

+8
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export function canUpdateDestructionList(
6363
return false;
6464
}
6565

66+
if (
67+
destructionList.status === "ready_to_delete" &&
68+
destructionList.plannedDestructionDate &&
69+
destructionList.processingStatus === "new"
70+
) {
71+
return false;
72+
}
73+
6674
if (!STATUSES_ELIGIBLE_FOR_EDIT.includes(destructionList.status)) {
6775
return false;
6876
}

frontend/src/lib/format/date.test.ts

+62-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe("timeAgo()", () => {
1919
beforeEach(mockDate);
2020
afterEach(unMockDate);
2121

22+
// Year tests (past and future)
2223
test("timeAgo() handles year ", () => {
2324
const yearAgo = new Date("2022-09-15:00:00");
2425
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
@@ -27,8 +28,17 @@ describe("timeAgo()", () => {
2728
const yearsAgo = new Date("2021-09-15:00:00");
2829
expect(timeAgo(yearsAgo)).toBe("2 jaren geleden");
2930
expect(timeAgo(yearsAgo, { shortFormat: true })).toBe("2j");
31+
32+
const yearAhead = new Date("2024-09-15:00:00");
33+
expect(timeAgo(yearAhead)).toBe("over 1 jaar");
34+
expect(timeAgo(yearAhead, { shortFormat: true })).toBe("over 1j");
35+
36+
const yearsAhead = new Date("2025-09-15:00:00");
37+
expect(timeAgo(yearsAhead)).toBe("over 2 jaren");
38+
expect(timeAgo(yearsAhead, { shortFormat: true })).toBe("over 2j");
3039
});
3140

41+
// Month tests (past and future)
3242
test("timeAgo() handles month ", () => {
3343
const monthAgo = new Date("2023-08-15:00:00");
3444
expect(timeAgo(monthAgo)).toBe("1 maand geleden");
@@ -37,8 +47,17 @@ describe("timeAgo()", () => {
3747
const monthsAgo = new Date("2023-07-15:00:00");
3848
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
3949
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");
50+
51+
const monthAhead = new Date("2023-10-15:00:00");
52+
expect(timeAgo(monthAhead)).toBe("over 1 maand");
53+
expect(timeAgo(monthAhead, { shortFormat: true })).toBe("over 1ma");
54+
55+
const monthsAhead = new Date("2023-11-15:00:00");
56+
expect(timeAgo(monthsAhead)).toBe("over 2 maanden");
57+
expect(timeAgo(monthsAhead, { shortFormat: true })).toBe("over 2ma");
4058
});
4159

60+
// Day tests (past and future)
4261
test("timeAgo() handles day ", () => {
4362
const dayAgo = new Date("2023-09-14:00:00");
4463
expect(timeAgo(dayAgo)).toBe("1 dag geleden");
@@ -47,8 +66,17 @@ describe("timeAgo()", () => {
4766
const daysAgo = new Date("2023-09-13:00:00");
4867
expect(timeAgo(daysAgo)).toBe("2 dagen geleden");
4968
expect(timeAgo(daysAgo, { shortFormat: true })).toBe("2d");
69+
70+
const dayAhead = new Date("2023-09-16:00:00");
71+
expect(timeAgo(dayAhead)).toBe("over 1 dag");
72+
expect(timeAgo(dayAhead, { shortFormat: true })).toBe("over 1d");
73+
74+
const daysAhead = new Date("2023-09-17:00:00");
75+
expect(timeAgo(daysAhead)).toBe("over 2 dagen");
76+
expect(timeAgo(daysAhead, { shortFormat: true })).toBe("over 2d");
5077
});
5178

79+
// Hour tests (past and future)
5280
test("timeAgo() handles hour ", () => {
5381
const hourAgo = new Date("2023-09-14:23:00");
5482
expect(timeAgo(hourAgo)).toBe("1 uur geleden");
@@ -57,8 +85,17 @@ describe("timeAgo()", () => {
5785
const hoursAgo = new Date("2023-09-14:22:00");
5886
expect(timeAgo(hoursAgo)).toBe("2 uur geleden");
5987
expect(timeAgo(hoursAgo, { shortFormat: true })).toBe("2u");
88+
89+
const hourAhead = new Date("2023-09-15:01:00");
90+
expect(timeAgo(hourAhead)).toBe("over 1 uur");
91+
expect(timeAgo(hourAhead, { shortFormat: true })).toBe("over 1u");
92+
93+
const hoursAhead = new Date("2023-09-15:02:00");
94+
expect(timeAgo(hoursAhead)).toBe("over 2 uur");
95+
expect(timeAgo(hoursAhead, { shortFormat: true })).toBe("over 2u");
6096
});
6197

98+
// Minute tests (past and future)
6299
test("timeAgo() handles minute ", () => {
63100
const minuteAgo = new Date("2023-09-14:23:59");
64101
expect(timeAgo(minuteAgo)).toBe("1 minuut geleden");
@@ -67,8 +104,17 @@ describe("timeAgo()", () => {
67104
const minutesAgo = new Date("2023-09-14:23:58");
68105
expect(timeAgo(minutesAgo)).toBe("2 minuten geleden");
69106
expect(timeAgo(minutesAgo, { shortFormat: true })).toBe("2m");
107+
108+
const minuteAhead = new Date("2023-09-15:00:01");
109+
expect(timeAgo(minuteAhead)).toBe("over 1 minuut");
110+
expect(timeAgo(minuteAhead, { shortFormat: true })).toBe("over 1m");
111+
112+
const minutesAhead = new Date("2023-09-15:00:02");
113+
expect(timeAgo(minutesAhead)).toBe("over 2 minuten");
114+
expect(timeAgo(minutesAhead, { shortFormat: true })).toBe("over 2m");
70115
});
71116

117+
// Less than a minute tests (past and future)
72118
test("timeAgo() handles less than a minute ", () => {
73119
const secondAgo = new Date("2023-09-14:23:59:59");
74120
expect(timeAgo(secondAgo)).toBe("Nu");
@@ -77,14 +123,17 @@ describe("timeAgo()", () => {
77123
const secondsAgo = new Date("2023-09-14:23:59:59");
78124
expect(timeAgo(secondsAgo)).toBe("Nu");
79125
expect(timeAgo(secondsAgo, { shortFormat: true })).toBe("0m");
80-
});
81126

82-
test("timeAgo() interprets future data as now ", () => {
83-
const yearFromNow = new Date("2024-09-15:00:00");
84-
expect(timeAgo(yearFromNow)).toBe("Nu");
85-
expect(timeAgo(yearFromNow, { shortFormat: true })).toBe("0m");
127+
const secondAhead = new Date("2023-09-15:00:00:01");
128+
expect(timeAgo(secondAhead)).toBe("zo meteen");
129+
expect(timeAgo(secondAhead, { shortFormat: true })).toBe("0m");
130+
131+
const secondsAhead = new Date("2023-09-15:00:00:02");
132+
expect(timeAgo(secondsAhead)).toBe("zo meteen");
133+
expect(timeAgo(secondsAhead, { shortFormat: true })).toBe("0m");
86134
});
87135

136+
// Combined scenarios (past and future)
88137
test("timeAgo() handles combined scenario ", () => {
89138
const yearAgo = new Date("2022-08-14:23:59:59");
90139
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
@@ -93,6 +142,14 @@ describe("timeAgo()", () => {
93142
const monthsAgo = new Date("2023-07-13:22:58:58");
94143
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
95144
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");
145+
146+
const yearAhead = new Date("2025-08-14:23:59:59");
147+
expect(timeAgo(yearAhead)).toBe("over 1 jaar");
148+
expect(timeAgo(yearAhead, { shortFormat: true })).toBe("over 1j");
149+
150+
const monthsAhead = new Date("2023-11-13:22:58:58");
151+
expect(timeAgo(monthsAhead)).toBe("over 1 maand");
152+
expect(timeAgo(monthsAhead, { shortFormat: true })).toBe("over 1ma");
96153
});
97154
});
98155

frontend/src/lib/format/date.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ interface TimeAgoOptions {
2323
}
2424

2525
/**
26-
* Calculate how long ago a given date was and return a human-readable string.
26+
* Calculate how long ago or how long until a given date and return a human-readable string in Dutch.
27+
* The date can be provided as a Date object or an ISO 8601 string.
28+
* Note that this function does currently not show dates like "1 jaar 1 maand 1 dag geleden", but would rather show "1 jaar geleden"
2729
* TODO: Consider using a specialized library.
2830
*
2931
* @param dateInput - The date to calculate the time difference from. It can be a Date object or an ISO 8601 string.
@@ -39,7 +41,7 @@ export function timeAgo(
3941

4042
// Check for invalid date input
4143
if (isNaN(date.getTime())) {
42-
throw new Error("Invalid date input");
44+
throw new Error("Ongeldige datum input");
4345
}
4446

4547
const now = new Date();
@@ -55,9 +57,21 @@ export function timeAgo(
5557
{ label: "dag", plural: "dagen", shortFormat: "d", seconds: 86400 },
5658
{ label: "uur", plural: "uur", shortFormat: "u", seconds: 3600 },
5759
{ label: "minuut", plural: "minuten", shortFormat: "m", seconds: 60 },
60+
{ label: "seconde", plural: "seconden", shortFormat: "s", seconds: 1 },
5861
];
5962

6063
let result = "";
64+
let isFuture = false;
65+
66+
if (seconds < 0) {
67+
isFuture = true;
68+
seconds = Math.abs(seconds);
69+
}
70+
71+
// Special case for "Nu" or "zo meteen"
72+
if (seconds < 60) {
73+
return shortFormat ? "0m" : isFuture ? "zo meteen" : "Nu";
74+
}
6175

6276
// Iterate over the intervals to determine the appropriate time unit
6377
for (const interval of intervals) {
@@ -70,15 +84,23 @@ export function timeAgo(
7084
? interval.label
7185
: interval.plural;
7286

73-
result += `${intervalCount}${shortFormat ? "" : " "}${label}${shortFormat ? "" : " geleden"}`;
74-
// Update seconds to the remainder for the next interval
75-
seconds %= interval.seconds;
87+
// Check if it's future or past
88+
if (isFuture) {
89+
result = `over ${intervalCount}${shortFormat ? "" : " "}${label}`;
90+
} else {
91+
// Special case to not include "geleden" for the short format
92+
if (shortFormat) {
93+
result = `${intervalCount}${shortFormat ? "" : " "}${label}`;
94+
} else {
95+
result = `${intervalCount}${shortFormat ? "" : " "}${label} geleden`;
96+
}
97+
}
7698
break;
7799
}
78100
}
79101

80-
// Return the result or default to "just now" or "0m" for short format
81-
return result.trim() || (shortFormat ? "0m" : "Nu");
102+
// Return the formatted time difference
103+
return result.trim();
82104
}
83105

84106
/**

frontend/src/pages/destructionlist/detail/DestructionListDetail.action.ts

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { JsonValue, TypedAction } from "../../../hooks";
55
import { User } from "../../../lib/api/auth";
66
import {
77
DestructionListItemUpdate,
8+
abortPlannedDestruction,
89
destroyDestructionList,
910
markDestructionListAsFinal,
1011
markDestructionListAsReadyToReview,
@@ -19,6 +20,7 @@ import { clearZaakSelection } from "../../../lib/zaakSelection/zaakSelection";
1920

2021
export type UpdateDestructionListAction<P = JsonValue> = TypedAction<
2122
| "DESTROY"
23+
| "CANCEL_DESTROY"
2224
| "MAKE_FINAL"
2325
| "PROCESS_REVIEW"
2426
| "READY_TO_REVIEW"
@@ -53,6 +55,8 @@ export async function destructionListUpdateAction({
5355
return await destructionListUpdateReviewerAction({ request, params });
5456
case "UPDATE_ZAKEN":
5557
return await destructionListUpdateZakenAction({ request, params });
58+
case "CANCEL_DESTROY":
59+
return await destructionListCancelDestroyAction({ request, params });
5660
default:
5761
throw new Error("INVALID ACTION TYPE SPECIFIED!");
5862
}
@@ -181,3 +185,16 @@ export async function destructionListUpdateZakenAction({
181185

182186
return redirect(`/destruction-lists/${params.uuid}/`);
183187
}
188+
189+
export async function destructionListCancelDestroyAction({
190+
request,
191+
}: ActionFunctionArgs) {
192+
const data = await request.json();
193+
const { payload } = data as UpdateDestructionListAction<{
194+
uuid: string;
195+
comment: string;
196+
}>;
197+
const { comment, uuid } = payload;
198+
await abortPlannedDestruction(uuid, { comment });
199+
return redirect(`/destruction-lists/${uuid}/`);
200+
}

0 commit comments

Comments
 (0)