feat: 整合門市領料日誌、API 文件存取、修改庫存與併發編號問題、供應商商品內聯編輯及日誌 UI 優化
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s

This commit is contained in:
2026-03-02 16:42:12 +08:00
parent 7dac2d1f77
commit 0a955fb993
33 changed files with 1424 additions and 853 deletions

View File

@@ -168,6 +168,28 @@ const fieldLabels: Record<string, string> = {
posted_by: '過帳者',
counted_qty: '盤點數量',
adjust_qty: '調整數量',
// 調撥單專有欄位
transit_warehouse_id: '在途倉庫',
transit_warehouse_name: '在途倉庫名稱',
dispatched_at: '出貨日期',
dispatched_by: '出貨人',
received_at: '收貨日期',
received_by: '收貨人',
reserved_quantity: '預扣數量',
snapshot_quantity: '異動前庫存 (快照)',
// 門市叫貨欄位
store_warehouse_id: '申請倉庫',
store_warehouse_name: '申請倉庫',
supply_warehouse_id: '供貨倉庫',
supply_warehouse_name: '供貨倉庫',
approved_by: '審核人',
approved_user_name: '審核人',
approved_at: '審核時間',
submitted_at: '提交時間',
reject_reason: '駁回原因',
status_label: '處理狀態',
transfer_order_id: '調撥單 ID',
transfer_order_name: '調撥單號',
};
// 狀態翻譯對照表
@@ -192,6 +214,7 @@ const statusMap: Record<string, string> = {
in_progress: '生產中',
// 調撥單狀態
voided: '已作廢',
dispatched: '已出貨',
};
// 主體類型解析 (Model 類名轉中文)
@@ -206,17 +229,7 @@ const subjectTypeMap: Record<string, string> = {
'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單',
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
'App\\Modules\\Core\\Models\\User': '使用者帳號',
'App\\Modules\\Core\\Models\\Role': '角色權限',
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
// 簡寫映射 (應對後端回傳 class_basename 的情況)
'Product': '商品資料',
'Warehouse': '倉庫資料',
@@ -231,7 +244,8 @@ const subjectTypeMap: Record<string, string> = {
'Recipe': '生產配方',
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫調撥單',
'InventoryTransferOrder': '庫調撥單',
'StoreRequisition': '門市叫貨單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',
@@ -264,17 +278,31 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
// 自訂欄位排序順序
const sortOrder = [
'doc_no', 'po_number', 'gr_number', 'production_number',
'doc_no', 'po_number', 'gr_number', 'production_number', 'transfer_order_name',
'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark',
'invoice_number', 'invoice_date', 'invoice_amount',
'total_amount', 'tax_amount', 'grand_total'
];
// 過濾掉通常會記錄但對使用者無用的內部鍵
// 過濾掉通常會記錄但對使用者無用的內部鍵,以及已被解析為名稱的原始 ID 欄位
const filteredKeys = allKeys
.filter(key =>
!['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token'].includes(key)
)
.filter(key => {
if (['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token', 'activityProperties'].includes(key)) return false;
// 隱藏冗餘的狀態標籤 (因為後端已統一替換 status 內容)
if (key === 'status_label') return false;
// 隱藏技術用的 ID 欄位 (如果已有對應的名稱欄位)
if (key.endsWith('_id')) {
const nameKey = key.replace('_id', '_name');
const userNameKey = key.replace('_id', '_user_name');
if (allKeys.includes(nameKey) || allKeys.includes(userNameKey)) return false;
}
// 特別隱藏調撥單 ID
if (key === 'transfer_order_id' && (allKeys.includes('transfer_order_name') || allKeys.includes('transfer_order_doc_no'))) return false;
return true;
})
.sort((a, b) => {
const indexA = sortOrder.indexOf(a);
const indexB = sortOrder.indexOf(b);
@@ -336,9 +364,13 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') {
return value.split('T')[0].split(' ')[0];
}
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time') && typeof value === 'string') {
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time' || key === 'submitted_at' || key === 'approved_at') && typeof value === 'string') {
try {
const date = new Date(value);
// 處理部分 ISO 字串包含 T 的情況
const normalizedValue = value.replace('T', ' ');
const date = new Date(normalizedValue);
if (isNaN(date.getTime())) return value;
return date.toLocaleString('zh-TW', {
timeZone: 'Asia/Taipei',
year: 'numeric',
@@ -537,65 +569,89 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
</TableRow>
</TableHeader>
<TableBody>
{/* 更新項目 */}
{activity.properties.items_diff.updated?.map((item: any, idx: number) => (
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200"></Badge>
{/* 1. 處理物件格式的 items_diff (現有的採購、盤點、調撥初始紀錄格式) */}
{!Array.isArray(activity.properties?.items_diff) && (
<>
{/* 更新項目 */}
{activity.properties?.items_diff?.updated?.map((item: any, idx: number) => (
<TableRow key={`upd-${idx}`} className="bg-blue-50/10 hover:bg-blue-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200"></Badge>
</TableCell>
<TableCell className="text-sm">
<div className="space-y-1 text-xs">
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.quantity}</span> <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
)}
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
)}
</div>
</TableCell>
</TableRow>
)) || null}
{/* 新增項目 */}
{activity.properties?.items_diff?.added?.map((item: any, idx: number) => (
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"></Badge>
</TableCell>
<TableCell className="text-sm">
: {item.new?.quantity ?? item.quantity} {item.unit_name || item.new?.unit_name || ''}
</TableCell>
</TableRow>
)) || null}
{/* 移除項目 */}
{activity.properties?.items_diff?.removed?.map((item: any, idx: number) => (
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200"></Badge>
</TableCell>
<TableCell className="text-sm text-gray-400">
: {item.old?.quantity ?? item.quantity} {item.unit_name || item.old?.unit_name || ''}
</TableCell>
</TableRow>
)) || null}
</>
)}
{/* 2. 處理陣列格式的 items_diff (調撥單過帳/收貨的複合紀錄格式) */}
{Array.isArray(activity.properties?.items_diff) && activity.properties.items_diff.map((item: any, idx: number) => (
<TableRow key={`trf-${idx}`} className="hover:bg-gray-50/50">
<TableCell className="font-medium">
{item.product_name}
<div className="text-[10px] text-gray-400 font-normal">: {item.batch_number}</div>
</TableCell>
<TableCell className="text-sm">
<div className="space-y-1">
{item.old?.quantity !== item.new?.quantity && item.old?.quantity !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.quantity}</span> <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
<TableCell className="text-center">
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-200"></Badge>
</TableCell>
<TableCell className="text-xs p-2">
<div className="flex flex-col gap-2">
{item.source_warehouse && (
<div className="flex items-center gap-2">
<span className="w-16 text-gray-400 truncate"> ({item.source_warehouse}):</span>
<span className="text-gray-400 line-through">{Number(item.source_before || 0).toFixed(0)}</span>
<span className="text-rose-600 font-bold"> {Number(item.source_after || 0).toFixed(0)}</span>
<span className="text-[10px] bg-rose-50 text-rose-600 px-1 rounded">-{item.quantity}</span>
</div>
)}
{item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
)}
{item.old?.adjust_qty !== item.new?.adjust_qty && (
<div>調: <span className="text-gray-500 line-through">{item.old?.adjust_qty ?? '0'}</span> <span className="text-blue-700 font-bold">{item.new?.adjust_qty ?? '0'}</span></div>
)}
{item.old?.unit_name !== item.new?.unit_name && item.old?.unit_name !== undefined && (
<div>: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
)}
{item.old?.subtotal !== item.new?.subtotal && item.old?.subtotal !== undefined && (
<div>: <span className="text-gray-500 line-through">${item.old.subtotal}</span> <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
)}
{item.old?.notes !== item.new?.notes && (
<div>: <span className="text-gray-500 line-through">{item.old?.notes || '-'}</span> <span className="text-blue-700 font-bold">{item.new?.notes || '-'}</span></div>
{item.target_warehouse && (
<div className="flex items-center gap-2">
<span className="w-16 text-gray-400 truncate"> ({item.target_warehouse}):</span>
<span className="text-gray-400 line-through">{Number(item.target_before || 0).toFixed(0)}</span>
<span className="text-emerald-600 font-bold"> {Number(item.target_after || 0).toFixed(0)}</span>
<span className="text-[10px] bg-emerald-50 text-emerald-600 px-1 rounded">+{item.quantity}</span>
</div>
)}
</div>
</TableCell>
</TableRow>
)) || null}
{/* 新增項目 */}
{activity.properties.items_diff.added?.map((item: any, idx: number) => (
<TableRow key={`add-${idx}`} className="bg-green-50/10 hover:bg-green-50/20">
<TableCell className="font-medium">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200"></Badge>
</TableCell>
<TableCell className="text-sm">
{item.quantity !== undefined ? `數量: ${item.quantity} ${item.unit_name || ''} / ` : ''}
{item.adjust_qty !== undefined ? `調整量: ${item.adjust_qty} / ` : ''}
{item.subtotal !== undefined ? `小計: $${item.subtotal}` : ''}
</TableCell>
</TableRow>
)) || null}
{/* 移除項目 */}
{activity.properties.items_diff.removed?.map((item: any, idx: number) => (
<TableRow key={`rem-${idx}`} className="bg-red-50/10 hover:bg-red-50/20">
<TableCell className="font-medium text-gray-400 line-through">{item.product_name}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200"></Badge>
</TableCell>
<TableCell className="text-sm text-gray-400">
: {item.quantity} {item.unit_name}
</TableCell>
</TableRow>
)) || null}
))}
</TableBody>
</Table>
</div>

View File

@@ -42,6 +42,7 @@ const subjectTypeMap: Record<string, string> = {
'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單',
'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單',
'App\\Modules\\Inventory\\Models\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
@@ -67,6 +68,7 @@ const subjectTypeMap: Record<string, string> = {
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫存調撥單',
'StoreRequisition': '門市叫貨單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',

View File

@@ -1,193 +0,0 @@
/**
* 新增供貨商品對話框
*/
import { useState, useMemo } from "react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/Components/ui/dialog";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/Components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/Components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import type { Product } from "@/types/product";
import type { SupplyProduct } from "@/types/vendor";
interface AddSupplyProductDialogProps {
open: boolean;
products: Product[];
existingSupplyProducts: SupplyProduct[];
onClose: () => void;
onAdd: (productId: string, lastPrice?: number) => void;
}
export default function AddSupplyProductDialog({
open,
products,
existingSupplyProducts,
onClose,
onAdd,
}: AddSupplyProductDialogProps) {
const [selectedProductId, setSelectedProductId] = useState<string>("");
const [lastPrice, setLastPrice] = useState<string>("");
const [openCombobox, setOpenCombobox] = useState(false);
// 過濾掉已經在供貨列表中的商品
const availableProducts = useMemo(() => {
const existingIds = new Set(existingSupplyProducts.map(sp => String(sp.productId)));
return products.filter(p => !existingIds.has(String(p.id)));
}, [products, existingSupplyProducts]);
const selectedProduct = availableProducts.find(p => p.id === selectedProductId);
const handleAdd = () => {
if (!selectedProductId) return;
const price = lastPrice ? parseFloat(lastPrice) : undefined;
onAdd(selectedProductId, price);
// 重置表單
setSelectedProductId("");
setLastPrice("");
};
const handleCancel = () => {
setSelectedProductId("");
setLastPrice("");
onClose();
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 商品選擇 */}
<div className="flex flex-col gap-2">
<Label className="text-sm font-medium"></Label>
<Popover open={openCombobox} onOpenChange={setOpenCombobox}>
<PopoverTrigger asChild>
<button
type="button"
role="combobox"
aria-expanded={openCombobox}
className="flex h-9 w-full items-center justify-between rounded-md border-2 border-grey-3 !bg-grey-5 px-3 py-1 text-sm font-normal text-grey-0 text-left outline-none transition-colors hover:!bg-grey-5 hover:border-primary/50 focus-visible:border-[var(--primary-main)] focus-visible:ring-[3px] focus-visible:ring-[var(--primary-main)]/20"
onClick={() => setOpenCombobox(!openCombobox)}
>
{selectedProduct ? (
<span className="font-medium text-gray-900">{selectedProduct.name}</span>
) : (
<span className="text-gray-400">...</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[450px] p-0 shadow-lg border-2 z-[9999]" align="start">
<Command>
<CommandInput placeholder="搜尋商品名稱..." />
<CommandList className="max-h-[300px]">
<CommandEmpty className="py-6 text-center text-sm text-gray-500">
</CommandEmpty>
<CommandGroup>
{availableProducts.map((product) => (
<CommandItem
key={product.id}
value={product.name}
onSelect={() => {
setSelectedProductId(product.id);
setOpenCombobox(false);
}}
className="cursor-pointer aria-selected:bg-primary/5 aria-selected:text-primary py-3"
>
<Check
className={cn(
"mr-2 h-4 w-4 text-primary",
selectedProductId === product.id ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex items-center justify-between flex-1">
<span className="font-medium">{product.name}</span>
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded">
{product.baseUnit?.name || (product.base_unit as any)?.name || product.base_unit || "個"}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 單位(自動帶入) */}
<div className="flex flex-col gap-2">
<Label className="text-sm font-medium text-gray-500"></Label>
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-gray-600 font-medium text-sm flex items-center">
{selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "-"}
</div>
</div>
{/* 上次採購價格 */}
<div>
<Label className="text-muted-foreground text-xs">
/ {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "單位"}
</Label>
<Input
type="number"
min="0"
step="any"
placeholder="輸入價格"
value={lastPrice}
onChange={(e) => setLastPrice(e.target.value)}
className="mt-1"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
size="sm"
onClick={handleCancel}
className="gap-2 button-outlined-primary"
>
</Button>
<Button
size="sm"
onClick={handleAdd}
disabled={!selectedProductId}
className="gap-2 button-filled-primary"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,119 +0,0 @@
/**
* 編輯供貨商品對話框
*/
import { useEffect, useState } from "react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/Components/ui/dialog";
import type { SupplyProduct } from "@/types/vendor";
interface EditSupplyProductDialogProps {
open: boolean;
product: SupplyProduct | null;
onClose: () => void;
onSave: (productId: string, lastPrice?: number) => void;
}
export default function EditSupplyProductDialog({
open,
product,
onClose,
onSave,
}: EditSupplyProductDialogProps) {
const [lastPrice, setLastPrice] = useState<string>("");
useEffect(() => {
if (product) {
setLastPrice(product.lastPrice?.toString() || "");
}
}, [product, open]);
const handleSave = () => {
if (!product) return;
const price = lastPrice ? parseFloat(lastPrice) : undefined;
onSave(product.productId, price);
setLastPrice("");
};
const handleCancel = () => {
setLastPrice("");
onClose();
};
if (!product) return null;
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 商品名稱(不可編輯) */}
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
value={product.productName}
disabled
className="mt-1 bg-muted"
/>
</div>
{/* 單位(不可編輯) */}
<div>
<Label className="text-muted-foreground text-xs"></Label>
<Input
value={product.unit}
disabled
className="mt-1 bg-muted"
/>
</div>
{/* 上次採購價格 */}
<div>
<Label className="text-muted-foreground text-xs"> / {product.baseUnit || "單位"}</Label>
<Input
type="number"
min="0"
step="any"
placeholder="輸入價格"
value={lastPrice}
onChange={(e) => setLastPrice(e.target.value)}
className="mt-1"
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
size="sm"
onClick={handleCancel}
className="gap-2 button-outlined-primary"
>
</Button>
<Button
size="sm"
onClick={handleSave}
className="gap-2 button-filled-primary"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,5 +1,7 @@
import { Pencil, Trash2 } from "lucide-react";
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,
@@ -8,90 +10,129 @@ import {
TableHeader,
TableRow,
} from "@/Components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog";
import type { SupplyProduct } from "@/types/vendor";
interface SupplyProductListProps {
products: SupplyProduct[];
onEdit: (product: SupplyProduct) => void;
onRemove: (product: SupplyProduct) => void;
items: SupplyProduct[];
allProducts: any[];
onRemoveItem: (index: number) => void;
onItemChange: (index: number, field: keyof SupplyProduct, value: string | number) => void;
}
export default function SupplyProductList({
products,
onEdit,
onRemove,
items,
allProducts,
onRemoveItem,
onItemChange,
}: SupplyProductListProps) {
return (
<div className="bg-white rounded-lg border shadow-sm">
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableRow className="bg-gray-50 hover:bg-gray-50 text-grey-0">
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right">
<TableHead className="w-[40%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead>
<TableHead className="w-[20%] text-left">
<div className="text-xs font-normal text-muted-foreground">()</div>
<span className="text-[10px] font-normal text-muted-foreground block">()</span>
</TableHead>
<TableHead className="text-center w-[150px]"></TableHead>
<TableHead className="text-center w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.length === 0 ? (
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
<TableCell colSpan={5} className="text-center text-muted-foreground py-12 italic text-sm">
</TableCell>
</TableRow>
) : (
products.map((product, index) => (
<TableRow key={product.id}>
items.map((item, index) => (
<TableRow key={item.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell>{product.productName}</TableCell>
<TableCell>
{product.baseUnit || product.unit || "-"}
<SearchableSelect
value={item.productId}
onValueChange={(value) => onItemChange(index, "productId", value)}
options={allProducts.map(p => ({
label: p.name,
value: String(p.id)
}))}
placeholder="選擇商品"
searchPlaceholder="搜尋商品..."
className="w-full h-10"
/>
</TableCell>
<TableCell>
{product.largeUnit && product.conversionRate ? (
<span className="text-sm text-gray-500">
1 {product.largeUnit} = {Number(product.conversionRate)} {product.baseUnit || product.unit}
</span>
) : (
"-"
)}
</TableCell>
<TableCell className="text-right">
{product.lastPrice ? (
<span>
${product.lastPrice.toLocaleString()} / {product.baseUnit || product.unit || "單位"}
</span>
) : (
"-"
)}
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(product)}
className="button-outlined-primary"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onRemove(product)}
className="button-outlined-error"
>
<Trash2 className="h-4 w-4" />
</Button>
<div className="h-10 px-3 py-2 bg-gray-50/50 border border-border rounded-md text-gray-600 font-medium text-sm flex items-center">
{item.baseUnit || "-"}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs">$</span>
<Input
type="number"
min="0"
step="any"
value={item.lastPrice === undefined ? "" : item.lastPrice}
onChange={(e) => onItemChange(index, "lastPrice", e.target.value === "" ? 0 : Number(e.target.value))}
placeholder="0.00"
className="pl-6 h-10 w-full text-right"
/>
</div>
</div>
</TableCell>
<TableCell className="text-center">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="button-outlined-error h-10 w-10 p-0"
title="移除項目"
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{item.productName || "此商品"}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
<AlertDialogAction
onClick={() => onRemoveItem(index)}
className="bg-red-600 hover:bg-red-700 text-white"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))
)}