This commit is contained in:
hamid zarghami
2025-07-08 08:56:45 +03:30
parent 47feb1472e
commit 9d0a1703fe
16 changed files with 382 additions and 53 deletions
+56 -2
View File
@@ -12,6 +12,15 @@ import NewMessage from './components/newMessage/NewMessage';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { IApiErrorRepsonse } from './types/error.types';
import ToastContainer from './components/Toast';
import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
import { refreshToken } from './pages/auth/service/Service';
declare global {
interface Window {
isRefreshingToken?: boolean;
}
}
i18next.init({
interpolation: { escapeValue: false },
lng: 'fa',
@@ -26,9 +35,54 @@ i18next.init({
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
if (error?.response?.statusCode === 401) {
console.log(401);
if (error?.response?.status === 401) {
try {
// Use a flag to track if refresh is in progress
if (window.isRefreshingToken) {
// Wait for the refresh to complete
await new Promise(resolve => {
const checkComplete = setInterval(() => {
if (!window.isRefreshingToken) {
clearInterval(checkComplete);
resolve(true);
}
}, 100);
});
return;
}
// Set the flag to indicate refresh is in progress
window.isRefreshingToken = true;
try {
const refreshTokenValue = await getRefreshToken();
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
if (data?.accessToken?.token) {
// Save the new token
await setToken(data?.accessToken?.token);
// If refresh token also returned, update it
if (data.refreshToken?.token) {
await setRefreshToken(data.refreshToken?.token);
}
// Retry the failed request
queryClient.invalidateQueries();
return;
} else {
throw new Error('Invalid token response');
}
} finally {
window.isRefreshingToken = false;
}
} catch (refreshError: unknown) {
console.error('Token refresh failed:', refreshError);
// Clear tokens and redirect to login
await removeToken();
await removeRefreshToken();
window.location.href = '/auth/login';
}
}
}
}),
+5
View File
@@ -59,3 +59,8 @@ export const timeAgo = (date: string | Date): string => {
return "همین الان";
};
export const convertToGB = (bytes: number): string => {
const GB = bytes / (1024 * 1024 * 1024);
return GB.toFixed(2);
};
+6 -1
View File
@@ -17,6 +17,9 @@
"header": {
"search": "جستجو"
},
"errors": {
"required": "این فیلد اجباری است"
},
"received": {
"title": "دریافتی ها",
"from_date": "از تاریخ",
@@ -116,7 +119,9 @@
"signature_description": "تصویر و توضیحات امضای خود را با دقت وارد کنید",
"upload_sign": "آپلود امضا",
"description_sign": "توضیحات امضا",
"select_section": "لطفا یک بخش انتخاب کنید"
"select_section": "لطفا یک بخش انتخاب کنید",
"title1": "عنوان",
"quota": "فضا/ استفاده شده "
},
"domain": {
"table": {
+7
View File
@@ -0,0 +1,7 @@
import axiosDanak from "@/config/axiosDanak";
import { RefreshTokenType } from "../types/Types";
export const refreshToken = async (params: RefreshTokenType) => {
const { data } = await axiosDanak.post(`/auth/refresh`, params);
return data;
};
+3
View File
@@ -0,0 +1,3 @@
export type RefreshTokenType = {
refreshToken: string;
};
+77 -46
View File
@@ -1,54 +1,47 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Table from '@/components/Table'
import { Switch } from "@/components/ui/switch"
import { ColumnType } from '@/components/types/TableTypes'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { AddressData, CreateAddressType } from './types/Types'
import { useGetDomains } from '../domain/hooks/useDomainData'
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
import { useCreateAddress, useGetAddress } from './hooks/useAddressData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
import ProgressBarEmail from './components/ProgressBarEmail'
import ToggleStatus from './components/ToggleStatus'
interface AddressData extends Record<string, unknown> {
id: number
title: string
email: string
status: boolean
}
const Address: FC = () => {
const { t } = useTranslation()
const [addresses] = useState<AddressData[]>([
{
id: 1,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 2,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 3,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 4,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
},
{
id: 5,
title: 'بازاریابی',
email: 'Marketing@example.com',
status: true
const schema = yup.object().shape({
title: yup.string().required(t('errors.required')),
username: yup.string().required(t('errors.required')),
password: yup.string().required(t('errors.required')),
domainId: yup.string().required(t('errors.required')),
})
const [search, setSearch] = useState<string>('')
const [status, setStatus] = useState<string>('all')
const { data: domain } = useGetDomains()
const { mutate: createAddress, isPending } = useCreateAddress()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status)
const { register, handleSubmit, formState: { errors },
} = useForm<CreateAddressType>({
resolver: yupResolver(schema),
defaultValues: {
domainId: domain?.data?.domain?.id || '',
}
])
})
const columns: ColumnType<AddressData>[] = [
{
@@ -59,7 +52,23 @@ const Address: FC = () => {
{
title: t('setting.email'),
key: 'email',
width: '300px'
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'),
@@ -68,7 +77,7 @@ const Address: FC = () => {
align: 'center',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
<Switch />
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
</div>
)
}
@@ -80,6 +89,17 @@ const Address: FC = () => {
{ value: 'inactive', label: t('setting.inactive') }
]
const onSubmit = (params: CreateAddressType) => {
createAddress(params, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
return (
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
<div className='flex-1'>
@@ -89,6 +109,7 @@ const Address: FC = () => {
placeholder={t('setting.status')}
items={statusOptions}
className='w-[120px]'
onChange={(e) => setStatus(e.target.value)}
/>
</div>
@@ -97,13 +118,15 @@ const Address: FC = () => {
variant='search'
placeholder={t('setting.search')}
className='xl:w-[300px] flex-1'
onChangeSearchFinal={(e) => setSearch(e)}
/>
</div>
</div>
<Table
isLoading={isPendingAddress}
columns={columns}
data={addresses}
data={addresses?.data?.users}
pagination={{
currentPage: 1,
totalPages: 6,
@@ -117,26 +140,28 @@ const Address: FC = () => {
{t('setting.add_address')}
</div>
<div className='mt-8 flex items-center justify-between'>
{/* <div className='mt-8 flex items-center justify-between'>
<div>
{t('setting.status')}
</div>
<div className='dltr pt-2'>
<Switch />
</div>
</div>
</div> */}
<div className='mt-6'>
<Input
label={t('setting.domain1')}
value={'example.com'}
value={domain?.data?.domain?.name}
readOnly
/>
</div>
<div className='mt-6'>
<Input
label={t('setting.title')}
label={t('setting.title1')}
{...register('title')}
error_text={errors.title?.message}
/>
</div>
@@ -144,9 +169,11 @@ const Address: FC = () => {
<Input
label={t('setting.username')}
className='text-left'
{...register('username')}
error_text={errors.username?.message}
/>
<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'>
@ example.com
@ {domain?.data?.domain?.name}
</div>
</div>
@@ -154,6 +181,8 @@ const Address: FC = () => {
<Input
label={t('setting.password')}
type='password'
{...register('password')}
error_text={errors.password?.message}
/>
</div>
@@ -170,6 +199,8 @@ const Address: FC = () => {
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
onClick={handleSubmit(onSubmit)}
loading={isPending}
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
@@ -0,0 +1,33 @@
import { convertToGB } from '@/config/func'
import { FC } from 'react'
import { AddressData } from '../types/Types'
const ProgressBarEmail: FC<{ item: AddressData }> = ({ item }) => {
const usedQuota = item.emailQuotaUsed || 0;
const totalQuota = item.emailQuota || 0;
const percentage = Math.min((usedQuota / totalQuota) * 100, 100);
return (
<div className='flex flex-col gap-2'>
<div className='flex justify-between text-sm'>
<span className='text-gray-600 dltr'>
{convertToGB(usedQuota)} GB / {convertToGB(totalQuota)} GB
</span>
{/* <span className='text-gray-500'>
{percentage.toFixed(1)}%
</span> */}
</div>
<div className='w-full bg-gray-200 dltr overflow-hidden rounded-full h-2'>
<div
className={`h-2 rounded-full transition-all duration-300 ${percentage > 90 ? 'bg-red-500' :
percentage > 70 ? 'bg-yellow-500' :
'bg-green-500'
}`}
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
)
}
export default ProgressBarEmail
@@ -0,0 +1,18 @@
import { Switch } from '@/components/ui/switch'
import { FC, useState } from 'react'
import { useToggleStatus } from '../hooks/useAddressData'
const ToggleStatus: FC<{ defaultStatus: boolean, id: string }> = ({ defaultStatus, id }) => {
const [isActive, setIsActive] = useState(defaultStatus)
const { mutate: toggleStatus } = useToggleStatus()
const handleToggleStatus = () => {
toggleStatus(id)
setIsActive(!isActive)
}
return (
<Switch checked={isActive} onCheckedChange={handleToggleStatus} />
)
}
export default ToggleStatus
@@ -0,0 +1,26 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { useQueryClient } from "@tanstack/react-query";
export const useGetAddress = (search: string, status: string) => {
return useQuery({
queryKey: ["address", search, status],
queryFn: () => api.getAddress(search, status),
});
};
export const useCreateAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["address"] });
},
});
};
export const useToggleStatus = () => {
return useMutation({
mutationFn: api.toggleStatus,
});
};
@@ -0,0 +1,21 @@
import axios from "@/config/axios";
import { CreateAddressType } from "../types/Types";
export const getAddress = async (search: string, status: string) => {
const query = new URLSearchParams();
if (search) query.set("q", search);
if (status && status !== "all")
query.set("isActive", status === "active" ? "1" : "0");
const { data } = await axios.get(`/users?${query.toString()}`);
return data;
};
export const createAddress = async (params: CreateAddressType) => {
const { data } = await axios.post(`/users`, params);
return data;
};
export const toggleStatus = async (id: string) => {
const { data } = await axios.patch(`/users/${id}/toggle-status`);
return data;
};
+58
View File
@@ -0,0 +1,58 @@
export type CreateAddressType = {
username: string;
password: string;
domainId: string;
title: string;
};
export interface AddressData extends Record<string, unknown> {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
title: string;
userName: string;
password: string;
emailAddress: string;
emailEnabled: boolean;
emailQuota: number;
emailQuotaUsed: number;
emailAccountId: string;
displayName: string;
isActive: boolean;
emailSignature: string | null;
role: string;
business: {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
danakSubscriptionId: string;
name: string;
slug: string;
logoUrl: string | null;
domain: string;
};
domain: {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
status: string;
isVerified: boolean;
isActive: boolean;
verifiedAt: string;
lastCheckedAt: string | null;
notes: string;
dkimEnabled: boolean;
dkimSelector: string;
dkimPrivateKey: string;
dkimPublicKey: string;
spfEnabled: boolean;
spfRecord: string | null;
dmarcEnabled: boolean;
dmarcPolicy: string | null;
business: string;
};
}
+2 -2
View File
@@ -19,11 +19,11 @@ const Signture: FC = () => {
</div>
</div>
<div className='flex-1 mt-5 xl:mt-0'>
<div className='flex-1 mt-4 xl:mt-0'>
<UploadBox
label={t('setting.upload_sign')}
/>
<div className='xl:mt-8'>
<div className='xl:mt-8 mt-5'>
<Textarea
label={t('setting.description_sign')}
/>
+1 -1
View File
@@ -1,5 +1,5 @@
export interface IBaseResponse {
statusCode: number;
status: number;
success: boolean;
}
export interface IResponse<T> extends IBaseResponse {