send , receive , detail email
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
VITE_TOKEN_NAME = 'dsc_token'
|
VITE_TOKEN_NAME = 'dmail_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token'
|
||||||
VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
||||||
VITE_BASE_URL = 'http://192.168.1.134:4000'
|
# VITE_BASE_URL = 'http://192.168.1.134:4000'
|
||||||
# VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
||||||
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||||
|
|||||||
+24
-3
@@ -1,4 +1,4 @@
|
|||||||
import { FC } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import './assets/fonts/irancell/style.css'
|
import './assets/fonts/irancell/style.css'
|
||||||
import 'react-quill-new/dist/quill.snow.css';
|
import 'react-quill-new/dist/quill.snow.css';
|
||||||
import 'react-loading-skeleton/dist/skeleton.css'
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
@@ -12,8 +12,10 @@ import NewMessage from './components/newMessage/NewMessage';
|
|||||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { IApiErrorRepsonse } from './types/error.types';
|
import { IApiErrorRepsonse } from './types/error.types';
|
||||||
import ToastContainer from './components/Toast';
|
import ToastContainer from './components/Toast';
|
||||||
import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
|
||||||
import { refreshToken } from './pages/auth/service/AuthService';
|
import { refreshToken } from './pages/auth/service/AuthService';
|
||||||
|
import { Paths } from './utils/Paths';
|
||||||
|
import Login from './pages/auth/Login';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -96,12 +98,31 @@ const queryClient = new QueryClient({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
|
||||||
|
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||||
|
useEffect(() => {
|
||||||
|
const checkLogin = async () => {
|
||||||
|
const token = await getToken();
|
||||||
|
if (token) {
|
||||||
|
setIsLogin('isLogin')
|
||||||
|
} else {
|
||||||
|
setIsLogin('isNotLogin')
|
||||||
|
if (window.location.href.split('auth').length === 1) {
|
||||||
|
window.location.href = Paths.login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkLogin();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<I18nextProvider i18n={i18next}>
|
<I18nextProvider i18n={i18next}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|
||||||
<Main />
|
{
|
||||||
|
isLogin === 'isLogin' ? <Main /> : isLogin === 'isNotLogin' ? <Login /> : null
|
||||||
|
}
|
||||||
<NewMessage />
|
<NewMessage />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const ToastContainer: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-[99999999999] flex flex-col gap-2">
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
<div
|
<div
|
||||||
key={toast.id}
|
key={toast.id}
|
||||||
|
|||||||
@@ -2,21 +2,37 @@ import { FC, useState, useEffect } from 'react'
|
|||||||
import { CloseCircle } from 'iconsax-react'
|
import { CloseCircle } from 'iconsax-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||||
|
import { useAuthStore } from '../../pages/auth/store/AuthStore'
|
||||||
|
import { useSendEmail, useSaveDraft } from '../../pages/received/hooks/useEmailData'
|
||||||
|
import { SendEmailDto } from '../../pages/received/types/Types'
|
||||||
import Input from '../Input'
|
import Input from '../Input'
|
||||||
import ReactQuill from 'react-quill-new';
|
import ReactQuill from 'react-quill-new';
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Select from '../Select'
|
import Select from '../Select'
|
||||||
import UploadButton from '../UploadButton'
|
import UploadButton from '../UploadButton'
|
||||||
|
import { toast } from '../Toast'
|
||||||
|
|
||||||
|
|
||||||
const NewMessage: FC = () => {
|
const NewMessage: FC = () => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const { setOpenNewMessage, openNewMessage } = useSharedStore()
|
const { setOpenNewMessage, openNewMessage } = useSharedStore()
|
||||||
|
const { email: userEmail } = useAuthStore()
|
||||||
const [isClosing, setIsClosing] = useState(false)
|
const [isClosing, setIsClosing] = useState(false)
|
||||||
const [shouldRender, setShouldRender] = useState(false)
|
const [shouldRender, setShouldRender] = useState(false)
|
||||||
const [isOpening, setIsOpening] = useState(false)
|
const [isOpening, setIsOpening] = useState(false)
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [to, setTo] = useState('')
|
||||||
|
const [toEmails, setToEmails] = useState<string[]>([])
|
||||||
|
const [subject, setSubject] = useState('')
|
||||||
|
const [priority, setPriority] = useState('')
|
||||||
|
|
||||||
|
|
||||||
|
// API hooks
|
||||||
|
const sendEmailMutation = useSendEmail()
|
||||||
|
const saveDraftMutation = useSaveDraft()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openNewMessage) {
|
if (openNewMessage) {
|
||||||
setShouldRender(true)
|
setShouldRender(true)
|
||||||
@@ -42,6 +58,108 @@ const NewMessage: FC = () => {
|
|||||||
setOpenNewMessage(false)
|
setOpenNewMessage(false)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseEmailAddresses = (emails: string[]) => {
|
||||||
|
return emails
|
||||||
|
.filter(email => email.length > 0)
|
||||||
|
.map(email => ({ address: email }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidEmail = (email: string) => {
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
return emailRegex.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter' && to.trim()) {
|
||||||
|
e.preventDefault()
|
||||||
|
const email = to.trim()
|
||||||
|
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
toast('لطفا یک ایمیل معتبر وارد کنید', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toEmails.includes(email)) {
|
||||||
|
toast('این ایمیل قبلاً اضافه شده است', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setToEmails([...toEmails, email])
|
||||||
|
setTo('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeEmail = (emailToRemove: string) => {
|
||||||
|
setToEmails(toEmails.filter(email => email !== emailToRemove))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (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) {
|
||||||
|
toast('حداقل یکی از فیلدها باید پر باشد', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailData: SendEmailDto = {
|
||||||
|
from: { address: userEmail || 'sender@example.com' },
|
||||||
|
to: parseEmailAddresses(toEmails),
|
||||||
|
subject,
|
||||||
|
html: value,
|
||||||
|
text: value.replace(/<[^>]*>/g, ''),
|
||||||
|
isDraft: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveDraftMutation.mutateAsync(emailData)
|
||||||
|
toast('پیشنویس ذخیره شد', 'success')
|
||||||
|
handleClose()
|
||||||
|
// Reset form
|
||||||
|
setTo('')
|
||||||
|
setToEmails([])
|
||||||
|
setSubject('')
|
||||||
|
setValue('')
|
||||||
|
setPriority('')
|
||||||
|
} catch {
|
||||||
|
toast('خطا در ذخیره پیشنویس', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityOptions = [
|
||||||
|
{ value: 'high', label: 'بالا' },
|
||||||
|
{ value: 'medium', label: 'متوسط' },
|
||||||
|
{ value: 'low', label: 'پایین' },
|
||||||
|
]
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
@@ -66,12 +184,44 @@ const NewMessage: FC = () => {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className='space-y-6 md:space-y-8'>
|
<div className='space-y-6 md:space-y-8'>
|
||||||
<Input
|
<div>
|
||||||
label={t('new_message.to')}
|
<label className='text-sm'>
|
||||||
/>
|
{t('new_message.to')}
|
||||||
|
</label>
|
||||||
|
<div className='w-full relative mt-1'>
|
||||||
|
<div className="w-full bg-white min-h-10 text-black flex flex-wrap items-center gap-1 px-4 py-2 text-xs rounded-xl border border-border">
|
||||||
|
{/* Email Tags */}
|
||||||
|
{toEmails.map((email, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-1 bg-[#EBEDF5] text-xs h-7 rounded-full px-3"
|
||||||
|
>
|
||||||
|
<span>{email}</span>
|
||||||
|
<CloseCircle
|
||||||
|
size={16}
|
||||||
|
color='#D52903'
|
||||||
|
className='cursor-pointer'
|
||||||
|
onClick={() => removeEmail(email)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Input Field */}
|
||||||
|
<input
|
||||||
|
value={to}
|
||||||
|
onChange={(e) => setTo(e.target.value)}
|
||||||
|
onKeyDown={handleToKeyDown}
|
||||||
|
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
|
||||||
|
className="flex-1 min-w-32 bg-transparent outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label={t('new_message.subject')}
|
label={t('new_message.subject')}
|
||||||
|
value={subject}
|
||||||
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -101,19 +251,25 @@ const NewMessage: FC = () => {
|
|||||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
items={[]}
|
items={priorityOptions}
|
||||||
placeholder={t('new_message.select_priority')}
|
placeholder={t('new_message.select_priority')}
|
||||||
className='xl:w-[190px] min-w-[130px]'
|
className='xl:w-[190px] min-w-[130px]'
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 justify-end flex gap-3'>
|
<div className='flex-1 justify-end flex gap-3'>
|
||||||
<Button
|
<Button
|
||||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
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')}
|
label={t('new_message.draft')}
|
||||||
|
onClick={handleSaveDraft}
|
||||||
|
loading={saveDraftMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||||
label={t('new_message.send')}
|
label={t('new_message.send')}
|
||||||
|
onClick={handleSend}
|
||||||
|
loading={sendEmailMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +277,8 @@ const NewMessage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,3 +69,14 @@ export const getRedirectUrl = () => {
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
return urlParams.get("redirect");
|
return urlParams.get("redirect");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return new Intl.DateTimeFormat("fa-IR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@ textarea::placeholder {
|
|||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
border: 1px solid #d0d0d0 !important;
|
border: 1px solid oklch(0.922 0 0) !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|||||||
+47
-1
@@ -18,7 +18,8 @@
|
|||||||
"search": "جستجو"
|
"search": "جستجو"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "این فیلد اجباری است"
|
"required": "این فیلد اجباری است",
|
||||||
|
"invalid_email": "فرمت ایمیل نامعتبر است"
|
||||||
},
|
},
|
||||||
"received": {
|
"received": {
|
||||||
"title": "دریافتی ها",
|
"title": "دریافتی ها",
|
||||||
@@ -161,5 +162,50 @@
|
|||||||
"answer": "پاسخ",
|
"answer": "پاسخ",
|
||||||
"forward": "ارسال به دیگران",
|
"forward": "ارسال به دیگران",
|
||||||
"message": "متن پبام"
|
"message": "متن پبام"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"welcome": "خوش آمدید",
|
||||||
|
"enter_information": "برای ثبت نام اطلاعات خود را وارد کنید.",
|
||||||
|
"user_info": "اطلاعات کاربر",
|
||||||
|
"name": "نام",
|
||||||
|
"enter_name": "نام خود را وارد کنید",
|
||||||
|
"family": "نام خانوادگی",
|
||||||
|
"enter_family": "نام خانوادگی خود را وارد کنید",
|
||||||
|
"select_date": "انتخاب تاریخ",
|
||||||
|
"dateofbirth": "تاریخ تولد",
|
||||||
|
"national_code": "کد ملی",
|
||||||
|
"national_code_enter": "کد ملی خود را وارد کنید",
|
||||||
|
"email": "ایمیل",
|
||||||
|
"enter_email": "ایمیل خود را وارد کنید",
|
||||||
|
"phonen_number": "شماره تماس",
|
||||||
|
"enter_phone_number": "شماره تماس خود را وارد کنید",
|
||||||
|
"password": "رمز عبور",
|
||||||
|
"enter_password": "رمز عبور انتخابی خود را وارد کنید",
|
||||||
|
"next": "ادامه",
|
||||||
|
"before_registered": "آیا قبلا ثبت نام کردید؟",
|
||||||
|
"login": "ورود",
|
||||||
|
"enter_info_login": "لطفا اطلاعات خود را وارد کنید.",
|
||||||
|
"mobile_or_email": " شماره موبایل یا ایمیل",
|
||||||
|
"enter_mobile_or_email": " شماره موبایل یا ایمیل خود را وارد کنید",
|
||||||
|
"have_account": "آیا حساب کاربری ندارید؟",
|
||||||
|
"register": "ثبت نام",
|
||||||
|
"enter_otp": "کد تایید را وارد کنید",
|
||||||
|
"verfify_code_text_1": "کد تایید برای ",
|
||||||
|
"verfify_code_text_2": "ارسال شد.",
|
||||||
|
"change_number": "تغییر شماره",
|
||||||
|
"verify_code": "کد تایید ",
|
||||||
|
"enter_verify_code": "کد تایید ارسالی را وارد کنید ",
|
||||||
|
"counter_otp": "مانده تا دریافت مجدد کد",
|
||||||
|
"login_with_password": "ورود با رمز عبور",
|
||||||
|
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||||
|
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||||
|
"forgot_password": "فراموشی رمز عبور",
|
||||||
|
"login_with_otp": "ورود با رمز عبور یکبار مصرف",
|
||||||
|
"otp_sent": "کد تایید ارسال شد",
|
||||||
|
"mobile_number": "شماره موبایل",
|
||||||
|
"enter_mobile_number": "شماره موبایل خود را وارد کنید",
|
||||||
|
"try_again": "ارسال مجدد",
|
||||||
|
"toast_resend_code": "کد جدیدی برای شما ارسال شد",
|
||||||
|
"old_version": "نسخه قدیمی داشبورد"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-22
@@ -1,40 +1,117 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import LogoImage from '../../assets/images/logo.svg'
|
import LogoImage from '../../assets/images/logo.svg'
|
||||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||||
import { useAuthStore } from './store/AuthStore'
|
import Input from '../../components/Input'
|
||||||
import LoginStep1 from './components/LoginStep1'
|
import { useTranslation } from 'react-i18next'
|
||||||
import LoginStep2 from './components/LoginStep2'
|
import { useFormik } from 'formik'
|
||||||
import LoginStep3 from './components/LoginStep3'
|
import * as Yup from 'yup'
|
||||||
|
import Error from '../../components/Error'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import { useLoginWithPassword } from './hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { setRefreshToken, setToken, getRedirectUrl } from '../../config/func'
|
||||||
|
import { toast } from '../../components/Toast'
|
||||||
|
import { Paths } from '../../utils/Paths'
|
||||||
|
|
||||||
|
type LoginFormType = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Login: FC = () => {
|
const Login: FC = () => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const loginWithPassword = useLoginWithPassword()
|
||||||
|
|
||||||
const { stepLogin, phone, email } = useAuthStore()
|
const formik = useFormik<LoginFormType>({
|
||||||
|
initialValues: {
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
email: Yup.string()
|
||||||
|
.email(t('errors.invalid_email'))
|
||||||
|
.required(t('errors.required')),
|
||||||
|
password: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
loginWithPassword.mutate(values, {
|
||||||
|
onSuccess(data) {
|
||||||
|
setToken(data?.data?.accessToken?.token)
|
||||||
|
setRefreshToken(data?.data?.refreshToken?.token)
|
||||||
|
const redirectUrl = getRedirectUrl();
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = Paths.received
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast(error?.response?.data?.error?.message[0], 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||||
<div className='flex-1 h-full flex flex-col'>
|
<div className='flex-1 h-full flex flex-col'>
|
||||||
|
|
||||||
<div className='flex relative flex-1 flex-col h-full justify-center'>
|
<div className='flex relative flex-1 flex-col h-full justify-center'>
|
||||||
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
|
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
|
||||||
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
|
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
|
||||||
{
|
|
||||||
stepLogin === 1 ?
|
<div>
|
||||||
<LoginStep1 />
|
<div className='mt-5'>
|
||||||
:
|
<h2 className='text-2xl font-bold'>
|
||||||
stepLogin === 2 && !!phone ?
|
{t('auth.welcome')}
|
||||||
<LoginStep2 />
|
</h2>
|
||||||
: stepLogin === 2 && !!email ?
|
<p className='text-description text-sm mt-2'>
|
||||||
<LoginStep3 />
|
{t('auth.enter_info_login')}
|
||||||
: stepLogin === 3 ?
|
</p>
|
||||||
<LoginStep3 />
|
</div>
|
||||||
: null
|
|
||||||
}
|
<div className='mt-16 space-y-6'>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label={t('auth.email')}
|
||||||
|
placeholder={t('auth.enter_email')}
|
||||||
|
type='email'
|
||||||
|
className='text-right'
|
||||||
|
name='email'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
/>
|
||||||
|
{formik.touched.email && formik.errors.email && (
|
||||||
|
<Error errorText={formik.errors.email} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label={t('auth.password')}
|
||||||
|
placeholder={t('auth.enter_password')}
|
||||||
|
type='password'
|
||||||
|
className='text-right'
|
||||||
|
name='password'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
/>
|
||||||
|
{formik.touched.password && formik.errors.password && (
|
||||||
|
<Error errorText={formik.errors.password} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.login')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
loading={loginWithPassword.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "../types/AuthTypes";
|
} from "../types/AuthTypes";
|
||||||
|
|
||||||
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||||
const { data } = await axios.post(`/auth/login/password`, params);
|
const { data } = await axios.post(`/auth/login`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+145
-14
@@ -1,6 +1,7 @@
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
import AvatarImage from '@/assets/images/avatar.svg'
|
import AvatarImage from '@/assets/images/avatar.svg'
|
||||||
import { DocumentDownload, EmojiHappy } from 'iconsax-react'
|
import { DocumentDownload, EmojiHappy } from 'iconsax-react'
|
||||||
import ForwardIcon from '@/assets/images/forward.svg'
|
import ForwardIcon from '@/assets/images/forward.svg'
|
||||||
@@ -8,12 +9,125 @@ import AnswerIcon from '@/assets/images/answer.svg'
|
|||||||
import Header from './Components/Header'
|
import Header from './Components/Header'
|
||||||
import ReactQuill from 'react-quill-new';
|
import ReactQuill from 'react-quill-new';
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import { useGetMessageDetail, useSendEmail, useSaveDraft } from './hooks/useEmailData'
|
||||||
|
import { useAuthStore } from '../auth/store/AuthStore'
|
||||||
|
import { SendEmailDto } from './types/Types'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { formatDate } from '@/config/func'
|
||||||
|
|
||||||
const DetailEmail: FC = () => {
|
const DetailEmail: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const { email: userEmail } = useAuthStore()
|
||||||
const [showAnswer, setShowAnswer] = useState<boolean>(false)
|
const [showAnswer, setShowAnswer] = useState<boolean>(false)
|
||||||
const [value, setValue] = useState<string>('')
|
const [value, setValue] = useState<string>('')
|
||||||
|
const [priority, setPriority] = useState<string>('')
|
||||||
|
|
||||||
|
// API hooks
|
||||||
|
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', !!id)
|
||||||
|
const sendEmailMutation = useSendEmail()
|
||||||
|
const saveDraftMutation = useSaveDraft()
|
||||||
|
|
||||||
|
const priorityOptions = [
|
||||||
|
{ value: 'high', label: 'بالا' },
|
||||||
|
{ value: 'medium', label: 'متوسط' },
|
||||||
|
{ value: 'low', label: 'پایین' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSendReply = async () => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
toast('لطفا متن پیام را وارد کنید', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageDetail) {
|
||||||
|
toast('اطلاعات پیام یافت نشد', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyData: 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, ''),
|
||||||
|
reference: {
|
||||||
|
inReplyTo: messageDetail.messageId,
|
||||||
|
originalMessageId: messageDetail.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEmailMutation.mutateAsync(replyData)
|
||||||
|
toast('پاسخ با موفقیت ارسال شد', 'success')
|
||||||
|
setShowAnswer(false)
|
||||||
|
setValue('')
|
||||||
|
setPriority('')
|
||||||
|
} catch {
|
||||||
|
toast('خطا در ارسال پاسخ', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveDraft = async () => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
toast('متن پیام خالی است', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageDetail) {
|
||||||
|
toast('اطلاعات پیام یافت نشد', 'error')
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveDraftMutation.mutateAsync(draftData)
|
||||||
|
toast('پیشنویس ذخیره شد', 'success')
|
||||||
|
} catch {
|
||||||
|
toast('خطا در ذخیره پیشنویس', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleForward = () => {
|
||||||
|
// TODO: Implement forward functionality
|
||||||
|
toast('قابلیت ارسال به دیگران به زودی اضافه خواهد شد', 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className='bg-white rounded-4xl p-8'>
|
||||||
|
<div className='animate-pulse'>
|
||||||
|
<div className='h-6 bg-gray-200 rounded mb-4'></div>
|
||||||
|
<div className='h-4 bg-gray-200 rounded mb-2'></div>
|
||||||
|
<div className='h-4 bg-gray-200 rounded mb-2'></div>
|
||||||
|
<div className='h-20 bg-gray-200 rounded'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !messageDetail) {
|
||||||
|
return (
|
||||||
|
<div className='bg-white rounded-4xl p-8'>
|
||||||
|
<div className='text-center text-red-500'>
|
||||||
|
خطا در بارگذاری پیام
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white rounded-4xl p-8'>
|
<div className='bg-white rounded-4xl p-8'>
|
||||||
@@ -23,7 +137,7 @@ const DetailEmail: FC = () => {
|
|||||||
<div className='flex xl:flex-row flex-col-reverse gap-4 xl:items-center'>
|
<div className='flex xl:flex-row flex-col-reverse gap-4 xl:items-center'>
|
||||||
<div className='flex gap-1 items-center'>
|
<div className='flex gap-1 items-center'>
|
||||||
<div className='font-extralight hidden xl:block'>{t('mail.subject')}</div>
|
<div className='font-extralight hidden xl:block'>{t('mail.subject')}</div>
|
||||||
<div className='leading-7 text-sm'>لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ</div>
|
<div className='leading-7 text-sm'>{messageDetail.subject || 'بدون موضوع'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -38,28 +152,41 @@ const DetailEmail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-end text-description xl:text-sm text-[10px]'>
|
<div className='flex justify-end text-description xl:text-sm text-[10px]'>
|
||||||
۵ اسفندماه ۱۴۰۳، ساعت ۱۲:۰۰
|
{formatDate(messageDetail.date)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 flex gap-4 items-center'>
|
<div className='mt-6 flex gap-4 items-center'>
|
||||||
<img src={AvatarImage} className='size-10' />
|
<img src={AvatarImage} className='size-10' />
|
||||||
<div className='text-sm'>مهرداد مظفری</div>
|
<div className='text-sm'>{messageDetail.from.name || messageDetail.from.address}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='xl:mt-8 mt-3 text-[13px] leading-7 font-light'>
|
<div className='xl:mt-8 mt-3 text-[13px] leading-7 font-light'>
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد کتابهای زیادی در شصت و سه درصد گذشته حال و آیندهلورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد کتابهای زیادی در شصت و سه درصد گذشته حال و آینده
|
{messageDetail.html && messageDetail.html.length > 0 ? (
|
||||||
</p>
|
<div dangerouslySetInnerHTML={{ __html: messageDetail.html.join('') }} />
|
||||||
|
) : (
|
||||||
|
<p>{messageDetail.text || 'محتوای پیام'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-10 text-sm font-bold'>
|
<div className='mt-10 text-sm font-bold'>
|
||||||
{t('mail.attachments')}
|
{t('mail.attachments')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex border-b border-border pb-10 mt-4 flex-wrap gap-2.5 items-center'>
|
<div className='flex border-b border-border pb-10 mt-4 flex-wrap gap-2.5 items-center'>
|
||||||
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
{messageDetail.attachments && messageDetail.attachments.length > 0 ? (
|
||||||
<span>Lorem Ipsum.pdf</span>
|
messageDetail.attachments.map((attachment, index) => (
|
||||||
<DocumentDownload size={20} color='#0038FF' />
|
<div key={index} className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
||||||
</div>
|
<span>{attachment.filename || `attachment-${index + 1}`}</span>
|
||||||
|
<DocumentDownload size={20} color='#0038FF' className='cursor-pointer' />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className='bg-[#EBEDF5] text-sm flex gap-2 h-10 rounded-full items-center px-4'>
|
||||||
|
<span>Lorem Ipsum.pdf</span>
|
||||||
|
<DocumentDownload size={20} color='#0038FF' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -73,7 +200,7 @@ const DetailEmail: FC = () => {
|
|||||||
<div>{t('mail.answer')}</div>
|
<div>{t('mail.answer')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
<div onClick={handleForward} className='flex gap-2 cursor-pointer items-center border-r border-border pr-5'>
|
||||||
<img src={ForwardIcon} className='w-[17px]' />
|
<img src={ForwardIcon} className='w-[17px]' />
|
||||||
<div>{t('mail.forward')}</div>
|
<div>{t('mail.forward')}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,19 +235,25 @@ const DetailEmail: FC = () => {
|
|||||||
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
<div className='flex flex-col gap-4 sm:flex-row pt-4'>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
items={[]}
|
items={priorityOptions}
|
||||||
placeholder={t('new_message.select_priority')}
|
placeholder={t('new_message.select_priority')}
|
||||||
className='xl:w-[190px]'
|
className='xl:w-[190px]'
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 justify-end flex gap-3'>
|
<div className='flex-1 justify-end flex gap-3'>
|
||||||
<Button
|
<Button
|
||||||
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
|
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')}
|
label={t('new_message.draft')}
|
||||||
|
onClick={handleSaveDraft}
|
||||||
|
loading={saveDraftMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
|
||||||
label={t('new_message.send')}
|
label={t('new_message.send')}
|
||||||
|
onClick={handleSendReply}
|
||||||
|
loading={sendEmailMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,8 +261,6 @@ const DetailEmail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-46
@@ -3,46 +3,43 @@ import { FC, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Table from '../../components/Table';
|
import Table from '../../components/Table';
|
||||||
import { ColumnType } from '../../components/types/TableTypes';
|
import { ColumnType } from '../../components/types/TableTypes';
|
||||||
import { InfoCircle, More, Refresh2, Trash, Edit, Archive, ArchiveSlash } from 'iconsax-react';
|
import { InfoCircle, More, Refresh2, Trash, Edit, Archive } from 'iconsax-react';
|
||||||
import { RowActionItem } from '../../components/RowActionsDropdown';
|
import { RowActionItem } from '../../components/RowActionsDropdown';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGetInbox } from './hooks/useEmailData';
|
||||||
interface Message extends Record<string, unknown> {
|
import { InboxMessage } from './types/Types';
|
||||||
id: string;
|
import { formatDate } from '@/config/func';
|
||||||
title: string;
|
|
||||||
sender: string;
|
|
||||||
date: string;
|
|
||||||
read: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const List: FC = () => {
|
const List: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isLoading] = useState(false);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]);
|
const [filters, setFilters] = useState<FilterValues>({});
|
||||||
|
|
||||||
const messageData: Message[] = [
|
const { data: inboxData, isLoading, refetch } = useGetInbox({
|
||||||
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true },
|
page: currentPage,
|
||||||
{ id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true },
|
limit: 10,
|
||||||
{ id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true },
|
...filters
|
||||||
];
|
});
|
||||||
|
|
||||||
const columns: ColumnType<Message>[] = [
|
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
|
||||||
|
|
||||||
|
const columns: ColumnType<InboxMessage>[] = [
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'subject',
|
||||||
title: 'عنوان پیام',
|
title: 'عنوان پیام',
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<div className="flex items-center">
|
<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" />}
|
{/* {!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.title}</span>
|
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sender',
|
key: 'from',
|
||||||
title: 'فرستنده',
|
title: 'فرستنده',
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<span className="truncate">{item.sender}</span>
|
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -50,14 +47,19 @@ const List: FC = () => {
|
|||||||
title: 'تاریخ',
|
title: 'تاریخ',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<span className="text-xs md:text-sm">{item.date}</span>
|
<span>{formatDate(item.date)}</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableActions = (
|
const tableActions = (
|
||||||
<div className='flex items-center gap-2 md:gap-4'>
|
<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"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
/>
|
||||||
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
<More size={18} color='black' className='rotate-90 cursor-pointer' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -71,10 +73,11 @@ const List: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterChange = (newFilters: FilterValues) => {
|
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: InboxMessage[]) => {
|
||||||
setSelectedMessages(selectedRows);
|
setSelectedMessages(selectedRows);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,16 +96,16 @@ const List: FC = () => {
|
|||||||
// اینجا منطق علامتگذاری را پیادهسازی کنید
|
// اینجا منطق علامتگذاری را پیادهسازی کنید
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowActions = (message: Message): RowActionItem[] => [
|
const getRowActions = (message: InboxMessage): RowActionItem[] => [
|
||||||
{
|
{
|
||||||
label: 'خروج از آرشیو',
|
label: message.seen ? 'علامتگذاری به عنوان خوانده نشده' : 'علامتگذاری به عنوان خوانده شده',
|
||||||
icon: <ArchiveSlash size={16} color="black" />,
|
icon: <Edit size={16} color="black" />,
|
||||||
onClick: () => console.log('خروج از آرشیو', message.id),
|
onClick: () => console.log('تغییر وضعیت خوانده شده', message.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'خوانده شده',
|
label: 'بایگانی',
|
||||||
icon: <Edit size={16} color="black" />,
|
icon: <Archive size={16} color="black" />,
|
||||||
onClick: () => console.log('علامتگذاری به عنوان خوانده شده', message.id),
|
onClick: () => console.log('بایگانی پیام', message.id),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'حذف',
|
label: 'حذف',
|
||||||
@@ -111,22 +114,29 @@ const List: FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = inboxData?.data?.results || [];
|
||||||
|
const pager = inboxData?.data?.pager;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
<div className='mt-2 md:mt-4 px-2 md:px-0'>
|
||||||
<h1 className="text-lg mb-4 md:mb-0">{t('received.title')}</h1>
|
<h1 className="text-lg mb-4 md:mb-0">{t('received.title')}</h1>
|
||||||
|
|
||||||
<Filters
|
<Filters
|
||||||
fields={[
|
fields={[
|
||||||
{ type: 'date', name: 'from_date', placeholder: t('received.from_date') },
|
{ type: 'date', name: 'dateFrom', placeholder: t('received.from_date') },
|
||||||
{ type: 'date', name: 'to_date', placeholder: t('received.to_date') },
|
{ type: 'date', name: 'dateTo', placeholder: t('received.to_date') },
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'status',
|
name: 'isRead',
|
||||||
placeholder: t('received.all'),
|
placeholder: t('received.all'),
|
||||||
options: [
|
options: [
|
||||||
{ value: 'all', label: t('received.all') },
|
{ value: '', label: t('received.all') },
|
||||||
{ value: 'read', label: t('received.read') },
|
{ value: 'true', label: t('received.read') },
|
||||||
{ value: 'unread', label: t('received.unread') }
|
{ value: 'false', label: t('received.unread') }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ type: 'input', name: 'search', placeholder: t('received.search') }
|
{ type: 'input', name: 'search', placeholder: t('received.search') }
|
||||||
@@ -135,9 +145,9 @@ const List: FC = () => {
|
|||||||
searchField="search"
|
searchField="search"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Table<Message>
|
<Table<InboxMessage>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={messageData}
|
data={messages}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
actions={tableActions}
|
actions={tableActions}
|
||||||
@@ -152,11 +162,11 @@ const List: FC = () => {
|
|||||||
}
|
}
|
||||||
selectable={true}
|
selectable={true}
|
||||||
rowActions={getRowActions}
|
rowActions={getRowActions}
|
||||||
pagination={{
|
pagination={pager ? {
|
||||||
totalPages: 10,
|
totalPages: pager.totalPages,
|
||||||
currentPage: 1,
|
currentPage: pager.page,
|
||||||
onPageChange: () => { }
|
onPageChange: handlePageChange
|
||||||
}}
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import * as api from '../service/EmailService';
|
||||||
|
import { SendEmailDto, MessageListQueryDto } from '../types/Types';
|
||||||
|
|
||||||
|
export const useSendEmail = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: SendEmailDto) => api.sendEmail(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSaveDraft = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: SendEmailDto) => api.saveDraft(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetInbox = (query: MessageListQueryDto) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['inbox', query],
|
||||||
|
queryFn: () => api.getInbox(query),
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMessageDetail = (messageId: string, enabled: boolean = true) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['message', messageId],
|
||||||
|
queryFn: () => api.getMessageDetail(messageId),
|
||||||
|
enabled: !!messageId && enabled,
|
||||||
|
staleTime: 1000 * 60 * 10, // 10 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMessage = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (messageId: string) => api.deleteMessage(messageId),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate inbox queries to refresh the list
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['inbox'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
SendEmailDto,
|
||||||
|
MessageListQueryDto,
|
||||||
|
InboxResponse,
|
||||||
|
MessageDetailResponse,
|
||||||
|
MessageDetail,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const sendEmail = async (params: SendEmailDto) => {
|
||||||
|
const { data } = await axios.post(`/email/send`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveDraft = async (params: SendEmailDto) => {
|
||||||
|
const { data } = await axios.post(`/email/draft`, {
|
||||||
|
...params,
|
||||||
|
isDraft: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInbox = async (
|
||||||
|
query: MessageListQueryDto
|
||||||
|
): Promise<InboxResponse> => {
|
||||||
|
const { data } = await axios.get(`/email/messages/inbox`, { params: query });
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessageDetail = async (
|
||||||
|
messageId: string
|
||||||
|
): Promise<MessageDetail> => {
|
||||||
|
const { data }: { data: MessageDetailResponse } = await axios.get(
|
||||||
|
`/email/messages/inbox/${messageId}`
|
||||||
|
);
|
||||||
|
return data.data.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMessage = async (messageId: string): Promise<void> => {
|
||||||
|
const { data } = await axios.delete(`/email/messages/inbox/${messageId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
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 DraftReferenceDto {
|
||||||
|
mailbox: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailEnvelopeDto {
|
||||||
|
from: EmailRecipientDto;
|
||||||
|
to: EmailRecipientDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendEmailDto {
|
||||||
|
mailbox?: string;
|
||||||
|
from: EmailRecipientDto;
|
||||||
|
replyTo?: EmailRecipientDto;
|
||||||
|
to: EmailRecipientDto[];
|
||||||
|
cc?: EmailRecipientDto[];
|
||||||
|
bcc?: EmailRecipientDto[];
|
||||||
|
headers?: EmailHeaderDto[];
|
||||||
|
subject?: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: EmailAttachmentDto[];
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
sess?: string;
|
||||||
|
ip?: string;
|
||||||
|
reference?: Record<string, unknown>;
|
||||||
|
isDraft?: boolean;
|
||||||
|
draft?: DraftReferenceDto;
|
||||||
|
sendTime?: string;
|
||||||
|
uploadOnly?: boolean;
|
||||||
|
envelope?: EmailEnvelopeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated types based on actual API response
|
||||||
|
export interface InboxMessage extends Record<string, unknown> {
|
||||||
|
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 interface InboxPager {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
prevPage: boolean;
|
||||||
|
nextPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InboxResponse {
|
||||||
|
statusCode: number;
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
pager: InboxPager;
|
||||||
|
message: string;
|
||||||
|
results: InboxMessage[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy types (keeping for backward compatibility)
|
||||||
|
export interface MessageDto {
|
||||||
|
id: string;
|
||||||
|
mailbox: string;
|
||||||
|
uid: number;
|
||||||
|
flags: string[];
|
||||||
|
envelope: {
|
||||||
|
date: string;
|
||||||
|
subject: string;
|
||||||
|
from: EmailRecipientDto[];
|
||||||
|
to: EmailRecipientDto[];
|
||||||
|
cc?: EmailRecipientDto[];
|
||||||
|
bcc?: EmailRecipientDto[];
|
||||||
|
messageId: string;
|
||||||
|
inReplyTo?: string;
|
||||||
|
references?: string[];
|
||||||
|
};
|
||||||
|
bodyStructure: Record<string, unknown>;
|
||||||
|
source?: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: EmailAttachmentDto[];
|
||||||
|
headers?: EmailHeaderDto[];
|
||||||
|
size: number;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageListResponse {
|
||||||
|
messages: MessageDto[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserMailboxMessageParamDto {
|
||||||
|
messageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New interface based on actual API response
|
||||||
|
export interface MessageDetailResponse {
|
||||||
|
statusCode: number;
|
||||||
|
success: boolean;
|
||||||
|
data: {
|
||||||
|
message: {
|
||||||
|
success: boolean;
|
||||||
|
id: number;
|
||||||
|
mailbox: string;
|
||||||
|
thread: string;
|
||||||
|
user: string;
|
||||||
|
envelope: {
|
||||||
|
from: string;
|
||||||
|
rcpt: Array<{
|
||||||
|
value: string;
|
||||||
|
formatted: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
from: {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
to: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
subject: string;
|
||||||
|
messageId: string;
|
||||||
|
date: string;
|
||||||
|
idate: string;
|
||||||
|
size: number;
|
||||||
|
seen: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
flagged: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
answered: boolean;
|
||||||
|
forwarded: boolean;
|
||||||
|
html: string[];
|
||||||
|
text: string;
|
||||||
|
attachments: EmailAttachmentDto[];
|
||||||
|
references: string[];
|
||||||
|
metaData: Record<string, unknown>;
|
||||||
|
verificationResults: {
|
||||||
|
tls: {
|
||||||
|
name: string;
|
||||||
|
standardName: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
spf: string;
|
||||||
|
dkim: string;
|
||||||
|
arc: boolean;
|
||||||
|
bimi: boolean;
|
||||||
|
};
|
||||||
|
contentType: {
|
||||||
|
value: string;
|
||||||
|
params: {
|
||||||
|
boundary: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageDetail {
|
||||||
|
success: boolean;
|
||||||
|
id: number;
|
||||||
|
mailbox: string;
|
||||||
|
thread: string;
|
||||||
|
user: string;
|
||||||
|
envelope: {
|
||||||
|
from: string;
|
||||||
|
rcpt: Array<{
|
||||||
|
value: string;
|
||||||
|
formatted: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
from: {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
to: Array<{
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
subject: string;
|
||||||
|
messageId: string;
|
||||||
|
date: string;
|
||||||
|
idate: string;
|
||||||
|
size: number;
|
||||||
|
seen: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
flagged: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
answered: boolean;
|
||||||
|
forwarded: boolean;
|
||||||
|
html: string[];
|
||||||
|
text: string;
|
||||||
|
attachments: EmailAttachmentDto[];
|
||||||
|
references: string[];
|
||||||
|
metaData: Record<string, unknown>;
|
||||||
|
verificationResults: {
|
||||||
|
tls: {
|
||||||
|
name: string;
|
||||||
|
standardName: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
spf: string;
|
||||||
|
dkim: string;
|
||||||
|
arc: boolean;
|
||||||
|
bimi: boolean;
|
||||||
|
};
|
||||||
|
contentType: {
|
||||||
|
value: string;
|
||||||
|
params: {
|
||||||
|
boundary: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user