added notification pagination
parent
1e1e08bdda
commit
b1ed49611c
|
@ -402,5 +402,16 @@
|
||||||
},
|
},
|
||||||
"noNotifications": "Keine Benachrichtigungen"
|
"noNotifications": "Keine Benachrichtigungen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"liveTimeAgo": {
|
||||||
|
"justNow": "Gerade eben",
|
||||||
|
"day": "Vor {{count}} Tag",
|
||||||
|
"day_plural": "Vor {{count}} Tage",
|
||||||
|
"hour": "Vor {{count}} Stunde",
|
||||||
|
"hour_plural": "Vor {{count}} Stunden",
|
||||||
|
"minute": "Vor {{count}} Minute",
|
||||||
|
"minute_plural": "Vor {{count}} Minuten",
|
||||||
|
"second": "Vor {{count}} Sekunde",
|
||||||
|
"second_plural": "Vor {{count}} Sekunden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -401,5 +401,16 @@
|
||||||
},
|
},
|
||||||
"noNotifications": "No notifications"
|
"noNotifications": "No notifications"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"liveTimeAgo": {
|
||||||
|
"justNow": "Just now",
|
||||||
|
"day": "{{count}} day ago",
|
||||||
|
"day_plural": "{{count}} days ago",
|
||||||
|
"hour": "{{count}} hour ago",
|
||||||
|
"hour_plural": "{{count}} hours ago",
|
||||||
|
"minute": "{{count}} minute ago",
|
||||||
|
"minute_plural": "{{count}} minutes ago",
|
||||||
|
"second": "{{count}} second ago",
|
||||||
|
"second_plural": "{{count}} seconds ago"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CheckOutlined,
|
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
@ -16,10 +15,12 @@ import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd";
|
||||||
import { Header } from "antd/es/layout/layout";
|
import { Header } from "antd/es/layout/layout";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useHeaderContext } from "../../Contexts/HeaderContext";
|
import { useHeaderContext } from "../../Contexts/HeaderContext";
|
||||||
import { FormatDatetime, myFetch } from "../../utils";
|
import { myFetch } from "../../utils";
|
||||||
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
|
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
|
||||||
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
|
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import LiveTimeAgo from "../LiveTimeAgo";
|
||||||
|
import MyPagination from "../MyPagination";
|
||||||
|
|
||||||
export default function HeaderMenu({
|
export default function HeaderMenu({
|
||||||
isSideMenuCollapsed,
|
isSideMenuCollapsed,
|
||||||
|
@ -31,17 +32,35 @@ export default function HeaderMenu({
|
||||||
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
|
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const fetchNotifications = (page = 1) => {
|
||||||
|
myFetch(`/notifications?page=${page}`, "GET").then((data) => {
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
headerContext.setNotificationResponse(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPaginationChange = (page) => {
|
||||||
|
headerContext.setPaginationPage(page);
|
||||||
|
headerContext.paginationPageRef.current = page;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fetch will only be called if the drawer is open and there are no notifications
|
// fetch will only be called if the drawer is open and there are no notifications
|
||||||
// further notifications will be fetched by the websocket
|
// further notifications will be fetched by the websocket
|
||||||
if (!isNotificationDrawerOpen || headerContext.notifications !== null)
|
if (!isNotificationDrawerOpen || headerContext.notficationResponse !== null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
myFetch("/notifications", "GET").then((data) =>
|
fetchNotifications(1);
|
||||||
headerContext.setNotifications(data.Notifications)
|
|
||||||
);
|
|
||||||
}, [isNotificationDrawerOpen]);
|
}, [isNotificationDrawerOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isNotificationDrawerOpen) return;
|
||||||
|
|
||||||
|
console.log("paginationPage", headerContext.paginationPage);
|
||||||
|
fetchNotifications(headerContext.paginationPage);
|
||||||
|
}, [headerContext.paginationPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
style={{
|
style={{
|
||||||
|
@ -105,41 +124,53 @@ export default function HeaderMenu({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{headerContext.totalNotifications === 0 ||
|
{isNotificationDrawerOpen && (
|
||||||
headerContext.notifications === null ? (
|
|
||||||
<div style={{ textAlign: "center" }}>
|
|
||||||
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
|
|
||||||
<Typography.Title level={5}>
|
|
||||||
{t("header.notificationDrawer.noNotifications")}
|
|
||||||
</Typography.Title>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<List
|
{headerContext.totalNotifications === 0 ||
|
||||||
dataSource={headerContext.notifications.sort((a, b) => {
|
headerContext.notficationResponse === null ? (
|
||||||
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
|
<div style={{ textAlign: "center" }}>
|
||||||
})}
|
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
|
||||||
renderItem={(item) => (
|
<Typography.Title level={5}>
|
||||||
<List.Item>
|
{t("header.notificationDrawer.noNotifications")}
|
||||||
<List.Item.Meta
|
</Typography.Title>
|
||||||
avatar={<NotificationTypeIcon type={item.Type} />}
|
</div>
|
||||||
title={item.Title}
|
) : (
|
||||||
description={FormatDatetime(item.CreatedAt)}
|
<List
|
||||||
|
dataSource={headerContext.notficationResponse.Notifications.sort(
|
||||||
|
(a, b) => {
|
||||||
|
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
footer={
|
||||||
|
<MyPagination
|
||||||
|
paginationPage={headerContext.paginationPage}
|
||||||
|
setPaginationPage={(page) => onPaginationChange(page)}
|
||||||
|
totalPages={headerContext.notficationResponse.TotalPages}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<NotificationTypeIcon type={item.Type} />}
|
||||||
|
title={item.Title}
|
||||||
|
description={<LiveTimeAgo startTime={item.CreatedAt} />}
|
||||||
|
/>
|
||||||
|
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
webSocketContext.SendSocketMessage(
|
webSocketContext.SendSocketMessage(
|
||||||
SentMessagesCommands.DeleteOneNotification,
|
SentMessagesCommands.DeleteOneNotification,
|
||||||
{
|
{
|
||||||
notificationId: item.Id,
|
notificationId: item.Id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormatDatetime } from "../../utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
// Calculate elapsed time in seconds, minutes, hours, or days
|
||||||
|
function calculatedTimeAgo(startTime, t) {
|
||||||
|
const currentTime = new Date();
|
||||||
|
const elapsedMilliseconds = currentTime - new Date(startTime);
|
||||||
|
|
||||||
|
let timeAgoText;
|
||||||
|
|
||||||
|
if (elapsedMilliseconds < 60000) {
|
||||||
|
const secondsAgo = Math.floor(elapsedMilliseconds / 1000);
|
||||||
|
timeAgoText =
|
||||||
|
secondsAgo === 0
|
||||||
|
? "just now"
|
||||||
|
: secondsAgo === 1
|
||||||
|
? t("liveTimeAgo.second", { count: secondsAgo })
|
||||||
|
: t("liveTimeAgo.second_plural", { count: secondsAgo });
|
||||||
|
} else if (elapsedMilliseconds < 3600000) {
|
||||||
|
const minutesAgo = Math.floor(elapsedMilliseconds / 60000);
|
||||||
|
timeAgoText =
|
||||||
|
minutesAgo === 1
|
||||||
|
? t("liveTimeAgo.minute", { count: minutesAgo })
|
||||||
|
: t("liveTimeAgo.minute_plural", { count: minutesAgo });
|
||||||
|
} else if (elapsedMilliseconds < 86400000) {
|
||||||
|
const hoursAgo = Math.floor(elapsedMilliseconds / 3600000);
|
||||||
|
timeAgoText =
|
||||||
|
hoursAgo === 1
|
||||||
|
? t("liveTimeAgo.hour", { count: hoursAgo })
|
||||||
|
: t("liveTimeAgo.hour_plural", { count: hoursAgo });
|
||||||
|
} else {
|
||||||
|
const daysAgo = Math.floor(elapsedMilliseconds / 86400000);
|
||||||
|
timeAgoText =
|
||||||
|
daysAgo === 1
|
||||||
|
? t("liveTimeAgo.day", { count: daysAgo })
|
||||||
|
: t("liveTimeAgo.day_plural", { count: daysAgo });
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeAgoText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LiveTimeAgo({ startTime }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [timeAgo, setTimeAgo] = useState(calculatedTimeAgo(startTime, t));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTimeAgo(calculatedTimeAgo(startTime, t));
|
||||||
|
}, 1000); // Update every second
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Clean up on component unmount
|
||||||
|
}, [startTime]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={FormatDatetime(startTime)}>
|
||||||
|
<span>{timeAgo}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,9 +5,11 @@ export default function MyPagination({
|
||||||
paginationPage,
|
paginationPage,
|
||||||
setPaginationPage,
|
setPaginationPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
size,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Pagination
|
<Pagination
|
||||||
|
size={size}
|
||||||
style={{ marginTop: AppStyle.app.margin, textAlign: "right" }}
|
style={{ marginTop: AppStyle.app.margin, textAlign: "right" }}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
current={paginationPage}
|
current={paginationPage}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { createContext, useContext, useState } from "react";
|
import { createContext, useContext, useRef, useState } from "react";
|
||||||
|
|
||||||
const preview = {
|
const preview = {
|
||||||
totalNotifications: 0,
|
totalNotifications: 0,
|
||||||
notifications: [],
|
notficationResponse: null,
|
||||||
|
paginationPage: 1,
|
||||||
|
paginationPageRef: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderContext = createContext(preview);
|
const HeaderContext = createContext(preview);
|
||||||
|
@ -11,16 +13,20 @@ export const useHeaderContext = () => useContext(HeaderContext);
|
||||||
|
|
||||||
export default function HeaderProvider({ children }) {
|
export default function HeaderProvider({ children }) {
|
||||||
const [totalNotifications, setTotalNotifications] = useState(0);
|
const [totalNotifications, setTotalNotifications] = useState(0);
|
||||||
// initially null, then set to [...] on first fetch
|
const [notficationResponse, setNotificationResponse] = useState(null);
|
||||||
const [notifications, setNotifications] = useState(null);
|
const [paginationPage, setPaginationPage] = useState(1);
|
||||||
|
const paginationPageRef = useRef(paginationPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderContext.Provider
|
<HeaderContext.Provider
|
||||||
value={{
|
value={{
|
||||||
totalNotifications,
|
totalNotifications,
|
||||||
setTotalNotifications,
|
setTotalNotifications,
|
||||||
notifications,
|
notficationResponse,
|
||||||
setNotifications,
|
setNotificationResponse,
|
||||||
|
paginationPage,
|
||||||
|
setPaginationPage,
|
||||||
|
paginationPageRef,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -109,7 +109,9 @@ export function handleWebSocketMessage(
|
||||||
groupTasksContext.setGroupTasks((arr) => {
|
groupTasksContext.setGroupTasks((arr) => {
|
||||||
const newArr = [...arr];
|
const newArr = [...arr];
|
||||||
|
|
||||||
if (newArr.length === 5) {
|
if (
|
||||||
|
newArr.length === Constants.GLOBALS.GROUP_TASKS_PAGINATION_LIMIT
|
||||||
|
) {
|
||||||
newArr.pop();
|
newArr.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -969,40 +971,66 @@ export function handleWebSocketMessage(
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ReceivedMessagesCommands.NewNotification:
|
case ReceivedMessagesCommands.NewNotification:
|
||||||
|
if (headerContext.paginationPageRef.current === 1) {
|
||||||
|
console.log("new body", body);
|
||||||
|
|
||||||
|
headerContext.setNotificationResponse((obj) => {
|
||||||
|
if (obj === null) return obj;
|
||||||
|
|
||||||
|
const newArr = [...obj.Notifications];
|
||||||
|
|
||||||
|
if (
|
||||||
|
newArr.length === Constants.GLOBALS.NOTIFICATIONS_PAGINATION_LIMIT
|
||||||
|
) {
|
||||||
|
newArr.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
newArr.unshift(body.Notification);
|
||||||
|
|
||||||
|
obj.Notifications = newArr;
|
||||||
|
obj.TotalPages = body.TotalPages;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
headerContext.setTotalNotifications(
|
headerContext.setTotalNotifications(
|
||||||
(totalNotifications) => totalNotifications + 1
|
(totalNotifications) => totalNotifications + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
headerContext.setNotifications((arr) => {
|
|
||||||
// only add notifications to the list if the list is not null
|
|
||||||
// this has to do with the get fetch that is executed when the list is empty
|
|
||||||
if (arr === null) return arr;
|
|
||||||
|
|
||||||
const newArr = [...arr];
|
|
||||||
|
|
||||||
newArr.push(body);
|
|
||||||
|
|
||||||
return newArr;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case ReceivedMessagesCommands.AllNotificationsDeleted:
|
case ReceivedMessagesCommands.AllNotificationsDeleted:
|
||||||
|
headerContext.setNotificationResponse(null);
|
||||||
headerContext.setTotalNotifications(0);
|
headerContext.setTotalNotifications(0);
|
||||||
|
headerContext.setPaginationPage(1);
|
||||||
headerContext.setNotifications([]);
|
|
||||||
break;
|
break;
|
||||||
case ReceivedMessagesCommands.OneNotificationDeleted:
|
case ReceivedMessagesCommands.OneNotificationDeleted:
|
||||||
headerContext.setTotalNotifications(
|
headerContext.setTotalNotifications(
|
||||||
(totalNotifications) => totalNotifications - 1
|
(totalNotifications) => totalNotifications - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
headerContext.setNotifications((arr) => {
|
headerContext.setNotificationResponse((obj) => {
|
||||||
if (arr === null) return arr;
|
if (obj === null) return obj;
|
||||||
|
|
||||||
let newArr = [...arr];
|
const newArr = [...obj.Notifications];
|
||||||
|
|
||||||
newArr = newArr.filter((notification) => notification.Id !== body);
|
const arrIndex = newArr.findIndex(
|
||||||
|
(notification) => notification.Id === body
|
||||||
|
);
|
||||||
|
|
||||||
return newArr;
|
if (arrIndex !== -1) {
|
||||||
|
newArr.splice(arrIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Notifications = newArr;
|
||||||
|
|
||||||
|
if (newArr.length === 0) {
|
||||||
|
const newPaginationPage = headerContext.paginationPageRef.current - 1;
|
||||||
|
|
||||||
|
headerContext.setPaginationPage(newPaginationPage);
|
||||||
|
headerContext.paginationPageRef.current = newPaginationPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -76,6 +76,7 @@ export const Constants = {
|
||||||
MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH: 2000,
|
MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH: 2000,
|
||||||
EQUIPMENT_DOCUMENTATIONS_PAGINATION_LIMIT: 3,
|
EQUIPMENT_DOCUMENTATIONS_PAGINATION_LIMIT: 3,
|
||||||
GROUP_TASKS_PAGINATION_LIMIT: 5,
|
GROUP_TASKS_PAGINATION_LIMIT: 5,
|
||||||
|
NOTIFICATIONS_PAGINATION_LIMIT: 10,
|
||||||
},
|
},
|
||||||
MAX_AVATAR_SIZE: 5 * 1024 * 1024,
|
MAX_AVATAR_SIZE: 5 * 1024 * 1024,
|
||||||
ACCEPTED_AVATAR_FILE_TYPES: [
|
ACCEPTED_AVATAR_FILE_TYPES: [
|
||||||
|
|
Loading…
Reference in New Issue