mark all as read + spam empty

This commit is contained in:
hamid zarghami
2025-07-27 14:44:51 +03:30
parent 2a41bf528c
commit 3c275dbcdd
13 changed files with 274 additions and 67 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ VITE_SOCKET_URL = 'wss://dmail-api.danakcorp.com/email'
# VITE_SOCKET_URL = 'ws://192.168.1.118:4000/email'
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.1.101:4000'
# VITE_BASE_URL = 'http://192.168.1.106:4000'
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
+69
View File
@@ -0,0 +1,69 @@
import { FC, useState } from 'react'
import DefaulModal from './DefaulModal'
import { useTranslation } from 'react-i18next'
import Button from './Button'
import Textarea from './Textarea'
type Props = {
isLoading?: boolean,
close: () => void,
isOpen: boolean,
onConfrim: (text?: string) => void,
label?: string,
isHasDescription?: boolean,
title?: string
}
const ModalConfrim: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [description, setDescription] = useState<string>('')
return (
<DefaulModal
open={props.isOpen}
close={props.close}
title_header={props.title ? props.title : t('confrim.subject')}
isHeader
>
<div className='mt-6'>
<div className='text-sm text-center'>
{
props.label ?
props.label
:
t('confrim.content')
}
{
props.isHasDescription &&
<div className='mt-4'>
<Textarea
placeholder={t('description')}
onChange={(e) => setDescription(e.target.value)}
value={description}
className='bg-transparent border border-gray-500 rounded-xl w-[300px] p-2'
label=''
/>
</div>
}
</div>
<div className='flex gap-4 justify-center mt-10'>
<Button
label={t('confrim.yes')}
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
loading={props.isLoading}
/>
<Button
label={t('confrim.cancel')}
className='bg-transparent text-black border border-primary'
onClick={props.close}
/>
</div>
</div>
</DefaulModal>
)
}
export default ModalConfrim
+8 -1
View File
@@ -156,6 +156,12 @@
"cancel": "انصراف",
"confirm_disable": "تایید غیر فعال سازی"
},
"confrim": {
"subject": "حذف",
"content": "برای حذف این آیتم مطمئن هستید؟",
"yes": "بله",
"cancel": "لغو"
},
"success": "عملیات با موفقیت انجام شد",
"attachment_select": "اضافه کردن فایل ضمیمه",
"domain": {
@@ -203,7 +209,8 @@
"Junk": "هرزنامه‌ها",
"SENT": "ارسال شده‌ها",
"TRASH": "سطل زباله",
"Sent Mail": "ارسال شده ها"
"Sent Mail": "ارسال شده ها",
"Trash": "سطل زباله"
},
"auth": {
"welcome": "خوش آمدید",
+34 -9
View File
@@ -2,13 +2,17 @@ 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 { InfoCircle, Refresh2, Trash, RecoveryConvert } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetTrashMessages } from './hooks/useTrashData';
import { useEmptyTrash, useGetTrashMessages } from './hooks/useTrashData';
import { TrashMessage } from './types/TrashTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
import Button from '@/components/Button';
import ModalConfrim from '@/components/ModalConfrim';
import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
const List: FC = () => {
const navigate = useNavigate();
@@ -16,7 +20,8 @@ const List: FC = () => {
const [previousCursor, setPreviousCursor] = useState<string | undefined>(undefined);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const [showModal, setShowModal] = useState(false);
const { mutate: emptyTrash, isPending } = useEmptyTrash();
const { data: trashData, isLoading, refetch, isFetching } = useGetTrashMessages({
next: nextCursor,
previous: previousCursor,
@@ -59,10 +64,15 @@ const List: FC = () => {
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
<Button
label='خالی کردن سطل زباله'
className='h-8'
onClick={() => setShowModal(true)}
loading={isPending}
/>
</div>
);
@@ -95,6 +105,18 @@ const List: FC = () => {
setSelectedMessages([]);
};
const handleTrash = () => {
emptyTrash(undefined, {
onSuccess: () => {
refetch();
setShowModal(false);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
});
};
const getRowActions = (message: TrashMessage): RowActionItem[] => [
{
label: 'بازگردانی',
@@ -108,10 +130,6 @@ const List: FC = () => {
},
];
const handleNext = (cursor?: string) => {
setNextCursor(cursor);
setPreviousCursor(undefined);
@@ -165,6 +183,13 @@ const List: FC = () => {
onPrevious: handlePrevious
} : undefined}
/>
<ModalConfrim
isOpen={showModal}
close={() => setShowModal(false)}
onConfrim={handleTrash}
isLoading={isPending}
/>
</div>
)
}
+7 -3
View File
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { getTrashMessages } from '../service/TrashService';
import { useMutation, useQuery } from '@tanstack/react-query';
import { emptyTrash, getTrashMessages } from '../service/TrashService';
import { MessageListQueryDto } from '../types/TrashTypes';
export const useGetTrashMessages = (query: MessageListQueryDto) => {
@@ -10,4 +10,8 @@ export const useGetTrashMessages = (query: MessageListQueryDto) => {
});
};
// Note: useRestoreMessage is now handled by useEmailActions hook
export const useEmptyTrash = () => {
return useMutation({
mutationFn: () => emptyTrash(),
});
};
+9
View File
@@ -1,3 +1,4 @@
import { MailboxEnum } from "@/pages/received/enum/Enum";
import axios from "../../../config/axios";
import { getMailboxId } from "../../../config/func";
import { MessageListQueryDto, TrashResponse } from "../types/TrashTypes";
@@ -25,3 +26,11 @@ export const getTrashMessages = async (
};
// Note: deleteMessagePermanently and restoreMessage are now handled by EmailActionsService
export const emptyTrash = async () => {
const mailboxId = getMailboxId(MailboxEnum.TRASH);
const { data } = await axios.patch(
`/email/messages/empty-trash?mailbox=${mailboxId}`
);
return data;
};
+35 -2
View File
@@ -6,7 +6,7 @@ import { ColumnType } from '../../components/types/TableTypes';
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, useSummarizeEmail } from './hooks/useEmailData';
import { useGetInbox, useMarkAllRead, useSummarizeEmail } from './hooks/useEmailData';
import { InboxMessage } from './types/Types';
import { formatDate } from '@/config/func';
import MarkAsRead from '@/assets/images/mark_as_read.svg'
@@ -15,6 +15,10 @@ import Favorite from './Components/Favorite';
import Summerize from './Components/Summerize';
import Intelligense from '@/assets/images/intelligense.svg'
import SummarizeModal from './Components/SummarizeModal';
import Button from '@/components/Button';
import ModalConfrim from '@/components/ModalConfrim';
import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
const List: FC = () => {
@@ -25,7 +29,9 @@ const List: FC = () => {
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const [openSummarizeModal, setOpenSummarizeModal] = useState(false);
const [showModal, setShowModal] = useState(false);
const { mutate: summarizeEmail, isPending: isSummarizing, data: summarizeData } = useSummarizeEmail();
const { mutate: markAllRead, isPending: isMarkingAllRead } = useMarkAllRead();
const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({
next: nextCursor,
@@ -77,9 +83,15 @@ const List: FC = () => {
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<Button
label='علامت‌گذاری همه به عنوان خوانده شده'
className='h-8'
onClick={() => setShowModal(true)}
loading={isMarkingAllRead}
/>
</div>
);
@@ -147,6 +159,18 @@ const List: FC = () => {
setSelectedMessages([]);
};
const handleMarkAll = () => {
markAllRead(undefined, {
onSuccess: () => {
refetch();
setShowModal(false);
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
});
};
const getInlineActions = (item: InboxMessage) => [
{
icon: <Favorite flagged={item.flagged} id={item.id.toString()} mailbox={item.mailbox} />,
@@ -285,6 +309,15 @@ const List: FC = () => {
/>
)}
<ModalConfrim
isOpen={showModal}
close={() => setShowModal(false)}
onConfrim={handleMarkAll}
isLoading={isMarkingAllRead}
title={'خواندن همه'}
label={'آیا از خواندن همه پیام ها اطمینان دارید؟'}
/>
</div>
)
}
@@ -130,4 +130,10 @@ export const useGenerateEmail = () => {
return useMutation({
mutationFn: (params: GenerateEmailRequest) => api.generateEmail(params),
});
};
export const useMarkAllRead = () => {
return useMutation({
mutationFn: () => api.markAllRead(),
});
};
@@ -9,6 +9,7 @@ import {
SummarizeEmailRequest,
GenerateEmailRequest,
} from "../types/Types";
import { MailboxEnum } from "../enum/Enum";
export const sendEmail = async (params: SendEmailDto) => {
const { data } = await axios.post(`/email/send`, params);
@@ -161,3 +162,11 @@ export const summarizeEmail = async (params: SummarizeEmailRequest) => {
const { data } = await axios.post(`/ai-assistant/summarize-email`, params);
return data;
};
export const markAllRead = async () => {
const mailboxId = getMailboxId(MailboxEnum.INBOX);
const { data } = await axios.patch(
`/email/messages/mark-all-read?mailbox=${mailboxId}`
);
return data;
};
+34 -4
View File
@@ -2,13 +2,17 @@ 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 { InfoCircle, Refresh2, Trash, RecoveryConvert } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetSpamMessages } from './hooks/useSpamData';
import { useEmptySpam, useGetSpamMessages } from './hooks/useSpamData';
import { SpamMessage } from './types/SpamTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
import Button from '@/components/Button';
import ModalConfrim from '@/components/ModalConfrim';
import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
const List: FC = () => {
const navigate = useNavigate();
@@ -16,6 +20,8 @@ const List: FC = () => {
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,
@@ -58,10 +64,15 @@ const List: FC = () => {
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
<Button
label='خالی کردن هرزنامه'
className='h-8'
onClick={() => setShowModal(true)}
loading={isPending}
/>
</div>
);
@@ -94,6 +105,18 @@ const List: FC = () => {
setSelectedMessages([]);
};
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: 'غیر اسپم',
@@ -160,6 +183,13 @@ const List: FC = () => {
onPrevious: handlePrevious
} : undefined}
/>
<ModalConfrim
isOpen={showModal}
close={() => setShowModal(false)}
onConfrim={handleSpam}
isLoading={isPending}
/>
</div>
)
}
+8 -2
View File
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { getSpamMessages } from '../service/SpamService';
import { useMutation, useQuery } from '@tanstack/react-query';
import { emptySpam, getSpamMessages } from '../service/SpamService';
import { MessageListQueryDto } from '../types/SpamTypes';
export const useGetSpamMessages = (query: MessageListQueryDto) => {
@@ -8,4 +8,10 @@ export const useGetSpamMessages = (query: MessageListQueryDto) => {
queryFn: () => getSpamMessages(query),
staleTime: 0
});
};
export const useEmptySpam = () => {
return useMutation({
mutationFn: () => emptySpam(),
});
};
+9
View File
@@ -1,6 +1,7 @@
import axios from "../../../config/axios";
import { getMailboxId } from "../../../config/func";
import { MessageListQueryDto, SpamResponse } from "../types/SpamTypes";
import { MailboxEnum } from "@/pages/received/enum/Enum";
export const getSpamMessages = async (
query: MessageListQueryDto
@@ -23,3 +24,11 @@ export const getSpamMessages = async (
});
return data;
};
export const emptySpam = async () => {
const mailboxId = getMailboxId(MailboxEnum.Junk);
const { data } = await axios.patch(
`/email/messages/empty-spam?mailbox=${mailboxId}`
);
return data;
};
+45 -45
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths";
// import { VitePWA } from "vite-plugin-pwa";
import { VitePWA } from "vite-plugin-pwa";
// https://vite.dev/config/
export default defineConfig({
@@ -10,49 +10,49 @@ export default defineConfig({
react(),
tailwindcss(),
tsconfigPaths(),
// VitePWA({
// registerType: "autoUpdate",
// devOptions: {
// enabled: false, // در حالت توسعه هم کار کند
// },
// manifest: {
// name: "Danak Mail",
// short_name: "Danak Mail",
// description: "Danak Mail",
// theme_color: "#ffffff",
// background_color: "#ffffff",
// display: "standalone",
// start_url: "/received", // صفحه اصلی PWA
// icons: [
// {
// src: "/icon-192x192.png",
// sizes: "192x192",
// type: "image/png",
// },
// {
// src: "/icon-512x512.png",
// sizes: "512x512",
// type: "image/png",
// },
// ],
// },
// // workbox: {
// // maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // افزایش به 4MB
// // runtimeCaching: [
// // {
// // urlPattern: /^https:\/\/admin-panel\.danakcorp\.com\//,
// // handler: "NetworkFirst",
// // options: {
// // cacheName: "api-cache",
// // expiration: {
// // maxEntries: 50,
// // maxAgeSeconds: 60 * 60 * 24,
// // },
// // networkTimeoutSeconds: 10,
// // },
// // },
// // ],
// // },
// }),
VitePWA({
registerType: "autoUpdate",
devOptions: {
enabled: false, // در حالت توسعه هم کار کند
},
manifest: {
name: "Danak Mail",
short_name: "Danak Mail",
description: "Danak Mail",
theme_color: "#ffffff",
background_color: "#ffffff",
display: "standalone",
start_url: "/received", // صفحه اصلی PWA
icons: [
{
src: "/icon-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
},
// workbox: {
// maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // افزایش به 4MB
// runtimeCaching: [
// {
// urlPattern: /^https:\/\/admin-panel\.danakcorp\.com\//,
// handler: "NetworkFirst",
// options: {
// cacheName: "api-cache",
// expiration: {
// maxEntries: 50,
// maxAgeSeconds: 60 * 60 * 24,
// },
// networkTimeoutSeconds: 10,
// },
// },
// ],
// },
}),
],
});