From 663dd1a9db86ab1e765607ca621178f46655e35e Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 12 Jul 2025 16:01:17 +0330 Subject: [PATCH] email actions + sent list + action list + archive list + trash list --- index.html | 25 ++- public/logo.svg | 15 ++ public/vite.svg | 4 +- src/App.tsx | 9 +- src/assets/images/logo-small.svg | 14 +- src/assets/images/logo.svg | 39 ++-- src/assets/images/mark_as_read.svg | 4 + src/components/Table.tsx | 212 +++++++++++------- src/components/newMessage/NewMessage.tsx | 5 +- src/components/types/TableTypes.ts | 2 + src/hooks/useEmailActions.tsx | 201 +++++++++++++++++ src/langs/fa.json | 2 +- src/pages/Trash/List.tsx | 124 +++++----- src/pages/Trash/hooks/useTrashData.tsx | 13 ++ src/pages/Trash/service/TrashService.ts | 11 + src/pages/Trash/types/TrashTypes.ts | 61 +++++ src/pages/archive/List.tsx | 122 +++++----- src/pages/archive/hooks/useArchiveData.tsx | 13 ++ src/pages/archive/service/ArchiveService.ts | 13 ++ src/pages/archive/types/ArchiveTypes.ts | 61 +++++ src/pages/auth/components/LoginStep2.tsx | 8 +- src/pages/draft/List.tsx | 43 +++- src/pages/favorite/List.tsx | 179 +++++++++++++++ src/pages/favorite/hooks/useFavoriteData.tsx | 13 ++ src/pages/favorite/service/FavoriteService.ts | 13 ++ src/pages/favorite/types/FavoriteTypes.ts | 61 +++++ src/pages/received/List.tsx | 82 +++++-- src/pages/received/service/EmailService.ts | 4 +- src/pages/received/types/Types.ts | 18 ++ src/pages/search/List.tsx | 192 ++++++++++++++++ src/pages/search/hooks/useSearchData.tsx | 12 + src/pages/search/service/SearchService.ts | 9 + src/pages/search/types/SearchTypes.ts | 59 +++++ src/pages/sent/List.tsx | 173 ++++++++++---- src/pages/sent/hooks/useSentData.tsx | 11 + src/pages/sent/service/SentService.ts | 28 +++ src/pages/sent/types/SentTypes.ts | 76 +++++++ src/pages/spam/List.tsx | 167 ++++++++++++++ src/pages/spam/hooks/useSpamData.tsx | 11 + src/pages/spam/service/SpamService.ts | 9 + src/pages/spam/types/SpamTypes.ts | 60 +++++ src/router/AppRouter.tsx | 7 + src/services/EmailActionsService.ts | 83 +++++++ src/shared/SideBar.tsx | 4 +- src/utils/Paths.ts | 1 + 45 files changed, 1947 insertions(+), 326 deletions(-) create mode 100644 public/logo.svg create mode 100644 src/assets/images/mark_as_read.svg create mode 100644 src/hooks/useEmailActions.tsx create mode 100644 src/pages/Trash/hooks/useTrashData.tsx create mode 100644 src/pages/Trash/service/TrashService.ts create mode 100644 src/pages/Trash/types/TrashTypes.ts create mode 100644 src/pages/archive/hooks/useArchiveData.tsx create mode 100644 src/pages/archive/service/ArchiveService.ts create mode 100644 src/pages/archive/types/ArchiveTypes.ts create mode 100644 src/pages/favorite/List.tsx create mode 100644 src/pages/favorite/hooks/useFavoriteData.tsx create mode 100644 src/pages/favorite/service/FavoriteService.ts create mode 100644 src/pages/favorite/types/FavoriteTypes.ts create mode 100644 src/pages/search/List.tsx create mode 100644 src/pages/search/hooks/useSearchData.tsx create mode 100644 src/pages/search/service/SearchService.ts create mode 100644 src/pages/search/types/SearchTypes.ts create mode 100644 src/pages/sent/hooks/useSentData.tsx create mode 100644 src/pages/sent/service/SentService.ts create mode 100644 src/pages/sent/types/SentTypes.ts create mode 100644 src/pages/spam/List.tsx create mode 100644 src/pages/spam/hooks/useSpamData.tsx create mode 100644 src/pages/spam/service/SpamService.ts create mode 100644 src/pages/spam/types/SpamTypes.ts create mode 100644 src/services/EmailActionsService.ts diff --git a/index.html b/index.html index e4b78ea..54045f4 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,16 @@ - - - - - Vite + React + TS - - -
- - - + + + + + + DMail | داناک + + + +
+ + + + \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..cfbb4dc --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/vite.svg b/public/vite.svg index e7b8dfb..30a42c2 100644 --- a/public/vite.svg +++ b/public/vite.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/App.tsx b/src/App.tsx index 6b1e830..31dfca1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { I18nextProvider } from 'react-i18next' import NewMessage from './components/newMessage/NewMessage'; import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { IApiErrorRepsonse } from './types/error.types'; -import ToastContainer from './components/Toast'; +import ToastContainer, { toast } from './components/Toast'; import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func'; import { refreshToken } from './pages/auth/service/AuthService'; import { Paths } from './utils/Paths'; @@ -37,6 +37,13 @@ i18next.init({ const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: async (error: IApiErrorRepsonse) => { + // نمایش پیغام خطا از API + const errorMessage = error?.response?.error?.message?.[0] || undefined; + + if (errorMessage) { + toast(errorMessage, 'error'); + } + if (error?.response?.status === 401) { try { // Use a flag to track if refresh is in progress diff --git a/src/assets/images/logo-small.svg b/src/assets/images/logo-small.svg index af5e4f3..30a42c2 100644 --- a/src/assets/images/logo-small.svg +++ b/src/assets/images/logo-small.svg @@ -1,13 +1,3 @@ - - - - - - - - - - - - + + diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg index 065a361..cfbb4dc 100644 --- a/src/assets/images/logo.svg +++ b/src/assets/images/logo.svg @@ -1,24 +1,15 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + diff --git a/src/assets/images/mark_as_read.svg b/src/assets/images/mark_as_read.svg new file mode 100644 index 0000000..f2872fc --- /dev/null +++ b/src/assets/images/mark_as_read.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Table.tsx b/src/components/Table.tsx index df20121..e25403e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -22,11 +22,23 @@ const Table = ({ selectable = false, selectedActions = null, onSelectionChange, + selectedRows: externalSelectedRows, + clearSelection = false, rowActions, pagination, }: TableProps): React.ReactElement => { - const [selectedRows, setSelectedRows] = useState([]); + const [internalSelectedRows, setInternalSelectedRows] = useState([]); + + // استفاده از selectedRows خارجی اگر موجود باشد، در غیر این صورت از internal استفاده کن + const selectedRows = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows; + + // پاک کردن انتخاب‌ها وقتی clearSelection تغییر کند + React.useEffect(() => { + if (clearSelection && externalSelectedRows === undefined) { + setInternalSelectedRows([]); + } + }, [clearSelection, externalSelectedRows]); const getRowClassName = (item: T, index: number): string => { if (typeof rowClassName === 'function') { @@ -48,9 +60,18 @@ const Table = ({ const handleSelectAll = () => { const newSelectedRows = selectedRows.length === data.length ? [] : [...data]; - setSelectedRows(newSelectedRows); - if (onSelectionChange) { - onSelectionChange(newSelectedRows); + + if (externalSelectedRows !== undefined) { + // اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن + if (onSelectionChange) { + onSelectionChange(newSelectedRows); + } + } else { + // در غیر این صورت internal state را به‌روزرسانی کن + setInternalSelectedRows(newSelectedRows); + if (onSelectionChange) { + onSelectionChange(newSelectedRows); + } } }; @@ -58,9 +79,18 @@ const Table = ({ const newSelectedRows = checked ? [...selectedRows, item] : selectedRows.filter((row) => row.id !== item.id); - setSelectedRows(newSelectedRows); - if (onSelectionChange) { - onSelectionChange(newSelectedRows); + + if (externalSelectedRows !== undefined) { + // اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن + if (onSelectionChange) { + onSelectionChange(newSelectedRows); + } + } else { + // در غیر این صورت internal state را به‌روزرسانی کن + setInternalSelectedRows(newSelectedRows); + if (onSelectionChange) { + onSelectionChange(newSelectedRows); + } } }; @@ -93,52 +123,60 @@ const Table = ({ return (
- {data.map((item, rowIndex) => ( -
handleRowClick(item)} - > - {selectable && ( -
e.stopPropagation()} - > - handleSingleRowSelect(item, !!checked)} - /> -
- )} + {data.map((item, rowIndex) => { + // Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری + const isUnread = 'seen' in item && !item.seen; + const bgColor = isUnread ? 'bg-white' : 'bg-gray-50'; + const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100'; + const fontWeight = isUnread ? 'font-semibold' : 'font-normal'; -
- {/* محتوای اصلی - ستون اول */} -
- {columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)} + return ( +
handleRowClick(item)} + > + {selectable && ( +
e.stopPropagation()} + > + handleSingleRowSelect(item, !!checked)} + /> +
+ )} + +
+ {/* محتوای اصلی - ستون اول */} +
+ {columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)} +
+ + {/* اطلاعات اضافی - سه ستون آخر در یک خط */} + {columns.length > 1 && ( +
+ {columns.slice(1).map((col) => ( +
+ {col.render ? col.render(item) : (item[col.key] as React.ReactNode)} +
+ ))} +
+ )}
- {/* اطلاعات اضافی - سه ستون آخر در یک خط */} - {columns.length > 1 && ( -
- {columns.slice(1).map((col) => ( -
- {col.render ? col.render(item) : (item[col.key] as React.ReactNode)} -
- ))} + {rowActions && ( +
e.stopPropagation()} + > +
)}
- - {rowActions && ( -
e.stopPropagation()} - > - -
- )} -
- ))} + ); + })}
); }; @@ -161,42 +199,50 @@ const Table = ({ ); } - return data.map((item, rowIndex) => ( - handleRowClick(item)} - > - {selectable && ( - e.stopPropagation()} - > - handleSingleRowSelect(item, !!checked)} - /> - - )} - {columns.map((col) => ( - - {col.render ? col.render(item) : item[col.key] as React.ReactNode} - - ))} - {rowActions && ( - e.stopPropagation()} - > - - - )} - - )); + return data.map((item, rowIndex) => { + // Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری + const isUnread = 'seen' in item && !item.seen; + const bgColor = isUnread ? 'bg-white' : 'bg-gray-50'; + const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100'; + const fontWeight = isUnread ? 'font-semibold' : 'font-normal'; + + return ( + handleRowClick(item)} + > + {selectable && ( + e.stopPropagation()} + > + handleSingleRowSelect(item, !!checked)} + /> + + )} + {columns.map((col) => ( + + {col.render ? col.render(item) : item[col.key] as React.ReactNode} + + ))} + {rowActions && ( + e.stopPropagation()} + > + + + )} + + ); + }); }; const renderHeader = () => { diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index 56c9ec7..9fae874 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -11,7 +11,7 @@ import Button from '../Button' import Select from '../Select' import UploadButton from '../UploadButton' import { toast } from '../Toast' -import { ErrorType } from '@/helpers/types' + const NewMessage: FC = () => { @@ -188,9 +188,6 @@ const NewMessage: FC = () => { handleClose() resetForm() }, - onError: (error: ErrorType) => { - toast(error.response?.data?.error.message[0], 'error') - }, onSettled: () => { // Reset loading states setIsSending(false) diff --git a/src/components/types/TableTypes.ts b/src/components/types/TableTypes.ts index 42566c2..66caa77 100644 --- a/src/components/types/TableTypes.ts +++ b/src/components/types/TableTypes.ts @@ -36,6 +36,8 @@ export interface TableProps { selectable?: boolean; selectedActions?: ReactNode; onSelectionChange?: (selectedRows: T[]) => void; + selectedRows?: T[]; + clearSelection?: boolean; rowActions?: (item: T) => RowActionItem[]; pagination?: PaginationConfig; } diff --git a/src/hooks/useEmailActions.tsx b/src/hooks/useEmailActions.tsx new file mode 100644 index 0000000..ea64efb --- /dev/null +++ b/src/hooks/useEmailActions.tsx @@ -0,0 +1,201 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + markMessageAsSeen, + moveMessageToArchive, + moveMessageToFavorite, + moveMessageToTrash, + deleteMessagePermanently, + sendEmail, + updateDraft, + deleteDraft, + sendDraft, + bulkAction, + unfavoriteMessage, + unarchiveMessage, + restoreMessage, +} from '../services/EmailActionsService'; +import { BulkActionType } from '../pages/received/types/Types'; +import { toast } from '../components/Toast'; + +export const useEmailActions = (customRefetch?: () => void) => { + const queryClient = useQueryClient(); + + // تمام query key های موجود در سیستم + const invalidateAllQueries = () => { + // صفحه‌های اصلی + queryClient.invalidateQueries({ queryKey: ['inbox'] }); + queryClient.invalidateQueries({ queryKey: ['drafts'] }); + queryClient.invalidateQueries({ queryKey: ['archive-messages'] }); + queryClient.invalidateQueries({ queryKey: ['favorite-messages'] }); + queryClient.invalidateQueries({ queryKey: ['spam-messages'] }); + queryClient.invalidateQueries({ queryKey: ['trash-messages'] }); + queryClient.invalidateQueries({ queryKey: ['search-messages'] }); + + // پیام‌های تکی + queryClient.invalidateQueries({ queryKey: ['message'] }); + queryClient.invalidateQueries({ queryKey: ['draft'] }); + + // صفحه‌هایی که از React Query استفاده نمی‌کنند (مثل sent) + if (customRefetch) { + customRefetch(); + } + }; + + const bulkActionMutation = useMutation({ + mutationFn: bulkAction, + onSuccess: (data) => { + toast(data?.data?.message || 'عملیات گروهی با موفقیت انجام شد', 'success'); + invalidateAllQueries(); + }, + }); + + const markAsSeenMutation = useMutation({ + mutationFn: markMessageAsSeen, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام به عنوان خوانده شده علامت‌گذاری شد', 'success'); + invalidateAllQueries(); + }, + }); + + const archiveMutation = useMutation({ + mutationFn: moveMessageToArchive, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام به آرشیو منتقل شد', 'success'); + invalidateAllQueries(); + }, + }); + + const favoriteMutation = useMutation({ + mutationFn: moveMessageToFavorite, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام به علاقه‌مندی‌ها اضافه شد', 'success'); + invalidateAllQueries(); + }, + }); + + const moveToTrashMutation = useMutation({ + mutationFn: moveMessageToTrash, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام به سطل زباله منتقل شد', 'success'); + invalidateAllQueries(); + }, + }); + + const deletePermanentlyMutation = useMutation({ + mutationFn: deleteMessagePermanently, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام به طور کامل حذف شد', 'success'); + invalidateAllQueries(); + }, + }); + + const sendEmailMutation = useMutation({ + mutationFn: sendEmail, + onSuccess: (data) => { + toast(data?.data?.message || 'ایمیل با موفقیت ارسال شد', 'success'); + invalidateAllQueries(); + }, + }); + + const updateDraftMutation = useMutation({ + mutationFn: ({ messageId, draftData }: { messageId: number; draftData: unknown }) => + updateDraft(messageId, draftData), + onSuccess: (data) => { + toast(data?.data?.message || 'پیش‌نویس به‌روزرسانی شد', 'success'); + invalidateAllQueries(); + }, + }); + + const deleteDraftMutation = useMutation({ + mutationFn: deleteDraft, + onSuccess: (data) => { + toast(data?.data?.message || 'پیش‌نویس حذف شد', 'success'); + invalidateAllQueries(); + }, + }); + + const sendDraftMutation = useMutation({ + mutationFn: sendDraft, + onSuccess: (data) => { + toast(data?.data?.message || 'پیش‌نویس ارسال شد', 'success'); + invalidateAllQueries(); + }, + }); + + const unfavoriteMutation = useMutation({ + mutationFn: unfavoriteMessage, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام از علاقه‌مندی‌ها حذف شد', 'success'); + invalidateAllQueries(); + }, + }); + + const unarchiveMutation = useMutation({ + mutationFn: unarchiveMessage, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام از آرشیو خارج شد', 'success'); + invalidateAllQueries(); + }, + }); + + const restoreMutation = useMutation({ + mutationFn: restoreMessage, + onSuccess: (data) => { + toast(data?.data?.message || 'پیام بازگردانی شد', 'success'); + invalidateAllQueries(); + }, + }); + + const isLoading = + bulkActionMutation.isPending || + markAsSeenMutation.isPending || + archiveMutation.isPending || + favoriteMutation.isPending || + moveToTrashMutation.isPending || + deletePermanentlyMutation.isPending || + sendEmailMutation.isPending || + updateDraftMutation.isPending || + deleteDraftMutation.isPending || + sendDraftMutation.isPending || + unfavoriteMutation.isPending || + unarchiveMutation.isPending || + restoreMutation.isPending; + + return { + // Bulk actions + bulkAction: bulkActionMutation.mutate, + bulkMarkAsSeen: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.SEEN }), + bulkArchive: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.ARCHIVE }), + bulkFavorite: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.FAVORITE }), + bulkMoveToTrash: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.TRASH }), + bulkDelete: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.DELETE }), + bulkJunk: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.JUNK }), + bulkUnfavorite: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.UNFAVORITE }), + bulkUnarchive: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.UNARCHIVE }), + bulkRestore: (messageIds: number[]) => + bulkActionMutation.mutate({ messageIds, action: BulkActionType.RESTORE }), + + // Single actions (keeping for backward compatibility) + markAsSeen: markAsSeenMutation.mutate, + archive: archiveMutation.mutate, + favorite: favoriteMutation.mutate, + moveToTrash: moveToTrashMutation.mutate, + deletePermanently: deletePermanentlyMutation.mutate, + sendEmail: sendEmailMutation.mutate, + updateDraft: updateDraftMutation.mutate, + deleteDraft: deleteDraftMutation.mutate, + sendDraft: sendDraftMutation.mutate, + unfavorite: unfavoriteMutation.mutate, + unarchive: unarchiveMutation.mutate, + restore: restoreMutation.mutate, + isLoading, + }; +}; \ No newline at end of file diff --git a/src/langs/fa.json b/src/langs/fa.json index f96bf2c..5d7804a 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -180,7 +180,7 @@ "phonen_number": "شماره تماس", "enter_phone_number": "شماره تماس خود را وارد کنید", "password": "رمز عبور", - "enter_password": "رمز عبور انتخابی خود را وارد کنید", + "enter_password": "رمز عبور خود را وارد کنید", "next": "ادامه", "before_registered": "آیا قبلا ثبت نام کردید؟", "login": "ورود", diff --git a/src/pages/Trash/List.tsx b/src/pages/Trash/List.tsx index 07742e6..0dd285f 100644 --- a/src/pages/Trash/List.tsx +++ b/src/pages/Trash/List.tsx @@ -1,46 +1,45 @@ import Filters, { FilterValues } from '../../components/Filters'; import { FC, useState } from 'react' -import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; import { InfoCircle, More, Refresh2, Trash, RecoveryConvert } from 'iconsax-react'; import { RowActionItem } from '../../components/RowActionsDropdown'; - -interface Message extends Record { - id: string; - title: string; - sender: string; - date: string; - read: boolean; -} +import { useNavigate } from 'react-router-dom'; +import { useGetTrashMessages } from './hooks/useTrashData'; +import { TrashMessage } from './types/TrashTypes'; +import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; const List: FC = () => { - const { t } = useTranslation(); - const [isLoading] = useState(false); - const [selectedMessages, setSelectedMessages] = useState([]); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); - const messageData: Message[] = [ - { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, - ]; + const { data: trashData, isLoading, refetch, isFetching } = useGetTrashMessages({ + page: currentPage, + limit: 10, + ...filters + }); - const columns: ColumnType[] = [ + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ { - key: 'title', + key: 'subject', title: 'عنوان پیام', render: (item) => ( -
- {!item.read &&
} - {item.title} -
+ {item.subject || 'بدون موضوع'} ) }, { - key: 'sender', + key: 'from', title: 'فرستنده', render: (item) => ( - {item.sender} +
+ {!item.seen &&
} + {item.from.name || item.from.address} +
) }, { @@ -48,14 +47,19 @@ const List: FC = () => { title: 'تاریخ', align: 'center', render: (item) => ( - {item.date} + {formatDate(item.date)} ) }, ]; const tableActions = (
- + refetch()} + />
); @@ -68,77 +72,95 @@ const List: FC = () => { ); const handleFilterChange = (newFilters: FilterValues) => { - console.log('Applied filters:', newFilters); + setFilters(newFilters); + setCurrentPage(1); }; - const handleSelectionChange = (selectedRows: Message[]) => { + const handleSelectionChange = (selectedRows: TrashMessage[]) => { setSelectedMessages(selectedRows); }; const handleDeleteSelected = () => { - console.log('حذف دائم پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق حذف دائم را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkDelete(messageIds); + setSelectedMessages([]); }; const handleRestoreSelected = () => { - console.log('بازگردانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق بازگردانی را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkRestore(messageIds); + setSelectedMessages([]); }; - const getRowActions = (message: Message): RowActionItem[] => [ + const getRowActions = (message: TrashMessage): RowActionItem[] => [ { label: 'بازگردانی', icon: , - onClick: () => console.log('بازگردانی پیام', message.id), + onClick: () => emailActions.restore(message.id), }, { label: 'حذف دائم', icon: , - onClick: () => console.log('حذف دائم پیام', message.id), + onClick: () => emailActions.deletePermanently(message.id), }, ]; + + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = trashData?.data?.trashMails || []; + const pager = trashData?.data?.pager; + return (
-

{t('trash.title')}

+

سطل آشغال

- + columns={columns} - data={messageData} - isLoading={isLoading} + data={messages} + isLoading={isLoading || emailActions.isLoading} showHeader={false} actions={tableActions} selectedActions={selectedActions} onSelectionChange={handleSelectionChange} - onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} + selectedRows={selectedMessages} + onRowClick={(id) => navigate(`/mail/${id}`)} noDataMessage={ -
+
-
سطل آشغال خالی است
+
هیچ پیامی یافت نشد
} selectable={true} rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} />
) diff --git a/src/pages/Trash/hooks/useTrashData.tsx b/src/pages/Trash/hooks/useTrashData.tsx new file mode 100644 index 0000000..83e858e --- /dev/null +++ b/src/pages/Trash/hooks/useTrashData.tsx @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { getTrashMessages } from '../service/TrashService'; +import { MessageListQueryDto } from '../types/TrashTypes'; + +export const useGetTrashMessages = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['trash-messages', query], + queryFn: () => getTrashMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; + +// Note: useRestoreMessage is now handled by useEmailActions hook \ No newline at end of file diff --git a/src/pages/Trash/service/TrashService.ts b/src/pages/Trash/service/TrashService.ts new file mode 100644 index 0000000..cff63e9 --- /dev/null +++ b/src/pages/Trash/service/TrashService.ts @@ -0,0 +1,11 @@ +import axios from "../../../config/axios"; +import { MessageListQueryDto, TrashResponse } from "../types/TrashTypes"; + +export const getTrashMessages = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/trash`, { params: query }); + return data; +}; + +// Note: deleteMessagePermanently and restoreMessage are now handled by EmailActionsService diff --git a/src/pages/Trash/types/TrashTypes.ts b/src/pages/Trash/types/TrashTypes.ts new file mode 100644 index 0000000..7e2829e --- /dev/null +++ b/src/pages/Trash/types/TrashTypes.ts @@ -0,0 +1,61 @@ +export interface EmailAddress { + address: string; + name: string; +} + +export interface TrashMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export interface MessageListQueryDto { + page?: number; + limit?: number; + order?: "asc" | "desc"; + search?: string; + dateFrom?: string; + dateTo?: string; + isRead?: string; +} + +export interface TrashResponse { + statusCode: number; + success: boolean; + data: { + trashMails: TrashMessage[]; + pager: { + page: number; + totalPages: number; + totalItems: number; + limit: number; + prevPage: boolean; + nextPage: boolean; + }; + }; +} diff --git a/src/pages/archive/List.tsx b/src/pages/archive/List.tsx index d689ac3..782f16a 100644 --- a/src/pages/archive/List.tsx +++ b/src/pages/archive/List.tsx @@ -1,46 +1,45 @@ import Filters, { FilterValues } from '../../components/Filters'; import { FC, useState } from 'react' -import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; import { InfoCircle, More, Refresh2, Trash, ArchiveSlash } from 'iconsax-react'; import { RowActionItem } from '../../components/RowActionsDropdown'; - -interface Message extends Record { - id: string; - title: string; - sender: string; - date: string; - read: boolean; -} +import { useNavigate } from 'react-router-dom'; +import { useGetArchiveMessages } from './hooks/useArchiveData'; +import { ArchiveMessage } from './types/ArchiveTypes'; +import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; const List: FC = () => { - const { t } = useTranslation(); - const [isLoading] = useState(false); - const [selectedMessages, setSelectedMessages] = useState([]); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); - const messageData: Message[] = [ - { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, - ]; + const { data: archiveData, isLoading, refetch, isFetching } = useGetArchiveMessages({ + page: currentPage, + limit: 10, + ...filters + }); - const columns: ColumnType[] = [ + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ { - key: 'title', + key: 'subject', title: 'عنوان پیام', render: (item) => ( -
- {!item.read &&
} - {item.title} -
+ {item.subject || 'بدون موضوع'} ) }, { - key: 'sender', + key: 'from', title: 'فرستنده', render: (item) => ( - {item.sender} +
+ {!item.seen &&
} + {item.from.name || item.from.address} +
) }, { @@ -48,14 +47,19 @@ const List: FC = () => { title: 'تاریخ', align: 'center', render: (item) => ( - {item.date} + {formatDate(item.date)} ) }, ]; const tableActions = (
- + refetch()} + />
); @@ -68,77 +72,93 @@ const List: FC = () => { ); const handleFilterChange = (newFilters: FilterValues) => { - console.log('Applied filters:', newFilters); + setFilters(newFilters); + setCurrentPage(1); }; - const handleSelectionChange = (selectedRows: Message[]) => { + const handleSelectionChange = (selectedRows: ArchiveMessage[]) => { setSelectedMessages(selectedRows); }; const handleDeleteSelected = () => { - console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق حذف را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); }; const handleUnarchiveSelected = () => { - console.log('خروج از آرشیو پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق خروج از آرشیو را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkUnarchive(messageIds); + setSelectedMessages([]); }; - const getRowActions = (message: Message): RowActionItem[] => [ + const getRowActions = (message: ArchiveMessage): RowActionItem[] => [ { label: 'خروج از آرشیو', icon: , - onClick: () => console.log('خروج از آرشیو', message.id), + onClick: () => emailActions.unarchive(message.id), }, { label: 'حذف', icon: , - onClick: () => console.log('حذف پیام', message.id), + onClick: () => emailActions.moveToTrash(message.id), }, ]; + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = archiveData?.data?.archiveMails || []; + const pager = archiveData?.data?.pager; + return (
-

{t('archive.title')}

+

آرشیو

- + columns={columns} - data={messageData} - isLoading={isLoading} + data={messages} + isLoading={isLoading || emailActions.isLoading} showHeader={false} actions={tableActions} selectedActions={selectedActions} onSelectionChange={handleSelectionChange} - onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} + selectedRows={selectedMessages} + onRowClick={(id) => navigate(`/mail/${id}`)} noDataMessage={ -
+
-
هیچ پیام آرشیو شده‌ای یافت نشد
+
هیچ پیامی یافت نشد
} selectable={true} rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} />
) diff --git a/src/pages/archive/hooks/useArchiveData.tsx b/src/pages/archive/hooks/useArchiveData.tsx new file mode 100644 index 0000000..4a8aaa4 --- /dev/null +++ b/src/pages/archive/hooks/useArchiveData.tsx @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { getArchiveMessages } from '../service/ArchiveService'; +import { MessageListQueryDto } from '../types/ArchiveTypes'; + +export const useGetArchiveMessages = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['archive-messages', query], + queryFn: () => getArchiveMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; + +// Note: useUnarchiveMessage is now handled by useEmailActions hook \ No newline at end of file diff --git a/src/pages/archive/service/ArchiveService.ts b/src/pages/archive/service/ArchiveService.ts new file mode 100644 index 0000000..bae4aee --- /dev/null +++ b/src/pages/archive/service/ArchiveService.ts @@ -0,0 +1,13 @@ +import axios from "../../../config/axios"; +import { MessageListQueryDto, ArchiveResponse } from "../types/ArchiveTypes"; + +export const getArchiveMessages = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/archive`, { + params: query, + }); + return data; +}; + +// Note: moveMessageToArchive and unarchiveMessage are now handled by EmailActionsService diff --git a/src/pages/archive/types/ArchiveTypes.ts b/src/pages/archive/types/ArchiveTypes.ts new file mode 100644 index 0000000..6de7791 --- /dev/null +++ b/src/pages/archive/types/ArchiveTypes.ts @@ -0,0 +1,61 @@ +export interface EmailAddress { + address: string; + name: string; +} + +export interface ArchiveMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export interface MessageListQueryDto { + page?: number; + limit?: number; + order?: "asc" | "desc"; + search?: string; + dateFrom?: string; + dateTo?: string; + isRead?: string; +} + +export interface ArchiveResponse { + statusCode: number; + success: boolean; + data: { + archiveMails: ArchiveMessage[]; + pager: { + page: number; + totalPages: number; + totalItems: number; + limit: number; + prevPage: boolean; + nextPage: boolean; + }; + }; +} diff --git a/src/pages/auth/components/LoginStep2.tsx b/src/pages/auth/components/LoginStep2.tsx index d31e5e1..f86ba37 100644 --- a/src/pages/auth/components/LoginStep2.tsx +++ b/src/pages/auth/components/LoginStep2.tsx @@ -9,7 +9,7 @@ import OTPInput from 'react-otp-input' import { useCountDown } from '../../../hooks/useCountDown' // import ArrowLeftIcon from '../../../assets/images/arrow-left.svg' import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData' -import { ErrorType } from '../../../helpers/types' + import { setRefreshToken } from '../../../config/func' import { setToken } from '../../../config/func' import { getRedirectUrl } from '../../../config/func' @@ -49,9 +49,6 @@ const LoginStep2: FC = () => { } window.location.href = Paths.received }, - onError(error: ErrorType) { - toast(error?.response?.data?.error?.message[0], 'error') - }, }) }, }) @@ -83,9 +80,6 @@ const LoginStep2: FC = () => { reset() toast(t('auth.toast_resend_code'), 'success') }, - onError: (error: ErrorType) => { - toast(error.response?.data?.error.message[0], 'error') - } }) } diff --git a/src/pages/draft/List.tsx b/src/pages/draft/List.tsx index 29d79e7..50babde 100644 --- a/src/pages/draft/List.tsx +++ b/src/pages/draft/List.tsx @@ -3,18 +3,20 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, Edit, Send } from 'iconsax-react'; import { RowActionItem } from '../../components/RowActionsDropdown'; import { useNavigate } from 'react-router-dom'; -import { useGetDrafts, useDeleteDraft } from './hooks/useDraftData'; +import { useGetDrafts } from './hooks/useDraftData'; import { DraftMessage } from './types/DraftTypes'; import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; const List: FC = () => { const navigate = useNavigate(); const { t } = useTranslation(); const [currentPage, setCurrentPage] = useState(1); const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); const { data: draftData, isLoading, refetch, isFetching } = useGetDrafts({ page: currentPage, @@ -22,7 +24,6 @@ const List: FC = () => { ...filters }); - const deleteMutation = useDeleteDraft(); const [selectedMessages, setSelectedMessages] = useState([]); const columns: ColumnType[] = [ @@ -80,10 +81,10 @@ const List: FC = () => { const selectedActions = ( <> - handleDeleteSelected()} + onClick={() => handleSendSelected()} className="cursor-pointer" /> { onClick={() => handleEditSelected()} className="cursor-pointer" /> + handleDeleteSelected()} + className="cursor-pointer" + /> ); const handleFilterChange = (newFilters: FilterValues) => { setFilters(newFilters); - setCurrentPage(1); // Reset to first page when filters change + setCurrentPage(1); }; const handleSelectionChange = (selectedRows: DraftMessage[]) => { @@ -106,14 +113,20 @@ const List: FC = () => { const handleDeleteSelected = () => { selectedMessages.forEach(message => { - deleteMutation.mutate(message.id.toString()); + emailActions.deleteDraft(message.id); + }); + setSelectedMessages([]); + }; + + const handleSendSelected = () => { + selectedMessages.forEach(message => { + emailActions.sendDraft(message.id); }); setSelectedMessages([]); }; const handleEditSelected = () => { if (selectedMessages.length === 1) { - // Navigate to edit page for single draft navigate(`/draft/edit/${selectedMessages[0].id}`); } else { console.log('ویرایش چندین پیش‌نویس انتخاب شده:', selectedMessages.map(m => m.id)); @@ -121,7 +134,11 @@ const List: FC = () => { }; const handleDeleteDraft = (messageId: number) => { - deleteMutation.mutate(messageId.toString()); + emailActions.deleteDraft(messageId); + }; + + const handleSendDraft = (messageId: number) => { + emailActions.sendDraft(messageId); }; const handleEditDraft = (messageId: number) => { @@ -129,6 +146,11 @@ const List: FC = () => { }; const getRowActions = (message: DraftMessage): RowActionItem[] => [ + { + label: 'ارسال', + icon: , + onClick: () => handleSendDraft(message.id), + }, { label: 'ویرایش', icon: , @@ -165,11 +187,12 @@ const List: FC = () => { columns={columns} data={messages} - isLoading={isLoading} + isLoading={isLoading || emailActions.isLoading} showHeader={false} actions={tableActions} selectedActions={selectedActions} onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} onRowClick={(id) => navigate(`/draft/edit/${id}`)} noDataMessage={
diff --git a/src/pages/favorite/List.tsx b/src/pages/favorite/List.tsx new file mode 100644 index 0000000..bbd6a15 --- /dev/null +++ b/src/pages/favorite/List.tsx @@ -0,0 +1,179 @@ +import Filters, { FilterValues } from '../../components/Filters'; +import { FC, useState } from 'react' +import Table from '../../components/Table'; +import { ColumnType } from '../../components/types/TableTypes'; +import { InfoCircle, More, Refresh2, Trash, Star, Archive } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; +import { useNavigate } from 'react-router-dom'; +import { useGetFavoriteMessages } from './hooks/useFavoriteData'; +import { FavoriteMessage } from './types/FavoriteTypes'; +import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; + +const List: FC = () => { + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); + + const { data: favoriteData, isLoading, refetch, isFetching } = useGetFavoriteMessages({ + page: currentPage, + limit: 10, + ...filters + }); + + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ + { + key: 'subject', + title: 'عنوان پیام', + render: (item) => ( + {item.subject || 'بدون موضوع'} + ) + }, + { + key: 'from', + title: 'فرستنده', + render: (item) => ( +
+ {!item.seen &&
} + {item.from.name || item.from.address} +
+ ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {formatDate(item.date)} + ) + }, + ]; + + const tableActions = ( +
+ refetch()} + /> + +
+ ); + + const selectedActions = ( + <> + handleUnfavoriteSelected()} className="cursor-pointer" /> + handleArchiveSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + + const handleFilterChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(1); + }; + + const handleSelectionChange = (selectedRows: FavoriteMessage[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); + }; + + const handleArchiveSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkArchive(messageIds); + setSelectedMessages([]); + }; + + const handleUnfavoriteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkUnfavorite(messageIds); + setSelectedMessages([]); + }; + + const getRowActions = (message: FavoriteMessage): RowActionItem[] => [ + { + label: 'حذف از علاقه‌مندی‌ها', + icon: , + onClick: () => emailActions.unfavorite(message.id), + }, + { + label: 'بایگانی', + icon: , + onClick: () => emailActions.archive(message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => emailActions.moveToTrash(message.id), + }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = favoriteData?.data?.favoriteMails || []; + const pager = favoriteData?.data?.pager; + + return ( +
+

علاقه‌مندی‌ها

+ + + + + columns={columns} + data={messages} + isLoading={isLoading || emailActions.isLoading} + showHeader={false} + actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} + onRowClick={(id) => navigate(`/mail/${id}`)} + noDataMessage={ +
+ +
هیچ پیامی یافت نشد
+
+ } + selectable={true} + rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} + /> +
+ ) +} + +export default List \ No newline at end of file diff --git a/src/pages/favorite/hooks/useFavoriteData.tsx b/src/pages/favorite/hooks/useFavoriteData.tsx new file mode 100644 index 0000000..edd6cf7 --- /dev/null +++ b/src/pages/favorite/hooks/useFavoriteData.tsx @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { getFavoriteMessages } from '../service/FavoriteService'; +import { MessageListQueryDto } from '../types/FavoriteTypes'; + +export const useGetFavoriteMessages = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['favorite-messages', query], + queryFn: () => getFavoriteMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; + +// Note: useUnfavoriteMessage is now handled by useEmailActions hook \ No newline at end of file diff --git a/src/pages/favorite/service/FavoriteService.ts b/src/pages/favorite/service/FavoriteService.ts new file mode 100644 index 0000000..41174a3 --- /dev/null +++ b/src/pages/favorite/service/FavoriteService.ts @@ -0,0 +1,13 @@ +import axios from "../../../config/axios"; +import { MessageListQueryDto, FavoriteResponse } from "../types/FavoriteTypes"; + +export const getFavoriteMessages = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/favorite`, { + params: query, + }); + return data; +}; + +// Note: moveMessageToFavorite and unfavoriteMessage are now handled by EmailActionsService diff --git a/src/pages/favorite/types/FavoriteTypes.ts b/src/pages/favorite/types/FavoriteTypes.ts new file mode 100644 index 0000000..23d74af --- /dev/null +++ b/src/pages/favorite/types/FavoriteTypes.ts @@ -0,0 +1,61 @@ +export interface EmailAddress { + address: string; + name: string; +} + +export interface FavoriteMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export interface MessageListQueryDto { + page?: number; + limit?: number; + order?: "asc" | "desc"; + search?: string; + dateFrom?: string; + dateTo?: string; + isRead?: string; +} + +export interface FavoriteResponse { + statusCode: number; + success: boolean; + data: { + favoriteMails: FavoriteMessage[]; + pager: { + page: number; + totalPages: number; + totalItems: number; + limit: number; + prevPage: boolean; + nextPage: boolean; + }; + }; +} diff --git a/src/pages/received/List.tsx b/src/pages/received/List.tsx index 6f6120a..2277480 100644 --- a/src/pages/received/List.tsx +++ b/src/pages/received/List.tsx @@ -3,18 +3,21 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2, Trash, Edit, Archive } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, Edit, Archive, ArchiveTick, Star1 } from 'iconsax-react'; import { RowActionItem } from '../../components/RowActionsDropdown'; import { useNavigate } from 'react-router-dom'; import { useGetInbox } from './hooks/useEmailData'; import { InboxMessage } from './types/Types'; import { formatDate } from '@/config/func'; +import MarkAsRead from '@/assets/images/mark_as_read.svg' +import { useEmailActions } from '@/hooks/useEmailActions'; const List: FC = () => { const navigate = useNavigate(); const { t } = useTranslation(); const [currentPage, setCurrentPage] = useState(1); const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({ page: currentPage, @@ -29,17 +32,16 @@ const List: FC = () => { key: 'subject', title: 'عنوان پیام', render: (item) => ( -
- {/* {!item.seen &&
} */} - {item.subject || 'بدون موضوع'} -
+ {item.subject || 'بدون موضوع'} ) }, { key: 'from', title: 'فرستنده', render: (item) => ( - {item.from.name || item.from.address} +
+ {item.from.name || item.from.address} +
) }, { @@ -60,21 +62,42 @@ const List: FC = () => { className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`} onClick={() => refetch()} /> -
); const selectedActions = ( <> - handleDeleteSelected()} className="cursor-pointer" /> - handleArchiveSelected()} className="cursor-pointer" /> - handleMarkAsReadSelected()} className="cursor-pointer" /> + handleMarkAsReadSelected()} + /> + handleArchiveSelected()} + className="cursor-pointer" + /> + + handleFavoriteSelected()} + /> + handleDeleteSelected()} + className="cursor-pointer" + /> + ); const handleFilterChange = (newFilters: FilterValues) => { setFilters(newFilters); - setCurrentPage(1); // Reset to first page when filters change + setCurrentPage(1); }; const handleSelectionChange = (selectedRows: InboxMessage[]) => { @@ -82,35 +105,49 @@ const List: FC = () => { }; const handleDeleteSelected = () => { - console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق حذف را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); }; const handleArchiveSelected = () => { - console.log('بایگانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق بایگانی را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkArchive(messageIds); + setSelectedMessages([]); }; const handleMarkAsReadSelected = () => { - console.log('علامت‌گذاری به عنوان خوانده شده:', selectedMessages.map(m => m.id)); - // اینجا منطق علامت‌گذاری را پیاده‌سازی کنید + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMarkAsSeen(messageIds); + setSelectedMessages([]); + }; + + const handleFavoriteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkFavorite(messageIds); + setSelectedMessages([]); }; const getRowActions = (message: InboxMessage): RowActionItem[] => [ { label: message.seen ? 'علامت‌گذاری به عنوان خوانده نشده' : 'علامت‌گذاری به عنوان خوانده شده', icon: , - onClick: () => console.log('تغییر وضعیت خوانده شده', message.id), + onClick: () => emailActions.markAsSeen(message.id), + }, + { + label: 'اضافه به علاقه‌مندی‌ها', + icon: , + onClick: () => emailActions.favorite(message.id), }, { label: 'بایگانی', icon: , - onClick: () => console.log('بایگانی پیام', message.id), + onClick: () => emailActions.archive(message.id), }, { label: 'حذف', icon: , - onClick: () => console.log('حذف پیام', message.id), + onClick: () => emailActions.moveToTrash(message.id), }, ]; @@ -148,14 +185,15 @@ const List: FC = () => { columns={columns} data={messages} - isLoading={isLoading} + isLoading={isLoading || emailActions.isLoading} showHeader={false} actions={tableActions} selectedActions={selectedActions} onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} onRowClick={(id) => navigate(`/mail/${id}`)} noDataMessage={ -
+
هیچ پیامی یافت نشد
diff --git a/src/pages/received/service/EmailService.ts b/src/pages/received/service/EmailService.ts index c32a058..5756d23 100644 --- a/src/pages/received/service/EmailService.ts +++ b/src/pages/received/service/EmailService.ts @@ -35,12 +35,12 @@ export const getMessageDetail = async ( messageId: string ): Promise => { const { data }: { data: MessageDetailResponse } = await axios.get( - `/email/messages/inbox/${messageId}` + `/email/messages/${messageId}` ); return data.data.message; }; export const deleteMessage = async (messageId: string): Promise => { - const { data } = await axios.delete(`/email/messages/inbox/${messageId}`); + const { data } = await axios.delete(`/email/messages/${messageId}`); return data; }; diff --git a/src/pages/received/types/Types.ts b/src/pages/received/types/Types.ts index 9c29dd8..22068ce 100644 --- a/src/pages/received/types/Types.ts +++ b/src/pages/received/types/Types.ts @@ -286,3 +286,21 @@ export interface MessageDetail { }; }; } + +export enum BulkActionType { + SEEN = "seen", + DELETE = "delete", + ARCHIVE = "archive", + FAVORITE = "favorite", + UNFAVORITE = "unfavorite", + UNARCHIVE = "unarchive", + RESTORE = "restore", + JUNK = "junk", + TRASH = "trash", +} + +// Interface for bulk action request +export interface BulkActionRequest { + messageIds: number[]; + action: BulkActionType; +} diff --git a/src/pages/search/List.tsx b/src/pages/search/List.tsx new file mode 100644 index 0000000..b7b4597 --- /dev/null +++ b/src/pages/search/List.tsx @@ -0,0 +1,192 @@ +import Filters, { FilterValues } from '../../components/Filters'; +import { FC, useState } from 'react' +import Table from '../../components/Table'; +import { ColumnType } from '../../components/types/TableTypes'; +import { InfoCircle, More, Refresh2, Trash, Archive, Star } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; +import { useNavigate } from 'react-router-dom'; +import { useSearchMessages } from './hooks/useSearchData'; +import { SearchMessage } from './types/SearchTypes'; +import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; + +const List: FC = () => { + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); + + const { data: searchData, isLoading, refetch, isFetching } = useSearchMessages({ + page: currentPage, + limit: 10, + ...filters + }); + + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ + { + key: 'subject', + title: 'عنوان پیام', + render: (item) => ( + {item.subject || 'بدون موضوع'} + ) + }, + { + key: 'from', + title: 'فرستنده', + render: (item) => ( +
+ {!item.seen &&
} + {item.from.name || item.from.address} +
+ ) + }, + { + key: 'mailbox', + title: 'پوشه', + render: (item) => ( + {item.mailbox} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {formatDate(item.date)} + ) + }, + ]; + + const tableActions = ( +
+ refetch()} + /> + +
+ ); + + const selectedActions = ( + <> + handleFavoriteSelected()} className="cursor-pointer" /> + handleArchiveSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + + const handleFilterChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(1); + }; + + const handleSelectionChange = (selectedRows: SearchMessage[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); + }; + + const handleArchiveSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkArchive(messageIds); + setSelectedMessages([]); + }; + + const handleFavoriteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkFavorite(messageIds); + setSelectedMessages([]); + }; + + const getRowActions = (message: SearchMessage): RowActionItem[] => [ + { + label: 'اضافه به علاقه‌مندی‌ها', + icon: , + onClick: () => emailActions.favorite(message.id), + }, + { + label: 'بایگانی', + icon: , + onClick: () => emailActions.archive(message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => emailActions.moveToTrash(message.id), + }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = searchData?.data?.results || []; + const pager = searchData?.data?.pager; + + return ( +
+

جستجو در پیام‌ها

+ + + + + columns={columns} + data={messages} + isLoading={isLoading || emailActions.isLoading} + showHeader={false} + actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} + onRowClick={(id) => navigate(`/mail/${id}`)} + noDataMessage={ +
+ +
+ {filters.q ? 'هیچ نتیجه‌ای یافت نشد' : 'برای جستجو کلمه مورد نظر را وارد کنید'} +
+
+ } + selectable={true} + rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} + /> +
+ ) +} + +export default List \ No newline at end of file diff --git a/src/pages/search/hooks/useSearchData.tsx b/src/pages/search/hooks/useSearchData.tsx new file mode 100644 index 0000000..61487ff --- /dev/null +++ b/src/pages/search/hooks/useSearchData.tsx @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { searchMessages } from '../service/SearchService'; +import { SearchQueryDto } from '../types/SearchTypes'; + +export const useSearchMessages = (query: SearchQueryDto) => { + return useQuery({ + queryKey: ['search-messages', query], + queryFn: () => searchMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: !!query.q, // Only run query if search term exists + }); +}; \ No newline at end of file diff --git a/src/pages/search/service/SearchService.ts b/src/pages/search/service/SearchService.ts new file mode 100644 index 0000000..b178bb0 --- /dev/null +++ b/src/pages/search/service/SearchService.ts @@ -0,0 +1,9 @@ +import axios from "../../../config/axios"; +import { SearchQueryDto, SearchResponse } from "../types/SearchTypes"; + +export const searchMessages = async ( + query: SearchQueryDto +): Promise => { + const { data } = await axios.get(`/email/search`, { params: query }); + return data; +}; diff --git a/src/pages/search/types/SearchTypes.ts b/src/pages/search/types/SearchTypes.ts new file mode 100644 index 0000000..3853b3f --- /dev/null +++ b/src/pages/search/types/SearchTypes.ts @@ -0,0 +1,59 @@ +export interface EmailAddress { + address: string; + name: string; +} + +export interface SearchMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export interface SearchQueryDto { + page?: number; + limit?: number; + q?: string; + dateFrom?: string; + dateTo?: string; + mailbox?: string; +} + +export interface SearchResponse { + success: boolean; + data: { + results: SearchMessage[]; + pager: { + page: number; + totalPages: number; + totalItems: number; + limit: number; + prevPage: boolean; + nextPage: boolean; + }; + }; +} diff --git a/src/pages/sent/List.tsx b/src/pages/sent/List.tsx index 83d8528..5180226 100644 --- a/src/pages/sent/List.tsx +++ b/src/pages/sent/List.tsx @@ -1,105 +1,190 @@ import Filters, { FilterValues } from '../../components/Filters'; import { FC, useState } from 'react' -import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2 } from 'iconsax-react'; - -interface Message extends Record { - id: string; - title: string; - sender: string; - date: string; - read: boolean; -} +import { InfoCircle, More, Refresh2, AttachSquare, Trash, Archive, Star } from 'iconsax-react'; +import { useGetSentMessages } from './hooks/useSentData'; +import { InboxMessage } from './types/SentTypes'; +import { useNavigate } from 'react-router-dom'; +import { Paths } from '../../utils/Paths'; +import { useEmailActions } from '@/hooks/useEmailActions'; +import { RowActionItem } from '../../components/RowActionsDropdown'; +import { formatDate } from '@/config/func'; const List: FC = () => { - const { t } = useTranslation(); - const [isLoading] = useState(false); + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); - const messageData: Message[] = [ - { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, - ]; + const { data: sentData, isLoading, refetch, isFetching } = useGetSentMessages({ + page: currentPage, + limit: 10, + ...filters + }); - const columns: ColumnType[] = [ + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ { - key: 'title', - title: 'عنوان پیام', + key: 'subject', + title: 'موضوع پیام', render: (item) => ( -
- {!item.read &&
} - {item.title} +
+ {item.subject} + {item.attachments && ( + + )}
) }, { - key: 'sender', - title: 'فرستنده', + key: 'to', + title: 'گیرنده', render: (item) => ( - {item.sender} +
+ {!item.seen &&
} + + {item.to.map(recipient => recipient.address).join(', ')} + +
) }, { key: 'date', - title: 'تاریخ', + title: 'تاریخ ارسال', align: 'center', render: (item) => ( - {item.date} + + {formatDate(item.date)} + ) }, ]; const tableActions = (
- + refetch()} + />
); + const selectedActions = ( + <> + handleFavoriteSelected()} className="cursor-pointer" /> + handleArchiveSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + const handleFilterChange = (newFilters: FilterValues) => { - console.log('Applied filters:', newFilters); + setFilters(newFilters); + setCurrentPage(1); }; + const handleRowClick = (id: number) => { + navigate(`${Paths.detailEmail}${id}`); + }; + + const handleSelectionChange = (selectedRows: InboxMessage[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); + }; + + const handleArchiveSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkArchive(messageIds); + setSelectedMessages([]); + }; + + const handleFavoriteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkFavorite(messageIds); + setSelectedMessages([]); + }; + + const getRowActions = (message: InboxMessage): RowActionItem[] => [ + { + label: 'اضافه به علاقه‌مندی‌ها', + icon: , + onClick: () => emailActions.favorite(message.id), + }, + { + label: 'بایگانی', + icon: , + onClick: () => emailActions.archive(message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => emailActions.moveToTrash(message.id), + }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = sentData?.data?.sentMails || []; + const pager = sentData?.data?.pager; + return (
-

{t('sent.title')}

+

ارسال شده

- + columns={columns} - data={messageData} - isLoading={isLoading} + data={messages} + isLoading={isLoading || emailActions.isLoading} showHeader={false} actions={tableActions} - onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} + onRowClick={handleRowClick} noDataMessage={ -
+
هیچ پیامی یافت نشد
} selectable={true} + rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} />
diff --git a/src/pages/sent/hooks/useSentData.tsx b/src/pages/sent/hooks/useSentData.tsx new file mode 100644 index 0000000..d1adcce --- /dev/null +++ b/src/pages/sent/hooks/useSentData.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { getSentMessages } from '../service/SentService'; +import { MessageListQueryDto } from '../types/SentTypes'; + +export const useGetSentMessages = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['sent-messages', query], + queryFn: () => getSentMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; \ No newline at end of file diff --git a/src/pages/sent/service/SentService.ts b/src/pages/sent/service/SentService.ts new file mode 100644 index 0000000..98c8904 --- /dev/null +++ b/src/pages/sent/service/SentService.ts @@ -0,0 +1,28 @@ +import axios from "../../../config/axios"; +import { + MessageListQueryDto, + InboxResponse, + MessageDetail, + MessageDetailResponse, +} from "../types/SentTypes"; + +export const getSentMessages = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/sent`, { params: query }); + return data; +}; + +export const getSentMessageDetail = async ( + messageId: number +): Promise => { + const { data }: { data: MessageDetailResponse } = await axios.get( + `/email/messages/sent/${messageId}` + ); + return data.data.message; +}; + +export const deleteSentMessage = async (messageId: number): Promise => { + const { data } = await axios.delete(`/email/messages/sent/${messageId}`); + return data; +}; diff --git a/src/pages/sent/types/SentTypes.ts b/src/pages/sent/types/SentTypes.ts new file mode 100644 index 0000000..1618b63 --- /dev/null +++ b/src/pages/sent/types/SentTypes.ts @@ -0,0 +1,76 @@ +export interface MessageListQueryDto { + page?: number; + limit?: number; + search?: string; + from_date?: string; + to_date?: string; + status?: "all" | "read" | "unread"; +} + +export interface EmailAddress { + address: string; + name: string; +} + +export interface ContentType { + value: string; + params: { + boundary: string; + }; +} + +export interface InboxMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: unknown[]; + contentType: ContentType; +} + +export interface Pager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean; + nextPage: boolean; +} + +export interface InboxResponse { + statusCode: number; + success: boolean; + data: { + pager: Pager; + sentMails: InboxMessage[]; + }; +} + +export interface MessageDetail extends InboxMessage { + body: string; +} + +export interface MessageDetailResponse { + statusCode: number; + success: boolean; + data: { + message: MessageDetail; + }; +} diff --git a/src/pages/spam/List.tsx b/src/pages/spam/List.tsx new file mode 100644 index 0000000..436d039 --- /dev/null +++ b/src/pages/spam/List.tsx @@ -0,0 +1,167 @@ +import Filters, { FilterValues } from '../../components/Filters'; +import { FC, useState } from 'react' +import Table from '../../components/Table'; +import { ColumnType } from '../../components/types/TableTypes'; +import { InfoCircle, More, Refresh2, Trash, RecoveryConvert } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; +import { useNavigate } from 'react-router-dom'; +import { useGetSpamMessages } from './hooks/useSpamData'; +import { SpamMessage } from './types/SpamTypes'; +import { formatDate } from '@/config/func'; +import { useEmailActions } from '@/hooks/useEmailActions'; + +const List: FC = () => { + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); + const emailActions = useEmailActions(); + + const { data: spamData, isLoading, refetch, isFetching } = useGetSpamMessages({ + page: currentPage, + limit: 10, + ...filters + }); + + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ + { + key: 'subject', + title: 'عنوان پیام', + render: (item) => ( + {item.subject || 'بدون موضوع'} + ) + }, + { + key: 'from', + title: 'فرستنده', + render: (item) => ( +
+ {!item.seen &&
} + {item.from.name || item.from.address} +
+ ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {formatDate(item.date)} + ) + }, + ]; + + const tableActions = ( +
+ refetch()} + /> + +
+ ); + + const selectedActions = ( + <> + handleNotSpamSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + + const handleFilterChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(1); + }; + + const handleSelectionChange = (selectedRows: SpamMessage[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + const messageIds = selectedMessages.map(message => message.id); + emailActions.bulkMoveToTrash(messageIds); + setSelectedMessages([]); + }; + + const handleNotSpamSelected = () => { + // TODO: پیاده‌سازی علامت‌گذاری به عنوان غیر اسپم - نیاز به API endpoint جداگانه + console.log('علامت‌گذاری به عنوان غیر اسپم:', selectedMessages.map(m => m.id)); + setSelectedMessages([]); + }; + + const getRowActions = (message: SpamMessage): RowActionItem[] => [ + { + label: 'غیر اسپم', + icon: , + onClick: () => console.log('علامت‌گذاری به عنوان غیر اسپم', message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => emailActions.moveToTrash(message.id), + }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = spamData?.data?.results || []; + const pager = spamData?.data?.pager; + + return ( +
+

پیام‌های اسپم

+ + + + + columns={columns} + data={messages} + isLoading={isLoading || emailActions.isLoading} + showHeader={false} + actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} + selectedRows={selectedMessages} + onRowClick={(id) => navigate(`/mail/${id}`)} + noDataMessage={ +
+ +
هیچ پیامی یافت نشد
+
+ } + selectable={true} + rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} + /> +
+ ) +} + +export default List \ No newline at end of file diff --git a/src/pages/spam/hooks/useSpamData.tsx b/src/pages/spam/hooks/useSpamData.tsx new file mode 100644 index 0000000..c2140f5 --- /dev/null +++ b/src/pages/spam/hooks/useSpamData.tsx @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { getSpamMessages } from '../service/SpamService'; +import { MessageListQueryDto } from '../types/SpamTypes'; + +export const useGetSpamMessages = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['spam-messages', query], + queryFn: () => getSpamMessages(query), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; \ No newline at end of file diff --git a/src/pages/spam/service/SpamService.ts b/src/pages/spam/service/SpamService.ts new file mode 100644 index 0000000..be48d0c --- /dev/null +++ b/src/pages/spam/service/SpamService.ts @@ -0,0 +1,9 @@ +import axios from "../../../config/axios"; +import { MessageListQueryDto, SpamResponse } from "../types/SpamTypes"; + +export const getSpamMessages = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/junk`, { params: query }); + return data; +}; diff --git a/src/pages/spam/types/SpamTypes.ts b/src/pages/spam/types/SpamTypes.ts new file mode 100644 index 0000000..9512591 --- /dev/null +++ b/src/pages/spam/types/SpamTypes.ts @@ -0,0 +1,60 @@ +export interface EmailAddress { + address: string; + name: string; +} + +export interface SpamMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: EmailAddress; + to: EmailAddress[]; + cc: EmailAddress[]; + bcc: EmailAddress[]; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: unknown[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export interface MessageListQueryDto { + page?: number; + limit?: number; + order?: "asc" | "desc"; + search?: string; + dateFrom?: string; + dateTo?: string; + isRead?: string; +} + +export interface SpamResponse { + success: boolean; + data: { + results: SpamMessage[]; + pager: { + page: number; + totalPages: number; + totalItems: number; + limit: number; + prevPage: boolean; + nextPage: boolean; + }; + }; +} diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 06b8015..69216ba 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -6,8 +6,12 @@ import SentList from '@/pages/sent/List' import DraftList from '@/pages/draft/List' import ArchiveList from '@/pages/archive/List' import TrashList from '@/pages/Trash/List' +import FavoriteList from '@/pages/favorite/List' +import SpamList from '@/pages/spam/List' +import SearchList from '@/pages/search/List' import DetailEmail from '@/pages/received/Detail' import Login from '@/pages/auth/Login' + const AppRouter: FC = () => { return ( @@ -16,6 +20,9 @@ const AppRouter: FC = () => { } /> } /> } /> + } /> + } /> + } /> } /> } /> diff --git a/src/services/EmailActionsService.ts b/src/services/EmailActionsService.ts new file mode 100644 index 0000000..c61e527 --- /dev/null +++ b/src/services/EmailActionsService.ts @@ -0,0 +1,83 @@ +import axios from "../config/axios"; +import { BulkActionRequest } from "../pages/received/types/Types"; + +// Bulk action for multiple messages +export const bulkAction = async (request: BulkActionRequest) => { + const { data } = await axios.post(`/email/messages/bulk-action`, request); + return data; +}; + +// Mark message as seen +export const markMessageAsSeen = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/seen`); + return data; +}; + +// Move message to archive +export const moveMessageToArchive = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/archive`); + return data; +}; + +// Move message to favorite +export const moveMessageToFavorite = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/favorite`); + return data; +}; + +// Move message to trash +export const moveMessageToTrash = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/trash`); + return data; +}; + +// Delete message permanently (only for trash mailbox) +export const deleteMessagePermanently = async (messageId: number) => { + const { data } = await axios.delete(`/email/messages/${messageId}`); + return data; +}; + +// Send email +export const sendEmail = async (emailData: unknown) => { + const { data } = await axios.post(`/email/send`, emailData); + return data; +}; + +// Update draft +export const updateDraft = async (messageId: number, draftData: unknown) => { + const { data } = await axios.patch( + `/email/messages/drafts/${messageId}`, + draftData + ); + return data; +}; + +// Delete draft +export const deleteDraft = async (messageId: number) => { + const { data } = await axios.delete(`/email/messages/drafts/${messageId}`); + return data; +}; + +// Send draft +export const sendDraft = async (messageId: number) => { + const { data } = await axios.post(`/email/messages/drafts/${messageId}/send`); + return data; +}; + +// Unfavorite message +export const unfavoriteMessage = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/unfavorite`); + return data; +}; + +// Unarchive message +export const unarchiveMessage = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/unarchive`); + return data; +}; + +// Restore message from trash +export const restoreMessage = async (messageId: number) => { + const { data } = await axios.patch(`/email/messages/${messageId}/restore`); + return data; +}; diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 8f636ee..c1b127a 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import LogoImage from '../assets/images/logo.svg' -import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Star, Trash } from 'iconsax-react' +import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Star1, Trash } from 'iconsax-react' import SideBarItem from './SideBarItem' import { useLocation } from 'react-router-dom' import { useSharedStore } from './store/sharedStore' @@ -98,7 +98,7 @@ const SideBar: FC = () => {
} + icon={} title={t('sidebar.favorite')} isActive={isActive(Paths.favorite)} link={Paths.favorite} diff --git a/src/utils/Paths.ts b/src/utils/Paths.ts index 65d52cb..d97c9a8 100644 --- a/src/utils/Paths.ts +++ b/src/utils/Paths.ts @@ -7,6 +7,7 @@ export const Paths = { trash: "/trash", favorite: "/favorite", spam: "/spam", + search: "/search", setting: "/setting", detailEmail: "/mail/", login: "/auth/login",