Files
star-erp/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx

244 lines
14 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
/**
*
*/
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";
2026-01-08 16:32:10 +08:00
import { formatCurrency } from "@/utils/purchase-order";
2025-12-30 15:03:19 +08:00
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">
2026-01-08 16:32:10 +08:00
<TableHead className="w-[20%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead>
2026-01-08 16:32:10 +08:00
<TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[12%] text-left"></TableHead>
2026-01-08 17:51:06 +08:00
<TableHead className="w-[15%] text-left"></TableHead>
2026-01-08 16:32:10 +08:00
<TableHead className="w-[15%] text-left"> / </TableHead>
2025-12-30 15:03:19 +08:00
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell
2026-01-08 17:51:06 +08:00
colSpan={isReadOnly ? 6 : 7}
2025-12-30 15:03:19 +08:00
className="text-center text-gray-400 py-12 italic"
>
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
</TableCell>
</TableRow>
) : (
2026-01-08 16:32:10 +08:00
items.map((item, index) => {
// 計算換算後的單價 (基本單位單價)
2026-01-08 17:51:06 +08:00
// unitPrice is derived from subtotal / quantity
const currentUnitPrice = item.unitPrice;
2026-01-08 16:32:10 +08:00
const convertedUnitPrice = item.selectedUnit === 'large' && item.conversion_rate
2026-01-08 17:51:06 +08:00
? currentUnitPrice / item.conversion_rate
: currentUnitPrice;
2025-12-30 15:03:19 +08:00
2026-01-08 16:32:10 +08:00
return (
<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>
2026-01-08 16:32:10 +08:00
{/* 數量 */}
<TableCell className="text-left">
{isReadOnly ? (
<span>{Math.floor(item.quantity)}</span>
) : (
2025-12-30 15:03:19 +08:00
<Input
type="number"
min="0"
2026-01-08 16:32:10 +08:00
step="1"
value={item.quantity === 0 ? "" : Math.floor(item.quantity)}
2025-12-30 15:03:19 +08:00
onChange={(e) =>
2026-01-08 16:32:10 +08:00
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
2025-12-30 15:03:19 +08:00
}
disabled={isDisabled}
2026-01-08 16:32:10 +08:00
className="h-10 text-left border-gray-200 w-24"
2025-12-30 15:03:19 +08:00
/>
2026-01-08 16:32:10 +08:00
)}
</TableCell>
{/* 單位選擇 */}
<TableCell>
{!isReadOnly && item.large_unit_id ? (
<Select
value={item.selectedUnit || 'base'}
onValueChange={(value) =>
onItemChange?.(index, "selectedUnit", value)
}
disabled={isDisabled}
>
<SelectTrigger className="h-10 border-gray-200 w-24">
<SelectValue />
</SelectTrigger>
<SelectContent className="z-[9999]">
<SelectItem value="base">{item.base_unit_name || "個"}</SelectItem>
<SelectItem value="large">{item.large_unit_name}</SelectItem>
</SelectContent>
</Select>
) : (
<span className="text-gray-500 font-medium">
{item.selectedUnit === 'large' && item.large_unit_name
? item.large_unit_name
: (item.base_unit_name || "個")}
</span>
)}
</TableCell>
{/* 換算基本單位 */}
<TableCell>
<div className="text-gray-700 font-medium">
<span>
{item.selectedUnit === 'large' && item.conversion_rate
? item.quantity * item.conversion_rate
: item.quantity}
</span>
<span className="ml-1 text-gray-500 text-sm">{item.base_unit_name || "個"}</span>
2025-12-30 15:03:19 +08:00
</div>
2026-01-08 16:32:10 +08:00
</TableCell>
2026-01-08 17:51:06 +08:00
{/* 總金額 (主要輸入欄位) */}
2026-01-08 16:32:10 +08:00
<TableCell className="text-left">
{isReadOnly ? (
2026-01-08 17:51:06 +08:00
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
2026-01-08 16:32:10 +08:00
) : (
<div className="space-y-1">
<Input
type="number"
min="0"
2026-01-08 17:51:06 +08:00
step="1"
value={item.subtotal || ""}
2026-01-08 16:32:10 +08:00
onChange={(e) =>
2026-01-08 17:51:06 +08:00
onItemChange?.(index, "subtotal", Number(e.target.value))
2026-01-08 16:32:10 +08:00
}
disabled={isDisabled}
className={`h-10 text-left w-32 ${
2026-01-08 17:51:06 +08:00
// 如果有數量但沒有金額,顯示錯誤樣式
item.quantity > 0 && (!item.subtotal || item.subtotal <= 0)
2026-01-08 16:32:10 +08:00
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
: "border-gray-200"
}`}
/>
2026-01-08 17:51:06 +08:00
{/* 錯誤提示 */}
{item.quantity > 0 && (!item.subtotal || item.subtotal <= 0) && (
2026-01-08 16:32:10 +08:00
<p className="text-[10px] text-red-600 font-medium">
2026-01-08 17:51:06 +08:00
2026-01-08 16:32:10 +08:00
</p>
)}
</div>
)}
</TableCell>
2025-12-30 15:03:19 +08:00
2026-01-08 17:51:06 +08:00
{/* 換算採購單價 / 基本單位 (顯示換算結果) */}
2026-01-08 16:32:10 +08:00
<TableCell className="text-left">
<div className="flex flex-col">
<div className="text-gray-500 font-medium text-sm">
{formatCurrency(convertedUnitPrice)} / {item.base_unit_name || "個"}
</div>
{convertedUnitPrice > 0 && item.previousPrice && item.previousPrice > 0 && (
<>
{convertedUnitPrice > item.previousPrice && (
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
: {formatCurrency(item.previousPrice)}
</p>
)}
{convertedUnitPrice < item.previousPrice && (
<p className="text-[10px] text-green-600 font-medium">
📉 : {formatCurrency(item.previousPrice)}
</p>
)}
</>
)}
</div>
2025-12-30 15:03:19 +08:00
</TableCell>
2026-01-08 16:32:10 +08:00
{/* 刪除按鈕 */}
{!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>
);
})
2025-12-30 15:03:19 +08:00
)}
</TableBody>
</Table>
</div>
);
}