diff --git a/.env b/.env index c70c7dd..1c0adab 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ VITE_TOKEN_NAME = 'dmail_token' VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token' VITE_DANAK_BASE_URL ='https://api.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.1.134:4000' -VITE_BASE_URL = 'https://dmail-api.danakcorp.com' +VITE_BASE_URL = 'http://192.168.1.134:4000' +# VITE_BASE_URL = 'https://dmail-api.danakcorp.com' VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg index 200db25..065a361 100644 --- a/src/assets/images/logo.svg +++ b/src/assets/images/logo.svg @@ -1,91 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Table.tsx b/src/components/Table.tsx index dac4838..df20121 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -85,7 +85,7 @@ const Table = ({ if (data.length === 0) { return ( -
+
{noDataMessage}
); @@ -155,7 +155,7 @@ const Table = ({ if (data.length === 0) { return ( - + {noDataMessage} diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index 0e835eb..56c9ec7 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -1,9 +1,9 @@ -import { FC, useState, useEffect } from 'react' +import { FC, useState, useEffect, useRef } from 'react' import { CloseCircle } from 'iconsax-react' import { useTranslation } from 'react-i18next' import { useSharedStore } from '@/shared/store/sharedStore' import { useAuthStore } from '../../pages/auth/store/AuthStore' -import { useSendEmail, useSaveDraft } from '../../pages/received/hooks/useEmailData' +import { useSendEmail } from '../../pages/received/hooks/useEmailData' import { SendEmailDto } from '../../pages/received/types/Types' import Input from '../Input' import ReactQuill from 'react-quill-new'; @@ -11,6 +11,7 @@ import Button from '../Button' import Select from '../Select' import UploadButton from '../UploadButton' import { toast } from '../Toast' +import { ErrorType } from '@/helpers/types' const NewMessage: FC = () => { @@ -28,10 +29,15 @@ const NewMessage: FC = () => { const [subject, setSubject] = useState('') const [priority, setPriority] = useState('') + // Loading states + const [isSending, setIsSending] = useState(false) + const [isSavingDraft, setIsSavingDraft] = useState(false) + + // Auto-add email functionality + const typingTimeoutRef = useRef(null) // API hooks const sendEmailMutation = useSendEmail() - const saveDraftMutation = useSaveDraft() useEffect(() => { if (openNewMessage) { @@ -70,63 +76,92 @@ const NewMessage: FC = () => { return emailRegex.test(email) } + const addEmailToList = (email: string) => { + if (!isValidEmail(email)) { + return + } + + if (toEmails.includes(email)) { + toast('این ایمیل قبلاً اضافه شده است', 'error') + return + } + + setToEmails([...toEmails, email]) + setTo('') + } + + const handleToChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + setTo(newValue) + + // Clear existing timeout + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current) + } + + // Set new timeout to auto-add email after user stops typing + if (newValue.trim()) { + typingTimeoutRef.current = window.setTimeout(() => { + if (isValidEmail(newValue.trim())) { + addEmailToList(newValue.trim()) + } + }, 1500) // Wait 1.5 seconds after user stops typing + } + } + + const handleToBlur = () => { + // Clear timeout when input loses focus + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current) + } + + // Auto-add email if valid when input loses focus + if (to.trim() && isValidEmail(to.trim())) { + addEmailToList(to.trim()) + } + } + const handleToKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && to.trim()) { e.preventDefault() - const email = to.trim() - if (!isValidEmail(email)) { - toast('لطفا یک ایمیل معتبر وارد کنید', 'error') - return + // Clear timeout when Enter is pressed + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current) } - if (toEmails.includes(email)) { - toast('این ایمیل قبلاً اضافه شده است', 'error') - return - } - - setToEmails([...toEmails, email]) - setTo('') + addEmailToList(to.trim()) } } + // Clean up timeout on component unmount + useEffect(() => { + return () => { + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current) + } + } + }, []) + const removeEmail = (emailToRemove: string) => { setToEmails(toEmails.filter(email => email !== emailToRemove)) } + const resetForm = () => { + setTo('') + setToEmails([]) + setSubject('') + setValue('') + setPriority('') + } - - const handleSend = async () => { - if (toEmails.length === 0 || !subject) { + const handleEmailAction = async (isDraft: boolean = false) => { + if (!isDraft && (toEmails.length === 0 || !subject)) { toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error') return } - const emailData: SendEmailDto = { - from: { address: userEmail || 'sender@example.com' }, - to: parseEmailAddresses(toEmails), - subject, - html: value, - text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version - } - - try { - await sendEmailMutation.mutateAsync(emailData) - toast('ایمیل با موفقیت ارسال شد', 'success') - handleClose() - // Reset form - setTo('') - setToEmails([]) - setSubject('') - setValue('') - setPriority('') - } catch { - toast('خطا در ارسال ایمیل', 'error') - } - } - - const handleSaveDraft = async () => { - if (toEmails.length === 0 && !subject && !value) { + if (isDraft && toEmails.length === 0 && !subject && !value) { toast('حداقل یکی از فیلدها باید پر باشد', 'error') return } @@ -136,25 +171,37 @@ const NewMessage: FC = () => { to: parseEmailAddresses(toEmails), subject, html: value, - text: value.replace(/<[^>]*>/g, ''), - isDraft: true, + text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version + ...(isDraft && { isDraft: true, uploadOnly: true }), } - try { - await saveDraftMutation.mutateAsync(emailData) - toast('پیش‌نویس ذخیره شد', 'success') - handleClose() - // Reset form - setTo('') - setToEmails([]) - setSubject('') - setValue('') - setPriority('') - } catch { - toast('خطا در ذخیره پیش‌نویس', 'error') + // Set appropriate loading state + if (isDraft) { + setIsSavingDraft(true) + } else { + setIsSending(true) } + + await sendEmailMutation.mutateAsync(emailData, { + onSuccess: (data) => { + toast(data.message, 'success') + handleClose() + resetForm() + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error.message[0], 'error') + }, + onSettled: () => { + // Reset loading states + setIsSending(false) + setIsSavingDraft(false) + } + }) } + const handleSend = () => handleEmailAction(false) + const handleSaveDraft = () => handleEmailAction(true) + const priorityOptions = [ { value: 'high', label: 'بالا' }, { value: 'medium', label: 'متوسط' }, @@ -170,7 +217,7 @@ const NewMessage: FC = () => { }`} onClick={handleClose} /> -
{/* Header */}
@@ -209,7 +256,8 @@ const NewMessage: FC = () => { {/* Input Field */} setTo(e.target.value)} + onChange={handleToChange} + onBlur={handleToBlur} onKeyDown={handleToKeyDown} placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""} className="flex-1 min-w-32 bg-transparent outline-none" @@ -263,13 +311,13 @@ const NewMessage: FC = () => { className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1' label={t('new_message.draft')} onClick={handleSaveDraft} - loading={saveDraftMutation.isPending} + loading={isSavingDraft} />
diff --git a/src/index.css b/src/index.css index aff5a3b..ae526c8 100644 --- a/src/index.css +++ b/src/index.css @@ -247,3 +247,17 @@ textarea::placeholder { .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { display: none; } + +/* انیمیشن چرخش معکوس */ +@keyframes spin-reverse { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} + +.animate-spin-reverse { + animation: spin-reverse 1s linear infinite; +} diff --git a/src/pages/draft/List.tsx b/src/pages/draft/List.tsx index dbdaeb4..29d79e7 100644 --- a/src/pages/draft/List.tsx +++ b/src/pages/draft/List.tsx @@ -5,42 +5,55 @@ import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react'; import { RowActionItem } from '../../components/RowActionsDropdown'; - -interface Message extends Record { - id: string; - title: string; - sender: string; - date: string; - read: boolean; -} +import { useNavigate } from 'react-router-dom'; +import { useGetDrafts, useDeleteDraft } from './hooks/useDraftData'; +import { DraftMessage } from './types/DraftTypes'; +import { formatDate } from '@/config/func'; const List: FC = () => { + const navigate = useNavigate(); const { t } = useTranslation(); - const [isLoading] = useState(false); - const [selectedMessages, setSelectedMessages] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [filters, setFilters] = useState({}); - 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: draftData, isLoading, refetch, isFetching } = useGetDrafts({ + page: currentPage, + limit: 10, + ...filters + }); - const columns: ColumnType[] = [ + const deleteMutation = useDeleteDraft(); + const [selectedMessages, setSelectedMessages] = useState([]); + + const columns: ColumnType[] = [ { key: 'title', - title: 'عنوان پیام', - render: (item) => ( -
- {!item.read &&
} - {item.title} + title: 'title', + render: () => ( +
+ پیش نویس
) }, { - key: 'sender', - title: 'فرستنده', + key: 'subject', + title: 'عنوان پیام', render: (item) => ( - {item.sender} +
+ {item.subject || 'بدون موضوع'} +
+ ) + }, + { + key: 'to', + title: 'گیرنده', + render: (item) => ( + + {item.to && item.to.length > 0 + ? (item.to[0].name || item.to[0].address) + : 'بدون گیرنده' + } + ) }, { @@ -48,87 +61,129 @@ const List: FC = () => { title: 'تاریخ', align: 'center', render: (item) => ( - {item.date} + {formatDate(item.date)} ) }, ]; const tableActions = (
- + refetch()} + />
); const selectedActions = ( <> - handleDeleteSelected()} className="cursor-pointer" /> - handleEditSelected()} className="cursor-pointer" /> + handleDeleteSelected()} + className="cursor-pointer" + /> + handleEditSelected()} + className="cursor-pointer" + /> ); const handleFilterChange = (newFilters: FilterValues) => { - console.log('Applied filters:', newFilters); + setFilters(newFilters); + setCurrentPage(1); // Reset to first page when filters change }; - const handleSelectionChange = (selectedRows: Message[]) => { + const handleSelectionChange = (selectedRows: DraftMessage[]) => { setSelectedMessages(selectedRows); }; const handleDeleteSelected = () => { - console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق حذف را پیاده‌سازی کنید + selectedMessages.forEach(message => { + deleteMutation.mutate(message.id.toString()); + }); + setSelectedMessages([]); }; const handleEditSelected = () => { - console.log('ویرایش پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); - // اینجا منطق ویرایش را پیاده‌سازی کنید + 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)); + } }; - const getRowActions = (message: Message): RowActionItem[] => [ + const handleDeleteDraft = (messageId: number) => { + deleteMutation.mutate(messageId.toString()); + }; + + const handleEditDraft = (messageId: number) => { + navigate(`/draft/edit/${messageId}`); + }; + + const getRowActions = (message: DraftMessage): RowActionItem[] => [ { label: 'ویرایش', icon: , - onClick: () => console.log('ویرایش پیام', message.id), + onClick: () => handleEditDraft(message.id), }, { label: 'حذف', icon: , - onClick: () => console.log('حذف پیام', message.id), + onClick: () => handleDeleteDraft(message.id), }, ]; + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const messages = draftData?.data?.draftsMail || []; + const pager = draftData?.data?.pager; + return (

{t('draft.title')}

- + columns={columns} - data={messageData} + data={messages} isLoading={isLoading} showHeader={false} actions={tableActions} selectedActions={selectedActions} onSelectionChange={handleSelectionChange} - onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} + onRowClick={(id) => navigate(`/draft/edit/${id}`)} noDataMessage={ -
+
هیچ پیش‌نویسی یافت نشد
} selectable={true} rowActions={getRowActions} + pagination={pager ? { + totalPages: pager.totalPages, + currentPage: pager.page, + onPageChange: handlePageChange + } : undefined} />
) diff --git a/src/pages/draft/hooks/useDraftData.tsx b/src/pages/draft/hooks/useDraftData.tsx new file mode 100644 index 0000000..36a3eac --- /dev/null +++ b/src/pages/draft/hooks/useDraftData.tsx @@ -0,0 +1,45 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import * as api from '../service/DraftService'; +import { MessageListQueryDto, DraftUpdateDto } from '../types/DraftTypes'; + +export const useGetDrafts = (query: MessageListQueryDto) => { + return useQuery({ + queryKey: ['drafts', query], + queryFn: () => api.getDrafts(query), + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; + +export const useGetDraftDetail = (messageId: string, enabled: boolean = true) => { + return useQuery({ + queryKey: ['draft', messageId], + queryFn: () => api.getDraftDetail(messageId), + enabled: !!messageId && enabled, + staleTime: 1000 * 60 * 10, // 10 minutes + }); +}; + +export const useDeleteDraft = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (messageId: string) => api.deleteDraft(messageId), + onSuccess: () => { + // Invalidate drafts queries to refresh the list + queryClient.invalidateQueries({ queryKey: ['drafts'] }); + }, + }); +}; + +export const useEditDraft = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ messageId, draftData }: { messageId: string; draftData: DraftUpdateDto }) => + api.editDraft(messageId, draftData), + onSuccess: () => { + // Invalidate drafts queries to refresh the list + queryClient.invalidateQueries({ queryKey: ['drafts'] }); + }, + }); +}; \ No newline at end of file diff --git a/src/pages/draft/service/DraftService.ts b/src/pages/draft/service/DraftService.ts new file mode 100644 index 0000000..31a1612 --- /dev/null +++ b/src/pages/draft/service/DraftService.ts @@ -0,0 +1,37 @@ +import axios from "../../../config/axios"; +import { + MessageListQueryDto, + InboxResponse, + InboxMessage, + DraftUpdateDto, +} from "../types/DraftTypes"; + +export const getDrafts = async ( + query: MessageListQueryDto +): Promise => { + const { data } = await axios.get(`/email/messages/drafts`, { params: query }); + return data; +}; + +export const deleteDraft = async (messageId: string): Promise => { + const { data } = await axios.delete(`/email/messages/drafts/${messageId}`); + return data; +}; + +export const editDraft = async ( + messageId: string, + draftData: DraftUpdateDto +): Promise => { + const { data } = await axios.put( + `/email/messages/drafts/${messageId}`, + draftData + ); + return data; +}; + +export const getDraftDetail = async ( + messageId: string +): Promise => { + const { data } = await axios.get(`/email/messages/drafts/${messageId}`); + return data.data.message; +}; diff --git a/src/pages/draft/types/DraftTypes.ts b/src/pages/draft/types/DraftTypes.ts new file mode 100644 index 0000000..b18b5a0 --- /dev/null +++ b/src/pages/draft/types/DraftTypes.ts @@ -0,0 +1,107 @@ +export interface EmailRecipientDto { + name?: string; + address: string; +} + +export interface EmailHeaderDto { + key: string; + value: string; +} + +export interface EmailAttachmentDto { + filename?: string; + contentType?: string; + encoding?: string; + contentTransferEncoding?: string; + contentDisposition?: "inline" | "attachment"; + content: string; + cid?: string; +} + +export interface MessageListQueryDto { + page?: number; + limit?: number; + order?: string; + search?: string; + from?: string; + subject?: string; + dateFrom?: string; + dateTo?: string; + hasAttachment?: boolean; + isRead?: boolean; + isFlagged?: boolean; +} + +export interface DraftMessage extends Record { + id: number; + mailbox: string; + thread: string; + from: { + address: string; + name: string; + }; + to: Array<{ + address: string; + name: string; + }>; + cc: Array<{ + address: string; + name: string; + }>; + bcc: Array<{ + address: string; + name: string; + }>; + messageId: string; + subject: string; + date: string; + idate: string; + intro: string; + attachments: boolean; + attachmentsList: EmailAttachmentDto[]; + size: number; + seen: boolean; + deleted: boolean; + flagged: boolean; + draft: boolean; + answered: boolean; + forwarded: boolean; + references: string[]; + contentType: { + value: string; + params: { + boundary: string; + }; + }; +} + +export type InboxMessage = DraftMessage; + +export interface InboxPager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean; + nextPage: boolean; +} + +export interface InboxResponse { + statusCode: number; + success: boolean; + data: { + pager: InboxPager; + draftsMail: DraftMessage[]; + }; +} + +export interface DraftUpdateDto { + from?: EmailRecipientDto; + to?: EmailRecipientDto[]; + cc?: EmailRecipientDto[]; + bcc?: EmailRecipientDto[]; + subject?: string; + text?: string; + html?: string; + attachments?: EmailAttachmentDto[]; +} diff --git a/src/pages/received/Components/SkeletonDetail.tsx b/src/pages/received/Components/SkeletonDetail.tsx new file mode 100644 index 0000000..3c5e1e5 --- /dev/null +++ b/src/pages/received/Components/SkeletonDetail.tsx @@ -0,0 +1,67 @@ +import { FC } from 'react' + +const SkeletonDetail: FC = () => { + return ( +
+
+ {/* Header Skeleton */} +
+ + {/* Subject and Tags Section */} +
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Sender Info */} +
+
+
+
+ + {/* Email Content */} +
+
+
+
+
+
+
+ + {/* Attachments Section */} +
+
+
+
+
+
+ + {/* Action Buttons */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} + +export default SkeletonDetail \ No newline at end of file diff --git a/src/pages/received/Detail.tsx b/src/pages/received/Detail.tsx index e520ad8..bf11148 100644 --- a/src/pages/received/Detail.tsx +++ b/src/pages/received/Detail.tsx @@ -9,11 +9,12 @@ import AnswerIcon from '@/assets/images/answer.svg' import Header from './Components/Header' import ReactQuill from 'react-quill-new'; import Select from '@/components/Select' -import { useGetMessageDetail, useSendEmail, useSaveDraft } from './hooks/useEmailData' +import { useGetMessageDetail, useSendEmail } from './hooks/useEmailData' import { useAuthStore } from '../auth/store/AuthStore' import { SendEmailDto } from './types/Types' import { toast } from '@/components/Toast' import { formatDate } from '@/config/func' +import SkeletonDetail from './Components/SkeletonDetail' const DetailEmail: FC = () => { @@ -27,7 +28,7 @@ const DetailEmail: FC = () => { // API hooks const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', !!id) const sendEmailMutation = useSendEmail() - const saveDraftMutation = useSaveDraft() + // const saveDraftMutation = useSaveDraft() const priorityOptions = [ { value: 'high', label: 'بالا' }, @@ -80,21 +81,21 @@ const DetailEmail: FC = () => { return } - const draftData: SendEmailDto = { - from: { address: userEmail || 'sender@example.com' }, - to: [{ address: messageDetail.from.address, name: messageDetail.from.name }], - subject: `Re: ${messageDetail.subject}`, - html: value, - text: value.replace(/<[^>]*>/g, ''), - isDraft: true, - reference: { - inReplyTo: messageDetail.messageId, - originalMessageId: messageDetail.id.toString() - } - } + // const draftData: SendEmailDto = { + // from: { address: userEmail || 'sender@example.com' }, + // to: [{ address: messageDetail.from.address, name: messageDetail.from.name }], + // subject: `Re: ${messageDetail.subject}`, + // html: value, + // text: value.replace(/<[^>]*>/g, ''), + // isDraft: true, + // reference: { + // inReplyTo: messageDetail.messageId, + // originalMessageId: messageDetail.id.toString() + // } + // } try { - await saveDraftMutation.mutateAsync(draftData) + // await saveDraftMutation.mutateAsync(draftData) toast('پیش‌نویس ذخیره شد', 'success') } catch { toast('خطا در ذخیره پیش‌نویس', 'error') @@ -108,14 +109,7 @@ const DetailEmail: FC = () => { if (isLoading) { return ( -
-
-
-
-
-
-
-
+ ) } @@ -247,7 +241,7 @@ const DetailEmail: FC = () => { className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1' label={t('new_message.draft')} onClick={handleSaveDraft} - loading={saveDraftMutation.isPending} + // loading={saveDraftMutation.isPending} />