support plan
This commit is contained in:
@@ -109,5 +109,6 @@ export const Pages = {
|
|||||||
support: {
|
support: {
|
||||||
create: "/support/create",
|
create: "/support/create",
|
||||||
list: "/support/list",
|
list: "/support/list",
|
||||||
|
detail: "/support/detail/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-1
@@ -777,6 +777,12 @@
|
|||||||
"technical_expert_access": "دسترسی به متخصصین فنی",
|
"technical_expert_access": "دسترسی به متخصصین فنی",
|
||||||
"on_site_support": "پشتیبانی در محل",
|
"on_site_support": "پشتیبانی در محل",
|
||||||
"on_site_training": "آموزش در محل",
|
"on_site_training": "آموزش در محل",
|
||||||
"ticket_limit": "ظرفیت ارسال تیکت"
|
"ticket_limit": "ظرفیت ارسال تیکت",
|
||||||
|
"plans": "پلن ها",
|
||||||
|
"plan_name": "نام پلن",
|
||||||
|
"price": "قیمت",
|
||||||
|
"duration": "مدت",
|
||||||
|
"features": "ویژگی ها",
|
||||||
|
"actions": "عملیات"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const CreatePlan: FC = () => {
|
|||||||
return {
|
return {
|
||||||
featureKey: feature.featureKey,
|
featureKey: feature.featureKey,
|
||||||
featureValue: feature.featureValue as string,
|
featureValue: feature.featureValue as string,
|
||||||
featureType: 'string',
|
featureType: 'text',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -99,7 +99,7 @@ const CreatePlan: FC = () => {
|
|||||||
features: preparedFeatures.map(feature => ({
|
features: preparedFeatures.map(feature => ({
|
||||||
featureKey: feature.featureKey,
|
featureKey: feature.featureKey,
|
||||||
featureValue: String(feature.featureValue),
|
featureValue: String(feature.featureValue),
|
||||||
featureType: feature.featureType as "string" | "boolean"
|
featureType: feature.featureType as "text" | "boolean"
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
createSupport.mutate(planData, {
|
createSupport.mutate(planData, {
|
||||||
|
|||||||
@@ -1,8 +1,59 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { useGetPlans } from './hooks/useSupportData'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Td from '../../components/Td'
|
||||||
|
import { NumberFormat } from '../../config/func'
|
||||||
|
import { Edit } from 'iconsax-react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
import Delete from './components/Delete'
|
||||||
const SupportList: FC = () => {
|
const SupportList: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { data, refetch } = useGetPlans()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>List</div>
|
<div className='mt-4'>
|
||||||
|
<div className='flex justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('support.plans')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
|
<table className='w-full text-sm '>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={t('support.plan_name')} />
|
||||||
|
<Td text={t('support.price')} />
|
||||||
|
<Td text={t('support.duration')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
data?.data?.supportPlans?.map((item: { id: string, name: string, price: number, duration: string }) => {
|
||||||
|
return (
|
||||||
|
<tr className='tr' key={item.id}>
|
||||||
|
<Td text={item.name} />
|
||||||
|
<Td text={NumberFormat(item.price)} />
|
||||||
|
<Td text={item.duration} />
|
||||||
|
<Td text={''}>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Link to={Pages.support.detail + item.id}>
|
||||||
|
<Edit size={20} color='#8C90A3' />
|
||||||
|
</Link>
|
||||||
|
<Delete id={item.id} refetch={refetch} />
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { FC, useState, useEffect } from 'react'
|
||||||
|
import Input from '../../components/Input'
|
||||||
|
import SwitchComponent from '../../components/Switch'
|
||||||
|
import Textarea from '../../components/Textarea'
|
||||||
|
import Button from '../../components/Button'
|
||||||
|
import { SupportPlanFeatureKey } from './enum/SupportEnum'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import PlanItem from './components/PlanItem'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { CreatePlanType } from './types/SupportTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import Select from '../../components/Select'
|
||||||
|
import { useGetPlanById, useUpdateSupport } from './hooks/useSupportData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
|
||||||
|
interface PlanFeature {
|
||||||
|
featureKey: string;
|
||||||
|
featureValue: string | boolean;
|
||||||
|
featureType: 'boolean' | 'text';
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdatePlan: FC = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { data: planData } = useGetPlanById(id ?? '')
|
||||||
|
const updateSupport = useUpdateSupport()
|
||||||
|
|
||||||
|
const [features, setFeatures] = useState<PlanFeature[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (planData?.data) {
|
||||||
|
const plan = planData.data?.supportPlan
|
||||||
|
const mappedFeatures = Object.values(SupportPlanFeatureKey).map(key => {
|
||||||
|
const existingFeature = plan.features.find((f: { featureKey: string }) => f.featureKey === key)
|
||||||
|
return {
|
||||||
|
featureKey: key,
|
||||||
|
featureValue: existingFeature?.featureValue ?? true,
|
||||||
|
featureType: existingFeature?.featureType ?? 'boolean',
|
||||||
|
isEnabled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setFeatures(mappedFeatures)
|
||||||
|
}
|
||||||
|
}, [planData])
|
||||||
|
|
||||||
|
const handleFeatureChange = (id: string, value: string | boolean, isEnabled: boolean) => {
|
||||||
|
setFeatures(prevFeatures =>
|
||||||
|
prevFeatures.map(feature =>
|
||||||
|
feature.featureKey === id ? { ...feature, featureValue: value, isEnabled } : feature
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFeatureTypeChange = (id: string, type: 'boolean' | 'text') => {
|
||||||
|
setFeatures(prevFeatures =>
|
||||||
|
prevFeatures.map(feature =>
|
||||||
|
feature.featureKey === id ? {
|
||||||
|
...feature,
|
||||||
|
featureType: type,
|
||||||
|
featureValue: type === 'boolean' ? true : '',
|
||||||
|
isEnabled: type === 'boolean' ? true : false
|
||||||
|
} : feature
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareFeaturesForSubmission = (features: PlanFeature[]) => {
|
||||||
|
return features.map(feature => {
|
||||||
|
if (feature.featureType === 'boolean') {
|
||||||
|
return {
|
||||||
|
featureKey: feature.featureKey,
|
||||||
|
featureValue: feature.isEnabled,
|
||||||
|
featureType: 'boolean',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
featureKey: feature.featureKey,
|
||||||
|
featureValue: feature.featureValue as string,
|
||||||
|
featureType: 'text',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formik = useFormik<CreatePlanType>({
|
||||||
|
enableReinitialize: true,
|
||||||
|
initialValues: {
|
||||||
|
name: planData?.data?.supportPlan?.name ?? '',
|
||||||
|
description: planData?.data?.supportPlan?.description ?? '',
|
||||||
|
price: planData?.data?.supportPlan?.price ?? 0,
|
||||||
|
duration: planData?.data?.supportPlan?.duration ?? 0,
|
||||||
|
isActive: planData?.data?.supportPlan?.isActive ?? true,
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
name: Yup.string().required('نام پلن الزامی است'),
|
||||||
|
description: Yup.string().required('توضیحات پلن الزامی است'),
|
||||||
|
price: Yup.number().required('قیمت پلن الزامی است'),
|
||||||
|
duration: Yup.number().required('مدت پلن الزامی است'),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
values.price = Number(values.price)
|
||||||
|
values.duration = Number(values.duration)
|
||||||
|
const preparedFeatures = prepareFeaturesForSubmission(features)
|
||||||
|
const planData: CreatePlanType = {
|
||||||
|
...values,
|
||||||
|
features: preparedFeatures.map(feature => ({
|
||||||
|
featureKey: feature.featureKey,
|
||||||
|
featureValue: String(feature.featureValue),
|
||||||
|
featureType: feature.featureType as "text" | "boolean"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
updateSupport.mutate({ id: id ?? '', data: planData }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('پلن با موفقیت ویرایش شد')
|
||||||
|
navigate(Pages.support.list)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!planData?.data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(formik.errors);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='text-lg font-semibold mb-6'>ویرایش پلن</div>
|
||||||
|
|
||||||
|
<div className='bg-white rounded-lg p-6 shadow-sm mb-6'>
|
||||||
|
<div className='text-base font-medium mb-4'>اطلاعات اصلی پلن</div>
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label='نام پلن'
|
||||||
|
placeholder='نام پلن'
|
||||||
|
{...formik.getFieldProps('name')}
|
||||||
|
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label='قیمت پلن'
|
||||||
|
placeholder='قیمت پلن'
|
||||||
|
{...formik.getFieldProps('price')}
|
||||||
|
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='مدت پلن'
|
||||||
|
placeholder='مدت پلن'
|
||||||
|
{...formik.getFieldProps('duration')}
|
||||||
|
error_text={formik.touched.duration && formik.errors.duration ? formik.errors.duration : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<Textarea
|
||||||
|
label='توضیحات پلن'
|
||||||
|
placeholder='توضیحات پلن'
|
||||||
|
{...formik.getFieldProps('description')}
|
||||||
|
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput'>
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.isActive}
|
||||||
|
onChange={(active: boolean) => formik.setFieldValue('isActive', active)}
|
||||||
|
label='فعال'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-white rounded-lg p-6 shadow-sm'>
|
||||||
|
<div className='text-base font-medium mb-4'>ویژگیهای پلن</div>
|
||||||
|
<div className='space-y-8'>
|
||||||
|
{features.map((feature) => (
|
||||||
|
<div key={feature.featureKey} className='border-b border-gray-100 pb-6 last:border-0'>
|
||||||
|
<div className='flex items-center gap-4 mb-4'>
|
||||||
|
<div className='w-48'>
|
||||||
|
<Select
|
||||||
|
label='نوع مقدار'
|
||||||
|
items={[
|
||||||
|
{ label: 'بله/خیر', value: 'boolean' },
|
||||||
|
{ label: 'مقدار', value: 'text' }
|
||||||
|
]}
|
||||||
|
value={feature.featureType}
|
||||||
|
onChange={(e) => handleFeatureTypeChange(feature.featureKey, e.target.value as 'boolean' | 'text')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlanItem
|
||||||
|
id={feature.featureKey}
|
||||||
|
label={t(`support.${feature.featureKey}`)}
|
||||||
|
value={feature.featureValue}
|
||||||
|
checked={feature.isEnabled}
|
||||||
|
featureType={feature.featureType}
|
||||||
|
onChange={handleFeatureChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
label='ویرایش پلن'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
disabled={!formik.isValid}
|
||||||
|
className='w-fit px-7'
|
||||||
|
isLoading={updateSupport.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdatePlan
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Trash } from 'iconsax-react'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useDeletePlan } from '../hooks/useSupportData'
|
||||||
|
import ModalConfrim from '../../../components/ModalConfrim'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string,
|
||||||
|
refetch: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Delete: FC<Props> = ({ id, refetch }) => {
|
||||||
|
const { mutate: deletePlan, isPending } = useDeletePlan()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const close = () => setIsOpen(false)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Trash size={20} color='#8C90A3' onClick={() => setIsOpen(true)} />
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isOpen}
|
||||||
|
close={close}
|
||||||
|
onConfrim={() => {
|
||||||
|
deletePlan(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
isLoading={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Delete
|
||||||
@@ -8,6 +8,13 @@ export const useCreateSupport = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateSupport = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: CreatePlanType }) =>
|
||||||
|
api.updatePlan(id, data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetPlans = () => {
|
export const useGetPlans = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["plans"],
|
queryKey: ["plans"],
|
||||||
@@ -21,3 +28,9 @@ export const useGetPlanById = (id: string) => {
|
|||||||
queryFn: () => api.getPlanById(id),
|
queryFn: () => api.getPlanById(id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeletePlan = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.deletePlan(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export const createPlan = async (plan: CreatePlanType) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updatePlan = async (id: string, plan: CreatePlanType) => {
|
||||||
|
const { data } = await axios.patch(`/support-plans/${id}`, plan);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPlans = async () => {
|
export const getPlans = async () => {
|
||||||
const { data } = await axios.get("/support-plans/list");
|
const { data } = await axios.get("/support-plans/list");
|
||||||
return data;
|
return data;
|
||||||
@@ -15,3 +20,8 @@ export const getPlanById = async (id: string) => {
|
|||||||
const { data } = await axios.get(`/support-plans/${id}`);
|
const { data } = await axios.get(`/support-plans/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deletePlan = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/support-plans/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ export type CreatePlanType = {
|
|||||||
features: {
|
features: {
|
||||||
featureKey: string;
|
featureKey: string;
|
||||||
featureValue: string | boolean;
|
featureValue: string | boolean;
|
||||||
featureType: "boolean" | "string";
|
featureType: "boolean" | "text";
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import UpdateSlider from '../pages/slider/Update'
|
|||||||
import Comments from '../pages/blog/Comments'
|
import Comments from '../pages/blog/Comments'
|
||||||
import CreatePlan from '../pages/support/Create'
|
import CreatePlan from '../pages/support/Create'
|
||||||
import SupportList from '../pages/support/List'
|
import SupportList from '../pages/support/List'
|
||||||
|
import UpdateSupport from '../pages/support/Update'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -145,6 +146,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.blog.comments} element={<Comments />} />
|
<Route path={Pages.blog.comments} element={<Comments />} />
|
||||||
<Route path={Pages.support.create} element={<CreatePlan />} />
|
<Route path={Pages.support.create} element={<CreatePlan />} />
|
||||||
<Route path={Pages.support.list} element={<SupportList />} />
|
<Route path={Pages.support.list} element={<SupportList />} />
|
||||||
|
<Route path={Pages.support.detail + ':id'} element={<UpdateSupport />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user