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 = 'https://negareh-api.dev.danakcorp.com'
|
||||||
# VITE_API_BASE_URL = 'http://10.56.147.88:4000'
|
VITE_API_BASE_URL = 'http://10.29.35.88:4000'
|
||||||
VITE_TOKEN_NAME = 'negareh_at'
|
VITE_TOKEN_NAME = 'negareh_at'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'negareh_art'
|
VITE_REFRESH_TOKEN_NAME = 'negareh_art'
|
||||||
|
|||||||
+17
-1
@@ -61,7 +61,9 @@ tbody tr:nth-child(odd) {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-right: 16px !important;
|
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 {
|
.readOny .rmdp-input {
|
||||||
background-color: #f5f5f5 !important;
|
background-color: #f5f5f5 !important;
|
||||||
@@ -79,3 +81,17 @@ tbody tr:nth-child(odd) {
|
|||||||
.dltr {
|
.dltr {
|
||||||
direction: ltr;
|
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 { clx } from '../../helpers/utils';
|
||||||
import StatusCircle from '@/components/StatusCircle';
|
import StatusCircle from '@/components/StatusCircle';
|
||||||
import { useOutsideClick } from '@/hooks/useOutSideClick';
|
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 Button from '@/components/Button';
|
||||||
import { t } from '@/locale';
|
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 Notifications: FC = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const ref = useOutsideClick(() => setShowModal(false));
|
const ref = useOutsideClick(() => setShowModal(false));
|
||||||
const [activeTab, setActiveTab] = useState<'read' | 'unread'>('unread');
|
const [activeTab, setActiveTab] = useState<'read' | 'unread'>('unread');
|
||||||
const [showModal, setShowModal] = useState<boolean>(false);
|
const [showModal, setShowModal] = useState<boolean>(false);
|
||||||
const posts: NotificationItemType[] = [];
|
const { data } = useGetUnseenCount();
|
||||||
// const getDashboard = useGetNotification('all')
|
const { mutate: readAll, isPending } = useReadAll();
|
||||||
|
const { data: notifications, fetchNextPage, hasNextPage } = useGetNotification(activeTab);
|
||||||
|
const notificationList: NotificationListItemType[] =
|
||||||
|
notifications?.pages.flatMap((page) => page.data) ?? [];
|
||||||
|
|
||||||
const handleAllRead = () => {
|
const handleAllRead = () => {
|
||||||
// TODO: Implement read all functionality
|
readAll(undefined, {
|
||||||
}
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notifications'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['unseen-count'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleRedirect = (type: NotificationTypeEnum) => {
|
const handleRedirect = (type: NotificationTypeEnum) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -66,9 +80,8 @@ const Notifications: FC = () => {
|
|||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
|
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
|
||||||
<Notification size={18} color="black" />
|
<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">
|
<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">
|
||||||
{/* {getDashboard.data?.pages?.[0]?.data?.notificationCount || 0} */}
|
{data?.data?.count}
|
||||||
2
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +119,7 @@ const Notifications: FC = () => {
|
|||||||
|
|
||||||
<div className='flex mt-4 justify-end'>
|
<div className='flex mt-4 justify-end'>
|
||||||
<Button
|
<Button
|
||||||
isLoading={false}
|
isLoading={isPending}
|
||||||
onClick={handleAllRead}
|
onClick={handleAllRead}
|
||||||
className={clx(
|
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',
|
'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>
|
||||||
|
|
||||||
<div className="mt-6 flex flex-col gap-4 text-xs overflow-auto">
|
<div className="mt-6 flex flex-col gap-4 text-xs overflow-auto">
|
||||||
{
|
<InfiniteScroll
|
||||||
posts.map((item: NotificationItemType) => (
|
dataLength={notificationList.length}
|
||||||
<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}>
|
next={fetchNextPage}
|
||||||
{
|
hasMore={!!hasNextPage}
|
||||||
!item.isRead &&
|
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">
|
<div className="mt-1">
|
||||||
<StatusCircle color="#00BA4B" />
|
<StatusCircle color="#00BA4B" />
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<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 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">
|
<div className="flex gap-1 items-center">
|
||||||
<StatusCircle color="#8C90A3" />
|
<StatusCircle color="#8C90A3" />
|
||||||
<div>{item.title}</div>
|
<div>{item.title}</div>
|
||||||
@@ -137,8 +163,8 @@ const Notifications: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))}
|
||||||
}
|
</InfiniteScroll>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import * as api from "../service/NotificationService";
|
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") => {
|
export const useGetNotification = (status: "all" | "read" | "unread") => {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: ["notifications", status],
|
queryKey: ["notifications", status],
|
||||||
queryFn: ({ pageParam = 1 }) => api.getNotifications(pageParam, status), // فراخوانی API برای گرفتن دادهها
|
queryFn: ({ pageParam }) =>
|
||||||
initialPageParam: 1, // صفحه اول به طور پیشفرض
|
api.getNotifications(pageParam ?? 1, status),
|
||||||
getNextPageParam: (lastPage) => {
|
initialPageParam: 1 as number | string,
|
||||||
const currentPage = lastPage.data?.pager.page;
|
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||||
const totalPages = lastPage.data?.pager.totalPages;
|
|
||||||
|
|
||||||
return currentPage < totalPages ? currentPage + 1 : undefined;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,3 +16,10 @@ export const useReadAll = () => {
|
|||||||
mutationFn: () => api.readAll(),
|
mutationFn: () => api.readAll(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetUnseenCount = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["unseen-count"],
|
||||||
|
queryFn: () => api.getUnseenCount(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
|
import type {
|
||||||
|
NotificationsResponseType,
|
||||||
|
UnseenCountResponseType,
|
||||||
|
} from "../types/NotificationTypes";
|
||||||
|
|
||||||
export const getNotifications = async (
|
export const getNotifications = async (
|
||||||
page: number,
|
pageOrCursor: number | string,
|
||||||
status: "all" | "read" | "unread"
|
status: "all" | "read" | "unread",
|
||||||
) => {
|
): Promise<NotificationsResponseType> => {
|
||||||
let query = ``;
|
let query = ``;
|
||||||
if (status !== "all") {
|
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;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readAll = async () => {
|
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;
|
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 = {
|
export type NotificationItemType = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
NotificationStatus,
|
NotificationStatus,
|
||||||
People,
|
People,
|
||||||
Printer,
|
Printer,
|
||||||
Profile2User,
|
|
||||||
Receipt21,
|
Receipt21,
|
||||||
Teacher,
|
Teacher,
|
||||||
User,
|
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-10 px-6 text-description text-sm font-bold">{t('sidebar.other')}</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<SideBarItem
|
|
||||||
icon={<Profile2User size={iconSizeSideBar} color="#4F5260" />}
|
|
||||||
title={'کاربران'}
|
|
||||||
isActive={isActive('/tickets')}
|
|
||||||
link={Paths.tickets.list}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<Messages3 size={iconSizeSideBar} color="#4F5260" />}
|
icon={<Messages3 size={iconSizeSideBar} color="#4F5260" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user