update
This commit is contained in:
@@ -55,6 +55,7 @@ const CreateInvoice: FC = () => {
|
|||||||
userId: !requestId
|
userId: !requestId
|
||||||
? Yup.string().required("انتخاب مشتری اجباری است.")
|
? Yup.string().required("انتخاب مشتری اجباری است.")
|
||||||
: Yup.string(),
|
: Yup.string(),
|
||||||
|
approvalDeadline: Yup.string().required("مهلت تایید اجباری است."),
|
||||||
}),
|
}),
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const validItems = items.filter(
|
const validItems = items.filter(
|
||||||
|
|||||||
@@ -1,23 +1,119 @@
|
|||||||
import { type FC } from 'react';
|
import { type FC, type ReactNode } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import moment from 'moment-jalaali';
|
import moment from 'moment-jalaali';
|
||||||
import { Edit } from 'iconsax-react';
|
import { Calendar, Edit, Profile2User, ReceiptText, WalletMoney } from 'iconsax-react';
|
||||||
import { Paths } from '@/config/Paths';
|
import { Paths } from '@/config/Paths';
|
||||||
import BackButton from '@/components/BackButton';
|
import BackButton from '@/components/BackButton';
|
||||||
import RefreshButton from '@/components/RefreshButton';
|
import RefreshButton from '@/components/RefreshButton';
|
||||||
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
|
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
|
||||||
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
||||||
import { formatItemDiscountDisplay } from './utils/invoiceItem';
|
import { formatItemDiscountDisplay } from './utils/invoiceItem';
|
||||||
|
import type { InvoiceItemType } from './types/Types';
|
||||||
|
|
||||||
const formatAmount = (amount: number) =>
|
const formatAmount = (amount: number) =>
|
||||||
Number(amount).toLocaleString('fa-IR') + ' ریال';
|
Number(amount).toLocaleString('fa-IR') + ' ریال';
|
||||||
|
|
||||||
|
type InfoFieldProps = {
|
||||||
|
label: string;
|
||||||
|
value: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InfoField: FC<InfoFieldProps> = ({ label, value, icon }) => (
|
||||||
|
<div className="rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-[12px] text-[#7D8597]">
|
||||||
|
{icon}
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[15px] font-medium text-[#1F2937]">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type SummaryRowProps = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
valueClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SummaryRow: FC<SummaryRowProps> = ({ label, value, valueClassName }) => (
|
||||||
|
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-white px-4 py-3">
|
||||||
|
<span className="text-[13px] text-[#6B7280]">{label}</span>
|
||||||
|
<span className={`text-[14px] font-semibold ${valueClassName ?? ''}`}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getConfirmStatusBadge = (confirmedAt: string | null | undefined) => {
|
||||||
|
if (!confirmedAt) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-amber-100 px-2.5 py-1 text-[11px] text-amber-800">
|
||||||
|
در انتظار تایید
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-1 text-[11px] text-green-800">
|
||||||
|
تایید شده
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InvoiceItemTableRow: FC<{ item: InvoiceItemType }> = ({ item }) => {
|
||||||
|
const discounts = formatItemDiscountDisplay(item);
|
||||||
|
const lineTotal = item.total ?? 0;
|
||||||
|
const convertToOrderUrl = `${Paths.convertToOrder}?${new URLSearchParams({
|
||||||
|
invoiceItemId: item.id,
|
||||||
|
}).toString()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.id} className="border-b border-border/40 text-[13px] last:border-b-0">
|
||||||
|
<td className="px-3 py-3 font-medium">{item.product?.title ?? '-'}</td>
|
||||||
|
<td className="px-3 py-3 text-[#6B7280]">{item.description || '-'}</td>
|
||||||
|
<td className="px-3 py-3">{item.quantity ?? '-'}</td>
|
||||||
|
<td className="px-3 py-3">
|
||||||
|
{item.unitPrice != null ? formatAmount(item.unitPrice) : '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3">{discounts.value}</td>
|
||||||
|
<td className="px-3 py-3">{discounts.percent}</td>
|
||||||
|
<td className="px-3 py-3 font-semibold">{formatAmount(lineTotal)}</td>
|
||||||
|
<td className="px-3 py-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{getConfirmStatusBadge(item.confirmedAt)}
|
||||||
|
{item.confirmedAt
|
||||||
|
? (
|
||||||
|
<span className="text-[11px] text-[#6B7280]">
|
||||||
|
{moment(item.confirmedAt).format('jYYYY/jMM/jDD HH:mm')}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3">
|
||||||
|
{item.id && item.confirmedAt
|
||||||
|
? (
|
||||||
|
<Link to={convertToOrderUrl}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-9 rounded-xl border border-primary px-3 text-[12px] text-primary transition-colors hover:bg-primary/5"
|
||||||
|
>
|
||||||
|
تبدیل به سفارش
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<span className="text-[12px] text-[#A0A6B4]">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const DetailPerfomaInvoice: FC = () => {
|
const DetailPerfomaInvoice: FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const { data: invoiceData, isLoading, refetch, isFetching } = useGetInvoiceDetail(id);
|
const { data: invoiceData, isLoading, refetch, isFetching, isError } = useGetInvoiceDetail(id);
|
||||||
const invoice = invoiceData?.data;
|
const invoice = invoiceData?.data;
|
||||||
|
|
||||||
if (isLoading || !invoice) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 flex min-h-[200px] items-center justify-center">
|
<div className="mt-5 flex min-h-[200px] items-center justify-center">
|
||||||
<div className="text-[#888888]">در حال بارگذاری...</div>
|
<div className="text-[#888888]">در حال بارگذاری...</div>
|
||||||
@@ -25,21 +121,43 @@ const DetailPerfomaInvoice: FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isError || !invoice) {
|
||||||
|
return (
|
||||||
|
<div className="mt-5 rounded-3xl border border-red-100 bg-red-50 p-6">
|
||||||
|
<div className="text-sm text-red-700">
|
||||||
|
دریافت اطلاعات پیش فاکتور با مشکل مواجه شد. لطفا دوباره تلاش کنید.
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const customerName =
|
const customerName =
|
||||||
invoice.user?.firstName && invoice.user?.lastName
|
invoice.user?.firstName && invoice.user?.lastName
|
||||||
? `${invoice.user.firstName} ${invoice.user.lastName}`
|
? `${invoice.user.firstName} ${invoice.user.lastName}`
|
||||||
: invoice.user?.phone ?? '-';
|
: invoice.user?.phone ?? '-';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5">
|
<div className="mt-5 space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<BackButton to={Paths.perfomaInvoice.list} />
|
<div className="space-y-3">
|
||||||
|
<BackButton to={Paths.perfomaInvoice.list} />
|
||||||
|
<div className="flex items-center gap-2 text-[#111827]">
|
||||||
|
<ReceiptText size={20} color="#111827" />
|
||||||
|
<h1 className="text-xl font-medium">
|
||||||
|
پیش فاکتور #{invoice.invoiceNumber}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||||
<Link to={Paths.perfomaInvoice.update + invoice.id}>
|
<Link to={Paths.perfomaInvoice.update + invoice.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 rounded-xl bg-primary px-5 py-2.5 text-sm hover:opacity-90"
|
className="flex items-center gap-2 rounded-xl bg-primary px-5 py-2.5 text-sm font-medium hover:opacity-90"
|
||||||
>
|
>
|
||||||
<Edit size={18} color="black" />
|
<Edit size={18} color="black" />
|
||||||
ویرایش
|
ویرایش
|
||||||
@@ -48,139 +166,105 @@ const DetailPerfomaInvoice: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-lg font-light mt-4">
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||||
پیش فاکتور #{invoice.invoiceNumber}
|
<div className="rounded-3xl border border-border/70 bg-white p-6 shadow-sm">
|
||||||
</h1>
|
<div className="font-medium text-[#111827]">اطلاعات پیش فاکتور</div>
|
||||||
|
|
||||||
<div className="mt-8 rounded-3xl bg-white p-6">
|
<div className="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<div className="font-light">اطلاعات پیش فاکتور</div>
|
<InfoField
|
||||||
|
label="مشتری"
|
||||||
|
value={customerName}
|
||||||
|
icon={<Profile2User size={14} color="#7D8597" />}
|
||||||
|
/>
|
||||||
|
<InfoField
|
||||||
|
label="تاریخ صدور"
|
||||||
|
value={invoice.createdAt ? moment(invoice.createdAt).format('jYYYY/jMM/jDD') : '-'}
|
||||||
|
icon={<Calendar size={14} color="#7D8597" />}
|
||||||
|
/>
|
||||||
|
<InfoField
|
||||||
|
label="مهلت تایید"
|
||||||
|
value={invoice.approvalDeadline ? moment(invoice.approvalDeadline).format('jYYYY/jMM/jDD') : '-'}
|
||||||
|
icon={<Calendar size={14} color="#7D8597" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-8 overflow-hidden rounded-2xl border border-border/70">
|
||||||
<div>
|
<div className="overflow-x-auto">
|
||||||
<div className="mb-1.5 text-[13px] text-[#888888]">مشتری</div>
|
<table className="w-full min-w-[840px] text-right">
|
||||||
<div className="text-[15px]">{customerName}</div>
|
<thead className="bg-[#F9FAFB]">
|
||||||
</div>
|
<tr className="text-[12px] text-[#6B7280]">
|
||||||
<div>
|
<th className="px-3 py-3 font-normal">محصول</th>
|
||||||
<div className="mb-1.5 text-[13px] text-[#888888]">تاریخ صدور</div>
|
<th className="px-3 py-3 font-normal">توضیحات</th>
|
||||||
<div className="text-[15px]">
|
<th className="px-3 py-3 font-normal">تعداد</th>
|
||||||
{invoice.createdAt
|
<th className="px-3 py-3 font-normal">مبلغ واحد</th>
|
||||||
? moment(invoice.createdAt).format('jYYYY/jMM/jDD')
|
<th className="px-3 py-3 font-normal">تخفیف (مبلغ)</th>
|
||||||
: '-'}
|
<th className="px-3 py-3 font-normal">تخفیف (درصد)</th>
|
||||||
</div>
|
<th className="px-3 py-3 font-normal">مبلغ کل</th>
|
||||||
</div>
|
<th className="px-3 py-3 font-normal">تایید</th>
|
||||||
<div>
|
<th className="px-3 py-3 font-normal">عملیات</th>
|
||||||
<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>
|
|
||||||
<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 discounts = formatItemDiscountDisplay(item);
|
|
||||||
const lineTotal = item.total ?? 0;
|
|
||||||
const convertToOrderUrl = `${Paths.convertToOrder}?${new URLSearchParams({
|
|
||||||
invoiceItemId: item.id,
|
|
||||||
}).toString()}`;
|
|
||||||
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">{discounts.value}</td>
|
|
||||||
<td className="py-3">{discounts.percent}</td>
|
|
||||||
<td className="py-3">{formatAmount(lineTotal)}</td>
|
|
||||||
<td className="py-3">
|
|
||||||
{item.confirmedAt
|
|
||||||
? moment(item.confirmedAt).format('jYYYY/jMM/jDD HH:mm')
|
|
||||||
: '-'}
|
|
||||||
</td>
|
|
||||||
<td className="py-3">
|
|
||||||
{item.id && item.confirmedAt && (
|
|
||||||
<Link
|
|
||||||
to={convertToOrderUrl}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="h-10 flex-shrink-0 rounded-xl border border-primary px-4 text-sm text-primary transition-colors hover:bg-primary/5"
|
|
||||||
>
|
|
||||||
تبدیل به سفارش
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
</thead>
|
||||||
})}
|
<tbody>
|
||||||
</tbody>
|
{invoice.items?.length
|
||||||
</table>
|
? invoice.items.map((item) => (
|
||||||
|
<InvoiceItemTableRow key={item.id} item={item} />
|
||||||
|
))
|
||||||
|
: (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={9} className="px-3 py-6 text-center text-sm text-[#888888]">
|
||||||
|
آیتمی برای این پیشفاکتور ثبت نشده است.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{invoice.enableTax && (
|
||||||
|
<div className="mt-4 rounded-xl border border-blue-100 bg-blue-50 px-3 py-2 text-[13px] text-blue-800">
|
||||||
|
مالیات بر ارزش افزوده (۱۰ درصد) فعال است.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invoice.paymentMethod && (
|
||||||
|
<div className="mt-6 rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
|
||||||
|
<div className="mb-2 text-[13px] text-[#7D8597]">روش پرداخت</div>
|
||||||
|
<div className="whitespace-pre-wrap text-[14px]">{invoice.paymentMethod}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invoice.description && (
|
||||||
|
<div className="mt-4 rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
|
||||||
|
<div className="mb-2 text-[13px] text-[#7D8597]">توضیحات</div>
|
||||||
|
<div className="whitespace-pre-wrap text-[14px]">{invoice.description}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{invoice.enableTax && (
|
<div className="h-fit rounded-3xl border border-border/70 bg-[#F7F9FF] p-5 shadow-sm">
|
||||||
<div className="mt-4 text-[13px] text-[#888888]">
|
<div className="mb-4 flex items-center gap-2 text-[#111827]">
|
||||||
مالیات بر ارزش افزوده (۱۰ درصد) فعال است
|
<WalletMoney size={18} color="#111827" />
|
||||||
|
<span className="font-medium">خلاصه مالی</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
|
<SummaryRow label="مبلغ کل" value={formatAmount(invoice.total ?? 0)} />
|
||||||
{invoice.paymentMethod && (
|
<SummaryRow
|
||||||
<div className="mt-6">
|
label="پرداخت شده"
|
||||||
<div className="mb-1.5 text-[13px] text-[#888888]">روش پرداخت</div>
|
value={formatAmount(invoice.paidAmount ?? 0)}
|
||||||
<div className="whitespace-pre-wrap text-[15px]">
|
valueClassName="text-green-700"
|
||||||
{invoice.paymentMethod}
|
/>
|
||||||
</div>
|
<SummaryRow
|
||||||
|
label="مانده"
|
||||||
|
value={formatAmount(invoice.balance ?? 0)}
|
||||||
|
valueClassName={invoice.balance > 0 ? 'text-amber-700' : 'text-[#111827]'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="mt-4 rounded-xl border border-border/70 bg-white px-4 py-3 text-[12px] text-[#6B7280]">
|
||||||
|
آخرین بروزرسانی:
|
||||||
{invoice.description && (
|
<span className="ms-1 font-medium text-[#111827]">
|
||||||
<div className="mt-6">
|
{invoice.createdAt ? moment(invoice.createdAt).format('jYYYY/jMM/jDD HH:mm') : '-'}
|
||||||
<div className="mb-1.5 text-[13px] text-[#888888]">توضیحات</div>
|
</span>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user