Files
dmail-front/src/pages/spam/List.tsx
T
2025-07-30 09:47:21 +03:30

277 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, Refresh2, Trash, RecoveryConvert } from 'iconsax-react';
import RowActionsDropdown, { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useEmptySpam, useGetSpamMessages } from './hooks/useSpamData';
import { SpamMessage } from './types/SpamTypes';
import { useEmailActions } from '@/hooks/useEmailActions';
import ModalConfrim from '@/components/ModalConfrim';
import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
const List: FC = () => {
const navigate = useNavigate();
const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);
const [previousCursor, setPreviousCursor] = useState<string | undefined>(undefined);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const [showModal, setShowModal] = useState(false);
const { mutate: emptySpam, isPending } = useEmptySpam();
const { data: spamData, isLoading, refetch, isFetching } = useGetSpamMessages({
next: nextCursor,
previous: previousCursor,
limit: 10,
...filters
});
const [selectedMessages, setSelectedMessages] = useState<SpamMessage[]>([]);
const [selectAll, setSelectAll] = useState(false);
const formatTime = (dateString: string) => {
try {
if (!dateString) return '--:--';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '--:--';
return new Intl.DateTimeFormat("fa-IR", {
hour: "2-digit",
minute: "2-digit",
}).format(date);
} catch (error) {
console.error('خطا در فرمت زمان:', error);
return '--:--';
}
};
const desktopColumns: ColumnType<SpamMessage>[] = [
{
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'from',
title: 'فرستنده',
render: (item) => (
<div className="flex items-center">
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span>{formatTime(item.date)}</span>
)
},
];
const mobileColumns: ColumnType<SpamMessage>[] = [
{
key: 'content',
title: 'محتوا',
render: (item) => (
<div className="flex flex-col gap-1 w-full">
<div className="flex items-center gap-2 w-full">
<span className="truncate font-medium text-sm flex-1 min-w-0">
{item.from.name || item.from.address}
</span>
<span className="text-xs text-gray-500 whitespace-nowrap">
{formatTime(item.date)}
</span>
</div>
<div className="truncate w-full text-sm font-medium">
{item.subject || 'بدون موضوع'}
</div>
<div className="truncate w-full text-xs text-gray-600">
{item.intro || 'بدون متن پیش‌نمایش'}
</div>
</div>
)
}
];
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
React.useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const columns = isMobile ? mobileColumns : desktopColumns;
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked);
if (checked) {
const messages = spamData?.data?.junkMails || [];
setSelectedMessages(messages);
} else {
setSelectedMessages([]);
}
};
const tableActions = (
<div className='flex items-center gap-4 md:gap-4'>
<div className="flex items-center gap-2">
<Checkbox
checked={selectAll}
onCheckedChange={handleSelectAll}
className="w-4 h-4"
/>
</div>
<Refresh2
size={18}
color='black'
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<RowActionsDropdown actions={[
{
label: 'حذف همه',
icon: <Trash size={18} color='black' />,
onClick: () => setShowModal(true)
}
]} />
</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);
setNextCursor(undefined);
setPreviousCursor(undefined);
};
const handleSelectionChange = (selectedRows: SpamMessage[]) => {
setSelectedMessages(selectedRows);
const messages = spamData?.data?.junkMails || [];
setSelectAll(selectedRows.length === messages.length && messages.length > 0);
};
const handleDeleteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds, selectedMessages[0].mailbox);
setSelectedMessages([]);
setSelectAll(false);
};
const handleNotSpamSelected = () => {
// TODO: پیاده‌سازی علامت‌گذاری به عنوان غیر اسپم - نیاز به API endpoint جداگانه
setSelectedMessages([]);
setSelectAll(false);
};
const handleSpam = () => {
emptySpam(undefined, {
onSuccess: () => {
refetch();
setShowModal(false);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
});
};
const getRowActions = (message: SpamMessage): RowActionItem[] => [
{
label: 'غیر اسپم',
icon: <RecoveryConvert size={16} color="black" />,
onClick: () => null,
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => emailActions.moveToTrash({ messageId: message.id, mailbox: message.mailbox }),
},
];
const handleNext = (cursor?: string) => {
setNextCursor(cursor);
setPreviousCursor(undefined);
};
const handlePrevious = (cursor?: string) => {
setPreviousCursor(cursor);
setNextCursor(undefined);
};
const messages = spamData?.data?.junkMails || [];
const pager = spamData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<div className='flex items-center justify-between'>
<h1>هرزنامه ها</h1>
</div>
<Filters
fields={[
{ type: 'date', name: 'datestart', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateend', placeholder: 'تا تاریخ' },
{ 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, item) => navigate(`/mail/${id}/${item.mailbox}`)}
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 ? {
hasNext: typeof pager.nextCursor === 'string',
nextCursor: typeof pager.nextCursor === 'string' ? pager.nextCursor : undefined,
previousCursor: typeof pager.previousCursor === 'string' ? pager.previousCursor : undefined,
onNext: handleNext,
onPrevious: handlePrevious
} : undefined}
/>
<ModalConfrim
isOpen={showModal}
close={() => setShowModal(false)}
onConfrim={handleSpam}
isLoading={isPending}
/>
</div>
)
}
export default List