Files
negareh-admin/src/pages/invoice/components/InvoiceItemRow.tsx
T
morteza d6a5697998
deploy to danak / build_and_deploy (push) Has been cancelled
up
2026-06-26 11:04:03 +03:30

198 lines
6.2 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 { 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;