328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import { useEffect, useState, type FC } from "react";
|
||
import Button from "@/components/Button";
|
||
import { Add, 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 { useFormik } from "formik";
|
||
import type { CreatePreInvoiceItemType, UpdatePreInvoiceType } from "./types/Types";
|
||
import InvoiceItemRow from "./components/InvoiceItemRow";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import UploadBox from "@/components/UploadBox";
|
||
import Textarea from "@/components/Textarea";
|
||
import DatePickerComponent from "@/components/DatePicker";
|
||
import { useGetInvoiceDetail, useUpdateInvoice } from "./hooks/useInvoiceData";
|
||
import { useMultiUpload } from "../uploader/hooks/useUploader";
|
||
import moment from "moment-jalaali";
|
||
import { getItemLineTotal } from "./utils/invoiceItem";
|
||
|
||
const createEmptyItem = (): CreatePreInvoiceItemType => ({
|
||
productId: "",
|
||
quantity: 1,
|
||
unitPrice: 0,
|
||
discount: 0,
|
||
discountPercent: null,
|
||
description: "",
|
||
});
|
||
|
||
const UpdateInvoice: FC = () => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
|
||
const { data: invoiceData, isLoading: isLoadingInvoice } =
|
||
useGetInvoiceDetail(id);
|
||
const { mutate: submitUpdate, isPending } = useUpdateInvoice();
|
||
const multiUpload = useMultiUpload();
|
||
|
||
const invoice = invoiceData?.data;
|
||
|
||
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([
|
||
createEmptyItem(),
|
||
]);
|
||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||
|
||
const formik = useFormik<
|
||
Pick<
|
||
UpdatePreInvoiceType,
|
||
"enableTax" | "approvalDeadline" | "paymentMethod" | "description"
|
||
>
|
||
>({
|
||
initialValues: {
|
||
enableTax: false,
|
||
approvalDeadline: "",
|
||
paymentMethod: "",
|
||
description: "",
|
||
},
|
||
onSubmit: async (values) => {
|
||
const validItems = items.filter(
|
||
(i) => i.productId && i.quantity > 0 && i.unitPrice >= 0
|
||
);
|
||
|
||
if (validItems.length === 0) {
|
||
toast(
|
||
"حداقل یک آیتم با محصول، تعداد و مبلغ واحد معتبر اضافه کنید.",
|
||
"error"
|
||
);
|
||
return;
|
||
}
|
||
|
||
const attachments: { type: string; url: string }[] =
|
||
invoice?.attachments?.map((a) => ({ type: a.type, url: a.url })) ?? [];
|
||
|
||
if (attachmentFiles.length) {
|
||
const uploadResult = await multiUpload.mutateAsync(attachmentFiles);
|
||
uploadResult?.data?.forEach((item) => {
|
||
attachments.push({
|
||
type: "uploads_attach",
|
||
url: item.url,
|
||
});
|
||
});
|
||
}
|
||
|
||
const params: UpdatePreInvoiceType = {
|
||
items: validItems.map((i) => ({
|
||
...(i.id ? { id: i.id } : {}),
|
||
productId: i.productId,
|
||
quantity: i.quantity,
|
||
unitPrice: i.unitPrice,
|
||
...(i.discountPercent != null && i.discountPercent > 0
|
||
? { discountPercent: i.discountPercent }
|
||
: { discount: i.discount || 0 }),
|
||
description: i.description || "",
|
||
})),
|
||
enableTax: values.enableTax,
|
||
...(values.approvalDeadline
|
||
? {
|
||
approvalDeadline: moment(
|
||
values.approvalDeadline,
|
||
"jYYYY/jMM/jDD"
|
||
).format("YYYY-MM-DD"),
|
||
}
|
||
: {}),
|
||
...(values.paymentMethod?.trim()
|
||
? { paymentMethod: values.paymentMethod.trim() }
|
||
: {}),
|
||
description: values.description,
|
||
...(attachments.length ? { attachments } : {}),
|
||
};
|
||
|
||
if (!id) return;
|
||
|
||
submitUpdate(
|
||
{ id, params },
|
||
{
|
||
onSuccess: () => {
|
||
toast("پیش فاکتور با موفقیت ویرایش شد", "success");
|
||
navigate(Paths.perfomaInvoice.list);
|
||
},
|
||
onError: (error) => {
|
||
toast(extractErrorMessage(error), "error");
|
||
},
|
||
}
|
||
);
|
||
},
|
||
});
|
||
|
||
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,
|
||
value: string | number | null
|
||
) => {
|
||
setItems((prev) =>
|
||
prev.map((item, itemIndex) =>
|
||
itemIndex === index ? { ...item, [field]: value } : item
|
||
)
|
||
);
|
||
};
|
||
|
||
const addItem = () => {
|
||
const current = items[items.length - 1];
|
||
const hasProduct = !!current?.productId;
|
||
const hasQuantity = (current?.quantity ?? 0) > 0;
|
||
const hasUnitPrice = (current?.unitPrice ?? 0) > 0;
|
||
|
||
if (!hasProduct || !hasQuantity || !hasUnitPrice) {
|
||
toast("لطفاً محصول، تعداد و مبلغ واحد را وارد کنید.", "error");
|
||
return;
|
||
}
|
||
setItems([...items, createEmptyItem()]);
|
||
};
|
||
|
||
const removeItem = (index: number) => {
|
||
if (items.length <= 1) return;
|
||
setItems(items.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
|
||
|
||
if (isLoadingInvoice || !invoice) {
|
||
return (
|
||
<div className="mt-5 flex items-center justify-center min-h-[200px]">
|
||
<div className="text-[#888888]">در حال بارگذاری...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="mt-5">
|
||
<div className="justify-between items-center flex">
|
||
<h1 className="text-lg font-light">
|
||
ویرایش پیش فاکتور #{invoice.invoiceNumber}
|
||
</h1>
|
||
<Button
|
||
className="w-fit px-5"
|
||
onClick={() => formik.handleSubmit()}
|
||
isLoading={isPending || multiUpload.isPending}
|
||
>
|
||
<div className="flex gap-2 items-center">
|
||
<TickSquare size={20} color="black" />
|
||
<div>ذخیره تغییرات</div>
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-3xl p-6 mt-8">
|
||
<div className="font-light">اطلاعات پیش فاکتور</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>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
{items.map((item, index) => (
|
||
<InvoiceItemRow
|
||
key={index}
|
||
item={item}
|
||
index={index}
|
||
onChange={handleItemChange}
|
||
onRemove={() => removeItem(index)}
|
||
canRemove={items.length > 1}
|
||
/>
|
||
))}
|
||
<div className="mt-4 flex justify-center">
|
||
<button
|
||
type="button"
|
||
onClick={addItem}
|
||
className="h-10 px-4 rounded-xl bg-primary flex items-center gap-2 hover:opacity-90 transition-opacity"
|
||
aria-label="افزودن آیتم"
|
||
>
|
||
<Add size={18} color="black" />
|
||
<span className="text-[13px] font-light">افزودن آیتم</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 flex gap-2 items-center">
|
||
<Checkbox
|
||
id="enableTax"
|
||
checked={formik.values.enableTax}
|
||
onCheckedChange={(checked) =>
|
||
formik.setFieldValue("enableTax", !!checked)
|
||
}
|
||
/>
|
||
<label
|
||
htmlFor="enableTax"
|
||
className="text-[13px] cursor-pointer select-none"
|
||
>
|
||
مالیات بر ارزش افزوده (۱۰ درصد)
|
||
</label>
|
||
</div>
|
||
|
||
<div className="rowTwoInput mt-6">
|
||
<DatePickerComponent
|
||
label="مهلت تایید"
|
||
placeholder="انتخاب تاریخ"
|
||
defaultValue={formik.values.approvalDeadline || undefined}
|
||
onChange={(date) => {
|
||
formik.setFieldValue("approvalDeadline", date);
|
||
formik.setFieldTouched("approvalDeadline", true);
|
||
}}
|
||
error_text={
|
||
formik.errors.approvalDeadline && formik.touched.approvalDeadline
|
||
? formik.errors.approvalDeadline
|
||
: ""
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<Textarea
|
||
label="روش پرداخت"
|
||
name="paymentMethod"
|
||
placeholder="روش پرداخت"
|
||
value={formik.values.paymentMethod}
|
||
onChange={(e) => formik.setFieldValue("paymentMethod", e.target.value)}
|
||
onBlur={formik.handleBlur}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<Textarea
|
||
label="توضیحات"
|
||
placeholder="توضیحات پیش فاکتور را وارد کنید..."
|
||
value={formik.values.description}
|
||
onChange={(e) => formik.setFieldValue("description", e.target.value)}
|
||
onBlur={formik.handleBlur}
|
||
name="description"
|
||
/>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<UploadBox
|
||
isMultiple
|
||
label="فایل ضمیمه"
|
||
onChange={setAttachmentFiles}
|
||
/>
|
||
</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]">
|
||
{totalAmount.toLocaleString("fa-IR")} ریال
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UpdateInvoice;
|