Files
negareh-admin/src/pages/invoice/Create.tsx
T
2026-07-08 20:42:57 +03:30

301 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState, type FC } from "react";
import Button from "@/components/Button";
import { Add, TickSquare } from "iconsax-react";
import { toast } from "@/shared/toast";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Paths } from "@/config/Paths";
import { extractErrorMessage } from "@/config/func";
import { useFormik } from "formik";
import * as Yup from "yup";
import type { CreatePreInvoiceType, CreatePreInvoiceItemType } 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 { useCreateInvoice } from "./hooks/useInvoiceData";
import { useGetRequestDetail } from "@/pages/requests/hooks/useRequestData";
import { useMultiUpload } from "../uploader/hooks/useUploader";
import moment from "moment-jalaali";
import { getItemLineTotal } from "./utils/invoiceItem";
import UserSearch from "../user/UserSearch";
const createEmptyItem = (): CreatePreInvoiceItemType => ({
productId: "",
quantity: 1,
unitPrice: 0,
discount: 0,
discountPercent: null,
description: "",
});
const CreateInvoice: FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const requestId = searchParams.get("requestId") ?? undefined;
const { mutate: submitInvoice, isPending } = useCreateInvoice();
const { data: requestData, isLoading: isLoadingRequest } = useGetRequestDetail(requestId ?? "");
const hydratedRequestId = useRef<string | null>(null);
const multiUpload = useMultiUpload()
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
const formik = useFormik<Pick<CreatePreInvoiceType, "userId" | "enableTax" | "approvalDeadline" | "paymentMethod" | "description">>({
initialValues: {
userId: "",
enableTax: false,
approvalDeadline: "",
paymentMethod: "",
description: "",
},
validationSchema: Yup.object({
userId: !requestId
? Yup.string().required("انتخاب مشتری اجباری است.")
: Yup.string(),
approvalDeadline: Yup.string().required("مهلت تایید اجباری است."),
}),
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 }[] = [];
if (attachmentFiles.length) {
const uploadResult = await multiUpload.mutateAsync(attachmentFiles);
uploadResult?.data?.forEach((item) => {
attachments.push({
type: "uploads_attach",
url: item.url,
});
});
}
const params: CreatePreInvoiceType = {
...(requestId ? { requestId } : { userId: values.userId }),
items: validItems.map((i) => ({
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 } : {}),
};
submitInvoice(params, {
onSuccess: () => {
toast("پیش فاکتور با موفقیت ثبت شد", "success");
navigate(Paths.perfomaInvoice.list);
},
onError: (error) => {
toast(extractErrorMessage(error), "error");
},
});
},
});
useEffect(() => {
const request = requestData?.data;
if (!requestId || !request || hydratedRequestId.current === requestId) return;
const mappedItems: CreatePreInvoiceItemType[] = request.items?.length
? request.items.map((item) => ({
productId: item.product?.id ?? "",
quantity: 1,
unitPrice: 0,
discount: 0,
discountPercent: null,
description: item.description ?? "",
}))
: [createEmptyItem()];
setItems(mappedItems);
hydratedRequestId.current = requestId;
}, [requestData, requestId]);
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);
return (
<div className="mt-5">
<div className="justify-between items-center flex">
<h1 className="text-lg font-light">پیش فاکتور جدید</h1>
<Button
className="w-fit px-5"
onClick={() => formik.handleSubmit()}
isLoading={isPending || multiUpload.isPending || isLoadingRequest}
>
<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>
{!requestId && (
<div className="rowTwoInput mt-6">
<div className="w-full">
<UserSearch
label="مشتری"
placeholder="جستجوی مشتری (نام، موبایل، ...)"
value={formik.values.userId}
onChange={(id) => {
formik.setFieldValue("userId", id);
formik.setFieldTouched("userId", true);
}}
/>
{formik.errors.userId && formik.touched.userId ? (
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
{formik.errors.userId}
</div>
) : null}
</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 ">
<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 CreateInvoice;