template
This commit is contained in:
@@ -149,10 +149,7 @@ const renderBlockToHTML = (block: Block): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate CSS for email clients
|
// Generate CSS for email clients
|
||||||
const generateEmailCSS = (
|
const generateEmailCSS = (options: ExportOptions): string => {
|
||||||
template: Template,
|
|
||||||
options: ExportOptions
|
|
||||||
): string => {
|
|
||||||
if (!options.includeCSS) return "";
|
if (!options.includeCSS) return "";
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -213,7 +210,7 @@ export const exportToHTML = (
|
|||||||
): string => {
|
): string => {
|
||||||
const finalOptions = { ...defaultExportOptions, ...options };
|
const finalOptions = { ...defaultExportOptions, ...options };
|
||||||
|
|
||||||
const css = generateEmailCSS(template, finalOptions);
|
const css = generateEmailCSS(finalOptions);
|
||||||
const bodyHTML = template.blocks
|
const bodyHTML = template.blocks
|
||||||
.map((block) => renderBlockToHTML(block))
|
.map((block) => renderBlockToHTML(block))
|
||||||
.join("");
|
.join("");
|
||||||
|
|||||||
@@ -1,26 +1,10 @@
|
|||||||
import Input from '@/components/Input'
|
|
||||||
import Select from '@/components/Select'
|
|
||||||
import Table from '@/components/Table'
|
|
||||||
import { ColumnType } from '@/components/types/TableTypes'
|
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { AddressData } from './types/Types'
|
||||||
import Button from '@/components/Button'
|
import AddressTable from './components/AddressTable'
|
||||||
import { TickCircle, Edit2, Trash } from 'iconsax-react'
|
import AddressForm from './components/AddressForm'
|
||||||
import { useForm } from 'react-hook-form'
|
import AddressDeleteModal from './components/AddressDeleteModal'
|
||||||
import { AddressData, CreateAddressType, UpdateAddressType } from './types/Types'
|
|
||||||
import { useGetDomains } from '../domain/hooks/useDomainData'
|
|
||||||
import { useCreateAddress, useGetAddress, useUpdateAddress, useDeleteAddress } from './hooks/useAddressData'
|
|
||||||
import { useGetSpace } from '../domain/hooks/useDomainData'
|
|
||||||
import { toast } from '@/components/Toast'
|
|
||||||
import { ErrorType } from '@/helpers/types'
|
|
||||||
import ProgressBarEmail from './components/ProgressBarEmail'
|
|
||||||
import ToggleStatus from './components/ToggleStatus'
|
|
||||||
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
|
|
||||||
import ModalConfrim from '@/components/ModalConfrim'
|
|
||||||
|
|
||||||
const Address: FC = () => {
|
const Address: FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
const [status, setStatus] = useState<string>('all')
|
const [status, setStatus] = useState<string>('all')
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1)
|
const [currentPage, setCurrentPage] = useState<number>(1)
|
||||||
@@ -29,70 +13,9 @@ const Address: FC = () => {
|
|||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
||||||
const [addressToDelete, setAddressToDelete] = useState<AddressData | null>(null)
|
const [addressToDelete, setAddressToDelete] = useState<AddressData | null>(null)
|
||||||
|
|
||||||
const { data: domain } = useGetDomains()
|
|
||||||
const { data: spaceData } = useGetSpace()
|
|
||||||
const { mutate: createAddress, isPending } = useCreateAddress()
|
|
||||||
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
|
||||||
const { mutate: deleteAddress } = useDeleteAddress()
|
|
||||||
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
|
|
||||||
|
|
||||||
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
|
||||||
|
|
||||||
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors
|
|
||||||
} = useForm<FormData>({
|
|
||||||
defaultValues: {
|
|
||||||
domainId: domain?.data?.domain?.id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateForm = (params: FormData): boolean => {
|
|
||||||
clearErrors()
|
|
||||||
let isValid = true
|
|
||||||
|
|
||||||
if (!params.title) {
|
|
||||||
setError('title', { message: t('errors.required') })
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.username) {
|
|
||||||
setError('username', { message: t('errors.required') })
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEditMode && !params.password) {
|
|
||||||
setError('password', { message: t('errors.required') })
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.domainId) {
|
|
||||||
setError('domainId', { message: t('errors.required') })
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// اعتبارسنجی quota
|
|
||||||
if (params.quotaMB) {
|
|
||||||
const quotaInBytes = params.quotaMB * 1024 * 1024
|
|
||||||
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
|
|
||||||
|
|
||||||
if (quotaInBytes > remainingSpace) {
|
|
||||||
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقیمانده است' })
|
|
||||||
isValid = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (address: AddressData) => {
|
const handleEdit = (address: AddressData) => {
|
||||||
setSelectedAddress(address)
|
setSelectedAddress(address)
|
||||||
setIsEditMode(true)
|
setIsEditMode(true)
|
||||||
reset({
|
|
||||||
title: address.title,
|
|
||||||
username: address.displayName,
|
|
||||||
password: '',
|
|
||||||
domainId: address.domain.id,
|
|
||||||
quotaMB: address.emailQuota ? Math.round(address.emailQuota / (1024 * 1024)) : undefined,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (address: AddressData) => {
|
const handleDelete = (address: AddressData) => {
|
||||||
@@ -100,153 +23,24 @@ const Address: FC = () => {
|
|||||||
setIsDeleteModalOpen(true)
|
setIsDeleteModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmDelete = () => {
|
|
||||||
if (addressToDelete) {
|
|
||||||
deleteAddress(addressToDelete.id, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
|
|
||||||
setIsDeleteModalOpen(false)
|
|
||||||
setAddressToDelete(null)
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
|
|
||||||
setIsDeleteModalOpen(false)
|
|
||||||
setAddressToDelete(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseDeleteModal = () => {
|
|
||||||
setIsDeleteModalOpen(false)
|
|
||||||
setAddressToDelete(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setSelectedAddress(null)
|
setSelectedAddress(null)
|
||||||
setIsEditMode(false)
|
setIsEditMode(false)
|
||||||
reset({
|
|
||||||
title: '',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
domainId: domain?.data?.domain?.id,
|
|
||||||
quotaMB: undefined,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRowActions = (address: AddressData): RowActionItem[] => [
|
const handleFormSuccess = () => {
|
||||||
{
|
setSelectedAddress(null)
|
||||||
label: 'ویرایش',
|
setIsEditMode(false)
|
||||||
icon: <Edit2 color='black' size={16} />,
|
|
||||||
onClick: () => handleEdit(address),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'حذف',
|
|
||||||
icon: <Trash color='red' size={16} />,
|
|
||||||
onClick: () => handleDelete(address),
|
|
||||||
className: 'text-red-600 hover:bg-red-50'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const columns: ColumnType<AddressData>[] = [
|
|
||||||
{
|
|
||||||
title: t('setting.address_title'),
|
|
||||||
key: 'title',
|
|
||||||
width: '200px'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.email'),
|
|
||||||
key: 'email',
|
|
||||||
width: '300px',
|
|
||||||
render: (item: AddressData) => (
|
|
||||||
<div key={item.id} className='dltr flex justify-end'>
|
|
||||||
{item.emailAddress}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.quota'),
|
|
||||||
key: 'quota',
|
|
||||||
width: '300px',
|
|
||||||
render: (item: AddressData) => {
|
|
||||||
return (
|
|
||||||
<ProgressBarEmail key={item.id} item={item} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('setting.status'),
|
|
||||||
key: 'status',
|
|
||||||
width: '120px',
|
|
||||||
align: 'center',
|
|
||||||
render: (item: AddressData) => (
|
|
||||||
<div key={item.id} className='dltr flex justify-end'>
|
|
||||||
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'عملیات',
|
|
||||||
key: 'actions',
|
|
||||||
width: '120px',
|
|
||||||
align: 'center',
|
|
||||||
render: (item: AddressData) => (
|
|
||||||
<RowActionsDropdown
|
|
||||||
key={item.id}
|
|
||||||
actions={getRowActions(item)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ value: 'all', label: 'همه' },
|
|
||||||
{ value: 'active', label: t('setting.active') },
|
|
||||||
{ value: 'inactive', label: t('setting.inactive') }
|
|
||||||
]
|
|
||||||
|
|
||||||
const onSubmit = (params: FormData) => {
|
|
||||||
if (!validateForm(params)) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditMode && selectedAddress) {
|
const handleDeleteSuccess = () => {
|
||||||
const updateParams: UpdateAddressType = {
|
setAddressToDelete(null)
|
||||||
title: params.title,
|
|
||||||
password: params.password || undefined,
|
|
||||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
|
||||||
}
|
|
||||||
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
|
|
||||||
handleCancelEdit()
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const createParams: CreateAddressType = {
|
|
||||||
...params,
|
|
||||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
|
||||||
}
|
|
||||||
createAddress(createParams, {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
toast(data?.data?.message, 'success')
|
|
||||||
reset()
|
|
||||||
},
|
|
||||||
onError: (error: ErrorType) => {
|
|
||||||
toast(error.response?.data?.error?.message[0], 'error')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDeleteModalClose = () => {
|
||||||
if (domain?.data?.domain?.id && !isEditMode) {
|
setIsDeleteModalOpen(false)
|
||||||
setValue('domainId', domain?.data?.domain?.id)
|
setAddressToDelete(null)
|
||||||
}
|
}
|
||||||
}, [domain, isEditMode])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
@@ -254,144 +48,29 @@ const Address: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
|
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
|
||||||
<div className='flex-1'>
|
<AddressTable
|
||||||
<div className='flex gap-2 justify-between'>
|
search={search}
|
||||||
<div>
|
status={status}
|
||||||
<Select
|
currentPage={currentPage}
|
||||||
placeholder={t('setting.status')}
|
onSearchChange={setSearch}
|
||||||
items={statusOptions}
|
onStatusChange={setStatus}
|
||||||
className='w-[120px]'
|
onPageChange={setCurrentPage}
|
||||||
onChange={(e) => setStatus(e.target.value)}
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1'>
|
<AddressForm
|
||||||
<Input
|
selectedAddress={selectedAddress}
|
||||||
variant='search'
|
isEditMode={isEditMode}
|
||||||
placeholder={t('setting.search')}
|
onCancel={handleCancelEdit}
|
||||||
className='xl:w-[300px] flex-1'
|
onSuccess={handleFormSuccess}
|
||||||
onChangeSearchFinal={(e) => setSearch(e)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
<AddressDeleteModal
|
||||||
isLoading={isPendingAddress}
|
|
||||||
columns={columns}
|
|
||||||
data={addresses?.data?.users}
|
|
||||||
pagination={{
|
|
||||||
currentPage: currentPage,
|
|
||||||
totalPages: addresses?.data?.pager?.totalPages || 1,
|
|
||||||
onPageChange: (page) => setCurrentPage(page)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
|
|
||||||
<div className='flex justify-between items-center'>
|
|
||||||
<div>
|
|
||||||
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
|
|
||||||
</div>
|
|
||||||
{isEditMode && (
|
|
||||||
<Button
|
|
||||||
variant='secondary'
|
|
||||||
onClick={handleCancelEdit}
|
|
||||||
className='px-4 w-fit text-xs'
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.domain1')}
|
|
||||||
value={domain?.data?.domain?.name}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.title1')}
|
|
||||||
{...register('title')}
|
|
||||||
error_text={errors.title?.message as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6 relative'>
|
|
||||||
<Input
|
|
||||||
label={t('setting.username')}
|
|
||||||
className='text-left'
|
|
||||||
{...register('username')}
|
|
||||||
error_text={errors.username?.message as string}
|
|
||||||
readOnly={isEditMode}
|
|
||||||
/>
|
|
||||||
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
|
|
||||||
@ {domain?.data?.domain?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label='حجم ایمیل (مگابایت)'
|
|
||||||
type='number'
|
|
||||||
{...register('quotaMB', { valueAsNumber: true })}
|
|
||||||
error_text={errors.quotaMB?.message as string}
|
|
||||||
placeholder='حجم ایمیل را به مگابایت وارد کنید'
|
|
||||||
/>
|
|
||||||
<div className='text-xs text-gray-500 mt-1'>
|
|
||||||
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Input
|
|
||||||
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
|
||||||
type='password'
|
|
||||||
{...register('password')}
|
|
||||||
error_text={errors.password?.message as string}
|
|
||||||
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{!isEditMode && (
|
|
||||||
<div className='mt-2 text-xs'>
|
|
||||||
<div>رمز عبور میبایست:</div>
|
|
||||||
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
|
|
||||||
<li>حداقل ۸ کاراکتر باشد</li>
|
|
||||||
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
|
|
||||||
<li>شامل اعداد باشد</li>
|
|
||||||
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='mt-9 flex justify-end'>
|
|
||||||
<Button
|
|
||||||
className='w-fit px-20'
|
|
||||||
onClick={handleSubmit(onSubmit)}
|
|
||||||
loading={isPending || isUpdating}
|
|
||||||
>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<TickCircle size={20} color='white' />
|
|
||||||
<div>
|
|
||||||
{isEditMode ? 'آپدیت' : t('setting.create')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ModalConfrim
|
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
close={handleCloseDeleteModal}
|
addressToDelete={addressToDelete}
|
||||||
onConfrim={handleConfirmDelete}
|
onClose={handleDeleteModalClose}
|
||||||
title={t('confrim.delete_address_title')}
|
onSuccess={handleDeleteSuccess}
|
||||||
label={addressToDelete ? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress) : ''}
|
|
||||||
isLoading={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import ModalConfrim from '@/components/ModalConfrim'
|
||||||
|
import { useDeleteAddress } from '../hooks/useAddressData'
|
||||||
|
import { AddressData } from '../types/Types'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { ErrorType } from '@/helpers/types'
|
||||||
|
|
||||||
|
interface AddressDeleteModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
addressToDelete: AddressData | null
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressDeleteModal: FC<AddressDeleteModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
addressToDelete,
|
||||||
|
onClose,
|
||||||
|
onSuccess
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { mutate: deleteAddress, isPending } = useDeleteAddress()
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (addressToDelete) {
|
||||||
|
deleteAddress(addressToDelete.id, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
|
||||||
|
onSuccess()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isOpen}
|
||||||
|
close={onClose}
|
||||||
|
onConfrim={handleConfirmDelete}
|
||||||
|
title={t('confrim.delete_address_title')}
|
||||||
|
label={
|
||||||
|
addressToDelete
|
||||||
|
? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddressDeleteModal
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import Input from '@/components/Input'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { FC, useEffect } from 'react'
|
||||||
|
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
|
||||||
|
import { useGetDomains } from '../../domain/hooks/useDomainData'
|
||||||
|
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
|
||||||
|
import { useGetSpace } from '../../domain/hooks/useDomainData'
|
||||||
|
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { ErrorType } from '@/helpers/types'
|
||||||
|
|
||||||
|
interface AddressFormProps {
|
||||||
|
selectedAddress: AddressData | null
|
||||||
|
isEditMode: boolean
|
||||||
|
onCancel: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCancel, onSuccess }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { data: templates } = useGetTemplates()
|
||||||
|
const { data: domain } = useGetDomains()
|
||||||
|
const { data: spaceData } = useGetSpace()
|
||||||
|
const { mutate: createAddress, isPending } = useCreateAddress()
|
||||||
|
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
|
||||||
|
|
||||||
|
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors, getValues
|
||||||
|
} = useForm<FormData>({
|
||||||
|
defaultValues: {
|
||||||
|
domainId: domain?.data?.domain?.id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateForm = (params: FormData): boolean => {
|
||||||
|
clearErrors()
|
||||||
|
let isValid = true
|
||||||
|
|
||||||
|
if (!params.title) {
|
||||||
|
setError('title', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.username) {
|
||||||
|
setError('username', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEditMode && !params.password) {
|
||||||
|
setError('password', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.domainId) {
|
||||||
|
setError('domainId', { message: t('errors.required') })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// اعتبارسنجی quota
|
||||||
|
if (params.quotaMB) {
|
||||||
|
const quotaInBytes = params.quotaMB * 1024 * 1024
|
||||||
|
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
if (quotaInBytes > remainingSpace) {
|
||||||
|
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقیمانده است' })
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (params: FormData) => {
|
||||||
|
if (!validateForm(params)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditMode && selectedAddress) {
|
||||||
|
const updateParams: UpdateAddressType = {
|
||||||
|
title: params.title,
|
||||||
|
password: params.password || undefined,
|
||||||
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
|
}
|
||||||
|
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
|
||||||
|
onSuccess()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const createParams: CreateAddressType = {
|
||||||
|
...params,
|
||||||
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
|
}
|
||||||
|
createAddress(createParams, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast(data?.data?.message, 'success')
|
||||||
|
reset()
|
||||||
|
onSuccess()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(error.response?.data?.error?.message[0], 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (domain?.data?.domain?.id && !isEditMode) {
|
||||||
|
setValue('domainId', domain?.data?.domain?.id)
|
||||||
|
setValue('templateId', domain?.data?.template)
|
||||||
|
}
|
||||||
|
}, [domain, isEditMode, setValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAddress && isEditMode) {
|
||||||
|
reset({
|
||||||
|
title: selectedAddress.title,
|
||||||
|
username: selectedAddress.displayName,
|
||||||
|
password: '',
|
||||||
|
domainId: selectedAddress.domain.id,
|
||||||
|
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
|
||||||
|
templateId: selectedAddress.template || undefined,
|
||||||
|
})
|
||||||
|
} else if (!isEditMode) {
|
||||||
|
reset({
|
||||||
|
title: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
domainId: domain?.data?.domain?.id,
|
||||||
|
quotaMB: undefined,
|
||||||
|
templateId: domain?.data?.template,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedAddress, isEditMode, reset, domain])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
|
||||||
|
</div>
|
||||||
|
{isEditMode && (
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
onClick={onCancel}
|
||||||
|
className='px-4 w-fit text-xs'
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={t('setting.domain1')}
|
||||||
|
value={domain?.data?.domain?.name}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={t('setting.title1')}
|
||||||
|
{...register('title')}
|
||||||
|
error_text={errors.title?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Select
|
||||||
|
placeholder='انتخاب قالب'
|
||||||
|
label={'قالب'}
|
||||||
|
items={
|
||||||
|
templates?.data?.templates
|
||||||
|
? templates.data.templates.map((template) => ({
|
||||||
|
value: template.id,
|
||||||
|
label: template.name
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
onChange={(e) => setValue('templateId', e.target.value)}
|
||||||
|
value={getValues('templateId')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 relative'>
|
||||||
|
<Input
|
||||||
|
label={t('setting.username')}
|
||||||
|
className='text-left'
|
||||||
|
{...register('username')}
|
||||||
|
error_text={errors.username?.message as string}
|
||||||
|
readOnly={isEditMode}
|
||||||
|
/>
|
||||||
|
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
|
||||||
|
@ {domain?.data?.domain?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label='حجم ایمیل (مگابایت)'
|
||||||
|
type='number'
|
||||||
|
{...register('quotaMB', { valueAsNumber: true })}
|
||||||
|
error_text={errors.quotaMB?.message as string}
|
||||||
|
placeholder='حجم ایمیل را به مگابایت وارد کنید'
|
||||||
|
/>
|
||||||
|
<div className='text-xs text-gray-500 mt-1'>
|
||||||
|
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
|
||||||
|
type='password'
|
||||||
|
{...register('password')}
|
||||||
|
error_text={errors.password?.message as string}
|
||||||
|
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isEditMode && (
|
||||||
|
<div className='mt-2 text-xs'>
|
||||||
|
<div>رمز عبور میبایست:</div>
|
||||||
|
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
|
||||||
|
<li>حداقل ۸ کاراکتر باشد</li>
|
||||||
|
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
|
||||||
|
<li>شامل اعداد باشد</li>
|
||||||
|
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='mt-9 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-20'
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
loading={isPending || isUpdating}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle size={20} color='white' />
|
||||||
|
<div>
|
||||||
|
{isEditMode ? 'آپدیت' : t('setting.create')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddressForm
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import Input from '@/components/Input'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Table from '@/components/Table'
|
||||||
|
import { ColumnType } from '@/components/types/TableTypes'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Edit2, Trash } from 'iconsax-react'
|
||||||
|
import { AddressData } from '../types/Types'
|
||||||
|
import { useGetAddress } from '../hooks/useAddressData'
|
||||||
|
import ProgressBarEmail from './ProgressBarEmail'
|
||||||
|
import ToggleStatus from './ToggleStatus'
|
||||||
|
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
|
||||||
|
|
||||||
|
interface AddressTableProps {
|
||||||
|
search: string
|
||||||
|
status: string
|
||||||
|
currentPage: number
|
||||||
|
onSearchChange: (search: string) => void
|
||||||
|
onStatusChange: (status: string) => void
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
onEdit: (address: AddressData) => void
|
||||||
|
onDelete: (address: AddressData) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressTable: FC<AddressTableProps> = ({
|
||||||
|
search,
|
||||||
|
status,
|
||||||
|
currentPage,
|
||||||
|
onSearchChange,
|
||||||
|
onStatusChange,
|
||||||
|
onPageChange,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
|
||||||
|
|
||||||
|
const getRowActions = (address: AddressData): RowActionItem[] => [
|
||||||
|
{
|
||||||
|
label: 'ویرایش',
|
||||||
|
icon: <Edit2 color='black' size={16} />,
|
||||||
|
onClick: () => onEdit(address),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'حذف',
|
||||||
|
icon: <Trash color='red' size={16} />,
|
||||||
|
onClick: () => onDelete(address),
|
||||||
|
className: 'text-red-600 hover:bg-red-50'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns: ColumnType<AddressData>[] = [
|
||||||
|
{
|
||||||
|
title: t('setting.address_title'),
|
||||||
|
key: 'title',
|
||||||
|
width: '200px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('setting.email'),
|
||||||
|
key: 'email',
|
||||||
|
width: '300px',
|
||||||
|
render: (item: AddressData) => (
|
||||||
|
<div key={item.id} className='dltr flex justify-end'>
|
||||||
|
{item.emailAddress}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('setting.quota'),
|
||||||
|
key: 'quota',
|
||||||
|
width: '300px',
|
||||||
|
render: (item: AddressData) => {
|
||||||
|
return (
|
||||||
|
<ProgressBarEmail key={item.id} item={item} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('setting.status'),
|
||||||
|
key: 'status',
|
||||||
|
width: '120px',
|
||||||
|
align: 'center',
|
||||||
|
render: (item: AddressData) => (
|
||||||
|
<div key={item.id} className='dltr flex justify-end'>
|
||||||
|
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'actions',
|
||||||
|
width: '120px',
|
||||||
|
align: 'center',
|
||||||
|
render: (item: AddressData) => (
|
||||||
|
<RowActionsDropdown
|
||||||
|
key={item.id}
|
||||||
|
actions={getRowActions(item)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'all', label: 'همه' },
|
||||||
|
{ value: 'active', label: t('setting.active') },
|
||||||
|
{ value: 'inactive', label: t('setting.inactive') }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex gap-2 justify-between'>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
placeholder={t('setting.status')}
|
||||||
|
items={statusOptions}
|
||||||
|
className='w-[120px]'
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => onStatusChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Input
|
||||||
|
variant='search'
|
||||||
|
placeholder={t('setting.search')}
|
||||||
|
className='xl:w-[300px] flex-1'
|
||||||
|
value={search}
|
||||||
|
onChangeSearchFinal={onSearchChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
isLoading={isPendingAddress}
|
||||||
|
columns={columns}
|
||||||
|
data={addresses?.data?.users}
|
||||||
|
pagination={{
|
||||||
|
currentPage: currentPage,
|
||||||
|
totalPages: addresses?.data?.pager?.totalPages || 1,
|
||||||
|
onPageChange: onPageChange
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddressTable
|
||||||
@@ -4,6 +4,7 @@ export type CreateAddressType = {
|
|||||||
domainId: string;
|
domainId: string;
|
||||||
title: string;
|
title: string;
|
||||||
quota?: number;
|
quota?: number;
|
||||||
|
templateId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateAddressType = {
|
export type UpdateAddressType = {
|
||||||
@@ -64,6 +65,7 @@ export interface AddressData extends Record<string, unknown> {
|
|||||||
dmarcPolicy: string | null;
|
dmarcPolicy: string | null;
|
||||||
business: string;
|
business: string;
|
||||||
};
|
};
|
||||||
|
template: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddressResponse {
|
export interface AddressResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user