notification admin

This commit is contained in:
hamid zarghami
2026-02-24 09:55:28 +03:30
parent 4f0f9cda73
commit 689376787e
7 changed files with 123 additions and 44 deletions
+2 -2
View File
@@ -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
View File
@@ -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);
}
+45 -19
View File
@@ -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;
-7
View File
@@ -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" />}