pay invoice
This commit is contained in:
@@ -30,4 +30,7 @@ export const Paths = {
|
|||||||
auth: {
|
auth: {
|
||||||
login: '/login',
|
login: '/login',
|
||||||
},
|
},
|
||||||
|
payment: {
|
||||||
|
payInvoice: '/payments/invoice/',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,13 @@ import ModalConfirm from '@/components/ModalConfirm'
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import type { RowDataType } from '@/components/types/TableTypes'
|
import type { RowDataType } from '@/components/types/TableTypes'
|
||||||
import { useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useConfirmInvoiceItem, useGetInvoiceDetail } from './hooks/useInvoiceData'
|
import { useConfirmInvoiceItem, useGetInvoiceDetail } from './hooks/useInvoiceData'
|
||||||
import { NumberFormat } from '@/config/func'
|
import { NumberFormat } from '@/config/func'
|
||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import type { InvoiceItem as ApiInvoiceItem } from './types/InvoiceTypes'
|
import type { InvoiceItem as ApiInvoiceItem } from './types/InvoiceTypes'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
interface TableInvoiceItem extends RowDataType {
|
interface TableInvoiceItem extends RowDataType {
|
||||||
id: string
|
id: string
|
||||||
@@ -135,7 +136,7 @@ const InvoiceDetail: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPaid = invoice && invoice.paidAmount >= invoice.total && invoice.total > 0
|
// const isPaid = invoice && invoice.paidAmount >= invoice.total && invoice.total > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4 text-sm'>
|
<div className='mt-4 text-sm'>
|
||||||
@@ -152,15 +153,13 @@ const InvoiceDetail: FC = () => {
|
|||||||
<Printer size={20} color='black' />
|
<Printer size={20} color='black' />
|
||||||
<span className='text-xs'>چاپ</span>
|
<span className='text-xs'>چاپ</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Link to={`${Paths.payment.payInvoice}${id}`}>
|
||||||
className={
|
<Button
|
||||||
isPaid
|
className='bg-blue-100 text-[#0047FF] w-[120px] h-9 '
|
||||||
? 'bg-[#E8F5E9] text-[#01AC45] w-[120px] h-9 text-xs'
|
>
|
||||||
: 'bg-[#E8F0FF] text-[#0047FF] w-[120px] h-9 text-xs'
|
پرداخت
|
||||||
}
|
</Button>
|
||||||
>
|
</Link>
|
||||||
{isPaid ? 'پرداخت شده' : 'پرداخت نشده'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { type FC, useState } from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import type { ItemsSelectType } from '@/components/Select'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useGetInvoiceDetail } from '@/pages/invoice/hooks/useInvoiceData'
|
||||||
|
import { usePayInvoice } from './hooks/usePaymentData'
|
||||||
|
import { useMultiUpload } from '@/pages/uploader/hooks/useUploader'
|
||||||
|
import { NumberFormat } from '@/config/func'
|
||||||
|
import { toast } from '@/shared/toast'
|
||||||
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import type { PayInvoiceParamsType } from './types/Types'
|
||||||
|
import { PaymentGatewayEnum, PaymentMethodEnum } from './enum/Enum'
|
||||||
|
const methodItems: ItemsSelectType[] = [
|
||||||
|
{ value: PaymentMethodEnum.Online, label: 'آنلاین' },
|
||||||
|
{ value: PaymentMethodEnum.Cash, label: 'نقد' },
|
||||||
|
{ value: PaymentMethodEnum.Credit, label: 'اعتباری' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const gatewayItems: ItemsSelectType[] = [
|
||||||
|
{ value: PaymentGatewayEnum.ZarinPal, label: 'زرینپال' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const formatPrice = (num: number) => `${NumberFormat(num)} تومان`
|
||||||
|
|
||||||
|
const PayInvoice: FC = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const { data, isPending } = useGetInvoiceDetail(id ?? '')
|
||||||
|
const { mutate: payInvoice, isPending: isPaying } = usePayInvoice()
|
||||||
|
const multiUpload = useMultiUpload()
|
||||||
|
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([])
|
||||||
|
|
||||||
|
const invoice = data?.data
|
||||||
|
|
||||||
|
const formik = useFormik<PayInvoiceParamsType>({
|
||||||
|
initialValues: {
|
||||||
|
amount: invoice?.balance ?? 0,
|
||||||
|
method: PaymentMethodEnum.Online,
|
||||||
|
gateway: PaymentGatewayEnum.ZarinPal,
|
||||||
|
description: '',
|
||||||
|
attachments: [],
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
amount: Yup.number()
|
||||||
|
.required('مبلغ اجباری است')
|
||||||
|
.min(1, 'مبلغ باید بیشتر از صفر باشد'),
|
||||||
|
method: Yup.string().required('روش پرداخت اجباری است'),
|
||||||
|
gateway: Yup.string().when('method', {
|
||||||
|
is: PaymentMethodEnum.Online,
|
||||||
|
then: (schema) => schema.required('درگاه پرداخت اجباری است'),
|
||||||
|
}),
|
||||||
|
description: Yup.string(),
|
||||||
|
attachments: Yup.array().of(Yup.string()),
|
||||||
|
}),
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
let attachments: string[] = values.attachments ?? []
|
||||||
|
|
||||||
|
if (attachmentFiles.length > 0) {
|
||||||
|
try {
|
||||||
|
const uploadResult = await multiUpload.mutateAsync(attachmentFiles)
|
||||||
|
attachments = (uploadResult?.data ?? []).map(
|
||||||
|
(item: { url?: string }) => item?.url ?? ''
|
||||||
|
).filter(Boolean)
|
||||||
|
} catch (error) {
|
||||||
|
toast((error as ErrorType)?.response?.data?.error?.message?.[0] ?? 'خطا در آپلود فایلها', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payInvoice(
|
||||||
|
{
|
||||||
|
invoiceId: id,
|
||||||
|
params: {
|
||||||
|
...values,
|
||||||
|
attachments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
const paymentUrl = res?.data?.paymentUrl
|
||||||
|
if (paymentUrl) {
|
||||||
|
toast('در حال انتقال به درگاه پرداخت', 'success')
|
||||||
|
window.location.href = paymentUrl
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast('پرداخت با موفقیت ثبت شد', 'success')
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast(
|
||||||
|
error?.response?.data?.error?.message?.[0] ?? 'خطا در ثبت پرداخت',
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isPending && !invoice) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 text-sm flex items-center justify-center min-h-[200px]">
|
||||||
|
<span className="text-[#8C90A3]">در حال بارگذاری...</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id || !invoice) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 text-sm flex items-center justify-center min-h-[200px]">
|
||||||
|
<span className="text-[#8C90A3]">صورت حساب یافت نشد</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 text-sm">
|
||||||
|
<div className="text-sm mb-6">پرداخت صورت حساب</div>
|
||||||
|
|
||||||
|
{/* خلاصه صورت حساب */}
|
||||||
|
<div className="bg-white rounded-2xl p-8 mb-6">
|
||||||
|
<div className="text-sm mb-4">اطلاعات صورت حساب</div>
|
||||||
|
<div className="flex flex-wrap gap-x-12 gap-y-3 text-xs">
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<span className="text-[#8C90A3]">شماره صورت حساب:</span>
|
||||||
|
<span className="text-black">{invoice.invoiceNumber}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<span className="text-[#8C90A3]">مبلغ کل:</span>
|
||||||
|
<span className="text-black">{formatPrice(invoice.total)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<span className="text-[#8C90A3]">پرداخت شده:</span>
|
||||||
|
<span className="text-black">{formatPrice(invoice.paidAmount)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<span className="text-[#8C90A3]">مانده:</span>
|
||||||
|
<span className="text-black font-medium">{formatPrice(invoice.balance)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* فرم پرداخت */}
|
||||||
|
<div className="bg-white rounded-2xl p-8">
|
||||||
|
<div className="text-sm mb-6">ثبت پرداخت</div>
|
||||||
|
|
||||||
|
<form onSubmit={formik.handleSubmit} className="flex flex-col gap-6">
|
||||||
|
<Input
|
||||||
|
label="مبلغ (تومان)"
|
||||||
|
placeholder="مبلغ را وارد کنید"
|
||||||
|
name="amount"
|
||||||
|
value={formik.values.amount ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'amount',
|
||||||
|
Number((e.target.value ?? '').replace(/,/g, '')) || 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
seprator
|
||||||
|
error_text={
|
||||||
|
formik.touched.amount && formik.errors.amount
|
||||||
|
? formik.errors.amount
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="روش پرداخت"
|
||||||
|
placeholder="انتخاب کنید"
|
||||||
|
items={methodItems}
|
||||||
|
name="method"
|
||||||
|
value={formik.values.method}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
error_text={
|
||||||
|
formik.touched.method && formik.errors.method
|
||||||
|
? formik.errors.method
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.values.method === PaymentMethodEnum.Online && (
|
||||||
|
<Select
|
||||||
|
label="درگاه پرداخت"
|
||||||
|
placeholder="انتخاب کنید"
|
||||||
|
items={gatewayItems}
|
||||||
|
name="gateway"
|
||||||
|
value={formik.values.gateway}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
error_text={
|
||||||
|
formik.touched.gateway && formik.errors.gateway
|
||||||
|
? formik.errors.gateway
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.method !== PaymentMethodEnum.Online && (
|
||||||
|
<>
|
||||||
|
<Textarea
|
||||||
|
label="توضیحات (اختیاری)"
|
||||||
|
placeholder="توضیحات پرداخت"
|
||||||
|
{...formik.getFieldProps('description')}
|
||||||
|
error_text={
|
||||||
|
formik.touched.description && formik.errors.description
|
||||||
|
? formik.errors.description
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UploadBox
|
||||||
|
label="پیوستها (اختیاری)"
|
||||||
|
isMultiple
|
||||||
|
onChange={setAttachmentFiles}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isPaying || multiUpload.isPending}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TickCircle size={20} color="black" />
|
||||||
|
<span>ثبت پرداخت</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PayInvoice
|
||||||
@@ -19,6 +19,7 @@ import AddCriticisms from '@/pages/criticisms/Add'
|
|||||||
import LearningList from '@/pages/learning/List'
|
import LearningList from '@/pages/learning/List'
|
||||||
import LearningDetail from '@/pages/learning/Detail'
|
import LearningDetail from '@/pages/learning/Detail'
|
||||||
import InvoiceDetail from '@/pages/invoice/Detail'
|
import InvoiceDetail from '@/pages/invoice/Detail'
|
||||||
|
import PayInvoice from '@/pages/payment/PayInvoice'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -47,6 +48,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Paths.learning.list} element={<LearningList />} />
|
<Route path={Paths.learning.list} element={<LearningList />} />
|
||||||
<Route path={`${Paths.learning.detail}:id`} element={<LearningDetail />} />
|
<Route path={`${Paths.learning.detail}:id`} element={<LearningDetail />} />
|
||||||
<Route path={`${Paths.proformaInvoice}/:id`} element={<InvoiceDetail />} />
|
<Route path={`${Paths.proformaInvoice}/:id`} element={<InvoiceDetail />} />
|
||||||
|
<Route path={`${Paths.payment.payInvoice}:id`} element={<PayInvoice />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user