178 lines
8.9 KiB
TypeScript
178 lines
8.9 KiB
TypeScript
/**
|
||
* 採購單商品表格元件
|
||
*/
|
||
|
||
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-[30%] text-left">商品名稱</TableHead>
|
||
<TableHead className="w-[15%] text-left">數量</TableHead>
|
||
<TableHead className="w-[10%] text-left">單位</TableHead>
|
||
<TableHead className="w-[20%] text-left">預估單價</TableHead>
|
||
<TableHead className="w-[20%] text-left">小計</TableHead>
|
||
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{items.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={isReadOnly ? 5 : 6}
|
||
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 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 ${isPriceAlert(item.unitPrice, item.previousPrice)
|
||
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
|
||
: "border-gray-200"
|
||
}`}
|
||
/>
|
||
{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>
|
||
);
|
||
}
|