update invoice

This commit is contained in:
hamid zarghami
2026-02-21 14:28:32 +03:30
parent 002e45de0b
commit f2ebdc9836
8 changed files with 384 additions and 11 deletions
+1
View File
@@ -54,6 +54,7 @@ export const Paths = {
list: '/invoice/list', list: '/invoice/list',
create: '/invoice/create', create: '/invoice/create',
detail: '/invoice/', detail: '/invoice/',
update: '/invoice/update/',
}, },
newOrder: '/new-order', newOrder: '/new-order',
orderDetails: '/order/', orderDetails: '/order/',
+6 -6
View File
@@ -40,13 +40,13 @@ const CreateInvoice: FC = () => {
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]); const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]); const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
const formik = useFormik<Pick<CreatePreInvoiceType, "userId" | "enableTax" | "approvalDeadline" | "paymentMethod" | "notes">>({ const formik = useFormik<Pick<CreatePreInvoiceType, "userId" | "enableTax" | "approvalDeadline" | "paymentMethod" | "description">>({
initialValues: { initialValues: {
userId: "", userId: "",
enableTax: false, enableTax: false,
approvalDeadline: "", approvalDeadline: "",
paymentMethod: "", paymentMethod: "",
notes: "", description: "",
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
userId: !requestId userId: !requestId
@@ -87,7 +87,7 @@ const CreateInvoice: FC = () => {
enableTax: values.enableTax, enableTax: values.enableTax,
approvalDeadline: moment(values.approvalDeadline, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'), approvalDeadline: moment(values.approvalDeadline, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'),
...(values.paymentMethod ? { paymentMethod: values.paymentMethod } : {}), ...(values.paymentMethod ? { paymentMethod: values.paymentMethod } : {}),
notes: values.notes, description: values.description,
...(attachments.length ? { attachments } : {}), ...(attachments.length ? { attachments } : {}),
}; };
@@ -243,10 +243,10 @@ const CreateInvoice: FC = () => {
<Textarea <Textarea
label="توضیحات" label="توضیحات"
placeholder="توضیحات پیش فاکتور را وارد کنید..." placeholder="توضیحات پیش فاکتور را وارد کنید..."
value={formik.values.notes} value={formik.values.description}
onChange={(e) => formik.setFieldValue("notes", e.target.value)} onChange={(e) => formik.setFieldValue("description", e.target.value)}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
name="notes" name="description"
/> />
</div> </div>
+5 -3
View File
@@ -179,9 +179,11 @@ const ProformaInvoice: FC = () => {
key: 'actions', key: 'actions',
title: '', title: '',
render: (item) => ( render: (item) => (
<Link to={Paths.perfomaInvoice.detail + item.id}> <div className='flex gap-2 items-center'>
<Eye size={20} color='#8C90A3' /> <Link to={Paths.perfomaInvoice.update + item.id}>
</Link> <Eye size={20} color='#8C90A3' />
</Link>
</div>
), ),
}, },
]} ]}
+315
View File
@@ -0,0 +1,315 @@
import { useEffect, useState, type FC } from "react";
import Button from "@/components/Button";
import { 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 * as Yup from "yup";
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";
const createEmptyItem = (): CreatePreInvoiceItemType => ({
productId: "",
quantity: 1,
unitPrice: 0,
discount: 0,
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: "",
},
validationSchema: Yup.object({
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 }[] =
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,
discount: i.discount || 0,
description: i.description || "",
})),
enableTax: values.enableTax,
approvalDeadline: moment(
values.approvalDeadline,
"jYYYY/jMM/jDD"
).format("YYYY-MM-DD"),
...(values.paymentMethod ? { paymentMethod: values.paymentMethod } : {}),
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,
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
) => {
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
);
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}
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="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}
error_text={
formik.errors.paymentMethod && formik.touched.paymentMethod
? formik.errors.paymentMethod
: ""
}
/>
</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;
+26
View File
@@ -1,5 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/InvoiceService"; import * as api from "../service/InvoiceService";
import type { UpdatePreInvoiceType } from "../types/Types";
export const useCreateInvoice = () => { export const useCreateInvoice = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -18,3 +19,28 @@ export const useGetInvoice = (page = 1) => {
queryFn: () => api.getInvoice(page), queryFn: () => api.getInvoice(page),
}); });
}; };
export const useGetInvoiceDetail = (id: string | undefined) => {
return useQuery({
queryKey: ["invoice", id],
queryFn: () => api.getInvoiceById(id!),
enabled: !!id,
});
};
export const useUpdateInvoice = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
params,
}: {
id: string;
params: UpdatePreInvoiceType;
}) => api.updateInvoice(id, params),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["invoice"] });
},
});
};
+16 -1
View File
@@ -1,5 +1,10 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { CreatePreInvoiceType, GetInvoiceResponseType } from "../types/Types"; import type {
CreatePreInvoiceType,
GetInvoiceResponseType,
InvoiceType,
UpdatePreInvoiceType,
} from "../types/Types";
export const createInvoice = async (params: CreatePreInvoiceType) => { export const createInvoice = async (params: CreatePreInvoiceType) => {
const { data } = await axios.post("/admin/invoice", params); const { data } = await axios.post("/admin/invoice", params);
@@ -12,3 +17,13 @@ export const getInvoice = async (page = 1): Promise<GetInvoiceResponseType> => {
}); });
return data; return data;
}; };
export const getInvoiceById = async (id: string) => {
const { data } = await axios.get<{ data: InvoiceType }>(`/admin/invoice/${id}`);
return data;
};
export const updateInvoice = async (id: string, params: UpdatePreInvoiceType) => {
const { data } = await axios.patch(`/admin/invoice/${id}`, params);
return data;
};
+13 -1
View File
@@ -1,4 +1,5 @@
export type CreatePreInvoiceItemType = { export type CreatePreInvoiceItemType = {
id?: string;
productId: string; productId: string;
quantity: number; quantity: number;
unitPrice: number; unitPrice: number;
@@ -12,13 +13,23 @@ export type InvoiceAttachmentType = {
}; };
export type CreatePreInvoiceType = { export type CreatePreInvoiceType = {
id?: string;
requestId?: string; requestId?: string;
userId?: string; userId?: string;
items: CreatePreInvoiceItemType[]; items: CreatePreInvoiceItemType[];
enableTax: boolean; enableTax: boolean;
approvalDeadline: string; approvalDeadline: string;
paymentMethod?: string; paymentMethod?: string;
notes?: string; description?: string;
attachments?: InvoiceAttachmentType[];
};
export type UpdatePreInvoiceType = {
items: CreatePreInvoiceItemType[];
enableTax: boolean;
approvalDeadline: string;
paymentMethod?: string;
description?: string;
attachments?: InvoiceAttachmentType[]; attachments?: InvoiceAttachmentType[];
}; };
@@ -91,6 +102,7 @@ export type InvoiceType = {
approvalDeadline: string; approvalDeadline: string;
attachments: InvoiceAttachmentType[]; attachments: InvoiceAttachmentType[];
paymentMethod: string; paymentMethod: string;
description?: string;
items: InvoiceItemType[]; items: InvoiceItemType[];
}; };
+2
View File
@@ -14,6 +14,7 @@ import CreateProduct from "@/pages/product/Create";
import CreateFeature from "@/pages/Features/Create"; import CreateFeature from "@/pages/Features/Create";
import PrintService from "@/pages/service/PrintService"; import PrintService from "@/pages/service/PrintService";
import DetailPerfomaInvoice from "@/pages/invoice/DetailPerfomaInvoice"; import DetailPerfomaInvoice from "@/pages/invoice/DetailPerfomaInvoice";
import UpdateInvoice from "@/pages/invoice/Update";
import AnnouncementList from "@/pages/annoncement/List"; import AnnouncementList from "@/pages/annoncement/List";
import AnnouncementUpdate from "@/pages/annoncement/Update"; import AnnouncementUpdate from "@/pages/annoncement/Update";
import AnnouncementCreate from "@/pages/annoncement/Create"; import AnnouncementCreate from "@/pages/annoncement/Create";
@@ -63,6 +64,7 @@ const MainRouter: FC = () => {
<Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} /> <Route path={Paths.perfomaInvoice.list} element={<ProformaInvoice />} />
<Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} /> <Route path={Paths.perfomaInvoice.create} element={<CreateInvoice />} />
<Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} /> <Route path={Paths.perfomaInvoice.detail + ":id"} element={<DetailPerfomaInvoice />} />
<Route path={Paths.perfomaInvoice.update + ":id"} element={<UpdateInvoice />} />
<Route path={Paths.requests.list} element={<RequestList />} /> <Route path={Paths.requests.list} element={<RequestList />} />
<Route path={Paths.requests.detail + ":id"} element={<RequestDetail />} /> <Route path={Paths.requests.detail + ":id"} element={<RequestDetail />} />
<Route path={Paths.product.list} element={<ProductList />} /> <Route path={Paths.product.list} element={<ProductList />} />