Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/#94 wachtperiodes #385

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Badge, field2Title } from "@maykin-ui/admin-ui";
import { Badge, Outline, field2Title } from "@maykin-ui/admin-ui";
import React from "react";

import { ProcessingStatus } from "../../lib/api/processingStatus";
import { timeAgo } from "../../lib/format/date";
import {
PROCESSING_STATUS_ICON_MAPPING,
PROCESSING_STATUS_LEVEL_MAPPING,
Expand All @@ -10,17 +11,45 @@ import {

type ProcessingStatusBadgeProps = {
processingStatus: ProcessingStatus;
plannedDestructionDate?: string | null;
};

export const ProcessingStatusBadge: React.FC<ProcessingStatusBadgeProps> = ({
processingStatus,
plannedDestructionDate,
}) => {
const getLevel = () => {
if (processingStatus === "new" && plannedDestructionDate) {
return "warning";
}
return PROCESSING_STATUS_LEVEL_MAPPING[processingStatus];
};

const getStatusIcon = () => {
if (processingStatus === "new" && plannedDestructionDate) {
return <Outline.ClockIcon />;
}
return PROCESSING_STATUS_ICON_MAPPING[processingStatus];
};

const getStatusText = () => {
if (processingStatus === "new" && plannedDestructionDate) {
const isPlannedDestructionDateInPast =
new Date(plannedDestructionDate) < new Date();
if (isPlannedDestructionDateInPast) {
return `Wordt vernietigd`;
}
return `Wordt vernietigd ${timeAgo(plannedDestructionDate, { shortFormat: true })}`;
}
return field2Title(PROCESSING_STATUS_MAPPING[processingStatus], {
unHyphen: false,
});
};

return (
<Badge level={PROCESSING_STATUS_LEVEL_MAPPING[processingStatus]}>
{PROCESSING_STATUS_ICON_MAPPING[processingStatus]}
{field2Title(PROCESSING_STATUS_MAPPING[processingStatus], {
unHyphen: false,
})}
<Badge level={getLevel()}>
{getStatusIcon()}
{getStatusText()}
</Badge>
);
};
1 change: 1 addition & 0 deletions frontend/src/fixtures/destructionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const FIXTURE_DESTRUCTION_LIST: DestructionList = {
containsSensitiveInfo: false,
status: "changes_requested",
processingStatus: "new",
plannedDestructionDate: null,
assignees: defaultAssignees,
assignee: defaultAssignees[0].user,
created: "2024-07-11T16:57",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/fixtures/destructionListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,23 @@ export const FIXTURE_DESTRUCTION_LIST_ITEM: DestructionListItem = {
extraZaakData: null,
zaak: zaakFactory(),
processingStatus: "new",
plannedDestructionDate: null,
};
export const FIXTURE_DESTRUCTION_LIST_ITEM_DELETED: DestructionListItem = {
pk: 2,
status: "suggested",
extraZaakData: null,
zaak: null,
processingStatus: "succeeded",
plannedDestructionDate: "2026-01-01T00:00:00Z",
};
export const FIXTURE_DESTRUCTION_LIST_ITEM_FAILED: DestructionListItem = {
pk: 3,
status: "suggested",
extraZaakData: null,
zaak: zaakFactory(),
processingStatus: "failed",
plannedDestructionDate: "2026-01-01T00:00:00Z",
};

export const destructionListItemFactory = createObjectFactory(
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/lib/api/destructionLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type DestructionList = {
assignees: DestructionListAssignee[];
author: User;
containsSensitiveInfo: boolean;
plannedDestructionDate: string | null;
created: string;
name: string;
status: DestructionListStatus;
Expand Down Expand Up @@ -220,3 +221,19 @@ export async function reassignDestructionList(
) {
return request("POST", `/destruction-lists/${uuid}/reassign/`, {}, data);
}

/**
* Abort the destruction of a destruction list.
* @param uuid
*/
export async function abortPlannedDestruction(
uuid: string,
data: { comment: string },
) {
return request(
"POST",
`/destruction-lists/${uuid}/abort_destruction/`,
{},
data,
);
}
1 change: 1 addition & 0 deletions frontend/src/lib/api/destructionListsItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type DestructionListItem = {
extraZaakData?: Record<string, unknown> | null;
zaak: Zaak | null;
processingStatus: ProcessingStatus;
plannedDestructionDate: string | null;
};

export interface ZaakItem extends Zaak {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/auth/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ export function canUpdateDestructionList(
return false;
}

if (
destructionList.status === "ready_to_delete" &&
destructionList.plannedDestructionDate &&
destructionList.processingStatus === "new"
) {
return false;
}

if (!STATUSES_ELIGIBLE_FOR_EDIT.includes(destructionList.status)) {
return false;
}
Expand Down
67 changes: 62 additions & 5 deletions frontend/src/lib/format/date.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("timeAgo()", () => {
beforeEach(mockDate);
afterEach(unMockDate);

// Year tests (past and future)
test("timeAgo() handles year ", () => {
const yearAgo = new Date("2022-09-15:00:00");
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
Expand All @@ -27,8 +28,17 @@ describe("timeAgo()", () => {
const yearsAgo = new Date("2021-09-15:00:00");
expect(timeAgo(yearsAgo)).toBe("2 jaren geleden");
expect(timeAgo(yearsAgo, { shortFormat: true })).toBe("2j");

const yearAhead = new Date("2024-09-15:00:00");
expect(timeAgo(yearAhead)).toBe("over 1 jaar");
expect(timeAgo(yearAhead, { shortFormat: true })).toBe("over 1j");

const yearsAhead = new Date("2025-09-15:00:00");
expect(timeAgo(yearsAhead)).toBe("over 2 jaren");
expect(timeAgo(yearsAhead, { shortFormat: true })).toBe("over 2j");
});

// Month tests (past and future)
test("timeAgo() handles month ", () => {
const monthAgo = new Date("2023-08-15:00:00");
expect(timeAgo(monthAgo)).toBe("1 maand geleden");
Expand All @@ -37,8 +47,17 @@ describe("timeAgo()", () => {
const monthsAgo = new Date("2023-07-15:00:00");
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");

const monthAhead = new Date("2023-10-15:00:00");
expect(timeAgo(monthAhead)).toBe("over 1 maand");
expect(timeAgo(monthAhead, { shortFormat: true })).toBe("over 1ma");

const monthsAhead = new Date("2023-11-15:00:00");
expect(timeAgo(monthsAhead)).toBe("over 2 maanden");
expect(timeAgo(monthsAhead, { shortFormat: true })).toBe("over 2ma");
});

// Day tests (past and future)
test("timeAgo() handles day ", () => {
const dayAgo = new Date("2023-09-14:00:00");
expect(timeAgo(dayAgo)).toBe("1 dag geleden");
Expand All @@ -47,8 +66,17 @@ describe("timeAgo()", () => {
const daysAgo = new Date("2023-09-13:00:00");
expect(timeAgo(daysAgo)).toBe("2 dagen geleden");
expect(timeAgo(daysAgo, { shortFormat: true })).toBe("2d");

const dayAhead = new Date("2023-09-16:00:00");
expect(timeAgo(dayAhead)).toBe("over 1 dag");
expect(timeAgo(dayAhead, { shortFormat: true })).toBe("over 1d");

const daysAhead = new Date("2023-09-17:00:00");
expect(timeAgo(daysAhead)).toBe("over 2 dagen");
expect(timeAgo(daysAhead, { shortFormat: true })).toBe("over 2d");
});

// Hour tests (past and future)
test("timeAgo() handles hour ", () => {
const hourAgo = new Date("2023-09-14:23:00");
expect(timeAgo(hourAgo)).toBe("1 uur geleden");
Expand All @@ -57,8 +85,17 @@ describe("timeAgo()", () => {
const hoursAgo = new Date("2023-09-14:22:00");
expect(timeAgo(hoursAgo)).toBe("2 uur geleden");
expect(timeAgo(hoursAgo, { shortFormat: true })).toBe("2u");

const hourAhead = new Date("2023-09-15:01:00");
expect(timeAgo(hourAhead)).toBe("over 1 uur");
expect(timeAgo(hourAhead, { shortFormat: true })).toBe("over 1u");

const hoursAhead = new Date("2023-09-15:02:00");
expect(timeAgo(hoursAhead)).toBe("over 2 uur");
expect(timeAgo(hoursAhead, { shortFormat: true })).toBe("over 2u");
});

// Minute tests (past and future)
test("timeAgo() handles minute ", () => {
const minuteAgo = new Date("2023-09-14:23:59");
expect(timeAgo(minuteAgo)).toBe("1 minuut geleden");
Expand All @@ -67,8 +104,17 @@ describe("timeAgo()", () => {
const minutesAgo = new Date("2023-09-14:23:58");
expect(timeAgo(minutesAgo)).toBe("2 minuten geleden");
expect(timeAgo(minutesAgo, { shortFormat: true })).toBe("2m");

const minuteAhead = new Date("2023-09-15:00:01");
expect(timeAgo(minuteAhead)).toBe("over 1 minuut");
expect(timeAgo(minuteAhead, { shortFormat: true })).toBe("over 1m");

const minutesAhead = new Date("2023-09-15:00:02");
expect(timeAgo(minutesAhead)).toBe("over 2 minuten");
expect(timeAgo(minutesAhead, { shortFormat: true })).toBe("over 2m");
});

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

test("timeAgo() interprets future data as now ", () => {
const yearFromNow = new Date("2024-09-15:00:00");
expect(timeAgo(yearFromNow)).toBe("Nu");
expect(timeAgo(yearFromNow, { shortFormat: true })).toBe("0m");
const secondAhead = new Date("2023-09-15:00:00:01");
expect(timeAgo(secondAhead)).toBe("zo meteen");
expect(timeAgo(secondAhead, { shortFormat: true })).toBe("0m");

const secondsAhead = new Date("2023-09-15:00:00:02");
expect(timeAgo(secondsAhead)).toBe("zo meteen");
expect(timeAgo(secondsAhead, { shortFormat: true })).toBe("0m");
});

// Combined scenarios (past and future)
test("timeAgo() handles combined scenario ", () => {
const yearAgo = new Date("2022-08-14:23:59:59");
expect(timeAgo(yearAgo)).toBe("1 jaar geleden");
Expand All @@ -93,6 +142,14 @@ describe("timeAgo()", () => {
const monthsAgo = new Date("2023-07-13:22:58:58");
expect(timeAgo(monthsAgo)).toBe("2 maanden geleden");
expect(timeAgo(monthsAgo, { shortFormat: true })).toBe("2ma");

const yearAhead = new Date("2025-08-14:23:59:59");
expect(timeAgo(yearAhead)).toBe("over 1 jaar");
expect(timeAgo(yearAhead, { shortFormat: true })).toBe("over 1j");

const monthsAhead = new Date("2023-11-13:22:58:58");
expect(timeAgo(monthsAhead)).toBe("over 1 maand");
expect(timeAgo(monthsAhead, { shortFormat: true })).toBe("over 1ma");
});
});

Expand Down
36 changes: 29 additions & 7 deletions frontend/src/lib/format/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ interface TimeAgoOptions {
}

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

// Check for invalid date input
if (isNaN(date.getTime())) {
throw new Error("Invalid date input");
throw new Error("Ongeldige datum input");
}

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

let result = "";
let isFuture = false;

if (seconds < 0) {
isFuture = true;
seconds = Math.abs(seconds);
}

// Special case for "Nu" or "zo meteen"
if (seconds < 60) {
return shortFormat ? "0m" : isFuture ? "zo meteen" : "Nu";
}

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

result += `${intervalCount}${shortFormat ? "" : " "}${label}${shortFormat ? "" : " geleden"}`;
// Update seconds to the remainder for the next interval
seconds %= interval.seconds;
// Check if it's future or past
if (isFuture) {
result = `over ${intervalCount}${shortFormat ? "" : " "}${label}`;
} else {
// Special case to not include "geleden" for the short format
if (shortFormat) {
result = `${intervalCount}${shortFormat ? "" : " "}${label}`;
} else {
result = `${intervalCount}${shortFormat ? "" : " "}${label} geleden`;
}
}
Xaohs marked this conversation as resolved.
Show resolved Hide resolved
break;
}
}

// Return the result or default to "just now" or "0m" for short format
return result.trim() || (shortFormat ? "0m" : "Nu");
// Return the formatted time difference
return result.trim();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { JsonValue, TypedAction } from "../../../hooks";
import { User } from "../../../lib/api/auth";
import {
DestructionListItemUpdate,
abortPlannedDestruction,
destroyDestructionList,
markDestructionListAsFinal,
markDestructionListAsReadyToReview,
Expand All @@ -19,6 +20,7 @@ import { clearZaakSelection } from "../../../lib/zaakSelection/zaakSelection";

export type UpdateDestructionListAction<P = JsonValue> = TypedAction<
| "DESTROY"
| "CANCEL_DESTROY"
| "MAKE_FINAL"
| "PROCESS_REVIEW"
| "READY_TO_REVIEW"
Expand Down Expand Up @@ -53,6 +55,8 @@ export async function destructionListUpdateAction({
return await destructionListUpdateReviewerAction({ request, params });
case "UPDATE_ZAKEN":
return await destructionListUpdateZakenAction({ request, params });
case "CANCEL_DESTROY":
return await destructionListCancelDestroyAction({ request, params });
default:
throw new Error("INVALID ACTION TYPE SPECIFIED!");
}
Expand Down Expand Up @@ -181,3 +185,16 @@ export async function destructionListUpdateZakenAction({

return redirect(`/destruction-lists/${params.uuid}/`);
}

export async function destructionListCancelDestroyAction({
request,
}: ActionFunctionArgs) {
const data = await request.json();
const { payload } = data as UpdateDestructionListAction<{
uuid: string;
comment: string;
}>;
const { comment, uuid } = payload;
await abortPlannedDestruction(uuid, { comment });
return redirect(`/destruction-lists/${uuid}/`);
}
Loading
Loading