feat(inventory): 支援庫存新增不使用批號模式與自動累加邏輯

This commit is contained in:
2026-02-06 15:56:50 +08:00
parent 6c259859cf
commit 200d1989bd
3 changed files with 75 additions and 40 deletions

View File

@@ -163,7 +163,7 @@ class InventoryController extends Controller
'items.*.productId' => 'required|exists:products,id', 'items.*.productId' => 'required|exists:products,id',
'items.*.quantity' => 'required|numeric|min:0.01', 'items.*.quantity' => 'required|numeric|min:0.01',
'items.*.unit_cost' => 'nullable|numeric|min:0', // 新增成本驗證 'items.*.unit_cost' => 'nullable|numeric|min:0', // 新增成本驗證
'items.*.batchMode' => 'required|in:existing,new', 'items.*.batchMode' => 'required|in:existing,new,none',
'items.*.inventoryId' => 'required_if:items.*.batchMode,existing|nullable|exists:inventories,id', 'items.*.inventoryId' => 'required_if:items.*.batchMode,existing|nullable|exists:inventories,id',
'items.*.originCountry' => 'required_if:items.*.batchMode,new|nullable|string|max:2', 'items.*.originCountry' => 'required_if:items.*.batchMode,new|nullable|string|max:2',
'items.*.expiryDate' => 'nullable|date', 'items.*.expiryDate' => 'nullable|date',
@@ -188,6 +188,26 @@ class InventoryController extends Controller
if (isset($item['unit_cost'])) { if (isset($item['unit_cost'])) {
$inventory->unit_cost = $item['unit_cost']; $inventory->unit_cost = $item['unit_cost'];
} }
} elseif ($item['batchMode'] === 'none') {
// 模式 C不使用批號 (自動累加至 NO-BATCH)
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
[
'product_id' => $item['productId'],
'batch_number' => 'NO-BATCH'
],
[
'quantity' => 0,
'unit_cost' => $item['unit_cost'] ?? 0,
'total_value' => 0,
'arrival_date' => $validated['inboundDate'],
'expiry_date' => null,
'origin_country' => 'TW',
]
);
if ($inventory->trashed()) {
$inventory->restore();
}
} else { } else {
// 模式 B建立新批號 // 模式 B建立新批號
$originCountry = $item['originCountry'] ?? 'TW'; $originCountry = $item['originCountry'] ?? 'TW';

View File

@@ -548,7 +548,7 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
<TableCell> <TableCell>
<div className="space-y-2"> <div className="space-y-2">
<SearchableSelect <SearchableSelect
value={item.batchMode === 'new' ? 'new_batch' : (item.inventoryId || "")} value={item.batchMode === 'none' ? 'no_batch' : (item.batchMode === 'new' ? 'new_batch' : (item.inventoryId || ""))}
onValueChange={(value) => { onValueChange={(value) => {
if (value === 'new_batch') { if (value === 'new_batch') {
handleUpdateItem(item.tempId, { handleUpdateItem(item.tempId, {
@@ -557,6 +557,15 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
originCountry: 'TW', originCountry: 'TW',
expiryDate: undefined 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
});
} else { } else {
const selectedBatch = (batchesCache[item.productId]?.batches || []).find(b => b.inventoryId === value); const selectedBatch = (batchesCache[item.productId]?.batches || []).find(b => b.inventoryId === value);
handleUpdateItem(item.tempId, { handleUpdateItem(item.tempId, {
@@ -568,9 +577,10 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
} }
}} }}
options={[ options={[
{ label: "📦 不使用批號 (自動累加)", value: "no_batch" },
{ label: "+ 建立新批號", value: "new_batch" }, { label: "+ 建立新批號", value: "new_batch" },
...(batchesCache[item.productId]?.batches || []).map(b => ({ ...(batchesCache[item.productId]?.batches || []).map(b => ({
label: `${b.batchNumber} - 庫存: ${b.quantity}`, label: `${b.batchNumber === 'NO-BATCH' ? '(無批號紀錄)' : b.batchNumber} - 庫存: ${b.quantity}`,
value: b.inventoryId value: b.inventoryId
})) }))
]} ]}
@@ -582,50 +592,55 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
{errors[`item-${index}-batch`]} {errors[`item-${index}-batch`]}
</p> </p>
)} )}
{item.batchMode === 'new' && ( {item.batchMode === 'new' && (
<> <div className="flex items-center gap-2 mt-2">
<div className="flex items-center gap-2 mt-2"> <div className="flex-1">
<div className="flex-1"> <Input
<Input value={item.originCountry || ""}
value={item.originCountry || ""} onChange={(e) => {
onChange={(e) => { const val = e.target.value.toUpperCase().slice(0, 2);
const val = e.target.value.toUpperCase().slice(0, 2); handleUpdateItem(item.tempId, { originCountry: val });
handleUpdateItem(item.tempId, { originCountry: val }); }}
}} maxLength={2}
maxLength={2} placeholder="產地"
placeholder="產地" className="h-8 text-xs text-center border-gray-300"
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)}
</div>
</div> </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">
<div className="mt-2 flex items-center gap-2"> {getBatchPreview(item.productId, product?.code, item.originCountry || 'TW', inboundDate)}
<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"
/>
</div>
</div> </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"
/>
</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>
)} )}
{item.batchMode === 'existing' && item.inventoryId && ( {item.batchMode === 'existing' && item.inventoryId && (
<div className="flex flax-col gap-1 mt-1"> <div className="flex flax-col gap-1 mt-1">
<div className="text-xs text-gray-500 font-mono"> <div className="text-xs text-gray-500 font-mono">
: {item.expiryDate || '未設定'} : {item.expiryDate || '無效期紀錄'}
</div> </div>
</div> </div>
)} )}

View File

@@ -174,7 +174,7 @@ export interface InboundItem {
largeUnit?: string; largeUnit?: string;
conversionRate?: number; conversionRate?: number;
selectedUnit?: 'base' | 'large'; selectedUnit?: 'base' | 'large';
batchMode?: 'existing' | 'new'; // 批號模式 batchMode?: 'existing' | 'new' | 'none'; // 批號模式
inventoryId?: string; // 選擇現有批號時的 ID inventoryId?: string; // 選擇現有批號時的 ID
batchNumber?: string; batchNumber?: string;
originCountry?: string; // 新增產地 originCountry?: string; // 新增產地