Files
star-erp/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx
sky121113 fad74df6ac
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 52s
更新採購單跟商品資料一些bug
2026-01-06 15:45:13 +08:00

199 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 採購單商品表格元件
*/
import { Trash2 } from "lucide-react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order";
import { isPriceAlert, formatCurrency } from "@/utils/purchase-order";
interface PurchaseOrderItemsTableProps {
items: PurchaseOrderItem[];
supplier?: Supplier;
isReadOnly?: boolean;
isDisabled?: boolean;
onAddItem?: () => void;
onRemoveItem?: (index: number) => void;
onItemChange?: (index: number, field: keyof PurchaseOrderItem, value: string | number) => void;
}
export function PurchaseOrderItemsTable({
items,
supplier,
isReadOnly = false,
isDisabled = false,
onRemoveItem,
onItemChange,
}: PurchaseOrderItemsTableProps) {
return (
<div className={`border rounded-lg overflow-hidden ${isDisabled ? "opacity-50 pointer-events-none grayscale" : ""}`}>
<Table>
<TableHeader>
<TableRow className="bg-gray-50 hover:bg-gray-50">
<TableHead className="w-[25%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead>
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell
colSpan={isReadOnly ? 6 : 7}
className="text-center text-gray-400 py-12 italic"
>
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
</TableCell>
</TableRow>
) : (
items.map((item, index) => (
<TableRow key={index}>
{/* 商品選擇 */}
<TableCell>
{isReadOnly ? (
<span className="font-medium">{item.productName}</span>
) : (
<Select
value={item.productId}
onValueChange={(value) =>
onItemChange?.(index, "productId", value)
}
disabled={isDisabled}
>
<SelectTrigger className="h-10 border-gray-200">
<SelectValue placeholder="選擇商品" />
</SelectTrigger>
<SelectContent>
{supplier?.commonProducts.map((product) => (
<SelectItem key={product.productId} value={product.productId}>
{product.productName}
</SelectItem>
))}
{(!supplier || supplier.commonProducts.length === 0) && (
<div className="p-2 text-sm text-gray-400 text-center"></div>
)}
</SelectContent>
</Select>
)}
</TableCell>
{/* 數量 */}
<TableCell className="text-left">
{isReadOnly ? (
<span>{Math.floor(item.quantity)}</span>
) : (
<Input
type="number"
min="0"
step="1"
value={item.quantity === 0 ? "" : Math.floor(item.quantity)}
onChange={(e) =>
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
}
disabled={isDisabled}
className="h-10 text-left border-gray-200 w-24"
/>
)}
</TableCell>
{/* 採購單位 */}
<TableCell>
<span className="text-gray-500 font-medium">{item.unit || "-"}</span>
</TableCell>
{/* 換算基本單位 */}
<TableCell>
<span className="text-gray-500 font-medium">
{item.conversion_rate && item.base_unit
? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}`
: "-"}
</span>
</TableCell>
{/* 單價 */}
<TableCell className="text-left">
{isReadOnly ? (
<span className="font-medium text-gray-900">{formatCurrency(item.unitPrice)}</span>
) : (
<div className="space-y-1">
<Input
type="number"
min="0"
step="0.1"
value={item.unitPrice || ""}
onChange={(e) =>
onItemChange?.(index, "unitPrice", Number(e.target.value))
}
disabled={isDisabled}
className={`h-10 text-left w-32 ${
// 如果有數量但沒有單價,顯示錯誤樣式
item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0)
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
: isPriceAlert(item.unitPrice, item.previousPrice)
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
: "border-gray-200"
}`}
/>
{/* 錯誤提示:有數量但沒有單價 */}
{item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && (
<p className="text-[10px] text-red-600 font-medium">
</p>
)}
{/* 價格警示:單價高於上次 */}
{item.unitPrice > 0 && isPriceAlert(item.unitPrice, item.previousPrice) && (
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
: {formatCurrency(item.previousPrice || 0)}
</p>
)}
</div>
)}
</TableCell>
{/* 小計 */}
<TableCell className="text-left">
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
</TableCell>
{/* 刪除按鈕 */}
{!isReadOnly && onRemoveItem && (
<TableCell className="text-center">
<Button
variant="ghost"
size="icon"
onClick={() => onRemoveItem(index)}
className="h-8 w-8 text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
}