+125
-64
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState, type FC } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type FC } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import { Add, TickSquare } from "iconsax-react";
|
||||
import { Add, Link, TickSquare } from "iconsax-react";
|
||||
import { toast } from "@/shared/toast";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Paths } from "@/config/Paths";
|
||||
import { extractErrorMessage } from "@/config/func";
|
||||
import { extractErrorMessage, getFileNameAndExtensionFromUrl } from "@/config/func";
|
||||
import { useFormik } from "formik";
|
||||
import type { CreatePreInvoiceItemType, UpdatePreInvoiceType } from "./types/Types";
|
||||
import InvoiceItemRow from "./components/InvoiceItemRow";
|
||||
@@ -16,6 +16,7 @@ import { useGetInvoiceDetail, useUpdateInvoice } from "./hooks/useInvoiceData";
|
||||
import { useMultiUpload } from "../uploader/hooks/useUploader";
|
||||
import moment from "moment-jalaali";
|
||||
import { getItemLineTotal } from "./utils/invoiceItem";
|
||||
import TrashWithConfrim from "@/components/TrashWithConfrim";
|
||||
|
||||
const createEmptyItem = (): CreatePreInvoiceItemType => ({
|
||||
productId: "",
|
||||
@@ -26,6 +27,17 @@ const createEmptyItem = (): CreatePreInvoiceItemType => ({
|
||||
description: "",
|
||||
});
|
||||
|
||||
const formatInvoiceUserName = (
|
||||
firstName?: string | null,
|
||||
lastName?: string | null,
|
||||
phone?: string | null
|
||||
) => {
|
||||
if (firstName && lastName) {
|
||||
return `${firstName} ${lastName}`;
|
||||
}
|
||||
return phone ?? "-";
|
||||
};
|
||||
|
||||
const UpdateInvoice: FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -37,10 +49,55 @@ const UpdateInvoice: FC = () => {
|
||||
|
||||
const invoice = invoiceData?.data;
|
||||
|
||||
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([
|
||||
createEmptyItem(),
|
||||
]);
|
||||
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
|
||||
const [currentAttachments, setCurrentAttachments] = useState<
|
||||
{ type: string; url: string }[]
|
||||
>([]);
|
||||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||||
const hydratedInvoiceId = useRef<string | null>(null);
|
||||
|
||||
const initialFormValues = useMemo(
|
||||
() => ({
|
||||
enableTax: invoice?.enableTax ?? false,
|
||||
approvalDeadline: invoice?.approvalDeadline
|
||||
? moment(invoice.approvalDeadline).format("jYYYY/jMM/jDD")
|
||||
: "",
|
||||
paymentMethod: invoice?.paymentMethod ?? "",
|
||||
description: invoice?.description ?? "",
|
||||
}),
|
||||
[invoice]
|
||||
);
|
||||
|
||||
const initialItems = useMemo<CreatePreInvoiceItemType[]>(
|
||||
() =>
|
||||
invoice?.items?.length
|
||||
? invoice.items.map((item) => ({
|
||||
id: item.id,
|
||||
productId: item.product?.id ?? "",
|
||||
quantity: item.quantity ?? 1,
|
||||
unitPrice: item.unitPrice ?? 0,
|
||||
discount: item.discount ?? 0,
|
||||
discountPercent:
|
||||
item.discountPercent != null && item.discountPercent > 0
|
||||
? item.discountPercent
|
||||
: null,
|
||||
description: item.description ?? "",
|
||||
}))
|
||||
: [createEmptyItem()],
|
||||
[invoice]
|
||||
);
|
||||
|
||||
const initialAttachments = useMemo(
|
||||
() => invoice?.attachments?.map((attachment) => ({ ...attachment })) ?? [],
|
||||
[invoice]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!invoice || hydratedInvoiceId.current === invoice.id) return;
|
||||
setItems(initialItems);
|
||||
setCurrentAttachments(initialAttachments);
|
||||
hydratedInvoiceId.current = invoice.id;
|
||||
}, [invoice, initialAttachments, initialItems]);
|
||||
|
||||
const formik = useFormik<
|
||||
Pick<
|
||||
@@ -48,12 +105,8 @@ const UpdateInvoice: FC = () => {
|
||||
"enableTax" | "approvalDeadline" | "paymentMethod" | "description"
|
||||
>
|
||||
>({
|
||||
initialValues: {
|
||||
enableTax: false,
|
||||
approvalDeadline: "",
|
||||
paymentMethod: "",
|
||||
description: "",
|
||||
},
|
||||
initialValues: initialFormValues,
|
||||
enableReinitialize: true,
|
||||
onSubmit: async (values) => {
|
||||
const validItems = items.filter(
|
||||
(i) => i.productId && i.quantity > 0 && i.unitPrice >= 0
|
||||
@@ -67,8 +120,7 @@ const UpdateInvoice: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments: { type: string; url: string }[] =
|
||||
invoice?.attachments?.map((a) => ({ type: a.type, url: a.url })) ?? [];
|
||||
const attachments = [...currentAttachments];
|
||||
|
||||
if (attachmentFiles.length) {
|
||||
const uploadResult = await multiUpload.mutateAsync(attachmentFiles);
|
||||
@@ -124,36 +176,6 @@ const UpdateInvoice: FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (invoice) {
|
||||
formik.setValues({
|
||||
enableTax: invoice.enableTax ?? false,
|
||||
approvalDeadline: invoice.approvalDeadline
|
||||
? moment(invoice.approvalDeadline).format("jYYYY/jMM/jDD")
|
||||
: "",
|
||||
paymentMethod: invoice.paymentMethod ?? "",
|
||||
description: invoice.description ?? "",
|
||||
});
|
||||
|
||||
const mappedItems: CreatePreInvoiceItemType[] =
|
||||
invoice.items?.length > 0
|
||||
? invoice.items.map((i) => ({
|
||||
id: i.id,
|
||||
productId: i.product?.id ?? "",
|
||||
quantity: i.quantity ?? 1,
|
||||
unitPrice: i.unitPrice ?? 0,
|
||||
discount: i.discount ?? 0,
|
||||
discountPercent:
|
||||
i.discountPercent != null && i.discountPercent > 0 ? i.discountPercent : null,
|
||||
description: i.description ?? "",
|
||||
}))
|
||||
: [createEmptyItem()];
|
||||
|
||||
setItems(mappedItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [invoice]);
|
||||
|
||||
const handleItemChange = (
|
||||
index: number,
|
||||
field: keyof CreatePreInvoiceItemType,
|
||||
@@ -184,7 +206,12 @@ const UpdateInvoice: FC = () => {
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const removeCurrentAttachment = (url: string) => {
|
||||
setCurrentAttachments((prev) => prev.filter((attachment) => attachment.url !== url));
|
||||
};
|
||||
|
||||
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
|
||||
const isSubmitting = isPending || multiUpload.isPending;
|
||||
|
||||
if (isLoadingInvoice || !invoice) {
|
||||
return (
|
||||
@@ -196,14 +223,17 @@ const UpdateInvoice: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="justify-between items-center flex">
|
||||
<h1 className="text-lg font-light">
|
||||
ویرایش پیش فاکتور #{invoice.invoiceNumber}
|
||||
</h1>
|
||||
<div className="justify-between items-center flex flex-wrap gap-4">
|
||||
<div>
|
||||
<h1 className="text-lg font-light">ویرایش پیش فاکتور #{invoice.invoiceNumber}</h1>
|
||||
<p className="text-xs text-[#888888] mt-1">
|
||||
اطلاعات مشتری، آیتمها و شرایط پرداخت را بهروزرسانی کنید.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="w-fit px-5"
|
||||
className="w-fit px-5 h-11"
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isPending || multiUpload.isPending}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<TickSquare size={20} color="black" />
|
||||
@@ -212,16 +242,24 @@ const UpdateInvoice: FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl p-6 mt-8">
|
||||
<div className="font-light">اطلاعات پیش فاکتور</div>
|
||||
<div className="bg-white rounded-3xl p-6 mt-8 border border-[#EEF2FF] shadow-sm">
|
||||
<div className="font-light text-base">اطلاعات پیش فاکتور</div>
|
||||
|
||||
<div className="mt-6 rowTwoInput">
|
||||
<div className="opacity-70">
|
||||
<div className="text-[13px] text-[#888888] mb-1.5">مشتری</div>
|
||||
<div className="text-[15px]">
|
||||
{invoice.user?.firstName && invoice.user?.lastName
|
||||
? `${invoice.user.firstName} ${invoice.user.lastName}`
|
||||
: invoice.user?.phone ?? "-"}
|
||||
<div className="mt-6 rounded-2xl bg-[#F8FAFF] border border-[#E8EEFF] p-4 rowTwoInput gap-4">
|
||||
<div>
|
||||
<div className="text-[12px] text-[#888888] mb-1">مشتری</div>
|
||||
<div className="text-[14px] font-medium">
|
||||
{formatInvoiceUserName(
|
||||
invoice.user?.firstName,
|
||||
invoice.user?.lastName,
|
||||
invoice.user?.phone
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[12px] text-[#888888] mb-1">تاریخ ایجاد</div>
|
||||
<div className="text-[14px] font-medium">
|
||||
{moment(invoice.createdAt).format("jYYYY/jMM/jDD")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +267,7 @@ const UpdateInvoice: FC = () => {
|
||||
<div className="mt-6">
|
||||
{items.map((item, index) => (
|
||||
<InvoiceItemRow
|
||||
key={index}
|
||||
key={item.id ?? index}
|
||||
item={item}
|
||||
index={index}
|
||||
onChange={handleItemChange}
|
||||
@@ -250,7 +288,7 @@ const UpdateInvoice: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-2 items-center">
|
||||
<div className="mt-8 pt-6 border-t border-[#F0F0F0] flex gap-2 items-center">
|
||||
<Checkbox
|
||||
id="enableTax"
|
||||
checked={formik.values.enableTax}
|
||||
@@ -313,9 +351,32 @@ const UpdateInvoice: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-[#F5F7FC] h-12 px-4 flex justify-between items-center rounded-xl">
|
||||
<div className="text-[#888888] text-[13px]">مبلغ کل:</div>
|
||||
<div className="font-semibold text-[13px]">
|
||||
{currentAttachments.length ? (
|
||||
<div className="mt-6">
|
||||
<div className="text-[13px] text-[#888888] mb-2">پیوستهای فعلی</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{currentAttachments.map((attachment) => (
|
||||
<div
|
||||
key={attachment.url}
|
||||
className="flex px-3 py-2 rounded-lg items-center gap-2 text-xs text-[#0037FF] border border-[#0037FF]"
|
||||
>
|
||||
<Link size={16} color="#0037FF" />
|
||||
<div>{getFileNameAndExtensionFromUrl(attachment.url).fileName}</div>
|
||||
<div className="w-8 flex justify-end">
|
||||
<TrashWithConfrim
|
||||
colorIcon="red"
|
||||
onDelete={() => removeCurrentAttachment(attachment.url)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 bg-[#F5F7FC] h-14 px-4 flex justify-between items-center rounded-xl border border-[#E6EBFA]">
|
||||
<div className="text-[#5F6782] text-[13px]">مبلغ کل آیتمها:</div>
|
||||
<div className="font-semibold text-[14px] text-[#1F2A60]">
|
||||
{totalAmount.toLocaleString("fa-IR")} ریال
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user