add payments in invoice detail
This commit is contained in:
@@ -1,106 +1,164 @@
|
||||
import { type FC } from 'react'
|
||||
import Button from '@/components/Button'
|
||||
import { AddSquare, TickSquare } from 'iconsax-react'
|
||||
import Input from '@/components/Input'
|
||||
import { COLORS } from '@/constants/colors'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import { type FC } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import moment from 'moment-jalaali';
|
||||
import { Edit } from 'iconsax-react';
|
||||
import { Paths } from '@/config/Paths';
|
||||
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
|
||||
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
||||
|
||||
const formatAmount = (amount: number) =>
|
||||
Number(amount).toLocaleString('fa-IR') + ' تومان';
|
||||
|
||||
const DetailPerfomaInvoice: FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { data: invoiceData, isLoading } = useGetInvoiceDetail(id);
|
||||
const invoice = invoiceData?.data;
|
||||
|
||||
if (isLoading || !invoice) {
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h1 className='text-lg font-light'>پیش فاکتور درخواست #123456</h1>
|
||||
<Button
|
||||
className='w-fit px-6'
|
||||
>
|
||||
<div className='flex gap-1.5'>
|
||||
<TickSquare size={18} color='black' />
|
||||
<div className='text-[13px]'>ثبت پیش فاکتور</div>
|
||||
<div className="mt-5 flex min-h-[200px] items-center justify-center">
|
||||
<div className="text-[#888888]">در حال بارگذاری...</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 bg-white p-6 rounded-3xl'>
|
||||
<div className='font-light'>اطلاعات پیش فاکتور</div>
|
||||
|
||||
<div className='mt-6 flex items-end gap-5'>
|
||||
<Input
|
||||
label='محصول'
|
||||
readOnly
|
||||
value='کارت ویزیت'
|
||||
/>
|
||||
|
||||
<Input
|
||||
label='توضیحات'
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label='تعداد'
|
||||
className='w-[100px]'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label='مبلغ واحد'
|
||||
className='w-[100px]'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label='تخفیف'
|
||||
className='w-[100px]'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label='مبلغ کل'
|
||||
className='w-[100px]'
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddSquare className='min-w-[30px]' size={40} color={COLORS.primary} />
|
||||
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex gap-2'>
|
||||
<Checkbox
|
||||
/>
|
||||
<div className='text-[13px]'>مالیات بر ارزش افزوده</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<UploadBox
|
||||
label='فایل ضمیمه'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label='نحوه پرداخت'
|
||||
placeholder='مبلغ 70 درصد پیش پرداخت / اعتبار 2 روز'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label='توضیحات'
|
||||
placeholder='1- قیمت ها بدون احتساب مالیات بر ارزش افزوده می باشد
|
||||
2- تیراژ تحویلی سفارشات با تقریب مثبت و منفی 15 % می باشد و مبلغ نهایی براساس تیراژ تحویلی محاسبه می گردد.
|
||||
3- سفارشات چاپی با اختلاف رنگ 10 الی 15 درصد با فایل طراحی شده می باشد.'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 bg-[#F5F7FC] h-12 px-3 flex justify-between items-center rounded-xl'>
|
||||
<div className='text-[#888888] text-[13px]'>مبلغ کل:</div>
|
||||
<div className='font-semibold text-[13px]'>۰ ﷼</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPerfomaInvoice
|
||||
const customerName =
|
||||
invoice.user?.firstName && invoice.user?.lastName
|
||||
? `${invoice.user.firstName} ${invoice.user.lastName}`
|
||||
: invoice.user?.phone ?? '-';
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-light">
|
||||
پیش فاکتور #{invoice.invoiceNumber}
|
||||
</h1>
|
||||
<Link to={Paths.perfomaInvoice.update + invoice.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-xl bg-primary px-5 py-2.5 text-sm hover:opacity-90"
|
||||
>
|
||||
<Edit size={18} color="black" />
|
||||
ویرایش
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-3xl bg-white p-6">
|
||||
<div className="font-light">اطلاعات پیش فاکتور</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<div className="mb-1.5 text-[13px] text-[#888888]">مشتری</div>
|
||||
<div className="text-[15px]">{customerName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1.5 text-[13px] text-[#888888]">تاریخ صدور</div>
|
||||
<div className="text-[15px]">
|
||||
{invoice.createdAt
|
||||
? moment(invoice.createdAt).format('jYYYY/jMM/jDD')
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1.5 text-[13px] text-[#888888]">مهلت تایید</div>
|
||||
<div className="text-[15px]">
|
||||
{invoice.approvalDeadline
|
||||
? moment(invoice.approvalDeadline).format('jYYYY/jMM/jDD')
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 overflow-x-auto">
|
||||
<table className="w-full min-w-[640px] text-right text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-[13px] text-[#888888]">
|
||||
<th className="pb-3 font-normal">محصول</th>
|
||||
<th className="pb-3 font-normal">توضیحات</th>
|
||||
<th className="pb-3 font-normal">تعداد</th>
|
||||
<th className="pb-3 font-normal">مبلغ واحد</th>
|
||||
<th className="pb-3 font-normal">تخفیف</th>
|
||||
<th className="pb-3 font-normal">مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoice.items?.map((item) => {
|
||||
const lineTotal =
|
||||
(item.quantity || 0) * (item.unitPrice || 0) -
|
||||
(item.discount || 0);
|
||||
return (
|
||||
<tr key={item.id} className="border-b border-border/50">
|
||||
<td className="py-3">{item.product?.title ?? '-'}</td>
|
||||
<td className="py-3">{item.description || '-'}</td>
|
||||
<td className="py-3">{item.quantity ?? '-'}</td>
|
||||
<td className="py-3">
|
||||
{item.unitPrice != null
|
||||
? formatAmount(item.unitPrice)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{item.discount != null
|
||||
? formatAmount(item.discount)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3">{formatAmount(lineTotal)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{invoice.enableTax && (
|
||||
<div className="mt-4 text-[13px] text-[#888888]">
|
||||
مالیات بر ارزش افزوده (۱۰ درصد) فعال است
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.paymentMethod && (
|
||||
<div className="mt-6">
|
||||
<div className="mb-1.5 text-[13px] text-[#888888]">روش پرداخت</div>
|
||||
<div className="whitespace-pre-wrap text-[15px]">
|
||||
{invoice.paymentMethod}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoice.description && (
|
||||
<div className="mt-6">
|
||||
<div className="mb-1.5 text-[13px] text-[#888888]">توضیحات</div>
|
||||
<div className="whitespace-pre-wrap text-[15px]">
|
||||
{invoice.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 space-y-2">
|
||||
<div className="flex h-12 items-center justify-between rounded-xl bg-[#F5F7FC] px-4">
|
||||
<div className="text-[13px] text-[#888888]">مبلغ کل:</div>
|
||||
<div className="text-[13px] font-semibold">
|
||||
{formatAmount(invoice.total ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-12 items-center justify-between rounded-xl bg-[#F5F7FC] px-4">
|
||||
<div className="text-[13px] text-[#888888]">پرداخت شده:</div>
|
||||
<div className="text-[13px] font-semibold text-green-700">
|
||||
{formatAmount(invoice.paidAmount ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-12 items-center justify-between rounded-xl bg-[#F5F7FC] px-4">
|
||||
<div className="text-[13px] text-[#888888]">مانده:</div>
|
||||
<div className="text-[13px] font-semibold">
|
||||
{formatAmount(invoice.balance ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{id && <InvoicePaymentsSection invoiceId={id} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPerfomaInvoice;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMemo, useState, type FC } from 'react'
|
||||
import { ProformaInvoiceStatusEnum } from './enum/InvoiceEnum'
|
||||
import Filters from '@/components/Filters'
|
||||
import Table from '@/components/Table'
|
||||
import { Eye, Add } from 'iconsax-react'
|
||||
import { Eye, Add, Edit2 } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import moment from 'moment-jalaali'
|
||||
@@ -180,9 +180,12 @@ const ProformaInvoice: FC = () => {
|
||||
title: '',
|
||||
render: (item) => (
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Link to={Paths.perfomaInvoice.update + item.id}>
|
||||
<Link to={Paths.perfomaInvoice.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<Link to={Paths.perfomaInvoice.update + item.id}>
|
||||
<Edit2 size={20} color='#0037FF' />
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import moment from 'moment-jalaali';
|
||||
import Table from '@/components/Table';
|
||||
import ModalConfrim from '@/components/ModalConfrim';
|
||||
import type { ColumnType } from '@/components/types/TableTypes';
|
||||
import { toast } from '@/shared/toast';
|
||||
import { extractErrorMessage } from '@/config/func';
|
||||
import {
|
||||
useConfirmCashPayment,
|
||||
useDenyCashPayment,
|
||||
useGetPayments,
|
||||
useVerifyOnlinePayment,
|
||||
} from '@/pages/payment/hooks/usePaymentData';
|
||||
import {
|
||||
PaymentMethodEnum,
|
||||
PaymentStatusEnum,
|
||||
} from '@/pages/payment/enum/PaymentEnum';
|
||||
import type { PaymentType } from '@/pages/payment/types/Types';
|
||||
|
||||
type Props = {
|
||||
invoiceId: string;
|
||||
};
|
||||
|
||||
const methodLabels: Record<PaymentMethodEnum, string> = {
|
||||
[PaymentMethodEnum.Online]: 'آنلاین',
|
||||
[PaymentMethodEnum.Cash]: 'نقدی',
|
||||
[PaymentMethodEnum.Credit]: 'اعتبار',
|
||||
};
|
||||
|
||||
const statusLabels: Record<PaymentStatusEnum, string> = {
|
||||
[PaymentStatusEnum.Pending]: 'در انتظار',
|
||||
[PaymentStatusEnum.Paid]: 'پرداخت شده',
|
||||
[PaymentStatusEnum.Failed]: 'ناموفق',
|
||||
[PaymentStatusEnum.Refunded]: 'بازگشت داده شده',
|
||||
};
|
||||
|
||||
const statusBadgeClass: Record<PaymentStatusEnum, string> = {
|
||||
[PaymentStatusEnum.Pending]: 'bg-amber-100 text-amber-800',
|
||||
[PaymentStatusEnum.Paid]: 'bg-green-100 text-green-800',
|
||||
[PaymentStatusEnum.Failed]: 'bg-red-100 text-red-800',
|
||||
[PaymentStatusEnum.Refunded]: 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number) =>
|
||||
Number(amount).toLocaleString('fa-IR') + ' تومان';
|
||||
|
||||
const InvoicePaymentsSection: FC<Props> = ({ invoiceId }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
type: 'cash-confirm' | 'cash-deny' | 'online-verify';
|
||||
payment: PaymentType;
|
||||
} | null>(null);
|
||||
|
||||
const { data, isLoading } = useGetPayments({
|
||||
page,
|
||||
limit: 10,
|
||||
invoiceId,
|
||||
});
|
||||
|
||||
const confirmCash = useConfirmCashPayment();
|
||||
const denyCash = useDenyCashPayment();
|
||||
const verifyOnline = useVerifyOnlinePayment();
|
||||
|
||||
const payments = data?.data ?? [];
|
||||
const meta = data?.meta;
|
||||
|
||||
const isActionLoading =
|
||||
confirmCash.isPending || denyCash.isPending || verifyOnline.isPending;
|
||||
|
||||
const handleConfirmAction = () => {
|
||||
if (!confirmAction) return;
|
||||
|
||||
const { type, payment } = confirmAction;
|
||||
const onSuccess = () => {
|
||||
toast('عملیات با موفقیت انجام شد', 'success');
|
||||
setConfirmAction(null);
|
||||
};
|
||||
const onError = (error: unknown) => {
|
||||
toast(extractErrorMessage(error), 'error');
|
||||
};
|
||||
|
||||
if (type === 'cash-confirm') {
|
||||
confirmCash.mutate(payment.id, { onSuccess, onError });
|
||||
return;
|
||||
}
|
||||
if (type === 'cash-deny') {
|
||||
denyCash.mutate(payment.id, { onSuccess, onError });
|
||||
return;
|
||||
}
|
||||
if (type === 'online-verify' && payment.token) {
|
||||
verifyOnline.mutate(payment.token, { onSuccess, onError });
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmLabel = () => {
|
||||
if (!confirmAction) return '';
|
||||
switch (confirmAction.type) {
|
||||
case 'cash-confirm':
|
||||
return 'آیا از تایید این پرداخت نقدی اطمینان دارید؟';
|
||||
case 'cash-deny':
|
||||
return 'آیا از رد این پرداخت نقدی اطمینان دارید؟';
|
||||
case 'online-verify':
|
||||
return 'آیا از تایید این پرداخت آنلاین اطمینان دارید؟';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnType<PaymentType>[] = [
|
||||
{
|
||||
title: 'مبلغ',
|
||||
key: 'amount',
|
||||
render: (item) => formatAmount(item.amount),
|
||||
},
|
||||
{
|
||||
title: 'روش پرداخت',
|
||||
key: 'method',
|
||||
render: (item) => methodLabels[item.method] ?? item.method,
|
||||
},
|
||||
{
|
||||
title: 'وضعیت',
|
||||
key: 'status',
|
||||
render: (item) => (
|
||||
<span
|
||||
className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs ${statusBadgeClass[item.status] ?? 'bg-gray-100 text-gray-800'}`}
|
||||
>
|
||||
{statusLabels[item.status] ?? item.status}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
key: 'createdAt',
|
||||
render: (item) =>
|
||||
item.createdAt
|
||||
? moment(item.createdAt).format('jYYYY/jMM/jDD HH:mm')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
key: 'actions',
|
||||
render: (item) => {
|
||||
const isPending = item.status === PaymentStatusEnum.Pending;
|
||||
const isCash = item.method === PaymentMethodEnum.Cash;
|
||||
const isOnline = item.method === PaymentMethodEnum.Online;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{isPending && isCash && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-green-600 px-2 py-1 text-xs text-white hover:opacity-90"
|
||||
onClick={() =>
|
||||
setConfirmAction({ type: 'cash-confirm', payment: item })
|
||||
}
|
||||
>
|
||||
تایید
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-red-600 px-2 py-1 text-xs text-white hover:opacity-90"
|
||||
onClick={() =>
|
||||
setConfirmAction({ type: 'cash-deny', payment: item })
|
||||
}
|
||||
>
|
||||
رد
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{isPending && isOnline && item.token && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg bg-primary px-2 py-1 text-xs text-black hover:opacity-90"
|
||||
onClick={() =>
|
||||
setConfirmAction({ type: 'online-verify', payment: item })
|
||||
}
|
||||
>
|
||||
تایید آنلاین
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-3xl p-6 mt-8">
|
||||
<div className="font-light">پرداختها</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={payments}
|
||||
isLoading={isLoading}
|
||||
noDataMessage="پرداختی برای این پیشفاکتور یافت نشد."
|
||||
pagination={
|
||||
meta && meta.totalPages > 1
|
||||
? {
|
||||
currentPage: meta.page,
|
||||
totalPages: meta.totalPages,
|
||||
onPageChange: setPage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={!!confirmAction}
|
||||
close={() => setConfirmAction(null)}
|
||||
onConfrim={handleConfirmAction}
|
||||
isloading={isActionLoading}
|
||||
label={getConfirmLabel()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvoicePaymentsSection;
|
||||
@@ -15,6 +15,7 @@ export const useConfirmCashPayment = () => {
|
||||
mutationFn: (paymentId: string) => api.confirmCashPayment(paymentId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['payments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['invoice'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -25,6 +26,7 @@ export const useDenyCashPayment = () => {
|
||||
mutationFn: (paymentId: string) => api.denyCashPayment(paymentId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['payments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['invoice'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -35,6 +37,7 @@ export const useVerifyOnlinePayment = () => {
|
||||
mutationFn: (token: string) => api.verifyOnlinePayment(token),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['payments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['invoice'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user