This commit is contained in:
2026-07-08 20:42:57 +03:30
parent f723392edf
commit 05d60a8067
2 changed files with 219 additions and 134 deletions
+1
View File
@@ -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(
+198 -114
View File
@@ -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">
<div className="space-y-3">
<BackButton to={Paths.perfomaInvoice.list} /> <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="مشتری"
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> value={customerName}
<div> icon={<Profile2User size={14} color="#7D8597" />}
<div className="mb-1.5 text-[13px] text-[#888888]">مشتری</div> />
<div className="text-[15px]">{customerName}</div> <InfoField
</div> label="تاریخ صدور"
<div> value={invoice.createdAt ? moment(invoice.createdAt).format('jYYYY/jMM/jDD') : '-'}
<div className="mb-1.5 text-[13px] text-[#888888]">تاریخ صدور</div> icon={<Calendar size={14} color="#7D8597" />}
<div className="text-[15px]"> />
{invoice.createdAt <InfoField
? moment(invoice.createdAt).format('jYYYY/jMM/jDD') label="مهلت تایید"
: '-'} value={invoice.approvalDeadline ? moment(invoice.approvalDeadline).format('jYYYY/jMM/jDD') : '-'}
</div> icon={<Calendar size={14} color="#7D8597" />}
</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>
<div className="mt-8 overflow-x-auto"> <div className="mt-8 overflow-hidden rounded-2xl border border-border/70">
<table className="w-full min-w-[640px] text-right text-sm"> <div className="overflow-x-auto">
<thead> <table className="w-full min-w-[840px] text-right">
<tr className="border-b border-border text-[13px] text-[#888888]"> <thead className="bg-[#F9FAFB]">
<th className="pb-3 font-normal">محصول</th> <tr className="text-[12px] text-[#6B7280]">
<th className="pb-3 font-normal">توضیحات</th> <th className="px-3 py-3 font-normal">محصول</th>
<th className="pb-3 font-normal">تعداد</th> <th className="px-3 py-3 font-normal">توضیحات</th>
<th className="pb-3 font-normal">مبلغ واحد</th> <th className="px-3 py-3 font-normal">تعداد</th>
<th className="pb-3 font-normal">تخفیف (مبلغ)</th> <th className="px-3 py-3 font-normal">مبلغ واحد</th>
<th className="pb-3 font-normal">تخفیف (درصد)</th> <th className="px-3 py-3 font-normal">تخفیف (مبلغ)</th>
<th className="pb-3 font-normal">مبلغ کل</th> <th className="px-3 py-3 font-normal">تخفیف (درصد)</th>
<th className="pb-3 font-normal">تاریخ تایید</th> <th className="px-3 py-3 font-normal">مبلغ کل</th>
<th className="pb-3 font-normal">عملیات</th> <th className="px-3 py-3 font-normal">تایید</th>
<th className="px-3 py-3 font-normal">عملیات</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{invoice.items?.map((item) => { {invoice.items?.length
const discounts = formatItemDiscountDisplay(item); ? invoice.items.map((item) => (
const lineTotal = item.total ?? 0; <InvoiceItemTableRow key={item.id} item={item} />
const convertToOrderUrl = `${Paths.convertToOrder}?${new URLSearchParams({ ))
invoiceItemId: item.id, : (
}).toString()}`; <tr>
return ( <td colSpan={9} className="px-3 py-6 text-center text-sm text-[#888888]">
<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> </td>
</tr> </tr>
); )}
})}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{invoice.enableTax && ( {invoice.enableTax && (
<div className="mt-4 text-[13px] text-[#888888]"> <div className="mt-4 rounded-xl border border-blue-100 bg-blue-50 px-3 py-2 text-[13px] text-blue-800">
مالیات بر ارزش افزوده (۱۰ درصد) فعال است مالیات بر ارزش افزوده (۱۰ درصد) فعال است.
</div> </div>
)} )}
{invoice.paymentMethod && ( {invoice.paymentMethod && (
<div className="mt-6"> <div className="mt-6 rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
<div className="mb-1.5 text-[13px] text-[#888888]">روش پرداخت</div> <div className="mb-2 text-[13px] text-[#7D8597]">روش پرداخت</div>
<div className="whitespace-pre-wrap text-[15px]"> <div className="whitespace-pre-wrap text-[14px]">{invoice.paymentMethod}</div>
{invoice.paymentMethod}
</div>
</div> </div>
)} )}
{invoice.description && ( {invoice.description && (
<div className="mt-6"> <div className="mt-4 rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
<div className="mb-1.5 text-[13px] text-[#888888]">توضیحات</div> <div className="mb-2 text-[13px] text-[#7D8597]">توضیحات</div>
<div className="whitespace-pre-wrap text-[15px]"> <div className="whitespace-pre-wrap text-[14px]">{invoice.description}</div>
{invoice.description}
</div>
</div> </div>
)} )}
</div>
<div className="mt-6 space-y-2"> <div className="h-fit rounded-3xl border border-border/70 bg-[#F7F9FF] p-5 shadow-sm">
<div className="flex h-12 items-center justify-between rounded-xl bg-[#F5F7FC] px-4"> <div className="mb-4 flex items-center gap-2 text-[#111827]">
<div className="text-[13px] text-[#888888]">مبلغ کل:</div> <WalletMoney size={18} color="#111827" />
<div className="text-[13px] font-semibold"> <span className="font-medium">خلاصه مالی</span>
{formatAmount(invoice.total ?? 0)}
</div> </div>
<div className="space-y-2">
<SummaryRow label="مبلغ کل" value={formatAmount(invoice.total ?? 0)} />
<SummaryRow
label="پرداخت شده"
value={formatAmount(invoice.paidAmount ?? 0)}
valueClassName="text-green-700"
/>
<SummaryRow
label="مانده"
value={formatAmount(invoice.balance ?? 0)}
valueClassName={invoice.balance > 0 ? 'text-amber-700' : 'text-[#111827]'}
/>
</div> </div>
<div className="flex h-12 items-center justify-between rounded-xl bg-[#F5F7FC] px-4"> <div className="mt-4 rounded-xl border border-border/70 bg-white px-4 py-3 text-[12px] text-[#6B7280]">
<div className="text-[13px] text-[#888888]">پرداخت شده:</div> آخرین بروزرسانی:
<div className="text-[13px] font-semibold text-green-700"> <span className="ms-1 font-medium text-[#111827]">
{formatAmount(invoice.paidAmount ?? 0)} {invoice.createdAt ? moment(invoice.createdAt).format('jYYYY/jMM/jDD HH:mm') : '-'}
</div> </span>
</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>