design invoice
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { useState, useEffect, type FC } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import Select from "@/components/Select";
|
||||
import { 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 { useGetUsers } from "@/pages/user/hooks/useUserData";
|
||||
import { useGetRequestDetail } from "@/pages/requests/hooks/useRequestData";
|
||||
import { useCreatePreInvoice } from "./hooks/useInvoiceData";
|
||||
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";
|
||||
|
||||
const createEmptyItem = (): CreatePreInvoiceItemType => ({
|
||||
productId: "",
|
||||
quantity: 1,
|
||||
unitPrice: 0,
|
||||
discount: 0,
|
||||
description: "",
|
||||
});
|
||||
|
||||
const CreateInvoice: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const requestId = searchParams.get("requestId") ?? undefined;
|
||||
|
||||
const { mutate: submitInvoice, isPending } = useCreatePreInvoice();
|
||||
const { data: usersData } = useGetUsers();
|
||||
const { data: requestData } = useGetRequestDetail(requestId || "");
|
||||
|
||||
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
|
||||
const [, setAttachmentFiles] = useState<File[]>([]);
|
||||
|
||||
const formik = useFormik<Pick<CreatePreInvoiceType, "userId" | "enableTax" | "approvalDeadline" | "notes">>({
|
||||
initialValues: {
|
||||
userId: "",
|
||||
enableTax: false,
|
||||
approvalDeadline: "",
|
||||
notes: "",
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
userId: Yup.string().required("انتخاب مشتری اجباری است."),
|
||||
approvalDeadline: Yup.string().required("نحوه پرداخت اجباری است."),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const validItems = items.filter(
|
||||
(i) => i.productId && i.quantity > 0 && i.unitPrice >= 0
|
||||
);
|
||||
|
||||
if (validItems.length === 0) {
|
||||
toast("حداقل یک آیتم با محصول، تعداد و مبلغ واحد معتبر اضافه کنید.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const params: CreatePreInvoiceType = {
|
||||
requestId,
|
||||
userId: values.userId,
|
||||
items: validItems.map((i) => ({
|
||||
productId: i.productId,
|
||||
quantity: i.quantity,
|
||||
unitPrice: i.unitPrice,
|
||||
discount: i.discount || 0,
|
||||
description: i.description || "",
|
||||
})),
|
||||
enableTax: values.enableTax,
|
||||
approvalDeadline: values.approvalDeadline,
|
||||
notes: values.notes,
|
||||
};
|
||||
|
||||
submitInvoice(params, {
|
||||
onSuccess: () => {
|
||||
toast("پیش فاکتور با موفقیت ثبت شد", "success");
|
||||
navigate(Paths.perfomaInvoice.list);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(extractErrorMessage(error), "error");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (requestId && requestData?.data?.user?.id) {
|
||||
formik.setFieldValue("userId", String(requestData.data.user.id));
|
||||
}
|
||||
}, [requestId, requestData?.data?.user?.id]);
|
||||
|
||||
const handleItemChange = (
|
||||
index: number,
|
||||
field: keyof CreatePreInvoiceItemType,
|
||||
value: string | number
|
||||
) => {
|
||||
const next = [...items];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
setItems(next);
|
||||
};
|
||||
|
||||
const addItem = (currentIndex: number) => {
|
||||
const current = items[currentIndex];
|
||||
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 + (i.quantity || 0) * (i.unitPrice || 0) - (i.discount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const users = usersData?.data ?? [];
|
||||
|
||||
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}
|
||||
>
|
||||
<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="rowTwoInput mt-6">
|
||||
<Select
|
||||
items={users.map((u) => ({
|
||||
label: u.firstName && u.lastName ? `${u.firstName} ${u.lastName}` : u.phone,
|
||||
value: String(u.id),
|
||||
}))}
|
||||
label="مشتری"
|
||||
placeholder="انتخاب"
|
||||
value={formik.values.userId}
|
||||
onChange={(e) => formik.setFieldValue("userId", e.target.value)}
|
||||
onBlur={formik.handleBlur}
|
||||
error_text={
|
||||
formik.errors.userId && formik.touched.userId ? formik.errors.userId : ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
{items.map((item, index) => (
|
||||
<InvoiceItemRow
|
||||
key={index}
|
||||
item={item}
|
||||
index={index}
|
||||
onChange={handleItemChange}
|
||||
onAdd={() => addItem(index)}
|
||||
onRemove={() => removeItem(index)}
|
||||
canRemove={items.length > 1}
|
||||
/>
|
||||
))}
|
||||
</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="mt-6">
|
||||
<Textarea
|
||||
label="نحوه پرداخت"
|
||||
placeholder="مثال: مبلغ ۷۰ درصد پیش پرداخت / اعتبار ۲ روز"
|
||||
value={formik.values.approvalDeadline}
|
||||
onChange={(e) => formik.setFieldValue("approvalDeadline", e.target.value)}
|
||||
onBlur={formik.handleBlur}
|
||||
error_text={
|
||||
formik.errors.approvalDeadline && formik.touched.approvalDeadline
|
||||
? formik.errors.approvalDeadline
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Textarea
|
||||
label="توضیحات"
|
||||
placeholder="توضیحات پیش فاکتور را وارد کنید..."
|
||||
value={formik.values.notes}
|
||||
onChange={(e) => formik.setFieldValue("notes", e.target.value)}
|
||||
onBlur={formik.handleBlur}
|
||||
name="notes"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<UploadBox 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;
|
||||
Reference in New Issue
Block a user