198 lines
6.2 KiB
TypeScript
198 lines
6.2 KiB
TypeScript
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 { 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 | null,
|
||
) => void;
|
||
onRemove: () => void;
|
||
canRemove: boolean;
|
||
onConvertToOrder?: () => void;
|
||
error?: Partial<Record<keyof CreatePreInvoiceItemType, string>>;
|
||
};
|
||
|
||
const InvoiceItemRow: FC<Props> = ({
|
||
item,
|
||
index,
|
||
onChange,
|
||
onRemove,
|
||
canRemove,
|
||
onConvertToOrder,
|
||
error,
|
||
}) => {
|
||
const total = getItemLineTotal(item);
|
||
const subTotal = getItemSubTotal(item);
|
||
const isPercentDiscount = item.discountPercent != null;
|
||
const convertToOrderUrl = item.id
|
||
? `${Paths.convertToOrder}?${new URLSearchParams({
|
||
invoiceItemId: item.id,
|
||
productId: item.productId,
|
||
}).toString()}`
|
||
: "";
|
||
|
||
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">
|
||
|
||
<div className="min-w-[140px] flex-1">
|
||
<ProductsSelect
|
||
value={item.productId}
|
||
onChange={handleProductChange}
|
||
error_text={error?.productId}
|
||
/>
|
||
</div>
|
||
|
||
<div className="min-w-[140px] flex-1">
|
||
<Input
|
||
label="توضیحات"
|
||
value={item.description}
|
||
onChange={(e) => onChange(index, "description", e.target.value)}
|
||
error_text={error?.description}
|
||
/>
|
||
</div>
|
||
|
||
<div className="w-[100px]">
|
||
<Input
|
||
label="تعداد"
|
||
type="number"
|
||
min={1}
|
||
value={item.quantity || ""}
|
||
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>
|
||
|
||
<div className="w-[120px]">
|
||
<Input
|
||
label="مبلغ واحد"
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={item.unitPrice ? String(item.unitPrice) : ""}
|
||
seprator
|
||
onChange={(e) => {
|
||
const raw = String(e.target.value).replace(/,/g, "");
|
||
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-[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
|
||
type={isPercentDiscount ? "number" : "text"}
|
||
inputMode="numeric"
|
||
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, "");
|
||
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>
|
||
|
||
<div className="w-[120px]">
|
||
<Input
|
||
label="مبلغ کل"
|
||
readOnly
|
||
value={total >= 0 ? total.toLocaleString("fa-IR") : "۰"}
|
||
unit="ریال"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-2 items-end h-10">
|
||
{canRemove && (
|
||
<button
|
||
type="button"
|
||
onClick={onRemove}
|
||
className="w-10 h-10 rounded-full border border-border flex items-center justify-center flex-shrink-0 hover:bg-red-50 hover:border-red-200 transition-colors"
|
||
aria-label="حذف آیتم"
|
||
>
|
||
<Trash size={18} color="#dc2626" />
|
||
</button>
|
||
)}
|
||
{item.id && (
|
||
<Link to={convertToOrderUrl}>
|
||
<button
|
||
type="button"
|
||
onClick={onConvertToOrder}
|
||
className="h-10 px-4 rounded-xl border border-primary text-primary text-sm hover:bg-primary/5 transition-colors flex-shrink-0"
|
||
>
|
||
تبدیل به سفارش
|
||
</button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default InvoiceItemRow;
|