invoice + plan

This commit is contained in:
hamid zarghami
2025-02-15 16:22:42 +03:30
parent 65faa4de5f
commit 8f39afeb48
20 changed files with 1125 additions and 262 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ const Select: FC<Props> = (props: Props) => {
<option value="" disabled selected>{props.placeholder}</option>
}
{
props.items.map((item) => {
props.items?.map((item) => {
return (
<option key={item.value} value={item.value}>
{item.label}
+1
View File
@@ -12,6 +12,7 @@ export const Pages = {
add: "/services/add",
list: "/services/list",
category: "/services/category",
plan: "/services/plan/",
},
transactions: "/transactions",
receipts: {
+27 -2
View File
@@ -163,7 +163,22 @@
"deactive": "غیرفعال",
"software_langauge": "زبان نرم افزار",
"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": "ذخیره",
"search": "جستجو",
@@ -361,7 +376,17 @@
"discount": "تخفیف(٪)",
"total_amount": "مبلغ کل(ریال)",
"tax": "مالیات بر ارزش افزوده (۰.۹ درصد)",
"total": "جمع"
"total": "جمع",
"error_empty": "همه فیلد ها را پر کنید",
"date_receipt": "تاریخ صورتحساب",
"last_date_receipt": "آخرین مهلت پرداخت",
"service": "سرویس",
"status": "وضعیت تایید",
"status_paid": "وضعیت پرداخت",
"PENDING": "در انتظار",
"PAID": "پرداخت شده",
"CANCELED": "لغو شده",
"EXPIRED": "منقضی شده"
},
"edit": "ویرایش",
"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;
};
+20
View File
@@ -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;
};
+381 -189
View File
@@ -1,222 +1,414 @@
import { FC } from 'react'
import { ChangeEvent, FC, Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import { Add, TickCircle, TickSquare } from 'iconsax-react'
import Select from '../../components/Select'
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 navigate = useNavigate()
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 (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div>
{t('receip.add_receip')}
</div>
<div>
<Button
className='px-5'
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
{
getCustomers.isPending ?
<PageLoading />
:
<Fragment>
<div className='flex justify-between items-center'>
<div>
{t('receip.submit_receip')}
{t('receip.add_receip')}
</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div>
{t('receip.customer_information')}
</div>
<div className='mt-8'>
<div className='w-full xl:w-1/2'>
<Select
label={t('receip.customer')}
items={[
{ label: 'محمد', value: 'mohammad' },
{ label: 'علی', value: 'ali' },
{ label: 'حسین', value: 'hossein' },
]}
/>
</div>
</div>
<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 items-center gap-2'>
<div className='text-description'>{t('receip.type_person')}</div>
<div>حقوقی</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.representativeـname')}</div>
<div>مهرداد مظفری</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.company_name')}</div>
<div>راوک</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.registration_number')}</div>
<div>۱۲۳۴</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.tel_company')}</div>
<div>۰۸۶۹۱۰۰۹۱۰۰۱</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.national_code')}</div>
<div>۰۰۱۲۳۴۵۶۷۸</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.postal_code')}</div>
<div>۰۰۱۲۳۴۵۶۷۸</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.state')}</div>
<div>مرکزی </div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.city')}</div>
<div>اراک </div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.address')}</div>
<div>اراک -خیابان شریعتی-خیابان جنت</div>
</div>
</div>
</div>
<div className='mt-10'>
<div>{t('receip.receipt_information')}</div>
</div>
<div className='mt-8 border-b border-dashed border-border pb-7 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'>
1
<Button
className='px-5'
onClick={handleSubmit}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('receip.submit_receip')}
</div>
</div>
</Button>
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.product_name')}
</div>
<Input
className='text-center'
/>
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div>
{t('receip.customer_information')}
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.count')}
</div>
<Input
type='number'
className='w-16 text-center'
/>
</div>
<div className='mt-8'>
<div className='w-full xl:w-1/2'>
<Select
label={t('receip.customer')}
placeholder={t('select')}
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
return {
label: item.firstName + ' ' + item.lastName,
value: item.id
}
})}
onChange={handleChangeCustomer}
/>
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.unit_amount')}
</div>
<Input
className='text-center'
/>
</div>
{
customer &&
<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 items-center gap-2'>
<div className='text-description'>{t('receip.type_person')}</div>
<div>حقوقی</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.representativeـname')}</div>
<div>{customer.firstName + ' ' + customer.lastName}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.company_name')}</div>
<div>راوک</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.registration_number')}</div>
<div>۱۲۳۴</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.tel_company')}</div>
<div>۰۸۶۹۱۰۰۹۱۰۰۱</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.national_code')}</div>
<div>۰۰۱۲۳۴۵۶۷۸</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.postal_code')}</div>
<div>۰۰۱۲۳۴۵۶۷۸</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.state')}</div>
<div>مرکزی </div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.city')}</div>
<div>اراک </div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.address')}</div>
<div>اراک -خیابان شریعتی-خیابان جنت</div>
</div>
</div>
</div>
}
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.discount')}
</div>
<Input
type='number'
className='w-16 text-center'
/>
</div>
<div className='mt-10'>
<div>{t('receip.receipt_information')}</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.total_amount')}
</div>
<Input
className='text-center bg-[#EBEDF5]'
/>
</div>
<div className='mt-8 border-b border-dashed border-border pb-7 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'>
{items.length + 1}
</div>
</div>
<div className='size-10 border border-border rounded-xl flex justify-center items-center'>
<Add size={20} color='black' />
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.product_name')}
</div>
<Input
className='text-center'
name='name'
value={formik.values.name}
onChange={formik.handleChange}
/>
</div>
<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>
{t('receip.tax')}
</div>
<div className='text-black'>
100,000
</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>
{t('receip.total')}
</div>
<div className='text-black'>
100,000
</div>
</div>
</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'
value={formik.values.count}
onChange={formik.handleChange}
/>
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.unit_amount')}
</div>
<Input
className='text-center'
name='unitPrice'
value={formik.values.unitPrice}
onChange={formik.handleChange}
/>
</div>
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</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'
value={formik.values.discount}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.total_amount')}
</div>
<Input
className='text-center bg-[#EBEDF5]'
readOnly
/>
</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' />
</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' />
</div>
</div>
)
})
}
<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>
{t('receip.tax')}
</div>
<div className='text-black'>
{((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 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>
{t('receip.total')}
</div>
<div className='text-black'>
{(
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 className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div>
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div>
</div>
</div>
</Fragment>
}
</div>
)
}
+60 -70
View File
@@ -1,15 +1,18 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useGetInvoices } from './hooks/useReceiptData'
import PageLoading from '../../components/PageLoading'
import { ReceiptItemType } from './types/ReceiptTypes'
import { NumberFormat } from '../../config/func'
const ReceiptsList: FC = () => {
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('unpaid')
const [activeTab, setActiveTab] = useState<"PENDING" | "PAID" | "CANCELED" | "EXPIRED">('PENDING')
const getInvoices = useGetInvoices(activeTab)
return (
<div className='mt-4'>
@@ -22,87 +25,74 @@ const ReceiptsList: FC = () => {
active={activeTab}
items={[
{
icon: <WalletRemove color={activeTab === 'unpaid' ? 'black' : '#8C90A3'} size={22} />,
icon: <WalletRemove color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_pay'),
value: 'unpaid'
value: 'PENDING'
},
{
icon: <WalletSearch color={activeTab === 'not_confimed' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_confrim'),
value: 'not_confimed'
},
{
icon: <WalletCheck color={activeTab === 'paid' ? 'black' : '#8C90A3'} size={22} />,
icon: <WalletCheck color={activeTab === 'PAID' ? 'black' : '#8C90A3'} size={22} />,
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'),
value: 'canceled'
value: 'CANCELED'
},
{
icon: <MoneyChange color={activeTab === 'returned' ? '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} />,
icon: <FolderOpen color={activeTab === 'EXPIRED' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.archive'),
value: 'archive'
value: 'EXPIRED'
},
]}
onChange={setActiveTab}
onChange={(value) => setActiveTab(value as "PENDING" | "PAID" | "CANCELED" | "EXPIRED")}
/>
</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('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={''} />
</tr>
</thead>
<tbody>
<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={''}>
<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={''} />
</tr>
</tbody>
</table>
</div>
{
getInvoices.isPending ?
<div className='mt-5'>
<PageLoading />
</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('ticket.number')} />
<Td text={t('receip.customer')} />
<Td text={t('receip.date_receipt')} />
<Td text={t('receip.last_date_receipt')} />
<Td text={t('receip.total')} />
<Td text={t('receip.service')} />
<Td text={t('receip.status')} />
<Td text={t('receip.status_paid')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
getInvoices.data?.data?.invoices?.map((item: ReceiptItemType, index: number) => {
return (
<tr className='tr'>
<Td text={String(index + 1)} />
<Td text={item.user.firstName + ' ' + item.user.lastName} />
<Td text={item.createdAt} />
<Td text={item.dueDate} />
<Td text={NumberFormat(item.totalPrice)} />
<Td text={t('receip.service')} />
<Td text={t(`receip.${item.status}`)} />
<Td text={item.paidAt ? t('receip.paid') : t('receip.not_paid')} />
<Td text={''} />
</tr>
)
})
}
</tbody>
</table>
</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;
};
+31
View File
@@ -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;
};
+21
View File
@@ -107,6 +107,7 @@ const ListService: FC = () => {
<Td text={t('service.company_developed')} />
<Td text={t('service.category')} />
<Td text={t('service.publish_date')} />
<Td text={t('service.plans')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
@@ -126,6 +127,26 @@ const ListService: FC = () => {
<Td text={item.author} />
<Td text={item.category?.title} />
<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={''}>
<ToggleStatusService
id={item.id}
+71
View File
@@ -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
+194
View File
@@ -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
+152
View File
@@ -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
+36
View File
@@ -1,9 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/ServiceService";
import {
CreatePlanType,
CreateServiceCategoryType,
CreateServiceType,
ToggleStatusCategoryType,
UpdatePlanType,
} from "../types/ServiceTypes";
export const useGetAllServices = (
@@ -80,3 +82,37 @@ export const useToggleStatusService = () => {
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 {
CreatePlanType,
CreateServiceCategoryType,
CreateServiceType,
ToggleStatusCategoryType,
UpdatePlanType,
} from "../types/ServiceTypes";
export const getAllServicess = async (
@@ -82,3 +84,28 @@ export const toggleStatusService = async (id: string) => {
const { data } = await axios.post(`/danak-services/toggle-status/${id}`);
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;
};
+30
View File
@@ -50,4 +50,34 @@ export type ServiceItemType = {
createDate: string;
isActive: boolean;
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;
};
+2
View File
@@ -51,6 +51,7 @@ import EditBankCard from '../pages/cardBank/Edit'
import PaymentList from '../pages/payment/List'
import LearningCategory from '../pages/learning/Category'
import CreateLearning from '../pages/learning/Create'
import Plans from '../pages/service/Plans'
const MainRouter: FC = () => {
@@ -76,6 +77,7 @@ const MainRouter: FC = () => {
<Route path={Pages.services.add} element={<AddService />} />
<Route path={Pages.services.list} element={<ListService />} />
<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.create} element={<CreateTicket />} />
<Route path={Pages.ticket.category} element={<TicketCategory />} />