discount
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, type FC } from "react";
|
||||
import { useEffect, useRef, useState, type FC } from "react";
|
||||
import Button from "@/components/Button";
|
||||
import Select from "@/components/Select";
|
||||
import { TickSquare } from "iconsax-react";
|
||||
@@ -16,14 +16,17 @@ 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";
|
||||
|
||||
const createEmptyItem = (): CreatePreInvoiceItemType => ({
|
||||
productId: "",
|
||||
quantity: 1,
|
||||
unitPrice: 0,
|
||||
discount: 0,
|
||||
discountPercent: null,
|
||||
description: "",
|
||||
});
|
||||
|
||||
@@ -35,6 +38,8 @@ const CreateInvoice: FC = () => {
|
||||
|
||||
const { mutate: submitInvoice, isPending } = useCreateInvoice();
|
||||
const { data: usersData } = useGetUsers();
|
||||
const { data: requestData, isLoading: isLoadingRequest } = useGetRequestDetail(requestId ?? "");
|
||||
const hydratedRequestId = useRef<string | null>(null);
|
||||
const multiUpload = useMultiUpload()
|
||||
|
||||
const [items, setItems] = useState<CreatePreInvoiceItemType[]>([createEmptyItem()]);
|
||||
@@ -81,7 +86,9 @@ const CreateInvoice: FC = () => {
|
||||
productId: i.productId,
|
||||
quantity: i.quantity,
|
||||
unitPrice: i.unitPrice,
|
||||
discount: i.discount || 0,
|
||||
...(i.discountPercent != null && i.discountPercent > 0
|
||||
? { discountPercent: i.discountPercent }
|
||||
: { discount: i.discount || 0 }),
|
||||
description: i.description || "",
|
||||
})),
|
||||
enableTax: values.enableTax,
|
||||
@@ -103,14 +110,35 @@ const CreateInvoice: FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
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: item.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
|
||||
value: string | number | null
|
||||
) => {
|
||||
const next = [...items];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
setItems(next);
|
||||
setItems((prev) =>
|
||||
prev.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, [field]: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const addItem = (currentIndex: number) => {
|
||||
@@ -131,10 +159,7 @@ const CreateInvoice: FC = () => {
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const totalAmount = items.reduce(
|
||||
(sum, i) => sum + (i.quantity || 0) * (i.unitPrice || 0) - (i.discount || 0),
|
||||
0
|
||||
);
|
||||
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
|
||||
|
||||
const users = usersData?.data ?? [];
|
||||
|
||||
@@ -145,7 +170,7 @@ const CreateInvoice: FC = () => {
|
||||
<Button
|
||||
className="w-fit px-5"
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isPending || multiUpload.isPending}
|
||||
isLoading={isPending || multiUpload.isPending || isLoadingRequest}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<TickSquare size={20} color="black" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Edit } from 'iconsax-react';
|
||||
import { Paths } from '@/config/Paths';
|
||||
import { useGetInvoiceDetail } from './hooks/useInvoiceData';
|
||||
import InvoicePaymentsSection from './components/InvoicePaymentsSection';
|
||||
import { formatItemDiscountDisplay } from './utils/invoiceItem';
|
||||
|
||||
const formatAmount = (amount: number) =>
|
||||
Number(amount).toLocaleString('fa-IR') + ' ریال';
|
||||
@@ -78,16 +79,16 @@ const DetailPerfomaInvoice: FC = () => {
|
||||
<th className="pb-3 font-normal">توضیحات</th>
|
||||
<th className="pb-3 font-normal">تعداد</th>
|
||||
<th className="pb-3 font-normal">مبلغ واحد</th>
|
||||
<th className="pb-3 font-normal">تخفیف</th>
|
||||
<th className="pb-3 font-normal">تخفیف (مبلغ)</th>
|
||||
<th className="pb-3 font-normal">تخفیف (درصد)</th>
|
||||
<th className="pb-3 font-normal">مبلغ کل</th>
|
||||
<th className="pb-3 font-normal">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoice.items?.map((item) => {
|
||||
const lineTotal =
|
||||
(item.quantity || 0) * (item.unitPrice || 0) -
|
||||
(item.discount || 0);
|
||||
const discounts = formatItemDiscountDisplay(item);
|
||||
const lineTotal = item.total ?? 0;
|
||||
return (
|
||||
<tr key={item.id} className="border-b border-border/50">
|
||||
<td className="py-3">{item.product?.title ?? '-'}</td>
|
||||
@@ -98,11 +99,8 @@ const DetailPerfomaInvoice: FC = () => {
|
||||
? formatAmount(item.unitPrice)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{item.discount != null
|
||||
? formatAmount(item.discount)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3">{discounts.value}</td>
|
||||
<td className="py-3">{discounts.percent}</td>
|
||||
<td className="py-3">{formatAmount(lineTotal)}</td>
|
||||
<td className="py-3">
|
||||
{item.id && (
|
||||
|
||||
@@ -16,12 +16,14 @@ 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: "",
|
||||
});
|
||||
|
||||
@@ -88,7 +90,9 @@ const UpdateInvoice: FC = () => {
|
||||
productId: i.productId,
|
||||
quantity: i.quantity,
|
||||
unitPrice: i.unitPrice,
|
||||
discount: i.discount || 0,
|
||||
...(i.discountPercent != null && i.discountPercent > 0
|
||||
? { discountPercent: i.discountPercent }
|
||||
: { discount: i.discount || 0 }),
|
||||
description: i.description || "",
|
||||
})),
|
||||
enableTax: values.enableTax,
|
||||
@@ -137,6 +141,8 @@ const UpdateInvoice: FC = () => {
|
||||
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()];
|
||||
@@ -149,11 +155,13 @@ const UpdateInvoice: FC = () => {
|
||||
const handleItemChange = (
|
||||
index: number,
|
||||
field: keyof CreatePreInvoiceItemType,
|
||||
value: string | number
|
||||
value: string | number | null
|
||||
) => {
|
||||
const next = [...items];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
setItems(next);
|
||||
setItems((prev) =>
|
||||
prev.map((item, itemIndex) =>
|
||||
itemIndex === index ? { ...item, [field]: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const addItem = (currentIndex: number) => {
|
||||
@@ -174,10 +182,7 @@ const UpdateInvoice: FC = () => {
|
||||
setItems(items.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const totalAmount = items.reduce(
|
||||
(sum, i) => sum + (i.quantity || 0) * (i.unitPrice || 0) - (i.discount || 0),
|
||||
0
|
||||
);
|
||||
const totalAmount = items.reduce((sum, i) => sum + getItemLineTotal(i), 0);
|
||||
|
||||
if (isLoadingInvoice || !invoice) {
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { type FC, type ChangeEvent } from "react";
|
||||
import Input from "@/components/Input";
|
||||
import SwitchComponent from "@/components/Switch";
|
||||
import ProductsSelect from "@/pages/order/components/ProductsSelect";
|
||||
import type { CreatePreInvoiceItemType } from "../types/Types";
|
||||
import {
|
||||
clampDiscountPercent,
|
||||
clampDiscountValue,
|
||||
getItemLineTotal,
|
||||
getItemSubTotal,
|
||||
} from "../utils/invoiceItem";
|
||||
import { Add, Trash } from "iconsax-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Paths } from "@/config/Paths";
|
||||
type Props = {
|
||||
item: CreatePreInvoiceItemType;
|
||||
index: number;
|
||||
onChange: (index: number, field: keyof CreatePreInvoiceItemType, value: string | number) => void;
|
||||
onChange: (
|
||||
index: number,
|
||||
field: keyof CreatePreInvoiceItemType,
|
||||
value: string | number | null,
|
||||
) => void;
|
||||
onAdd: () => void;
|
||||
onRemove: () => void;
|
||||
canRemove: boolean;
|
||||
@@ -26,12 +37,25 @@ const InvoiceItemRow: FC<Props> = ({
|
||||
onConvertToOrder,
|
||||
error,
|
||||
}) => {
|
||||
const total = (item.quantity || 0) * (item.unitPrice || 0) - (item.discount || 0);
|
||||
const total = getItemLineTotal(item);
|
||||
const subTotal = getItemSubTotal(item);
|
||||
const isPercentDiscount = item.discountPercent != null;
|
||||
|
||||
const handleProductChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange(index, "productId", e.target.value);
|
||||
};
|
||||
|
||||
const handleDiscountTypeChange = (isPercent: boolean) => {
|
||||
if (isPercent) {
|
||||
onChange(index, "discount", 0);
|
||||
onChange(index, "discountPercent", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(index, "discountPercent", null);
|
||||
onChange(index, "discount", 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-end gap-4 mt-6">
|
||||
|
||||
@@ -58,9 +82,14 @@ const InvoiceItemRow: FC<Props> = ({
|
||||
type="number"
|
||||
min={1}
|
||||
value={item.quantity || ""}
|
||||
onChange={(e) =>
|
||||
onChange(index, "quantity", e.target.value ? Number(e.target.value) : 0)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const nextQuantity = e.target.value ? Number(e.target.value) : 0;
|
||||
const nextSubTotal = nextQuantity * (item.unitPrice || 0);
|
||||
onChange(index, "quantity", nextQuantity);
|
||||
if (!isPercentDiscount) {
|
||||
onChange(index, "discount", clampDiscountValue(item.discount, nextSubTotal));
|
||||
}
|
||||
}}
|
||||
error_text={error?.quantity}
|
||||
/>
|
||||
</div>
|
||||
@@ -74,23 +103,54 @@ const InvoiceItemRow: FC<Props> = ({
|
||||
seprator
|
||||
onChange={(e) => {
|
||||
const raw = String(e.target.value).replace(/,/g, "");
|
||||
onChange(index, "unitPrice", raw ? Number(raw) : 0);
|
||||
const nextUnitPrice = raw ? Number(raw) : 0;
|
||||
const nextSubTotal = (item.quantity || 0) * nextUnitPrice;
|
||||
onChange(index, "unitPrice", nextUnitPrice);
|
||||
if (!isPercentDiscount) {
|
||||
onChange(index, "discount", clampDiscountValue(item.discount, nextSubTotal));
|
||||
}
|
||||
}}
|
||||
error_text={error?.unitPrice}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[120px]">
|
||||
<div className="w-[150px]">
|
||||
<div className="mb-1 flex items-center justify-between gap-2 text-sm">
|
||||
<span>تخفیف </span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={isPercentDiscount ? "text-[11px] text-black" : "text-[11px] text-[#888888]"}>
|
||||
درصد
|
||||
</span>
|
||||
<SwitchComponent active={isPercentDiscount} onChange={handleDiscountTypeChange} />
|
||||
<span className={!isPercentDiscount ? "text-[11px] text-black" : "text-[11px] text-[#888888]"}>
|
||||
مبلغ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label="تخفیف"
|
||||
type="text"
|
||||
type={isPercentDiscount ? "number" : "text"}
|
||||
inputMode="numeric"
|
||||
value={item.discount ? String(item.discount) : ""}
|
||||
seprator
|
||||
min={0}
|
||||
max={isPercentDiscount ? 100 : subTotal}
|
||||
value={
|
||||
isPercentDiscount
|
||||
? item.discountPercent ? String(item.discountPercent) : ""
|
||||
: item.discount ? String(item.discount) : ""
|
||||
}
|
||||
seprator={!isPercentDiscount}
|
||||
onChange={(e) => {
|
||||
const raw = String(e.target.value).replace(/,/g, "");
|
||||
onChange(index, "discount", raw ? Number(raw) : 0);
|
||||
const value = raw ? Number(raw) : 0;
|
||||
if (isPercentDiscount) {
|
||||
onChange(index, "discountPercent", clampDiscountPercent(value));
|
||||
onChange(index, "discount", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(index, "discount", clampDiscountValue(value, subTotal));
|
||||
onChange(index, "discountPercent", null);
|
||||
}}
|
||||
unit={isPercentDiscount ? "٪" : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,9 +4,20 @@ export type CreatePreInvoiceItemType = {
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
discount: number;
|
||||
discountPercent?: number | null;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type InvoiceItemSubmitType = {
|
||||
id?: string;
|
||||
productId: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
description: string;
|
||||
discount?: number;
|
||||
discountPercent?: number;
|
||||
};
|
||||
|
||||
export type InvoiceAttachmentType = {
|
||||
type: string;
|
||||
url: string;
|
||||
@@ -16,7 +27,7 @@ export type CreatePreInvoiceType = {
|
||||
id?: string;
|
||||
requestId?: string;
|
||||
userId?: string;
|
||||
items: CreatePreInvoiceItemType[];
|
||||
items: InvoiceItemSubmitType[];
|
||||
enableTax: boolean;
|
||||
approvalDeadline: string;
|
||||
paymentMethod?: string;
|
||||
@@ -25,7 +36,7 @@ export type CreatePreInvoiceType = {
|
||||
};
|
||||
|
||||
export type UpdatePreInvoiceType = {
|
||||
items: CreatePreInvoiceItemType[];
|
||||
items: InvoiceItemSubmitType[];
|
||||
enableTax: boolean;
|
||||
approvalDeadline: string;
|
||||
paymentMethod?: string;
|
||||
@@ -80,6 +91,7 @@ export type InvoiceItemType = {
|
||||
unitPrice: number;
|
||||
subTotal: number | null;
|
||||
discount: number | null;
|
||||
discountPercent: number | null;
|
||||
total: number | null;
|
||||
description: string;
|
||||
confirmedAt: string | null;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { CreatePreInvoiceItemType } from '../types/Types';
|
||||
|
||||
export function getItemSubTotal(
|
||||
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice'>,
|
||||
): number {
|
||||
return (item.quantity || 0) * (item.unitPrice || 0);
|
||||
}
|
||||
|
||||
export function clampDiscountValue(value: number, itemPrice: number): number {
|
||||
return Math.min(Math.max(value || 0, 0), Math.max(itemPrice || 0, 0));
|
||||
}
|
||||
|
||||
export function clampDiscountPercent(value: number): number {
|
||||
return Math.min(Math.max(value || 0, 0), 100);
|
||||
}
|
||||
|
||||
export function getItemDiscountAmount(
|
||||
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice' | 'discount' | 'discountPercent'>,
|
||||
): number {
|
||||
const subTotal = getItemSubTotal(item);
|
||||
if (item.discountPercent != null && item.discountPercent > 0) {
|
||||
return Math.round((subTotal * clampDiscountPercent(item.discountPercent)) / 100);
|
||||
}
|
||||
return clampDiscountValue(item.discount || 0, subTotal);
|
||||
}
|
||||
|
||||
export function getItemLineTotal(
|
||||
item: Pick<CreatePreInvoiceItemType, 'quantity' | 'unitPrice' | 'discount' | 'discountPercent'>,
|
||||
): number {
|
||||
const subTotal = getItemSubTotal(item);
|
||||
return subTotal - getItemDiscountAmount(item);
|
||||
}
|
||||
|
||||
export function formatItemDiscountDisplay(item: {
|
||||
discount?: number | null;
|
||||
discountPercent?: number | null;
|
||||
}): { value: string; percent: string } {
|
||||
return {
|
||||
value:
|
||||
item.discount != null && item.discount > 0
|
||||
? Number(item.discount).toLocaleString('fa-IR') + ' ریال'
|
||||
: '-',
|
||||
percent:
|
||||
item.discountPercent != null && item.discountPercent > 0
|
||||
? `${item.discountPercent}٪`
|
||||
: '-',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user