notification admin
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
VITE_API_BASE_URL = 'https://negareh-api.dev.danakcorp.com'
|
||||
# VITE_API_BASE_URL = 'http://10.56.147.88:4000'
|
||||
# VITE_API_BASE_URL = 'https://negareh-api.dev.danakcorp.com'
|
||||
VITE_API_BASE_URL = 'http://10.29.35.88:4000'
|
||||
VITE_TOKEN_NAME = 'negareh_at'
|
||||
VITE_REFRESH_TOKEN_NAME = 'negareh_art'
|
||||
|
||||
+17
-1
@@ -61,7 +61,9 @@ tbody tr:nth-child(odd) {
|
||||
width: 100% !important;
|
||||
margin: 0px;
|
||||
padding-right: 16px !important;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
.readOny .rmdp-input {
|
||||
background-color: #f5f5f5 !important;
|
||||
@@ -79,3 +81,17 @@ tbody tr:nth-child(odd) {
|
||||
.dltr {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.modalGlass2 {
|
||||
background: rgba(255, 255, 255, 0.502);
|
||||
background-blend-mode: multiply;
|
||||
|
||||
/* backdrop-filter: blur(5px); */
|
||||
}
|
||||
|
||||
.modalGlass3 {
|
||||
background: rgba(62, 61, 61, 0.2);
|
||||
background-blend-mode: multiply;
|
||||
|
||||
backdrop-filter: blur(44px);
|
||||
}
|
||||
|
||||
@@ -4,20 +4,34 @@ import XIcon from '../../assets/images/close-circle.svg';
|
||||
import { clx } from '../../helpers/utils';
|
||||
import StatusCircle from '@/components/StatusCircle';
|
||||
import { useOutsideClick } from '@/hooks/useOutSideClick';
|
||||
import { type NotificationItemType, NotificationTypeEnum } from './types/NotificationTypes';
|
||||
import { type NotificationListItemType, NotificationTypeEnum } from './types/NotificationTypes';
|
||||
import Button from '@/components/Button';
|
||||
import { t } from '@/locale';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useGetNotification, useGetUnseenCount, useReadAll } from './hooks/useNotificationData';
|
||||
import { timeAgo } from '@/config/func';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import MoonLoader from 'react-spinners/MoonLoader';
|
||||
|
||||
const Notifications: FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const ref = useOutsideClick(() => setShowModal(false));
|
||||
const [activeTab, setActiveTab] = useState<'read' | 'unread'>('unread');
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const posts: NotificationItemType[] = [];
|
||||
// const getDashboard = useGetNotification('all')
|
||||
const { data } = useGetUnseenCount();
|
||||
const { mutate: readAll, isPending } = useReadAll();
|
||||
const { data: notifications, fetchNextPage, hasNextPage } = useGetNotification(activeTab);
|
||||
const notificationList: NotificationListItemType[] =
|
||||
notifications?.pages.flatMap((page) => page.data) ?? [];
|
||||
|
||||
const handleAllRead = () => {
|
||||
// TODO: Implement read all functionality
|
||||
}
|
||||
readAll(undefined, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['unseen-count'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRedirect = (type: NotificationTypeEnum) => {
|
||||
switch (type) {
|
||||
@@ -66,9 +80,8 @@ const Notifications: FC = () => {
|
||||
<div ref={ref}>
|
||||
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
|
||||
<Notification size={18} color="black" />
|
||||
<div className="absolute top-0 right-0 -mt-1 -mr-1 rounded-full bg-red-500 text-white text-[8px] size-3 flex pt-0.5 pl-[1px] justify-center items-center">
|
||||
{/* {getDashboard.data?.pages?.[0]?.data?.notificationCount || 0} */}
|
||||
2
|
||||
<div className="absolute top-0 right-0 -mt-1 -mr-1 rounded-full bg-red-500 text-white text-[8px] size-3 flex pt-0.5 pl-px justify-center items-center">
|
||||
{data?.data?.count}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +119,7 @@ const Notifications: FC = () => {
|
||||
|
||||
<div className='flex mt-4 justify-end'>
|
||||
<Button
|
||||
isLoading={false}
|
||||
isLoading={isPending}
|
||||
onClick={handleAllRead}
|
||||
className={clx(
|
||||
'flex-1 border bg-transparent text-xs h-7 rounded-md cursor-pointer text-white border-white border-opacity-70 flex justify-center items-center',
|
||||
@@ -117,19 +130,32 @@ const Notifications: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 text-xs overflow-auto">
|
||||
{
|
||||
posts.map((item: NotificationItemType) => (
|
||||
<div onClick={() => handleRedirect(item.type)} className="bg-white cursor-pointer h-fit gap-4 items-start rounded-xl bg-opacity-60 p-3 mb-4 flex" key={item.id}>
|
||||
{
|
||||
!item.isRead &&
|
||||
<InfiniteScroll
|
||||
dataLength={notificationList.length}
|
||||
next={fetchNextPage}
|
||||
hasMore={!!hasNextPage}
|
||||
loader={
|
||||
<div className="flex justify-center">
|
||||
<MoonLoader color="black" size={20} />
|
||||
</div>
|
||||
}
|
||||
scrollableTarget="notificationsContainer"
|
||||
>
|
||||
{notificationList.map((item) => (
|
||||
<div
|
||||
onClick={() => handleRedirect(item.title as NotificationTypeEnum)}
|
||||
className="bg-white cursor-pointer h-fit gap-4 items-start rounded-xl bg-opacity-60 p-3 mb-4 flex"
|
||||
key={item.id}
|
||||
>
|
||||
{item.seenAt === null && (
|
||||
<div className="mt-1">
|
||||
<StatusCircle color="#00BA4B" />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div>
|
||||
<div className="truncate max-w-[200px]">{item.message}</div>
|
||||
<div className="truncate max-w-[200px]">{item.content}</div>
|
||||
<div className="mt-2 flex gap-2 text-[10px] text-description">
|
||||
<div>{/* timeAgo(item.createdAt) */}</div>
|
||||
<div>{timeAgo(item.createdAt)}</div>
|
||||
<div className="flex gap-1 items-center">
|
||||
<StatusCircle color="#8C90A3" />
|
||||
<div>{item.title}</div>
|
||||
@@ -137,8 +163,8 @@ const Notifications: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import * as api from "../service/NotificationService";
|
||||
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
export const useGetNotification = (status: "all" | "read" | "unread") => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["notifications", status],
|
||||
queryFn: ({ pageParam = 1 }) => api.getNotifications(pageParam, status), // فراخوانی API برای گرفتن دادهها
|
||||
initialPageParam: 1, // صفحه اول به طور پیشفرض
|
||||
getNextPageParam: (lastPage) => {
|
||||
const currentPage = lastPage.data?.pager.page;
|
||||
const totalPages = lastPage.data?.pager.totalPages;
|
||||
|
||||
return currentPage < totalPages ? currentPage + 1 : undefined;
|
||||
},
|
||||
queryFn: ({ pageParam }) =>
|
||||
api.getNotifications(pageParam ?? 1, status),
|
||||
initialPageParam: 1 as number | string,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,3 +16,10 @@ export const useReadAll = () => {
|
||||
mutationFn: () => api.readAll(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUnseenCount = () => {
|
||||
return useQuery({
|
||||
queryKey: ["unseen-count"],
|
||||
queryFn: () => api.getUnseenCount(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import axios from "../../../config/axios";
|
||||
import type {
|
||||
NotificationsResponseType,
|
||||
UnseenCountResponseType,
|
||||
} from "../types/NotificationTypes";
|
||||
|
||||
export const getNotifications = async (
|
||||
page: number,
|
||||
status: "all" | "read" | "unread"
|
||||
) => {
|
||||
pageOrCursor: number | string,
|
||||
status: "all" | "read" | "unread",
|
||||
): Promise<NotificationsResponseType> => {
|
||||
let query = ``;
|
||||
if (status !== "all") {
|
||||
query = `&isRead=${status === "read" ? 1 : 0}`;
|
||||
query = `&status=${status === "read" ? "seen" : "unseen"}`;
|
||||
}
|
||||
const { data } = await axios.get(`/notifications?page=${page}${query}`);
|
||||
const pageParam =
|
||||
typeof pageOrCursor === "number"
|
||||
? `page=${pageOrCursor}`
|
||||
: `cursor=${encodeURIComponent(pageOrCursor)}`;
|
||||
const { data } = await axios.get<NotificationsResponseType>(
|
||||
`/admin/notifications?${pageParam}${query}&limit=10`,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const readAll = async () => {
|
||||
const { data } = await axios.patch(`/notifications/read-all`);
|
||||
const { data } = await axios.put(`/admin/notifications/read/all`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUnseenCount = async (): Promise<UnseenCountResponseType> => {
|
||||
const { data } = await axios.get<UnseenCountResponseType>(
|
||||
`/admin/notifications/unseen-count`,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
import type { BaseResponse } from "@/shared/types/Types";
|
||||
|
||||
export type UnseenCountType = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type UnseenCountResponseType = BaseResponse<UnseenCountType>;
|
||||
|
||||
/** آیتم نوتیفیکیشن در پاسخ لیست API */
|
||||
export type NotificationListItemType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
user: string;
|
||||
admin: string | null;
|
||||
title: string;
|
||||
content: string;
|
||||
seenAt: string | null;
|
||||
};
|
||||
|
||||
export type NotificationsResponseType = BaseResponse<NotificationListItemType[]> & {
|
||||
nextCursor: string | null;
|
||||
};
|
||||
|
||||
export type NotificationItemType = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
NotificationStatus,
|
||||
People,
|
||||
Printer,
|
||||
Profile2User,
|
||||
Receipt21,
|
||||
Teacher,
|
||||
User,
|
||||
@@ -118,12 +117,6 @@ const SideBar: FC = () => {
|
||||
<div className="mt-10 px-6 text-description text-sm font-bold">{t('sidebar.other')}</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<SideBarItem
|
||||
icon={<Profile2User size={iconSizeSideBar} color="#4F5260" />}
|
||||
title={'کاربران'}
|
||||
isActive={isActive('/tickets')}
|
||||
link={Paths.tickets.list}
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Messages3 size={iconSizeSideBar} color="#4F5260" />}
|
||||
|
||||
Reference in New Issue
Block a user