crud guide service

This commit is contained in:
hamid zarghami
2025-08-19 15:44:52 +03:30
parent e5566be14d
commit dc7b6f0a85
10 changed files with 532 additions and 2 deletions
+145
View File
@@ -0,0 +1,145 @@
import { FC, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import Button from '../../components/Button'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import Switch from '../../components/Switch'
import { TickCircle } from 'iconsax-react'
import { useCreateGuide } from './hooks/useGuideData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { Pages } from '../../config/Pages'
const Create: FC = () => {
const { t } = useTranslation('global')
const { id } = useParams()
const navigate = useNavigate()
const [isActive, setIsActive] = useState<boolean>(true)
const createGuide = useCreateGuide()
type GuideFormValues = {
title: string;
description: string;
content: string;
order: number | string;
}
const formik = useFormik<GuideFormValues>({
initialValues: {
title: '',
description: '',
content: '',
order: 1,
},
validationSchema: Yup.object({
title: Yup.string().required(t('errors.required')),
description: Yup.string().required(t('errors.required')),
content: Yup.string().required(t('errors.required')),
order: Yup.number().required(t('errors.required')),
}),
onSubmit: (values) => {
if (!id) return
createGuide.mutate({
...values,
order: Number(values.order),
isActive,
serviceId: id || '',
}, {
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.services.guide + id)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
}
})
}
})
return (
<div className='w-full mt-4'>
<div className='flex w-full justify-between items-center'>
<div>
{t('guide.submit_guide')}
</div>
<div>
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={createGuide.isPending}
>
<div className='flex gap-2'>
<TickCircle
className='size-5'
color='#fff'
/>
<div>{t('submit')}</div>
</div>
</Button>
</div>
</div>
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
<div className='xl:col-span-2'>
<div className='mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<Input
label={t('title')}
name='title'
value={formik.values.title}
onChange={formik.handleChange}
error_text={formik.touched.title && formik.errors.title ? String(formik.errors.title) : ''}
/>
<div className='mt-5'>
<Textarea
label={t('description')}
name='description'
value={formik.values.description}
onChange={formik.handleChange}
error_text={formik.touched.description && formik.errors.description ? String(formik.errors.description) : ''}
/>
</div>
<div className='mt-5'>
<Textarea
label={t('guide.content')}
name='content'
value={formik.values.content}
onChange={formik.handleChange}
error_text={formik.touched.content && formik.errors.content ? String(formik.errors.content) : ''}
/>
</div>
</div>
</div>
<div className='bg-white mt-10 w-full text-xs h-fit px-5 py-7 rounded-3xl'>
<div>
<Switch
active={isActive}
onChange={(v) => setIsActive(v)}
label={t('status')}
/>
</div>
<div className='mt-5'>
<Input
label={t('guide.order')}
name='order'
type='number'
value={formik.values.order}
onChange={formik.handleChange}
error_text={formik.touched.order && formik.errors.order ? String(formik.errors.order) : ''}
/>
</div>
</div>
</div>
</div>
)
}
export default Create
+109
View File
@@ -0,0 +1,109 @@
import { FC } from 'react'
import { useParams } from 'react-router-dom'
import { useGetGuides } from './hooks/useGuideData'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import { Add, Eye } from 'iconsax-react'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import PageLoading from '../../components/PageLoading'
import Td from '../../components/Td'
import StatusWithText from '../../components/StatusWithText'
import TrashWithConfrim from '../../components/TrashWithConfrim'
import { useDeleteGuide } from './hooks/useGuideData'
type GuideItem = {
id: string;
title: string;
description: string;
content: string;
order: number;
isActive: boolean;
}
const List: FC = () => {
const { t } = useTranslation('global')
const { id } = useParams()
const getGuides = useGetGuides(id || '')
const deleteGuide = useDeleteGuide(id || '')
const guides: GuideItem[] = getGuides.data?.data?.guides ?? []
return (
<div className='mt-4'>
<div className='flex w-full justify-between items-center'>
<div>
{t('guide.list_guide')}
</div>
<div>
<Link to={Pages.services.guide + id + '/create'}>
<Button
className='px-5'
>
<div className='flex gap-2'>
<Add
className='size-5'
color='#fff'
/>
<div>{t('guide.submit_guide')}</div>
</div>
</Button>
</Link>
</div>
</div>
{
getGuides.isPending ?
<PageLoading />
:
<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('number')} />
<Td text={t('title')} />
<Td text={t('description')} />
<Td text={t('guide.order')} />
<Td text={t('status')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
guides.map((item: GuideItem, index: number) => (
<tr key={item.id} className='tr'>
<Td text={String(index + 1)} />
<Td text={item.title} />
<Td text={item.description} />
<Td text={String(item.order)} />
<Td text=''>
<StatusWithText
variant={item.isActive ? 'success' : 'error'}
text={item.isActive ? t('active') : t('inactive')}
/>
</Td>
<Td text=''>
<div className='flex items-center gap-2'>
<Link to={Pages.services.guide + id + '/' + item.id}>
<Eye size={20} color='#8C90A3' />
</Link>
<TrashWithConfrim
onDelete={() => deleteGuide.mutate(item.id)}
isLoading={deleteGuide.isPending}
/>
</div>
</Td>
</tr>
))
}
</tbody>
</table>
</div>
}
</div>
)
}
export default List
+162
View File
@@ -0,0 +1,162 @@
import { FC, useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import Button from '../../components/Button'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import Switch from '../../components/Switch'
import { TickCircle } from 'iconsax-react'
import { useGetGuideDetail, useUpdateGuide } from './hooks/useGuideData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { Pages } from '../../config/Pages'
type GuideFormValues = {
title: string;
description: string;
content: string;
order: number | string;
}
const Update: FC = () => {
const { t } = useTranslation('global')
const { id, guideId } = useParams()
const navigate = useNavigate()
const [isActive, setIsActive] = useState<boolean>(true)
const getDetail = useGetGuideDetail(guideId || '')
const updateGuide = useUpdateGuide(id || '')
const formik = useFormik<GuideFormValues>({
initialValues: {
title: '',
description: '',
content: '',
order: 1,
},
validationSchema: Yup.object({
title: Yup.string().required(t('errors.required')),
description: Yup.string().required(t('errors.required')),
content: Yup.string().required(t('errors.required')),
order: Yup.number().required(t('errors.required')),
}),
onSubmit: (values) => {
if (!guideId) return
updateGuide.mutate({
id: guideId,
params: {
...values,
order: Number(values.order),
isActive,
}
}, {
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.services.guide + id)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
}
})
}
})
useEffect(() => {
if (getDetail.data?.data?.guide) {
const g = getDetail.data?.data?.guide
formik.setValues({
title: g.title,
description: g.description,
content: g.content,
order: g.order,
})
setIsActive(Boolean(g.isActive))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDetail.data])
return (
<div className='w-full mt-4'>
<div className='flex w-full justify-between items-center'>
<div>
{t('edit')}
</div>
<div>
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={updateGuide.isPending}
>
<div className='flex gap-2'>
<TickCircle
className='size-5'
color='#fff'
/>
<div>{t('submit')}</div>
</div>
</Button>
</div>
</div>
<div className='grid grid-cols-1 xl:grid-cols-3 gap-6'>
<div className='xl:col-span-2'>
<div className='mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<Input
label={t('title')}
name='title'
value={formik.values.title}
onChange={formik.handleChange}
error_text={formik.touched.title && formik.errors.title ? String(formik.errors.title) : ''}
/>
<div className='mt-5'>
<Textarea
label={t('description')}
name='description'
value={formik.values.description}
onChange={formik.handleChange}
error_text={formik.touched.description && formik.errors.description ? String(formik.errors.description) : ''}
/>
</div>
<div className='mt-5'>
<Textarea
label={t('guide.content')}
name='content'
value={formik.values.content}
onChange={formik.handleChange}
error_text={formik.touched.content && formik.errors.content ? String(formik.errors.content) : ''}
/>
</div>
</div>
</div>
<div className='bg-white mt-10 w-full text-xs h-fit px-5 py-7 rounded-3xl'>
<div>
<Switch
active={isActive}
onChange={(v) => setIsActive(v)}
label={t('status')}
/>
</div>
<div className='mt-5'>
<Input
label={t('guide.order')}
name='order'
type='number'
value={formik.values.order}
onChange={formik.handleChange}
error_text={formik.touched.order && formik.errors.order ? String(formik.errors.order) : ''}
/>
</div>
</div>
</div>
</div>
)
}
export default Update
+53
View File
@@ -0,0 +1,53 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createGuide,
deleteGuide,
getGuideDetail,
getGuides,
updateGuide,
UpdateGuideType,
} from "../service/GuuideService";
import { CreateGuideType } from "../types/Types";
export const useGetGuides = (id: string) => {
return useQuery({
queryKey: ["guides", id],
queryFn: () => getGuides(id),
enabled: !!id,
});
};
export const useCreateGuide = () => {
return useMutation({
mutationFn: (variables: CreateGuideType) => createGuide(variables),
});
};
export const useDeleteGuide = (serviceId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (guideId: string) => deleteGuide(guideId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["guides", serviceId] });
},
});
};
export const useGetGuideDetail = (id: string) => {
return useQuery({
queryKey: ["guide-detail", id],
queryFn: () => getGuideDetail(id),
enabled: !!id,
});
};
export const useUpdateGuide = (serviceId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, params }: { id: string; params: UpdateGuideType }) =>
updateGuide(id, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["guides", serviceId] });
},
});
};
+35
View File
@@ -0,0 +1,35 @@
import axios from "../../../config/axios";
import { CreateGuideType } from "../types/Types";
export const getGuides = async (id: string) => {
const { data } = await axios.get(`/danak-services/${id}/guides`);
return data;
};
export const createGuide = async (params: CreateGuideType) => {
const { data } = await axios.post(`/danak-services/guides`, params);
return data;
};
export const deleteGuide = async (id: string) => {
const { data } = await axios.delete(`/danak-services/guides/${id}`);
return data;
};
export const getGuideDetail = async (id: string) => {
const { data } = await axios.get(`/danak-services/guides/${id}`);
return data;
};
export type UpdateGuideType = {
title: string;
description: string;
content: string;
order: number;
isActive: boolean;
};
export const updateGuide = async (id: string, params: UpdateGuideType) => {
const { data } = await axios.patch(`/danak-services/guides/${id}`, params);
return data;
};
+8
View File
@@ -0,0 +1,8 @@
export type CreateGuideType = {
title: string;
description: string;
content: string;
order: number;
isActive: boolean;
serviceId: string;
};
+5 -1
View File
@@ -168,7 +168,11 @@ const ListService: FC = () => {
<Eye size={20} color='#8C90A3' />
</Link>
</Td>
<Td text={''} />
<Td text={''}>
<Link to={Pages.services.guide + item.id}>
<Button label='راهنمای سرویس' className='px-4' />
</Link>
</Td>
</tr>
)
})