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