pay invoice
This commit is contained in:
@@ -30,4 +30,7 @@ export const Paths = {
|
||||
auth: {
|
||||
login: '/login',
|
||||
},
|
||||
payment: {
|
||||
payInvoice: '/payments/invoice/',
|
||||
},
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import ModalConfirm from '@/components/ModalConfirm'
|
||||
import Button from '@/components/Button'
|
||||
import Table from '@/components/Table'
|
||||
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 { useConfirmInvoiceItem, useGetInvoiceDetail } from './hooks/useInvoiceData'
|
||||
import { NumberFormat } from '@/config/func'
|
||||
import moment from 'moment-jalaali'
|
||||
import type { InvoiceItem as ApiInvoiceItem } from './types/InvoiceTypes'
|
||||
import { Paths } from '@/config/Paths'
|
||||
|
||||
interface TableInvoiceItem extends RowDataType {
|
||||
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 (
|
||||
<div className='mt-4 text-sm'>
|
||||
@@ -152,15 +153,13 @@ const InvoiceDetail: FC = () => {
|
||||
<Printer size={20} color='black' />
|
||||
<span className='text-xs'>چاپ</span>
|
||||
</Button>
|
||||
<Button
|
||||
className={
|
||||
isPaid
|
||||
? 'bg-[#E8F5E9] text-[#01AC45] w-[120px] h-9 text-xs'
|
||||
: 'bg-[#E8F0FF] text-[#0047FF] w-[120px] h-9 text-xs'
|
||||
}
|
||||
>
|
||||
{isPaid ? 'پرداخت شده' : 'پرداخت نشده'}
|
||||
</Button>
|
||||
<Link to={`${Paths.payment.payInvoice}${id}`}>
|
||||
<Button
|
||||
className='bg-blue-100 text-[#0047FF] w-[120px] h-9 '
|
||||
>
|
||||
پرداخت
|
||||
</Button>
|
||||
</Link>
|
||||
</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 LearningDetail from '@/pages/learning/Detail'
|
||||
import InvoiceDetail from '@/pages/invoice/Detail'
|
||||
import PayInvoice from '@/pages/payment/PayInvoice'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
return (
|
||||
@@ -47,6 +48,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Paths.learning.list} element={<LearningList />} />
|
||||
<Route path={`${Paths.learning.detail}:id`} element={<LearningDetail />} />
|
||||
<Route path={`${Paths.proformaInvoice}/:id`} element={<InvoiceDetail />} />
|
||||
<Route path={`${Paths.payment.payInvoice}:id`} element={<PayInvoice />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user