email actions + sent list + action list + archive list + trash list
This commit is contained in:
+73
-51
@@ -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<string, unknown> {
|
||||
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<Message[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
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<Message>[] = [
|
||||
const [selectedMessages, setSelectedMessages] = useState<TrashMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<TrashMessage>[] = [
|
||||
{
|
||||
key: 'title',
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
|
||||
<span className="truncate">{item.title}</span>
|
||||
</div>
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'sender',
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.sender}</span>
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -48,14 +47,19 @@ const List: FC = () => {
|
||||
title: 'تاریخ',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span className="text-xs md:text-sm">{item.date}</span>
|
||||
<span>{formatDate(item.date)}</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2 size={18} color='black' className="cursor-pointer" />
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
@@ -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: <RecoveryConvert size={16} color="black" />,
|
||||
onClick: () => console.log('بازگردانی پیام', message.id),
|
||||
onClick: () => emailActions.restore(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف دائم',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
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 (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">{t('trash.title')}</h1>
|
||||
<h1 className="text-lg mb-4 md:mb-0">سطل آشغال</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'date', name: 'from_date', placeholder: t('trash.from_date') },
|
||||
{ type: 'date', name: 'to_date', placeholder: t('trash.to_date') },
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'status',
|
||||
placeholder: t('trash.all'),
|
||||
name: 'isRead',
|
||||
placeholder: 'همه',
|
||||
options: [
|
||||
{ value: 'all', label: t('trash.all') },
|
||||
{ value: 'read', label: t('trash.read') },
|
||||
{ value: 'unread', label: t('trash.unread') }
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: 'true', label: 'خوانده شده' },
|
||||
{ value: 'false', label: 'خوانده نشده' }
|
||||
]
|
||||
},
|
||||
{ type: 'input', name: 'search', placeholder: t('trash.search') }
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="search"
|
||||
/>
|
||||
|
||||
<Table<Message>
|
||||
<Table<TrashMessage>
|
||||
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={
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">سطل آشغال خالی است</div>
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { MessageListQueryDto, TrashResponse } from "../types/TrashTypes";
|
||||
|
||||
export const getTrashMessages = async (
|
||||
query: MessageListQueryDto
|
||||
): Promise<TrashResponse> => {
|
||||
const { data } = await axios.get(`/email/messages/trash`, { params: query });
|
||||
return data;
|
||||
};
|
||||
|
||||
// Note: deleteMessagePermanently and restoreMessage are now handled by EmailActionsService
|
||||
@@ -0,0 +1,61 @@
|
||||
export interface EmailAddress {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TrashMessage extends Record<string, unknown> {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+71
-51
@@ -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<string, unknown> {
|
||||
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<Message[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
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<Message>[] = [
|
||||
const [selectedMessages, setSelectedMessages] = useState<ArchiveMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<ArchiveMessage>[] = [
|
||||
{
|
||||
key: 'title',
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
|
||||
<span className="truncate">{item.title}</span>
|
||||
</div>
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'sender',
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.sender}</span>
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -48,14 +47,19 @@ const List: FC = () => {
|
||||
title: 'تاریخ',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span className="text-xs md:text-sm">{item.date}</span>
|
||||
<span>{formatDate(item.date)}</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2 size={18} color='black' className="cursor-pointer" />
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
@@ -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: <ArchiveSlash size={16} color="black" />,
|
||||
onClick: () => console.log('خروج از آرشیو', message.id),
|
||||
onClick: () => emailActions.unarchive(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
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 (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">{t('archive.title')}</h1>
|
||||
<h1 className="text-lg mb-4 md:mb-0">آرشیو</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'date', name: 'from_date', placeholder: t('archive.from_date') },
|
||||
{ type: 'date', name: 'to_date', placeholder: t('archive.to_date') },
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'status',
|
||||
placeholder: t('archive.all'),
|
||||
name: 'isRead',
|
||||
placeholder: 'همه',
|
||||
options: [
|
||||
{ value: 'all', label: t('archive.all') },
|
||||
{ value: 'read', label: t('archive.read') },
|
||||
{ value: 'unread', label: t('archive.unread') }
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: 'true', label: 'خوانده شده' },
|
||||
{ value: 'false', label: 'خوانده نشده' }
|
||||
]
|
||||
},
|
||||
{ type: 'input', name: 'search', placeholder: t('archive.search') }
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="search"
|
||||
/>
|
||||
|
||||
<Table<Message>
|
||||
<Table<ArchiveMessage>
|
||||
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={
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">هیچ پیام آرشیو شدهای یافت نشد</div>
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { MessageListQueryDto, ArchiveResponse } from "../types/ArchiveTypes";
|
||||
|
||||
export const getArchiveMessages = async (
|
||||
query: MessageListQueryDto
|
||||
): Promise<ArchiveResponse> => {
|
||||
const { data } = await axios.get(`/email/messages/archive`, {
|
||||
params: query,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// Note: moveMessageToArchive and unarchiveMessage are now handled by EmailActionsService
|
||||
@@ -0,0 +1,61 @@
|
||||
export interface EmailAddress {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ArchiveMessage extends Record<string, unknown> {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+33
-10
@@ -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<FilterValues>({});
|
||||
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<DraftMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<DraftMessage>[] = [
|
||||
@@ -80,10 +81,10 @@ const List: FC = () => {
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<Trash
|
||||
<Send
|
||||
size={18}
|
||||
color='black'
|
||||
onClick={() => handleDeleteSelected()}
|
||||
onClick={() => handleSendSelected()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<Edit
|
||||
@@ -92,12 +93,18 @@ const List: FC = () => {
|
||||
onClick={() => handleEditSelected()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<Trash
|
||||
size={18}
|
||||
color='black'
|
||||
onClick={() => 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: <Send size={16} color="black" />,
|
||||
onClick: () => handleSendDraft(message.id),
|
||||
},
|
||||
{
|
||||
label: 'ویرایش',
|
||||
icon: <Edit size={16} color="black" />,
|
||||
@@ -165,11 +187,12 @@ const List: FC = () => {
|
||||
<Table<DraftMessage>
|
||||
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={
|
||||
<div className="flex flex-col items-center ">
|
||||
|
||||
@@ -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<FilterValues>({});
|
||||
const emailActions = useEmailActions();
|
||||
|
||||
const { data: favoriteData, isLoading, refetch, isFetching } = useGetFavoriteMessages({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
...filters
|
||||
});
|
||||
|
||||
const [selectedMessages, setSelectedMessages] = useState<FavoriteMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<FavoriteMessage>[] = [
|
||||
{
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
title: 'تاریخ',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span>{formatDate(item.date)}</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<Star size={18} color='black' onClick={() => handleUnfavoriteSelected()} className="cursor-pointer" />
|
||||
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
|
||||
<Trash size={18} color='black' onClick={() => 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: <Star size={16} color="black" />,
|
||||
onClick: () => emailActions.unfavorite(message.id),
|
||||
},
|
||||
{
|
||||
label: 'بایگانی',
|
||||
icon: <Archive size={16} color="black" />,
|
||||
onClick: () => emailActions.archive(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
onClick: () => emailActions.moveToTrash(message.id),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const messages = favoriteData?.data?.favoriteMails || [];
|
||||
const pager = favoriteData?.data?.pager;
|
||||
|
||||
return (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">علاقهمندیها</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'isRead',
|
||||
placeholder: 'همه',
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: 'true', label: 'خوانده شده' },
|
||||
{ value: 'false', label: 'خوانده نشده' }
|
||||
]
|
||||
},
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="search"
|
||||
/>
|
||||
|
||||
<Table<FavoriteMessage>
|
||||
columns={columns}
|
||||
data={messages}
|
||||
isLoading={isLoading || emailActions.isLoading}
|
||||
showHeader={false}
|
||||
actions={tableActions}
|
||||
selectedActions={selectedActions}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedRows={selectedMessages}
|
||||
onRowClick={(id) => navigate(`/mail/${id}`)}
|
||||
noDataMessage={
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default List
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { MessageListQueryDto, FavoriteResponse } from "../types/FavoriteTypes";
|
||||
|
||||
export const getFavoriteMessages = async (
|
||||
query: MessageListQueryDto
|
||||
): Promise<FavoriteResponse> => {
|
||||
const { data } = await axios.get(`/email/messages/favorite`, {
|
||||
params: query,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// Note: moveMessageToFavorite and unfavoriteMessage are now handled by EmailActionsService
|
||||
@@ -0,0 +1,61 @@
|
||||
export interface EmailAddress {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FavoriteMessage extends Record<string, unknown> {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+60
-22
@@ -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<FilterValues>({});
|
||||
const emailActions = useEmailActions();
|
||||
|
||||
const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({
|
||||
page: currentPage,
|
||||
@@ -29,17 +32,16 @@ const List: FC = () => {
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{/* {!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />} */}
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
</div>
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
<div className="flex items-center">
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -60,21 +62,42 @@ const List: FC = () => {
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
|
||||
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
|
||||
<Edit size={18} color='black' onClick={() => handleMarkAsReadSelected()} className="cursor-pointer" />
|
||||
<img
|
||||
src={MarkAsRead}
|
||||
className='w-[18px] cursor-pointer'
|
||||
onClick={() => handleMarkAsReadSelected()}
|
||||
/>
|
||||
<ArchiveTick
|
||||
size={18}
|
||||
color='black'
|
||||
onClick={() => handleArchiveSelected()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<InfoCircle size={18} color='black' className="cursor-pointer" />
|
||||
<Star1
|
||||
size={18}
|
||||
color='black'
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleFavoriteSelected()}
|
||||
/>
|
||||
<Trash
|
||||
size={18}
|
||||
color='#CD0000'
|
||||
onClick={() => handleDeleteSelected()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 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: <Edit size={16} color="black" />,
|
||||
onClick: () => console.log('تغییر وضعیت خوانده شده', message.id),
|
||||
onClick: () => emailActions.markAsSeen(message.id),
|
||||
},
|
||||
{
|
||||
label: 'اضافه به علاقهمندیها',
|
||||
icon: <Star1 size={16} color="black" />,
|
||||
onClick: () => emailActions.favorite(message.id),
|
||||
},
|
||||
{
|
||||
label: 'بایگانی',
|
||||
icon: <Archive size={16} color="black" />,
|
||||
onClick: () => console.log('بایگانی پیام', message.id),
|
||||
onClick: () => emailActions.archive(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
onClick: () => console.log('حذف پیام', message.id),
|
||||
onClick: () => emailActions.moveToTrash(message.id),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -148,14 +185,15 @@ const List: FC = () => {
|
||||
<Table<InboxMessage>
|
||||
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={
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
|
||||
@@ -35,12 +35,12 @@ export const getMessageDetail = async (
|
||||
messageId: string
|
||||
): Promise<MessageDetail> => {
|
||||
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<void> => {
|
||||
const { data } = await axios.delete(`/email/messages/inbox/${messageId}`);
|
||||
const { data } = await axios.delete(`/email/messages/${messageId}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<FilterValues>({});
|
||||
const emailActions = useEmailActions();
|
||||
|
||||
const { data: searchData, isLoading, refetch, isFetching } = useSearchMessages({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
...filters
|
||||
});
|
||||
|
||||
const [selectedMessages, setSelectedMessages] = useState<SearchMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<SearchMessage>[] = [
|
||||
{
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'mailbox',
|
||||
title: 'پوشه',
|
||||
render: (item) => (
|
||||
<span className="text-xs bg-gray-100 px-2 py-1 rounded">{item.mailbox}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
title: 'تاریخ',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span>{formatDate(item.date)}</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
|
||||
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
|
||||
<Trash size={18} color='black' onClick={() => 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: <Star size={16} color="black" />,
|
||||
onClick: () => emailActions.favorite(message.id),
|
||||
},
|
||||
{
|
||||
label: 'بایگانی',
|
||||
icon: <Archive size={16} color="black" />,
|
||||
onClick: () => emailActions.archive(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
onClick: () => emailActions.moveToTrash(message.id),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const messages = searchData?.data?.results || [];
|
||||
const pager = searchData?.data?.pager;
|
||||
|
||||
return (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">جستجو در پیامها</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'input', name: 'q', placeholder: 'جستجو در عنوان، متن یا فرستنده...' },
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'mailbox',
|
||||
placeholder: 'تمام پوشهها',
|
||||
options: [
|
||||
{ value: '', label: 'تمام پوشهها' },
|
||||
{ value: 'INBOX', label: 'دریافتی' },
|
||||
{ value: 'Sent', label: 'ارسال شده' },
|
||||
{ value: 'Drafts', label: 'پیشنویس' },
|
||||
{ value: 'Trash', label: 'سطل آشغال' },
|
||||
{ value: 'Archive', label: 'بایگانی' },
|
||||
{ value: 'Junk', label: 'اسپم' },
|
||||
]
|
||||
}
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="q"
|
||||
/>
|
||||
|
||||
<Table<SearchMessage>
|
||||
columns={columns}
|
||||
data={messages}
|
||||
isLoading={isLoading || emailActions.isLoading}
|
||||
showHeader={false}
|
||||
actions={tableActions}
|
||||
selectedActions={selectedActions}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedRows={selectedMessages}
|
||||
onRowClick={(id) => navigate(`/mail/${id}`)}
|
||||
noDataMessage={
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">
|
||||
{filters.q ? 'هیچ نتیجهای یافت نشد' : 'برای جستجو کلمه مورد نظر را وارد کنید'}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default List
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { SearchQueryDto, SearchResponse } from "../types/SearchTypes";
|
||||
|
||||
export const searchMessages = async (
|
||||
query: SearchQueryDto
|
||||
): Promise<SearchResponse> => {
|
||||
const { data } = await axios.get(`/email/search`, { params: query });
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
export interface EmailAddress {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SearchMessage extends Record<string, unknown> {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+129
-44
@@ -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<string, unknown> {
|
||||
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<FilterValues>({});
|
||||
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<Message>[] = [
|
||||
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<InboxMessage>[] = [
|
||||
{
|
||||
key: 'title',
|
||||
title: 'عنوان پیام',
|
||||
key: 'subject',
|
||||
title: 'موضوع پیام',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
|
||||
<span className="truncate">{item.title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate">{item.subject}</span>
|
||||
{item.attachments && (
|
||||
<AttachSquare size={16} className="text-gray-500 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'sender',
|
||||
title: 'فرستنده',
|
||||
key: 'to',
|
||||
title: 'گیرنده',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.sender}</span>
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">
|
||||
{item.to.map(recipient => recipient.address).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
title: 'تاریخ',
|
||||
title: 'تاریخ ارسال',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span className="text-xs md:text-sm">{item.date}</span>
|
||||
<span className="text-xs md:text-sm">
|
||||
{formatDate(item.date)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2 size={18} color='black' className="cursor-pointer" />
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
|
||||
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
|
||||
<Trash size={18} color='black' onClick={() => 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: <Star size={16} color="black" />,
|
||||
onClick: () => emailActions.favorite(message.id),
|
||||
},
|
||||
{
|
||||
label: 'بایگانی',
|
||||
icon: <Archive size={16} color="black" />,
|
||||
onClick: () => emailActions.archive(message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
onClick: () => emailActions.moveToTrash(message.id),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const messages = sentData?.data?.sentMails || [];
|
||||
const pager = sentData?.data?.pager;
|
||||
|
||||
return (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">{t('sent.title')}</h1>
|
||||
<h1 className="text-lg mb-4 md:mb-0">ارسال شده</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'date', name: 'from_date', placeholder: t('sent.from_date') },
|
||||
{ type: 'date', name: 'to_date', placeholder: t('sent.to_date') },
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'status',
|
||||
placeholder: t('sent.all'),
|
||||
name: 'isRead',
|
||||
placeholder: 'همه',
|
||||
options: [
|
||||
{ value: 'all', label: t('sent.all') },
|
||||
{ value: 'read', label: t('sent.read') },
|
||||
{ value: 'unread', label: t('sent.unread') }
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: 'true', label: 'خوانده شده' },
|
||||
{ value: 'false', label: 'خوانده نشده' }
|
||||
]
|
||||
},
|
||||
{ type: 'input', name: 'search', placeholder: t('sent.search') }
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="search"
|
||||
/>
|
||||
|
||||
<Table<Message>
|
||||
<Table<InboxMessage>
|
||||
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={
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import axios from "../../../config/axios";
|
||||
import {
|
||||
MessageListQueryDto,
|
||||
InboxResponse,
|
||||
MessageDetail,
|
||||
MessageDetailResponse,
|
||||
} from "../types/SentTypes";
|
||||
|
||||
export const getSentMessages = async (
|
||||
query: MessageListQueryDto
|
||||
): Promise<InboxResponse> => {
|
||||
const { data } = await axios.get(`/email/messages/sent`, { params: query });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getSentMessageDetail = async (
|
||||
messageId: number
|
||||
): Promise<MessageDetail> => {
|
||||
const { data }: { data: MessageDetailResponse } = await axios.get(
|
||||
`/email/messages/sent/${messageId}`
|
||||
);
|
||||
return data.data.message;
|
||||
};
|
||||
|
||||
export const deleteSentMessage = async (messageId: number): Promise<void> => {
|
||||
const { data } = await axios.delete(`/email/messages/sent/${messageId}`);
|
||||
return data;
|
||||
};
|
||||
@@ -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<string, unknown> {
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -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<FilterValues>({});
|
||||
const emailActions = useEmailActions();
|
||||
|
||||
const { data: spamData, isLoading, refetch, isFetching } = useGetSpamMessages({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
...filters
|
||||
});
|
||||
|
||||
const [selectedMessages, setSelectedMessages] = useState<SpamMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<SpamMessage>[] = [
|
||||
{
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'from',
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
title: 'تاریخ',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span>{formatDate(item.date)}</span>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const tableActions = (
|
||||
<div className='flex items-center gap-2 md:gap-4'>
|
||||
<Refresh2
|
||||
size={18}
|
||||
color='black'
|
||||
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
|
||||
onClick={() => refetch()}
|
||||
/>
|
||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||
</div>
|
||||
);
|
||||
|
||||
const selectedActions = (
|
||||
<>
|
||||
<RecoveryConvert size={18} color='black' onClick={() => handleNotSpamSelected()} className="cursor-pointer" />
|
||||
<Trash size={18} color='black' onClick={() => 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: <RecoveryConvert size={16} color="black" />,
|
||||
onClick: () => console.log('علامتگذاری به عنوان غیر اسپم', message.id),
|
||||
},
|
||||
{
|
||||
label: 'حذف',
|
||||
icon: <Trash size={16} color="red" />,
|
||||
onClick: () => emailActions.moveToTrash(message.id),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const messages = spamData?.data?.results || [];
|
||||
const pager = spamData?.data?.pager;
|
||||
|
||||
return (
|
||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||
<h1 className="text-lg mb-4 md:mb-0">پیامهای اسپم</h1>
|
||||
|
||||
<Filters
|
||||
fields={[
|
||||
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
|
||||
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
|
||||
{
|
||||
type: 'select',
|
||||
name: 'isRead',
|
||||
placeholder: 'همه',
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: 'true', label: 'خوانده شده' },
|
||||
{ value: 'false', label: 'خوانده نشده' }
|
||||
]
|
||||
},
|
||||
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
searchField="search"
|
||||
/>
|
||||
|
||||
<Table<SpamMessage>
|
||||
columns={columns}
|
||||
data={messages}
|
||||
isLoading={isLoading || emailActions.isLoading}
|
||||
showHeader={false}
|
||||
actions={tableActions}
|
||||
selectedActions={selectedActions}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedRows={selectedMessages}
|
||||
onRowClick={(id) => navigate(`/mail/${id}`)}
|
||||
noDataMessage={
|
||||
<div className="flex flex-col items-center">
|
||||
<InfoCircle size={32} className="text-gray-300 mb-2" />
|
||||
<div className="text-sm">هیچ پیامی یافت نشد</div>
|
||||
</div>
|
||||
}
|
||||
selectable={true}
|
||||
rowActions={getRowActions}
|
||||
pagination={pager ? {
|
||||
totalPages: pager.totalPages,
|
||||
currentPage: pager.page,
|
||||
onPageChange: handlePageChange
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default List
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { MessageListQueryDto, SpamResponse } from "../types/SpamTypes";
|
||||
|
||||
export const getSpamMessages = async (
|
||||
query: MessageListQueryDto
|
||||
): Promise<SpamResponse> => {
|
||||
const { data } = await axios.get(`/email/messages/junk`, { params: query });
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface EmailAddress {
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpamMessage extends Record<string, unknown> {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user