invoice + plan
This commit is contained in:
@@ -34,7 +34,7 @@ const Select: FC<Props> = (props: Props) => {
|
|||||||
<option value="" disabled selected>{props.placeholder}</option>
|
<option value="" disabled selected>{props.placeholder}</option>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
props.items.map((item) => {
|
props.items?.map((item) => {
|
||||||
return (
|
return (
|
||||||
<option key={item.value} value={item.value}>
|
<option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const Pages = {
|
|||||||
add: "/services/add",
|
add: "/services/add",
|
||||||
list: "/services/list",
|
list: "/services/list",
|
||||||
category: "/services/category",
|
category: "/services/category",
|
||||||
|
plan: "/services/plan/",
|
||||||
},
|
},
|
||||||
transactions: "/transactions",
|
transactions: "/transactions",
|
||||||
receipts: {
|
receipts: {
|
||||||
|
|||||||
+27
-2
@@ -163,7 +163,22 @@
|
|||||||
"deactive": "غیرفعال",
|
"deactive": "غیرفعال",
|
||||||
"software_langauge": "زبان نرم افزار",
|
"software_langauge": "زبان نرم افزار",
|
||||||
"link": "لینک",
|
"link": "لینک",
|
||||||
"danak_sujescet": "پیشنهاد داناک"
|
"danak_sujescet": "پیشنهاد داناک",
|
||||||
|
"plans": "پلن ها",
|
||||||
|
"add_plan": "افزودن پلن",
|
||||||
|
"active_plan": "پلن فعال"
|
||||||
|
},
|
||||||
|
"plan": {
|
||||||
|
"plans_list": "لیست پلن های ",
|
||||||
|
"new_plan": "پلن جدید",
|
||||||
|
"add_plan": "افزودن پلن",
|
||||||
|
"time": "مدت زمان",
|
||||||
|
"price": "قیمت",
|
||||||
|
"cancel": "لغو",
|
||||||
|
"submit_plan": "ثبت پلن",
|
||||||
|
"error_submit": "حداقل یک پلن باید اضافه شود",
|
||||||
|
"success_submit": "پلن با موفقیت ثبت شد",
|
||||||
|
"edit_plan": "ویرایش پلن"
|
||||||
},
|
},
|
||||||
"save": "ذخیره",
|
"save": "ذخیره",
|
||||||
"search": "جستجو",
|
"search": "جستجو",
|
||||||
@@ -361,7 +376,17 @@
|
|||||||
"discount": "تخفیف(٪)",
|
"discount": "تخفیف(٪)",
|
||||||
"total_amount": "مبلغ کل(ریال)",
|
"total_amount": "مبلغ کل(ریال)",
|
||||||
"tax": "مالیات بر ارزش افزوده (۰.۹ درصد)",
|
"tax": "مالیات بر ارزش افزوده (۰.۹ درصد)",
|
||||||
"total": "جمع"
|
"total": "جمع",
|
||||||
|
"error_empty": "همه فیلد ها را پر کنید",
|
||||||
|
"date_receipt": "تاریخ صورتحساب",
|
||||||
|
"last_date_receipt": "آخرین مهلت پرداخت",
|
||||||
|
"service": "سرویس",
|
||||||
|
"status": "وضعیت تایید",
|
||||||
|
"status_paid": "وضعیت پرداخت",
|
||||||
|
"PENDING": "در انتظار",
|
||||||
|
"PAID": "پرداخت شده",
|
||||||
|
"CANCELED": "لغو شده",
|
||||||
|
"EXPIRED": "منقضی شده"
|
||||||
},
|
},
|
||||||
"edit": "ویرایش",
|
"edit": "ویرایش",
|
||||||
"transaction": {
|
"transaction": {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as api from "../service/CustomerService";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const useGetCustomers = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["invoices"],
|
||||||
|
queryFn: () => api.getCustomers(),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
|
||||||
|
export const getCustomers = async () => {
|
||||||
|
const { data } = await axios.get(`/users/customers`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export type CustomerItemType = {
|
||||||
|
birthDate: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string | null;
|
||||||
|
firstName: string;
|
||||||
|
id: string;
|
||||||
|
invoices: unknown[];
|
||||||
|
lastName: string;
|
||||||
|
nationalCode: string;
|
||||||
|
phone: string;
|
||||||
|
role: {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
// Add other properties of role if needed
|
||||||
|
};
|
||||||
|
subscriptions: unknown[];
|
||||||
|
tickets: unknown[];
|
||||||
|
updatedAt: string;
|
||||||
|
userName: string | null;
|
||||||
|
};
|
||||||
+204
-12
@@ -1,16 +1,86 @@
|
|||||||
import { FC } from 'react'
|
import { ChangeEvent, FC, Fragment, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Button from '../../components/Button'
|
import Button from '../../components/Button'
|
||||||
import { Add, TickCircle, TickSquare } from 'iconsax-react'
|
import { Add, TickCircle, TickSquare } from 'iconsax-react'
|
||||||
import Select from '../../components/Select'
|
import Select from '../../components/Select'
|
||||||
import Input from '../../components/Input'
|
import Input from '../../components/Input'
|
||||||
|
import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { useGetCustomers } from '../customer/hooks/useCustomerData'
|
||||||
|
import { CustomerItemType } from '../customer/types/CustomerTypes'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import { useCreateInvoice } from './hooks/useReceiptData'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
|
||||||
const CreateReceipt: FC = () => {
|
const CreateReceipt: FC = () => {
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
|
||||||
|
const [customer, setCustomer] = useState<CustomerItemType>()
|
||||||
|
const getCustomers = useGetCustomers()
|
||||||
|
const createInvoice = useCreateInvoice()
|
||||||
|
|
||||||
|
const formik = useFormik<ReceiptCreateItemsType>({
|
||||||
|
initialValues: {
|
||||||
|
name: '',
|
||||||
|
count: '',
|
||||||
|
unitPrice: '',
|
||||||
|
discount: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
name: Yup.string().required(t('errors.required')),
|
||||||
|
count: Yup.string().required(t('errors.required')),
|
||||||
|
unitPrice: Yup.string().required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
setItems((prev) => [...prev, {
|
||||||
|
count: +values.count,
|
||||||
|
discount: +values.discount,
|
||||||
|
name: values.name,
|
||||||
|
unitPrice: +values.unitPrice
|
||||||
|
}])
|
||||||
|
formik.resetForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const value = e.target.value
|
||||||
|
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
|
||||||
|
setCustomer(customer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (customer) {
|
||||||
|
const params: CreateReceiptType = {
|
||||||
|
userId: customer?.id,
|
||||||
|
items: items
|
||||||
|
}
|
||||||
|
|
||||||
|
createInvoice.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
navigate(Pages.receipts.index)
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error.response?.data?.error.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
|
{
|
||||||
|
getCustomers.isPending ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<Fragment>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
<div>
|
<div>
|
||||||
{t('receip.add_receip')}
|
{t('receip.add_receip')}
|
||||||
@@ -18,6 +88,7 @@ const CreateReceipt: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className='px-5'
|
className='px-5'
|
||||||
|
onClick={handleSubmit}
|
||||||
>
|
>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<TickCircle size={20} color='white' />
|
<TickCircle size={20} color='white' />
|
||||||
@@ -39,15 +110,20 @@ const CreateReceipt: FC = () => {
|
|||||||
<div className='w-full xl:w-1/2'>
|
<div className='w-full xl:w-1/2'>
|
||||||
<Select
|
<Select
|
||||||
label={t('receip.customer')}
|
label={t('receip.customer')}
|
||||||
items={[
|
placeholder={t('select')}
|
||||||
{ label: 'محمد', value: 'mohammad' },
|
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||||
{ label: 'علی', value: 'ali' },
|
return {
|
||||||
{ label: 'حسین', value: 'hossein' },
|
label: item.firstName + ' ' + item.lastName,
|
||||||
]}
|
value: item.id
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
onChange={handleChangeCustomer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
customer &&
|
||||||
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
|
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
|
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
@@ -56,7 +132,7 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='text-description'>{t('receip.representativeـname')}</div>
|
<div className='text-description'>{t('receip.representativeـname')}</div>
|
||||||
<div>مهرداد مظفری</div>
|
<div>{customer.firstName + ' ' + customer.lastName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='text-description'>{t('receip.company_name')}</div>
|
<div className='text-description'>{t('receip.company_name')}</div>
|
||||||
@@ -96,6 +172,7 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className='mt-10'>
|
||||||
<div>{t('receip.receipt_information')}</div>
|
<div>{t('receip.receipt_information')}</div>
|
||||||
@@ -107,7 +184,7 @@ const CreateReceipt: FC = () => {
|
|||||||
{t('receip.number')}
|
{t('receip.number')}
|
||||||
</div>
|
</div>
|
||||||
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
||||||
1
|
{items.length + 1}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,6 +194,9 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className='text-center'
|
||||||
|
name='name'
|
||||||
|
value={formik.values.name}
|
||||||
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,6 +207,9 @@ const CreateReceipt: FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
type='number'
|
type='number'
|
||||||
className='w-16 text-center'
|
className='w-16 text-center'
|
||||||
|
name='count'
|
||||||
|
value={formik.values.count}
|
||||||
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -136,6 +219,9 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className='text-center'
|
className='text-center'
|
||||||
|
name='unitPrice'
|
||||||
|
value={formik.values.unitPrice}
|
||||||
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,6 +232,9 @@ const CreateReceipt: FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
type='number'
|
type='number'
|
||||||
className='w-16 text-center'
|
className='w-16 text-center'
|
||||||
|
name='discount'
|
||||||
|
value={formik.values.discount}
|
||||||
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,13 +244,112 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
className='text-center bg-[#EBEDF5]'
|
className='text-center bg-[#EBEDF5]'
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
<div onClick={() => {
|
||||||
|
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
||||||
|
toast.error(t('receip.error_empty'))
|
||||||
|
} else {
|
||||||
|
formik.handleSubmit()
|
||||||
|
}
|
||||||
|
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
||||||
|
<Add size={20} color='black' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
items.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div key={item.name} className='mt-8 flex items-end justify-between text-xs text-description'>
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.number')}
|
||||||
|
</div>
|
||||||
|
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.product_name')}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className='text-center'
|
||||||
|
name='name'
|
||||||
|
readOnly
|
||||||
|
value={item.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.count')}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
className='w-16 text-center'
|
||||||
|
name='count'
|
||||||
|
readOnly
|
||||||
|
value={item.count}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.unit_amount')}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className='text-center'
|
||||||
|
name='unitPrice'
|
||||||
|
readOnly
|
||||||
|
value={item.unitPrice}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.discount')}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
className='w-16 text-center'
|
||||||
|
name='discount'
|
||||||
|
readOnly
|
||||||
|
value={item.discount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-2 items-center'>
|
||||||
|
<div>
|
||||||
|
{t('receip.total_amount')}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className='text-center bg-[#EBEDF5]'
|
||||||
|
readOnly
|
||||||
|
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={() => {
|
||||||
|
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
|
||||||
|
toast.error(t('receip.error_empty'))
|
||||||
|
} else {
|
||||||
|
formik.handleSubmit()
|
||||||
|
}
|
||||||
|
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
|
||||||
<Add size={20} color='black' />
|
<Add size={20} color='black' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='mt-8 flex flex-col gap-4 items-end'>
|
<div className='mt-8 flex flex-col gap-4 items-end'>
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
||||||
@@ -169,7 +357,7 @@ const CreateReceipt: FC = () => {
|
|||||||
{t('receip.tax')}
|
{t('receip.tax')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-black'>
|
<div className='text-black'>
|
||||||
100,000
|
{((items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0)) * 0.09).toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
|
||||||
@@ -177,14 +365,16 @@ const CreateReceipt: FC = () => {
|
|||||||
{t('receip.total')}
|
{t('receip.total')}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-black'>
|
<div className='text-black'>
|
||||||
100,000
|
{(
|
||||||
|
items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 1.09
|
||||||
|
).toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
{t('ticket.title_hint')}
|
{t('ticket.title_hint')}
|
||||||
</div>
|
</div>
|
||||||
@@ -217,6 +407,8 @@ const CreateReceipt: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-55
@@ -1,15 +1,18 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Tabs from '../../components/Tabs'
|
import Tabs from '../../components/Tabs'
|
||||||
import { Calendar, CloseCircle, Eye, FolderOpen, MoneyChange, WalletCheck, WalletRemove, WalletSearch } from 'iconsax-react'
|
import { CloseCircle, FolderOpen, WalletCheck, WalletRemove } from 'iconsax-react'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import { Link } from 'react-router-dom'
|
import { useGetInvoices } from './hooks/useReceiptData'
|
||||||
import { Pages } from '../../config/Pages'
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import { ReceiptItemType } from './types/ReceiptTypes'
|
||||||
|
import { NumberFormat } from '../../config/func'
|
||||||
|
|
||||||
const ReceiptsList: FC = () => {
|
const ReceiptsList: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [activeTab, setActiveTab] = useState<string>('unpaid')
|
const [activeTab, setActiveTab] = useState<"PENDING" | "PAID" | "CANCELED" | "EXPIRED">('PENDING')
|
||||||
|
const getInvoices = useGetInvoices(activeTab)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -22,87 +25,74 @@ const ReceiptsList: FC = () => {
|
|||||||
active={activeTab}
|
active={activeTab}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: <WalletRemove color={activeTab === 'unpaid' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <WalletRemove color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('receip.dont_pay'),
|
label: t('receip.dont_pay'),
|
||||||
value: 'unpaid'
|
value: 'PENDING'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <WalletSearch color={activeTab === 'not_confimed' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <WalletCheck color={activeTab === 'PAID' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('receip.dont_confrim'),
|
|
||||||
value: 'not_confimed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <WalletCheck color={activeTab === 'paid' ? 'black' : '#8C90A3'} size={22} />,
|
|
||||||
label: t('receip.paid'),
|
label: t('receip.paid'),
|
||||||
value: 'paid'
|
value: 'PAID'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CloseCircle color={activeTab === 'canceled' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <CloseCircle color={activeTab === 'CANCELED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('receip.canceled'),
|
label: t('receip.canceled'),
|
||||||
value: 'canceled'
|
value: 'CANCELED'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MoneyChange color={activeTab === 'returned' ? 'black' : '#8C90A3'} size={22} />,
|
icon: <FolderOpen color={activeTab === 'EXPIRED' ? 'black' : '#8C90A3'} size={22} />,
|
||||||
label: t('receip.returned'),
|
|
||||||
value: 'returned'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Calendar color={activeTab === 'time' ? 'black' : '#8C90A3'} size={22} />,
|
|
||||||
label: t('receip.its_time'),
|
|
||||||
value: 'time'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <FolderOpen color={activeTab === 'archive' ? 'black' : '#8C90A3'} size={22} />,
|
|
||||||
label: t('receip.archive'),
|
label: t('receip.archive'),
|
||||||
value: 'archive'
|
value: 'EXPIRED'
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={setActiveTab}
|
onChange={(value) => setActiveTab(value as "PENDING" | "PAID" | "CANCELED" | "EXPIRED")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
getInvoices.isPending ?
|
||||||
|
<div className='mt-5'>
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||||
<table className='w-full text-sm '>
|
<table className='w-full text-sm '>
|
||||||
<thead className='thead'>
|
<thead className='thead'>
|
||||||
<tr>
|
<tr>
|
||||||
<Td text={t('ticket.number')} />
|
<Td text={t('ticket.number')} />
|
||||||
<Td text={t('ticket.title')} />
|
<Td text={t('receip.customer')} />
|
||||||
<Td text={t('ticket.team')} />
|
<Td text={t('receip.date_receipt')} />
|
||||||
<Td text={t('ticket.date')} />
|
<Td text={t('receip.last_date_receipt')} />
|
||||||
<Td text={t('ticket.status')} />
|
<Td text={t('receip.total')} />
|
||||||
<Td text={t('ticket.priority')} />
|
<Td text={t('receip.service')} />
|
||||||
<Td text={t('ticket.detail')} />
|
<Td text={t('receip.status')} />
|
||||||
|
<Td text={t('receip.status_paid')} />
|
||||||
<Td text={''} />
|
<Td text={''} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{
|
||||||
|
getInvoices.data?.data?.invoices?.map((item: ReceiptItemType, index: number) => {
|
||||||
|
return (
|
||||||
<tr className='tr'>
|
<tr className='tr'>
|
||||||
<Td text={t('ticket.number')} />
|
<Td text={String(index + 1)} />
|
||||||
<Td text={t('ticket.title')} />
|
<Td text={item.user.firstName + ' ' + item.user.lastName} />
|
||||||
<Td text={t('ticket.team')} />
|
<Td text={item.createdAt} />
|
||||||
<Td text={t('ticket.date')} />
|
<Td text={item.dueDate} />
|
||||||
<Td text={t('ticket.status')} />
|
<Td text={NumberFormat(item.totalPrice)} />
|
||||||
<Td text={t('ticket.priority')} />
|
<Td text={t('receip.service')} />
|
||||||
<Td text={t('ticket.detail')} />
|
<Td text={t(`receip.${item.status}`)} />
|
||||||
<Td text={''}>
|
<Td text={item.paidAt ? t('receip.paid') : t('receip.not_paid')} />
|
||||||
<Link to={Pages.receipts.detail + '1'}>
|
|
||||||
<Eye size={20} color='black' />
|
|
||||||
</Link>
|
|
||||||
</Td>
|
|
||||||
</tr>
|
|
||||||
<tr className='tr'>
|
|
||||||
<Td text={t('ticket.number')} />
|
|
||||||
<Td text={t('ticket.title')} />
|
|
||||||
<Td text={t('ticket.team')} />
|
|
||||||
<Td text={t('ticket.date')} />
|
|
||||||
<Td text={t('ticket.status')} />
|
|
||||||
<Td text={t('ticket.priority')} />
|
|
||||||
<Td text={t('ticket.detail')} />
|
|
||||||
<Td text={''} />
|
<Td text={''} />
|
||||||
</tr>
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/ReceiptService";
|
||||||
|
import { CreateReceiptType } from "../types/ReceiptTypes";
|
||||||
|
|
||||||
|
export const useGetInvoices = (status: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["invoices", status],
|
||||||
|
queryFn: () => api.getInvoces(status),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateInvoice = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateReceiptType) => api.createInvoice(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import { CreateReceiptType } from "../types/ReceiptTypes";
|
||||||
|
|
||||||
|
export const getInvoces = async (status: string) => {
|
||||||
|
const { data } = await axios.get(`/invoices?status=${status}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createInvoice = async (params: CreateReceiptType) => {
|
||||||
|
const { data } = await axios.post(`/invoices`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export type CreateReceiptType = {
|
||||||
|
userId: string;
|
||||||
|
items: ReceiptCreateItemsType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReceiptCreateItemsType = {
|
||||||
|
name: string;
|
||||||
|
count: string | number;
|
||||||
|
unitPrice: string | number;
|
||||||
|
discount: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReceiptItemType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
name: string;
|
||||||
|
count: string | number;
|
||||||
|
unitPrice: string | number;
|
||||||
|
discount: string | number;
|
||||||
|
totalPrice: number;
|
||||||
|
subscriptionPlan?: string | null;
|
||||||
|
items: ReceiptItemType[];
|
||||||
|
user: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
dueDate: string;
|
||||||
|
status: "PENDING" | "PAID" | "CANCELED" | "EXPIRED";
|
||||||
|
paidAt: string;
|
||||||
|
};
|
||||||
@@ -107,6 +107,7 @@ const ListService: FC = () => {
|
|||||||
<Td text={t('service.company_developed')} />
|
<Td text={t('service.company_developed')} />
|
||||||
<Td text={t('service.category')} />
|
<Td text={t('service.category')} />
|
||||||
<Td text={t('service.publish_date')} />
|
<Td text={t('service.publish_date')} />
|
||||||
|
<Td text={t('service.plans')} />
|
||||||
<Td text={t('ticket.status')} />
|
<Td text={t('ticket.status')} />
|
||||||
<Td text={t('ticket.detail')} />
|
<Td text={t('ticket.detail')} />
|
||||||
<Td text={''} />
|
<Td text={''} />
|
||||||
@@ -126,6 +127,26 @@ const ListService: FC = () => {
|
|||||||
<Td text={item.author} />
|
<Td text={item.author} />
|
||||||
<Td text={item.category?.title} />
|
<Td text={item.category?.title} />
|
||||||
<Td text={moment(item.createDate).format('jYYYY-jMM-jDD')} />
|
<Td text={moment(item.createDate).format('jYYYY-jMM-jDD')} />
|
||||||
|
<Td text=''>
|
||||||
|
{
|
||||||
|
item.subscriptionCount === 0 ?
|
||||||
|
<Link to={Pages.services.plan + item.id}>
|
||||||
|
<Button
|
||||||
|
className='h-8 bg-transparent border border-black text-black text-xs'
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Add size={20} color='black' />
|
||||||
|
<div>{t('service.add_plan')}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
:
|
||||||
|
<Link to={Pages.services.plan + item.id} className='flex gap-1 text-sm text-[#0047FF]'>
|
||||||
|
<div>{item.subscriptionCount}</div>
|
||||||
|
<div>{t('service.active_plan')}</div>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
</Td>
|
||||||
<Td text={''}>
|
<Td text={''}>
|
||||||
<ToggleStatusService
|
<ToggleStatusService
|
||||||
id={item.id}
|
id={item.id}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { FC, Fragment } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import CreatePlan from './components/CreatePlan'
|
||||||
|
import { useGetPlans } from './hooks/useServiceData'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import Td from '../../components/Td'
|
||||||
|
import { PlanItemType } from './types/ServiceTypes'
|
||||||
|
import ToggleStatusPlan from './components/ToggleStatusPlan'
|
||||||
|
import EditPlan from './components/EditPlan'
|
||||||
|
|
||||||
|
const Plans: FC = () => {
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const getPlans = useGetPlans(id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
{
|
||||||
|
getPlans.isPending ?
|
||||||
|
<PageLoading />
|
||||||
|
:
|
||||||
|
<Fragment>
|
||||||
|
<div className='flex w-full justify-between items-center'>
|
||||||
|
<div>
|
||||||
|
{t('plan.plans_list') + ' ' + getPlans.data?.data?.service?.name}
|
||||||
|
</div>
|
||||||
|
<CreatePlan />
|
||||||
|
</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('title')} />
|
||||||
|
<Td text={t('plan.time')} />
|
||||||
|
<Td text={t('plan.price')} />
|
||||||
|
<Td text={t('status')} />
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
getPlans.data?.data?.subscriptions.map((item: PlanItemType) => (
|
||||||
|
<tr key={item.id} className='tr'>
|
||||||
|
<Td text={item.name} />
|
||||||
|
<Td text={item.duration + ''} />
|
||||||
|
<Td text={item.price + ''} />
|
||||||
|
<Td text={''}>
|
||||||
|
<ToggleStatusPlan defaultActive={item.isActive} id={item.id} />
|
||||||
|
</Td>
|
||||||
|
<Td text=''>
|
||||||
|
<EditPlan
|
||||||
|
id={item.id}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Plans
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Add, AddCircle, TickCircle, Trash } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DefaulModal from '../../../components/DefaulModal'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { CreateItemPlanType, CreatePlanType } from '../types/ServiceTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { clx } from '../../../helpers/utils'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { useCreatePlan, useGetPlans } from '../hooks/useServiceData'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const CreatePlan: FC = () => {
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false)
|
||||||
|
const [items, setItems] = useState<CreateItemPlanType[]>([])
|
||||||
|
const getPlans = useGetPlans(id)
|
||||||
|
const createPlan = useCreatePlan()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateItemPlanType>({
|
||||||
|
initialValues: {
|
||||||
|
name: '',
|
||||||
|
duration: '',
|
||||||
|
isActive: true,
|
||||||
|
price: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
name: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
duration: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
price: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
const vals = {
|
||||||
|
...values,
|
||||||
|
duration: +values.duration,
|
||||||
|
price: +values.price,
|
||||||
|
}
|
||||||
|
setItems([...items, vals])
|
||||||
|
formik.resetForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDeleteItem = (index: number) => {
|
||||||
|
const newItems = items.filter((_, i) => i !== index)
|
||||||
|
setItems(newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
toast.error(t('plan.error_submit'))
|
||||||
|
} else {
|
||||||
|
const params: CreatePlanType = {
|
||||||
|
serviceId: id ? id : '',
|
||||||
|
subs: items
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlan.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
formik.resetForm()
|
||||||
|
setShowModal(false)
|
||||||
|
toast.success(t('plan.success_submit'))
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className='px-5'
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Add
|
||||||
|
className='size-5'
|
||||||
|
color='#fff'
|
||||||
|
/>
|
||||||
|
<div>{t('plan.new_plan')}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={showModal}
|
||||||
|
close={() => setShowModal(false)}
|
||||||
|
title_header={t('plan.add_plan') + ' ' + getPlans.data?.data?.service?.name}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
{
|
||||||
|
items.map((item, index: number) => {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-between items-center gap-8 mt-4'>
|
||||||
|
<Input
|
||||||
|
className='bg-description bg-opacity-15 border-none'
|
||||||
|
value={item.name}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className='bg-description bg-opacity-15 border-none'
|
||||||
|
value={item.duration}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className='bg-description bg-opacity-15 border-none'
|
||||||
|
value={item.price}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trash
|
||||||
|
size={24}
|
||||||
|
color='#D52903'
|
||||||
|
className='min-w-6'
|
||||||
|
onClick={() => handleDeleteItem(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<div className={clx(
|
||||||
|
'flex justify-between items-start gap-8',
|
||||||
|
items.length > 0 && 'mt-8',
|
||||||
|
)}>
|
||||||
|
<Input
|
||||||
|
label={t('title')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='name'
|
||||||
|
value={formik.values.name}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('plan.time')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='duration'
|
||||||
|
value={formik.values.duration}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.duration && formik.errors.duration ? formik.errors.duration : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('plan.price')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='price'
|
||||||
|
value={formik.values.price}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div onClick={() => formik.handleSubmit()} className='size-10 min-w-10 mt-7 rounded-xl border border-black flex justify-center items-center'>
|
||||||
|
<AddCircle size={20} color='black' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12 border-t border-border pt-5 flex justify-end'>
|
||||||
|
<div className='flex gap-5 items-center'>
|
||||||
|
<Button
|
||||||
|
className='w-[150px] bg-white bg-opacity-15 text-description'
|
||||||
|
label={t('plan.cancel')}
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
size={20}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('plan.submit_plan')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePlan
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { Edit, TickCircle } from 'iconsax-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DefaulModal from '../../../components/DefaulModal'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { CreateItemPlanType, UpdatePlanType } from '../types/ServiceTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { clx } from '../../../helpers/utils'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { useGetPlanDetail, useUpdatePlan } from '../hooks/useServiceData'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditPlan: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false)
|
||||||
|
const getPlanDetail = useGetPlanDetail(showModal ? props.id : undefined)
|
||||||
|
const updatePlan = useUpdatePlan(props.id)
|
||||||
|
|
||||||
|
const formik = useFormik<CreateItemPlanType>({
|
||||||
|
initialValues: {
|
||||||
|
name: '',
|
||||||
|
duration: '',
|
||||||
|
isActive: true,
|
||||||
|
price: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
name: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
duration: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
price: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
const params: UpdatePlanType = {
|
||||||
|
serviceId: id,
|
||||||
|
duration: +values.duration,
|
||||||
|
price: +values.price,
|
||||||
|
name: values.name,
|
||||||
|
}
|
||||||
|
updatePlan.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
setShowModal(false)
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (getPlanDetail.data) {
|
||||||
|
formik.setValues({
|
||||||
|
name: getPlanDetail.data.data?.subscription.name,
|
||||||
|
duration: getPlanDetail.data.data?.subscription.duration + '',
|
||||||
|
price: getPlanDetail.data.data?.subscription.price + '',
|
||||||
|
isActive: getPlanDetail.data.data?.subscription.isActive
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [getPlanDetail.data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Edit
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
size={20}
|
||||||
|
color='#0047FF'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={showModal}
|
||||||
|
close={() => setShowModal(false)}
|
||||||
|
title_header={t('plan.edit_plan')}
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div className='mt-6'>
|
||||||
|
|
||||||
|
<div className={clx(
|
||||||
|
'flex justify-between items-start gap-8',
|
||||||
|
)}>
|
||||||
|
<Input
|
||||||
|
label={t('title')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='name'
|
||||||
|
value={formik.values.name}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('plan.time')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='duration'
|
||||||
|
value={formik.values.duration}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.duration && formik.errors.duration ? formik.errors.duration : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('plan.price')}
|
||||||
|
className='bg-white bg-opacity-30'
|
||||||
|
name='price'
|
||||||
|
value={formik.values.price}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12 border-t border-border pt-5 flex justify-end'>
|
||||||
|
<div className='flex gap-5 items-center'>
|
||||||
|
<Button
|
||||||
|
className='w-[150px] bg-white bg-opacity-15 text-description'
|
||||||
|
label={t('plan.cancel')}
|
||||||
|
onClick={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={updatePlan.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
size={20}
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('save')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPlan
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import SwitchComponent from '../../../components/Switch'
|
||||||
|
import { useToggleStatusPlan } from '../hooks/useServiceData'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultActive: boolean,
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleStatusPlan: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
|
||||||
|
const toggleStatus = useToggleStatusPlan()
|
||||||
|
|
||||||
|
const handleChange = (value: boolean) => {
|
||||||
|
setIsActive(value)
|
||||||
|
toggleStatus.mutate(props.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwitchComponent
|
||||||
|
active={isActive}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleStatusPlan
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/ServiceService";
|
import * as api from "../service/ServiceService";
|
||||||
import {
|
import {
|
||||||
|
CreatePlanType,
|
||||||
CreateServiceCategoryType,
|
CreateServiceCategoryType,
|
||||||
CreateServiceType,
|
CreateServiceType,
|
||||||
ToggleStatusCategoryType,
|
ToggleStatusCategoryType,
|
||||||
|
UpdatePlanType,
|
||||||
} from "../types/ServiceTypes";
|
} from "../types/ServiceTypes";
|
||||||
|
|
||||||
export const useGetAllServices = (
|
export const useGetAllServices = (
|
||||||
@@ -80,3 +82,37 @@ export const useToggleStatusService = () => {
|
|||||||
mutationFn: (id: string) => api.toggleStatusService(id),
|
mutationFn: (id: string) => api.toggleStatusService(id),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreatePlan = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreatePlanType) => api.createPlan(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetPlans = (serviceId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["plans", serviceId],
|
||||||
|
queryFn: () => api.getPlans(serviceId),
|
||||||
|
enabled: !!serviceId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToggleStatusPlan = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: string) => api.planToggleStatus(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetPlanDetail = (planId?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["plans", planId],
|
||||||
|
queryFn: () => api.getPlanDetail(planId),
|
||||||
|
enabled: !!planId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatePlan = (id: string) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: UpdatePlanType) => api.updatePlan(id, variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
import {
|
import {
|
||||||
|
CreatePlanType,
|
||||||
CreateServiceCategoryType,
|
CreateServiceCategoryType,
|
||||||
CreateServiceType,
|
CreateServiceType,
|
||||||
ToggleStatusCategoryType,
|
ToggleStatusCategoryType,
|
||||||
|
UpdatePlanType,
|
||||||
} from "../types/ServiceTypes";
|
} from "../types/ServiceTypes";
|
||||||
|
|
||||||
export const getAllServicess = async (
|
export const getAllServicess = async (
|
||||||
@@ -82,3 +84,28 @@ export const toggleStatusService = async (id: string) => {
|
|||||||
const { data } = await axios.post(`/danak-services/toggle-status/${id}`);
|
const { data } = await axios.post(`/danak-services/toggle-status/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPlan = async (params: CreatePlanType) => {
|
||||||
|
const { data } = await axios.post(`/subscriptions`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlans = async (serviceId?: string) => {
|
||||||
|
const { data } = await axios.get(`/subscriptions/service/${serviceId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const planToggleStatus = async (id: string) => {
|
||||||
|
const { data } = await axios.post(`/subscriptions/toggle-status/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlanDetail = async (id?: string) => {
|
||||||
|
const { data } = await axios.get(`/subscriptions/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePlan = async (id: string, params: UpdatePlanType) => {
|
||||||
|
const { data } = await axios.patch(`/subscriptions/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -50,4 +50,34 @@ export type ServiceItemType = {
|
|||||||
createDate: string;
|
createDate: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
subscriptionCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateItemPlanType = {
|
||||||
|
name: string;
|
||||||
|
duration: string | number;
|
||||||
|
price: string | number;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreatePlanType = {
|
||||||
|
subs: CreateItemPlanType[];
|
||||||
|
serviceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlanItemType = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
duration: string | number;
|
||||||
|
price: string | number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdatePlanType = {
|
||||||
|
name: string;
|
||||||
|
duration: string | number;
|
||||||
|
price: string | number;
|
||||||
|
isActive?: boolean;
|
||||||
|
serviceId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import EditBankCard from '../pages/cardBank/Edit'
|
|||||||
import PaymentList from '../pages/payment/List'
|
import PaymentList from '../pages/payment/List'
|
||||||
import LearningCategory from '../pages/learning/Category'
|
import LearningCategory from '../pages/learning/Category'
|
||||||
import CreateLearning from '../pages/learning/Create'
|
import CreateLearning from '../pages/learning/Create'
|
||||||
|
import Plans from '../pages/service/Plans'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.services.add} element={<AddService />} />
|
<Route path={Pages.services.add} element={<AddService />} />
|
||||||
<Route path={Pages.services.list} element={<ListService />} />
|
<Route path={Pages.services.list} element={<ListService />} />
|
||||||
<Route path={Pages.services.category} element={<Category />} />
|
<Route path={Pages.services.category} element={<Category />} />
|
||||||
|
<Route path={Pages.services.plan + ':id'} element={<Plans />} />
|
||||||
<Route path={Pages.ticket.list} element={<TicketList />} />
|
<Route path={Pages.ticket.list} element={<TicketList />} />
|
||||||
<Route path={Pages.ticket.create} element={<CreateTicket />} />
|
<Route path={Pages.ticket.create} element={<CreateTicket />} />
|
||||||
<Route path={Pages.ticket.category} element={<TicketCategory />} />
|
<Route path={Pages.ticket.category} element={<TicketCategory />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user