pay invoice

This commit is contained in:
hamid zarghami
2026-02-24 12:08:03 +03:30
parent 7a34e74e02
commit f5c152ccc6
4 changed files with 261 additions and 11 deletions
+3
View File
@@ -30,4 +30,7 @@ export const Paths = {
auth: {
login: '/login',
},
payment: {
payInvoice: '/payments/invoice/',
},
}
+10 -11
View File
@@ -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>
+246
View File
@@ -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
+2
View File
@@ -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>