Files
star-erp/resources/js/Pages/Warehouse/AddInventory.tsx

770 lines
43 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
/**
*
*/
2026-01-22 15:39:35 +08:00
import { useState, useEffect } from "react";
import { Plus, Trash2, Calendar, ArrowLeft, Save, Boxes } from "lucide-react";
2025-12-30 15:03:19 +08:00
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { SearchableSelect } from "@/Components/ui/searchable-select";
2025-12-30 15:03:19 +08:00
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router } from "@inertiajs/react";
import { Warehouse, InboundItem, InboundReason } from "@/types/warehouse";
import { getCurrentDateTime } from "@/utils/format";
import { toast } from "sonner";
2026-01-07 13:06:49 +08:00
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
import ScannerInput from "@/Components/Inventory/ScannerInput";
2025-12-30 15:03:19 +08:00
interface Product {
id: string;
name: string;
2026-01-22 15:39:35 +08:00
code: string;
barcode?: string;
2026-01-08 16:32:10 +08:00
baseUnit: string;
largeUnit?: string;
conversionRate?: number;
costPrice?: number;
2025-12-30 15:03:19 +08:00
}
2026-01-22 15:39:35 +08:00
interface Batch {
inventoryId: string;
batchNumber: string;
originCountry: string;
expiryDate: string | null;
quantity: number;
isDeleted?: boolean;
location?: string;
2026-01-22 15:39:35 +08:00
}
2025-12-30 15:03:19 +08:00
interface Props {
warehouse: Warehouse;
products: Product[];
}
const INBOUND_REASONS: InboundReason[] = [
"期初建檔",
"盤點調整",
"實際入庫未走採購流程",
"生產加工成品入庫",
"其他",
];
export default function AddInventoryPage({ warehouse, products }: Props) {
const [inboundDate, setInboundDate] = useState(getCurrentDateTime());
const [reason, setReason] = useState<InboundReason>("期初建檔");
const [notes, setNotes] = useState("");
const [items, setItems] = useState<InboundItem[]>([]);
const [errors, setErrors] = useState<Record<string, string>>({});
2026-01-22 15:39:35 +08:00
const [batchesCache, setBatchesCache] = useState<Record<string, { batches: Batch[], nextSequences: Record<string, string> }>>({});
// 取得商品批號與流水號
const fetchProductBatches = async (productId: string, originCountry?: string, arrivalDate?: string) => {
if (!productId) return;
const country = originCountry || 'TW';
const date = arrivalDate || inboundDate.split('T')[0];
const cacheKey = `${country}_${date}`;
// 如果該商品的批號列表尚未載入,強制載入
const existingCache = batchesCache[productId];
const hasBatches = existingCache && existingCache.batches.length >= 0 && existingCache.batches !== undefined;
const hasThisSequence = existingCache?.nextSequences?.[cacheKey];
// 若 batches 尚未載入,或特定條件的 sequence 尚未載入,則呼叫 API
if (!hasBatches || !hasThisSequence) {
try {
const response = await fetch(`/api/warehouses/${warehouse.id}/inventory/batches/${productId}?originCountry=${country}&arrivalDate=${date}`);
const data = await response.json();
setBatchesCache(prev => {
const existingProductCache = prev[productId] || { batches: [], nextSequences: {} };
return {
...prev,
[productId]: {
batches: data.batches,
nextSequences: {
...existingProductCache.nextSequences,
[cacheKey]: data.nextSequence
}
}
};
});
} catch (error) {
console.error("Failed to fetch batches", error);
}
}
};
// 當 items 變動、日期變動時,確保資料同步
useEffect(() => {
items.forEach(item => {
if (item.productId) {
// 無論 batchMode 為何,都要載入批號列表
// 若使用者切換到 new 模式,則額外傳入 originCountry 以取得正確流水號
const country = item.batchMode === 'new' ? item.originCountry : undefined;
fetchProductBatches(item.productId, country, inboundDate.split('T')[0]);
}
});
}, [items, inboundDate]);
2025-12-30 15:03:19 +08:00
// 處理掃碼輸入
const handleScan = async (code: string, mode: 'continuous' | 'single') => {
const cleanCode = code.trim();
// 1. 搜尋商品 (優先比對 Code, Barcode, ID)
let product = products.find(p => p.code === cleanCode || p.barcode === cleanCode || p.id === cleanCode);
// 如果前端找不到,嘗試 API 搜尋 (Fallback)
if (!product) {
try {
// 這裡假設有 API 可以搜尋商品,若沒有則會失敗
// 使用 Product/Index 的搜尋邏輯 (Inertia Props 比較難已 AJAX 取得)
// 替代方案:直接請求 /products?search=CLEAN_CODE&per_page=1
// 加上 header 確認是 JSON 請求
const response = await fetch(`/products?search=${encodeURIComponent(cleanCode)}&per_page=1`, {
headers: {
'X-Requested-With': 'XMLHttpRequest', // 強制 AJAX 識別
}
});
if (response.ok) {
const data = await response.json();
// Inertia 回傳的是 component props 結構,或 partial props
// 根據 ProductController::index回傳 props.products.data
if (data.props && data.props.products && data.props.products.data && data.props.products.data.length > 0) {
const foundProduct = data.props.products.data[0];
// 轉換格式以符合 AddInventory 的 Product 介面
product = {
id: foundProduct.id,
name: foundProduct.name,
code: foundProduct.code,
barcode: foundProduct.barcode,
baseUnit: foundProduct.baseUnit?.name || '個',
largeUnit: foundProduct.largeUnit?.name,
conversionRate: foundProduct.conversionRate,
costPrice: foundProduct.costPrice,
};
}
}
} catch (err) {
console.error("API Search failed", err);
}
}
if (!product) {
toast.error(`找不到商品: ${code}`);
return;
}
// 2. 連續模式:尋找最近一筆相同商品並 +1
if (mode === 'continuous') {
let foundIndex = -1;
// 從後往前搜尋,找到最近加入的那一筆
for (let i = items.length - 1; i >= 0; i--) {
if (items[i].productId === product.id) {
foundIndex = i;
break;
}
}
if (foundIndex !== -1) {
// 更新數量
const newItems = [...items];
const currentQty = newItems[foundIndex].quantity || 0;
newItems[foundIndex] = {
...newItems[foundIndex],
quantity: currentQty + 1
};
setItems(newItems);
toast.success(`${product.name} 數量 +1 (總數: ${currentQty + 1})`);
return;
}
}
// 3. 單筆模式 或 連續模式但尚未加入過:新增一筆
const newItem: InboundItem = {
tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
productId: product.id,
productName: product.name,
quantity: 1,
unit: product.baseUnit, // 僅用於顯示當前選擇單位的名稱
baseUnit: product.baseUnit,
largeUnit: product.largeUnit,
conversionRate: product.conversionRate,
selectedUnit: 'base',
batchMode: 'existing', // 預設選擇現有批號 (需要使用者確認/輸入)
originCountry: 'TW',
unit_cost: product.costPrice || 0,
};
setItems(prev => [...prev, newItem]);
toast.success(`已加入 ${product.name}`);
};
2025-12-30 15:03:19 +08:00
// 新增明細行
const handleAddItem = () => {
const defaultProduct = products.length > 0 ? products[0] : { id: "", name: "", code: "", baseUnit: "個", costPrice: 0 };
2025-12-30 15:03:19 +08:00
const newItem: InboundItem = {
tempId: `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
productId: defaultProduct.id,
productName: defaultProduct.name,
quantity: 0,
2026-01-08 16:32:10 +08:00
unit: defaultProduct.baseUnit, // 僅用於顯示當前選擇單位的名稱
baseUnit: defaultProduct.baseUnit,
largeUnit: defaultProduct.largeUnit,
conversionRate: defaultProduct.conversionRate,
selectedUnit: 'base',
2026-01-22 15:39:35 +08:00
batchMode: 'existing', // 預設選擇現有批號
originCountry: 'TW',
unit_cost: defaultProduct.costPrice || 0,
2025-12-30 15:03:19 +08:00
};
setItems([...items, newItem]);
};
// 刪除明細行
const handleRemoveItem = (tempId: string) => {
setItems(items.filter((item) => item.tempId !== tempId));
};
// 更新明細行
const handleUpdateItem = (tempId: string, updates: Partial<InboundItem>) => {
setItems(
items.map((item) =>
item.tempId === tempId ? { ...item, ...updates } : item
)
);
};
// 處理商品變更
const handleProductChange = (tempId: string, productId: string) => {
const product = products.find((p) => p.id === productId);
2026-01-08 16:32:10 +08:00
2025-12-30 15:03:19 +08:00
if (product) {
handleUpdateItem(tempId, {
productId,
productName: product.name,
2026-01-08 16:32:10 +08:00
unit: product.baseUnit,
baseUnit: product.baseUnit,
largeUnit: product.largeUnit,
conversionRate: product.conversionRate,
selectedUnit: 'base',
2026-01-22 15:39:35 +08:00
batchMode: 'existing',
inventoryId: undefined, // 清除已選擇的批號
expiryDate: undefined,
unit_cost: product.costPrice || 0,
2025-12-30 15:03:19 +08:00
});
}
};
// 驗證表單
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!reason) {
newErrors.reason = "請選擇入庫原因";
}
if (reason === "其他" && !notes.trim()) {
newErrors.notes = "原因為「其他」時,備註為必填";
}
if (items.length === 0) {
newErrors.items = "請至少新增一筆庫存明細";
}
items.forEach((item, index) => {
if (!item.productId) {
newErrors[`item-${index}-product`] = "請選擇商品";
}
if (item.quantity <= 0) {
newErrors[`item-${index}-quantity`] = "數量必須大於 0";
}
2026-01-22 15:39:35 +08:00
if (item.batchMode === 'existing' && !item.inventoryId) {
newErrors[`item-${index}-batch`] = "請選擇批號";
}
if (item.batchMode === 'new' && !item.originCountry) {
newErrors[`item-${index}-country`] = "新批號必須輸入產地";
}
2025-12-30 15:03:19 +08:00
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 處理儲存
const handleSave = () => {
if (!validateForm()) {
toast.error("請檢查表單內容");
return;
}
router.post(`/warehouses/${warehouse.id}/inventory`, {
inboundDate,
reason,
notes,
2026-01-08 16:32:10 +08:00
items: items.map(item => {
// 如果選擇大單位,則換算為基本單位數量
const finalQuantity = item.selectedUnit === 'large' && item.conversionRate
? item.quantity * item.conversionRate
: item.quantity;
return {
productId: item.productId,
quantity: finalQuantity,
2026-01-22 15:39:35 +08:00
batchMode: item.batchMode,
inventoryId: item.inventoryId,
originCountry: item.originCountry,
expiryDate: item.expiryDate,
unit_cost: item.unit_cost,
location: item.location,
2026-01-08 16:32:10 +08:00
};
})
2025-12-30 15:03:19 +08:00
}, {
onSuccess: () => {
toast.success("庫存記錄已儲存");
router.get(`/warehouses/${warehouse.id}/inventory`);
},
onError: (err) => {
toast.error("儲存失敗,請檢查輸入內容");
console.error(err);
}
});
};
2026-01-22 15:39:35 +08:00
// 生成批號預覽
const getBatchPreview = (productId: string | undefined, productCode: string | undefined, country: string, dateStr: string) => {
if (!productCode || !productId) return "--";
try {
// 直接字串處理,避免時區問題且確保與 fetchProductBatches 的 key 一致
const datePart = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr;
const [yyyy, mm, dd] = datePart.split('-');
const dateFormatted = `${yyyy}${mm}${dd}`;
const cacheKey = `${country}_${datePart}`;
const seq = batchesCache[productId]?.nextSequences?.[cacheKey] || "XX";
return `${productCode}-${country}-${dateFormatted}-${seq}`;
} catch (e) {
return "--";
}
};
2025-12-30 15:03:19 +08:00
return (
2026-01-07 13:06:49 +08:00
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name, "手動入庫")}>
2025-12-30 15:03:19 +08:00
<Head title={`新增庫存 - ${warehouse.name}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* 頁面標題與導航 - 已於先前任務優化 */}
<div className="mb-6">
<div className="mb-6">
<Link href={`/warehouses/${warehouse.id}/inventory`}>
<Button
variant="outline"
className="gap-2 button-outlined-primary"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
</div>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Boxes className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
2025-12-30 15:03:19 +08:00
<span className="font-semibold text-gray-900">{warehouse.name}</span>
</p>
</div>
<Button
onClick={handleSave}
className="button-filled-primary"
>
<Save className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* 表單內容 */}
<div className="space-y-6">
{/* 基本資訊區塊 */}
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<h3 className="font-semibold text-lg border-b pb-2"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 倉庫 */}
<div className="space-y-2">
<Label className="text-gray-700"></Label>
<Input
value={warehouse.name}
disabled
className="bg-gray-50 border-gray-200"
/>
</div>
{/* 入庫日期 */}
<div className="space-y-2">
<Label htmlFor="inbound-date" className="text-gray-700">
<span className="text-red-500">*</span>
</Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" />
2025-12-30 15:03:19 +08:00
<Input
id="inbound-date"
type="datetime-local"
value={inboundDate}
onChange={(e) => setInboundDate(e.target.value)}
className="border-gray-300 pl-9"
2025-12-30 15:03:19 +08:00
/>
</div>
</div>
{/* 入庫原因 */}
<div className="space-y-2">
<Label htmlFor="reason" className="text-gray-700">
<span className="text-red-500">*</span>
</Label>
<SearchableSelect
value={reason}
onValueChange={(value) => setReason(value as InboundReason)}
options={INBOUND_REASONS.map((r) => ({ label: r, value: r }))}
placeholder="選擇入庫原因"
className="border-gray-300"
/>
2025-12-30 15:03:19 +08:00
{errors.reason && (
<p className="text-sm text-red-500">{errors.reason}</p>
)}
</div>
{/* 備註 */}
<div className="space-y-2 md:col-span-2">
<Label htmlFor="notes" className="text-gray-700">
{reason === "其他" && <span className="text-red-500">*</span>}
</Label>
<Textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="請輸入備註說明..."
className="border-gray-300 resize-none min-h-[100px]"
/>
{errors.notes && (
<p className="text-sm text-red-500">{errors.notes}</p>
)}
</div>
</div>
</div>
{/* 庫存明細區塊 */}
<div className="bg-white rounded-lg shadow-sm border p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-lg"></h3>
<p className="text-sm text-gray-500">
</p>
</div>
<Button
type="button"
onClick={handleAddItem}
variant="outline"
className="button-outlined-primary"
>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
{/* 掃碼輸入區 */}
<ScannerInput
onScan={handleScan}
className="bg-gray-50/50"
/>
2025-12-30 15:03:19 +08:00
{errors.items && (
<p className="text-sm text-red-500">{errors.items}</p>
)}
{items.length > 0 ? (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-gray-50/50">
2026-01-22 15:39:35 +08:00
<TableHead className="w-[180px]">
2025-12-30 15:03:19 +08:00
<span className="text-red-500">*</span>
</TableHead>
2026-01-22 15:39:35 +08:00
<TableHead className="w-[220px]">
<span className="text-red-500">*</span>
</TableHead>
<TableHead className="w-[100px]">
</TableHead>
2026-01-22 15:39:35 +08:00
<TableHead className="w-[100px]">
2025-12-30 15:03:19 +08:00
<span className="text-red-500">*</span>
</TableHead>
2026-01-22 15:39:35 +08:00
<TableHead className="w-[90px]"></TableHead>
<TableHead className="w-[120px]">
{warehouse.type === 'vending' ? '貨道' : '儲位'}
</TableHead>
2026-01-22 15:39:35 +08:00
<TableHead className="w-[50px]"></TableHead>
2025-12-30 15:03:19 +08:00
</TableRow>
</TableHeader>
<TableBody>
2026-01-08 16:32:10 +08:00
{items.map((item, index) => {
// 計算轉換數量
const convertedQuantity = item.selectedUnit === 'large' && item.conversionRate
? item.quantity * item.conversionRate
: item.quantity;
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
// Find product code
const product = products.find(p => p.id === item.productId);
2026-01-08 16:32:10 +08:00
return (
<TableRow key={item.tempId}>
{/* 商品 */}
<TableCell>
<SearchableSelect
2026-01-08 16:32:10 +08:00
value={item.productId}
onValueChange={(value) =>
handleProductChange(item.tempId, value)
}
options={products.map((p) => ({ label: p.name, value: p.id }))}
placeholder="選擇商品"
searchPlaceholder="搜尋商品..."
className="border-gray-300"
/>
2026-01-08 16:32:10 +08:00
{errors[`item-${index}-product`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-product`]}
</p>
)}
</TableCell>
2025-12-30 15:03:19 +08:00
2026-01-22 15:39:35 +08:00
{/* 批號與產地控制 */}
<TableCell>
<div className="space-y-2">
<SearchableSelect
value={item.batchMode === 'none' ? 'no_batch' : (item.batchMode === 'new' ? 'new_batch' : (item.inventoryId || ""))}
2026-01-22 15:39:35 +08:00
onValueChange={(value) => {
if (value === 'new_batch') {
handleUpdateItem(item.tempId, {
batchMode: 'new',
inventoryId: undefined,
originCountry: 'TW',
expiryDate: undefined
});
} else if (value === 'no_batch') {
// 嘗試匹配現有的 NO-BATCH 紀錄
const existingNoBatch = (batchesCache[item.productId]?.batches || []).find(b => b.batchNumber === 'NO-BATCH');
handleUpdateItem(item.tempId, {
batchMode: 'none',
inventoryId: existingNoBatch?.inventoryId || undefined,
originCountry: 'TW',
expiryDate: undefined
});
2026-01-22 15:39:35 +08:00
} else {
const selectedBatch = (batchesCache[item.productId]?.batches || []).find(b => b.inventoryId === value);
handleUpdateItem(item.tempId, {
batchMode: 'existing',
inventoryId: value,
originCountry: selectedBatch?.originCountry,
expiryDate: selectedBatch?.expiryDate || undefined,
location: selectedBatch?.location || item.location,
2026-01-22 15:39:35 +08:00
});
}
}}
options={[
{ label: "📦 不使用批號 (自動累加)", value: "no_batch" },
2026-01-22 15:39:35 +08:00
{ label: "+ 建立新批號", value: "new_batch" },
...(batchesCache[item.productId]?.batches || []).map(b => {
const isNoBatch = b.batchNumber === 'NO-BATCH';
const showLocation = isNoBatch || warehouse.type === 'vending';
const locationInfo = (showLocation && b.location) ? ` [${b.location}]` : '';
const batchLabel = isNoBatch ? '(無批號紀錄)' : b.batchNumber;
return {
label: `${batchLabel}${locationInfo} - 庫存: ${b.quantity}`,
value: b.inventoryId
};
})
2026-01-22 15:39:35 +08:00
]}
placeholder="選擇或新增批號"
className="border-gray-300"
/>
{errors[`item-${index}-batch`] && (
<p className="text-xs text-red-500">
{errors[`item-${index}-batch`]}
</p>
)}
{item.batchMode === 'new' && (
<div className="flex items-center gap-2 mt-2">
<div className="flex-1">
<Input
value={item.originCountry || ""}
onChange={(e) => {
const val = e.target.value.toUpperCase().slice(0, 2);
handleUpdateItem(item.tempId, { originCountry: val });
}}
maxLength={2}
placeholder="產地"
className="h-8 text-xs text-center border-gray-300"
/>
</div>
<div className="flex-[3] text-xs bg-primary-50/50 text-primary-main px-2 py-1 rounded border border-primary-200/50 font-mono overflow-hidden whitespace-nowrap">
{getBatchPreview(item.productId, product?.code, item.originCountry || 'TW', inboundDate)}
2026-01-22 15:39:35 +08:00
</div>
</div>
)}
{/* 新增效期輸入 (僅在建立新批號模式下) */}
{item.batchMode === 'new' && (
<div className="mt-2 flex items-center gap-2">
<span className="text-xs text-gray-500 whitespace-nowrap">:</span>
<div className="relative flex-1">
<Calendar className="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-gray-400 pointer-events-none" />
<Input
type="date"
value={item.expiryDate || ""}
onChange={(e) =>
handleUpdateItem(item.tempId, {
expiryDate: e.target.value,
})
}
className="h-8 pl-8 text-xs border-gray-300 w-full"
/>
2026-01-22 15:39:35 +08:00
</div>
</div>
)}
{item.batchMode === 'none' && (
<div className="mt-1 px-2 py-1 bg-amber-50 text-amber-700 text-[10px] rounded border border-amber-100 flex items-center gap-1">
<span className="shrink-0 font-bold">INFO</span>
</div>
2026-01-22 15:39:35 +08:00
)}
{item.batchMode === 'existing' && item.inventoryId && (
<div className="flex flax-col gap-1 mt-1">
<div className="text-xs text-gray-500 font-mono">
: {item.expiryDate || '無效期紀錄'}
</div>
2026-01-22 15:39:35 +08:00
</div>
)}
</div>
</TableCell>
{/* 單價 */}
<TableCell>
<Input
type="number"
min="0"
step="any"
value={item.unit_cost || 0}
onChange={(e) =>
handleUpdateItem(item.tempId, {
unit_cost: parseFloat(e.target.value) || 0,
})
}
className="border-gray-300 bg-gray-50 text-right"
placeholder="0"
/>
</TableCell>
2026-01-08 16:32:10 +08:00
{/* 數量 */}
<TableCell>
<Input
type="number"
min="1"
step="any"
2026-01-08 16:32:10 +08:00
value={item.quantity || ""}
onChange={(e) =>
handleUpdateItem(item.tempId, {
quantity: parseFloat(e.target.value) || 0,
})
}
className="border-gray-300 text-right"
2026-01-08 16:32:10 +08:00
/>
{item.selectedUnit === 'large' && item.conversionRate && (
<div className="text-xs text-gray-500 mt-1">
: {convertedQuantity} {item.baseUnit || "個"}
</div>
)}
2026-01-08 16:32:10 +08:00
{errors[`item-${index}-quantity`] && (
<p className="text-xs text-red-500 mt-1">
{errors[`item-${index}-quantity`]}
</p>
)}
</TableCell>
2025-12-30 15:03:19 +08:00
2026-01-08 16:32:10 +08:00
{/* 單位 */}
<TableCell>
{item.largeUnit ? (
<SearchableSelect
value={item.selectedUnit || ""}
2026-01-08 16:32:10 +08:00
onValueChange={(value) =>
handleUpdateItem(item.tempId, {
selectedUnit: value as 'base' | 'large',
unit: value === 'base' ? item.baseUnit : item.largeUnit
})
}
options={[
{ label: item.baseUnit || "個", value: "base" },
{ label: item.largeUnit || "", value: "large" }
]}
className="border-gray-300"
/>
2026-01-08 16:32:10 +08:00
) : (
<div className="text-sm text-gray-700 font-medium px-3 py-2 bg-gray-50 border border-gray-200 rounded-md">
{item.baseUnit || "個"}
</div>
2026-01-08 16:32:10 +08:00
)}
</TableCell>
{/* 儲位/貨道 */}
<TableCell>
<Input
value={item.location || ""}
onChange={(e) => handleUpdateItem(item.tempId, { location: e.target.value })}
className="border-gray-300"
placeholder={warehouse.type === 'vending' ? "貨道 (如: A1)" : "儲位 (選填)"}
/>
</TableCell>
2026-01-08 16:32:10 +08:00
{/* 刪除按鈕 */}
<TableCell>
<Button
type="button"
variant="outline"
2026-01-08 16:32:10 +08:00
size="icon"
onClick={() => handleRemoveItem(item.tempId)}
className="button-outlined-error h-8 w-8"
2026-01-08 16:32:10 +08:00
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
2025-12-30 15:03:19 +08:00
</TableBody>
</Table>
</div>
) : (
<div className="border border-dashed rounded-lg p-12 text-center text-gray-500 bg-gray-50/30">
<p className="text-base font-medium"></p>
<p className="text-sm mt-1"></p>
</div>
)}
</div>
</div>
</div>
</AuthenticatedLayout >
2025-12-30 15:03:19 +08:00
);
}