create support plan
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
VITE_TOKEN_NAME = 'admin_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||
VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://192.168.1.117:4000'
|
||||
# VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||
VITE_BASE_URL = 'http://192.168.1.117:4000'
|
||||
@@ -106,4 +106,8 @@ export const Pages = {
|
||||
payment: {
|
||||
list: "/payments/list",
|
||||
},
|
||||
support: {
|
||||
create: "/support/create",
|
||||
list: "/support/list",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -763,5 +763,20 @@
|
||||
"accept_confrim": "آیا از تایید این پرداخت مطمئن هستید؟",
|
||||
"reject_comment_error": "لطفا دلیل رد پرداخت را وارد کنید",
|
||||
"receip_image": "تصویر فیش واریزی"
|
||||
},
|
||||
"support": {
|
||||
"support_plan": "پلن پشتیبانی",
|
||||
"support_plan_list": "لیست پلن پشتیبانی",
|
||||
"support_plan_create": "ایجاد پلن پشتیبانی",
|
||||
"support_plan_edit": "ویرایش پلن پشتیبانی",
|
||||
"learning_docs": "راهنما و مستندات آموزشی",
|
||||
"ticket_support": "ارسال تیکت",
|
||||
"response_time": "زمان پاسخگویی به تیکت",
|
||||
"phone_support": "پاسخگویی تلفنی",
|
||||
"backup_version": "نسخه بکاپ",
|
||||
"technical_expert_access": "دسترسی به متخصصین فنی",
|
||||
"on_site_support": "پشتیبانی در محل",
|
||||
"on_site_training": "آموزش در محل",
|
||||
"ticket_limit": "ظرفیت ارسال تیکت"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { FC, useState } 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 { useCreateSupport } from './hooks/useSupportData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
interface PlanFeature {
|
||||
featureKey: string;
|
||||
featureValue: string | boolean;
|
||||
featureType: 'boolean' | 'text';
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
const CreatePlan: FC = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('global')
|
||||
const createSupport = useCreateSupport()
|
||||
const [features, setFeatures] = useState<PlanFeature[]>(
|
||||
Object.values(SupportPlanFeatureKey).map(key => ({
|
||||
featureKey: key,
|
||||
featureValue: true,
|
||||
featureType: 'boolean',
|
||||
isEnabled: true
|
||||
}))
|
||||
)
|
||||
|
||||
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: 'string',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formik = useFormik<CreatePlanType>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
duration: 0,
|
||||
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 "string" | "boolean"
|
||||
}))
|
||||
}
|
||||
createSupport.mutate(planData, {
|
||||
onSuccess: () => {
|
||||
toast.success('پلن با موفقیت ساخته شد')
|
||||
navigate(Pages.support.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
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 || !formik.dirty}
|
||||
className='w-fit px-7'
|
||||
isLoading={createSupport.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePlan
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
const SupportList: FC = () => {
|
||||
return (
|
||||
<div>List</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SupportList
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChangeEvent, FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import CheckBoxComponent from '../../../components/CheckBoxComponent'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
label: string
|
||||
value: string | boolean
|
||||
checked: boolean
|
||||
featureType: 'boolean' | 'text'
|
||||
onChange: (id: string, value: string | boolean, checked: boolean) => void
|
||||
}
|
||||
|
||||
const PlanItem: FC<Props> = ({ id, label, value, checked, featureType, onChange }) => {
|
||||
const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newChecked = e.target.checked
|
||||
onChange(id, value, newChecked)
|
||||
}
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.value, checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id} className='mt-4 text-sm flex gap-4 items-center'>
|
||||
<div className='flex-1 whitespace-nowrap'>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{featureType === 'text' && (
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
placeholder='مقدار'
|
||||
value={value as string}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{featureType === 'boolean' && (
|
||||
<CheckBoxComponent
|
||||
checked={checked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanItem
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum SupportPlanFeatureKey {
|
||||
LEARNING_DOCS = "learning_docs", // راهنما و مستندات آموزشی
|
||||
TICKET_SUPPORT = "ticket_support", // ارسال تیکت
|
||||
RESPONSE_TIME = "response_time", // زمان پاسخگویی به تیکت
|
||||
PHONE_SUPPORT = "phone_support", // پاسخگویی تلفنی
|
||||
BACKUP_VERSION = "backup_version", // نسخه بکاپ
|
||||
TECHNICAL_EXPERT_ACCESS = "technical_expert_access", // دسترسی به متخصصین فنی
|
||||
ON_SITE_SUPPORT = "on_site_support", // پشتیبانی در محل
|
||||
ON_SITE_TRAINING = "on_site_training", // آموزش در محل
|
||||
TICKET_LIMIT = "ticket_limit", // ظرفیت ارسال تیکت
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/SupportService";
|
||||
import { CreatePlanType } from "../types/SupportTypes";
|
||||
|
||||
export const useCreateSupport = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreatePlanType) => api.createPlan(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPlans = () => {
|
||||
return useQuery({
|
||||
queryKey: ["plans"],
|
||||
queryFn: () => api.getPlans(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPlanById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["plan", id],
|
||||
queryFn: () => api.getPlanById(id),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreatePlanType } from "../types/SupportTypes";
|
||||
|
||||
export const createPlan = async (plan: CreatePlanType) => {
|
||||
const { data } = await axios.post("/support-plans", plan);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPlans = async () => {
|
||||
const { data } = await axios.get("/support-plans/list");
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPlanById = async (id: string) => {
|
||||
const { data } = await axios.get(`/support-plans/${id}`);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export type CreatePlanType = {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
duration: number;
|
||||
isActive: boolean;
|
||||
features: {
|
||||
featureKey: string;
|
||||
featureValue: string | boolean;
|
||||
featureType: "boolean" | "string";
|
||||
}[];
|
||||
};
|
||||
@@ -65,6 +65,8 @@ import CreateSlider from '../pages/slider/Create'
|
||||
import SliderList from '../pages/slider/List'
|
||||
import UpdateSlider from '../pages/slider/Update'
|
||||
import Comments from '../pages/blog/Comments'
|
||||
import CreatePlan from '../pages/support/Create'
|
||||
import SupportList from '../pages/support/List'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -141,6 +143,8 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.sliders.list} element={<SliderList />} />
|
||||
<Route path={Pages.sliders.detail + ':id'} element={<UpdateSlider />} />
|
||||
<Route path={Pages.blog.comments} element={<Comments />} />
|
||||
<Route path={Pages.support.create} element={<CreatePlan />} />
|
||||
<Route path={Pages.support.list} element={<SupportList />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-1
@@ -2,7 +2,7 @@ import { FC, useEffect } from 'react'
|
||||
import LogoImage from '../assets/images/logo.svg'
|
||||
import LogoSmall from '../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Home2, Logout, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
|
||||
import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
@@ -93,6 +93,15 @@ const SideBar: FC = () => {
|
||||
name='services'
|
||||
activeName='services'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Headphone variant={isActive('support') ? 'Bold' : 'Outline'} color={isActive('support') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.support')}
|
||||
isActive={isActive('support')}
|
||||
link={Pages.support.list}
|
||||
activeName='support_plan'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<People variant={isActive('customers') ? 'Bold' : 'Outline'} color={isActive('customers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.customers')}
|
||||
@@ -165,6 +174,7 @@ const SideBar: FC = () => {
|
||||
link={Pages.sliders.list}
|
||||
activeName='blogs'
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user