Files
star-erp/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx
sky121113 ac6a81b3d2
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 58s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 倉庫業務屬性、庫存成本追蹤與採購單功能更新
1. 倉庫管理:新增業務類型 (Owned/External/Customer) 與車牌資訊與司機欄位。
2. 庫存管理:實作成本追蹤 (unit_cost, total_value),更新列表與撥補單顯示。
3. 採購單:新增採購日期 (order_date),調整欄位名稱與順序。
4. 前端優化:更新相關 TS Type 定義與 UI 顯示。
2026-01-26 17:27:34 +08:00

226 lines
13 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 { SearchableSelect } from "@/Components/ui/searchable-select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order";
import { 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-[20%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead>
<TableHead className="w-[12%] text-left"></TableHead>
<TableHead className="w-[12%] 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) => {
// 計算換算後的單價 (基本單位單價)
// 單價由 小計 / 數量 推導得出
const currentUnitPrice = item.unitPrice;
const convertedUnitPrice = item.selectedUnit === 'large' && item.conversion_rate
? currentUnitPrice / item.conversion_rate
: currentUnitPrice;
return (
<TableRow key={index}>
{/* 商品選擇 */}
<TableCell>
{isReadOnly ? (
<span className="font-medium">{item.productName}</span>
) : (
<SearchableSelect
value={item.productId}
onValueChange={(value) =>
onItemChange?.(index, "productId", value)
}
disabled={isDisabled}
options={supplier?.commonProducts.map((p) => ({ label: p.productName, value: p.productId })) || []}
placeholder="選擇商品"
searchPlaceholder="搜尋商品..."
emptyText="無可用商品"
className="w-full"
/>
)}
</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="text-left w-24"
/>
)}
</TableCell>
{/* 單位選擇 */}
<TableCell>
{!isReadOnly && item.large_unit_id ? (
<SearchableSelect
value={item.selectedUnit || 'base'}
onValueChange={(value) =>
onItemChange?.(index, "selectedUnit", value)
}
disabled={isDisabled}
options={[
{ label: item.base_unit_name || "個", value: "base" },
{ label: item.large_unit_name || "", value: "large" }
]}
className="w-24"
/>
) : (
<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>
</div>
</TableCell>
{/* 換算採購單價 / 基本單位 (顯示換算結果 - SWAPPED HERE) */}
<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>
</TableCell>
{/* 總金額 (主要輸入欄位 - SWAPPED HERE) */}
<TableCell className="text-left">
{isReadOnly ? (
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
) : (
<div className="space-y-1">
<Input
type="number"
min="0"
step="1"
value={item.subtotal || ""}
onChange={(e) =>
onItemChange?.(index, "subtotal", Number(e.target.value))
}
disabled={isDisabled}
className={`text-left w-32 ${
// 如果有數量但沒有金額,顯示錯誤樣式
item.quantity > 0 && (!item.subtotal || item.subtotal <= 0)
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
: ""
}`}
/>
{/* 錯誤提示 */}
{item.quantity > 0 && (!item.subtotal || item.subtotal <= 0) && (
<p className="text-[10px] text-red-600 font-medium">
</p>
)}
</div>
)}
</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>
);
}