first commit
This commit is contained in:
325
app/Http/Controllers/InventoryController.php
Normal file
325
app/Http/Controllers/InventoryController.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
public function index(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
||||
{
|
||||
$warehouse->load([
|
||||
'inventories.product.category',
|
||||
'inventories.lastIncomingTransaction',
|
||||
'inventories.lastOutgoingTransaction'
|
||||
]);
|
||||
$allProducts = \App\Models\Product::with('category')->get();
|
||||
|
||||
// 1. 準備 availableProducts
|
||||
$availableProducts = $allProducts->map(function ($product) {
|
||||
return [
|
||||
'id' => (string) $product->id, // Frontend expects string
|
||||
'name' => $product->name,
|
||||
'type' => $product->category ? $product->category->name : '其他', // 暫時用 Category Name 當 Type
|
||||
];
|
||||
});
|
||||
|
||||
// 2. 準備 inventories (模擬批號)
|
||||
// 2. 準備 inventories
|
||||
// 資料庫結構為 (warehouse_id, product_id) 唯一,故為扁平列表
|
||||
$inventories = $warehouse->inventories->map(function ($inv) {
|
||||
return [
|
||||
'id' => (string) $inv->id,
|
||||
'warehouseId' => (string) $inv->warehouse_id,
|
||||
'productId' => (string) $inv->product_id,
|
||||
'productName' => $inv->product->name,
|
||||
'productCode' => $inv->product->code ?? 'N/A',
|
||||
'unit' => $inv->product->base_unit ?? '個',
|
||||
'quantity' => (float) $inv->quantity,
|
||||
'safetyStock' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null,
|
||||
'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態
|
||||
'batchNumber' => 'BATCH-' . $inv->id, // DB 無批號,暫時模擬,某些 UI 可能還會用到
|
||||
'expiryDate' => '2099-12-31', // DB 無效期,暫時模擬
|
||||
'lastInboundDate' => $inv->lastIncomingTransaction ? ($inv->lastIncomingTransaction->actual_time ? $inv->lastIncomingTransaction->actual_time->format('Y-m-d') : $inv->lastIncomingTransaction->created_at->format('Y-m-d')) : null,
|
||||
'lastOutboundDate' => $inv->lastOutgoingTransaction ? ($inv->lastOutgoingTransaction->actual_time ? $inv->lastOutgoingTransaction->actual_time->format('Y-m-d') : $inv->lastOutgoingTransaction->created_at->format('Y-m-d')) : null,
|
||||
];
|
||||
});
|
||||
|
||||
// 3. 準備 safetyStockSettings
|
||||
$safetyStockSettings = $warehouse->inventories->filter(function($inv) {
|
||||
return !is_null($inv->safety_stock);
|
||||
})->map(function ($inv) {
|
||||
return [
|
||||
'id' => 'ss-' . $inv->id,
|
||||
'warehouseId' => (string) $inv->warehouse_id,
|
||||
'productId' => (string) $inv->product_id,
|
||||
'productName' => $inv->product->name,
|
||||
'productType' => $inv->product->category ? $inv->product->category->name : '其他',
|
||||
'safetyStock' => (float) $inv->safety_stock,
|
||||
'createdAt' => $inv->created_at->toIso8601String(),
|
||||
'updatedAt' => $inv->updated_at->toIso8601String(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/Inventory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventories' => $inventories,
|
||||
'safetyStockSettings' => $safetyStockSettings,
|
||||
'availableProducts' => $availableProducts,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(\App\Models\Warehouse $warehouse)
|
||||
{
|
||||
// 取得所有商品供前端選單使用
|
||||
$products = \App\Models\Product::select('id', 'name', 'base_unit')->get()->map(function ($product) {
|
||||
return [
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'unit' => $product->base_unit,
|
||||
];
|
||||
});
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/AddInventory', [
|
||||
'warehouse' => $warehouse,
|
||||
'products' => $products,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'inboundDate' => 'required|date',
|
||||
'reason' => 'required|string',
|
||||
'notes' => 'nullable|string',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.productId' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
]);
|
||||
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $warehouse) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
// 取得或建立庫存紀錄
|
||||
$inventory = $warehouse->inventories()->firstOrCreate(
|
||||
['product_id' => $item['productId']],
|
||||
['quantity' => 0, 'safety_stock' => null]
|
||||
);
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
// 更新庫存
|
||||
$inventory->update(['quantity' => $newQty]);
|
||||
|
||||
// 寫入異動紀錄
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動入庫',
|
||||
'quantity' => $item['quantity'],
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => $validated['reason'] . ($validated['notes'] ? ' - ' . $validated['notes'] : ''),
|
||||
'actual_time' => $validated['inboundDate'],
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||
->with('success', '庫存記錄已儲存成功');
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
|
||||
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
||||
if (str_starts_with($inventoryId, 'mock-inv-')) {
|
||||
return redirect()->back()->with('error', '無法編輯範例資料');
|
||||
}
|
||||
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||
|
||||
// 轉換為前端需要的格式
|
||||
$inventoryData = [
|
||||
'id' => (string) $inventory->id,
|
||||
'warehouseId' => (string) $inventory->warehouse_id,
|
||||
'productId' => (string) $inventory->product_id,
|
||||
'productName' => $inventory->product->name,
|
||||
'quantity' => (float) $inventory->quantity,
|
||||
'batchNumber' => 'BATCH-' . $inventory->id, // Mock
|
||||
'expiryDate' => '2099-12-31', // Mock
|
||||
'lastInboundDate' => $inventory->updated_at->format('Y-m-d'),
|
||||
'lastOutboundDate' => null,
|
||||
];
|
||||
|
||||
// 整理異動紀錄
|
||||
$transactions = $inventory->transactions->map(function ($tx) {
|
||||
return [
|
||||
'id' => (string) $tx->id,
|
||||
'type' => $tx->type,
|
||||
'quantity' => (float) $tx->quantity,
|
||||
'balanceAfter' => (float) $tx->balance_after,
|
||||
'reason' => $tx->reason,
|
||||
'userName' => $tx->user ? $tx->user->name : '系統',
|
||||
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
||||
];
|
||||
});
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/EditInventory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventory' => $inventoryData,
|
||||
'transactions' => $transactions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
// 若是 product ID (舊邏輯),先轉為 inventory
|
||||
// 但新路由我們傳的是 inventory ID
|
||||
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
||||
|
||||
$inventory = \App\Models\Inventory::find($inventoryId);
|
||||
|
||||
// 如果找不到 (可能是舊路由傳 product ID)
|
||||
if (!$inventory) {
|
||||
$inventory = $warehouse->inventories()->where('product_id', $inventoryId)->first();
|
||||
}
|
||||
|
||||
if (!$inventory) {
|
||||
return redirect()->back()->with('error', '找不到庫存紀錄');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'quantity' => 'required|numeric|min:0',
|
||||
// 以下欄位改為 nullable,支援新表單
|
||||
'type' => 'nullable|string',
|
||||
'operation' => 'nullable|in:add,subtract,set',
|
||||
'reason' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
// 新增日期欄位驗證 (雖然暫不儲存到 DB)
|
||||
'batchNumber' => 'nullable|string',
|
||||
'expiryDate' => 'nullable|date',
|
||||
'lastInboundDate' => 'nullable|date',
|
||||
'lastOutboundDate' => 'nullable|date',
|
||||
]);
|
||||
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $inventory) {
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $validated['quantity'];
|
||||
|
||||
// 判斷操作模式
|
||||
if (isset($validated['operation'])) {
|
||||
$changeQty = 0;
|
||||
switch ($validated['operation']) {
|
||||
case 'add':
|
||||
$changeQty = $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -$validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
$changeQty = $newQty - $currentQty;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 來自編輯頁面,直接 Set
|
||||
$changeQty = $newQty - $currentQty;
|
||||
}
|
||||
|
||||
// 更新庫存
|
||||
$inventory->update(['quantity' => $newQty]);
|
||||
|
||||
// 異動類型映射
|
||||
$type = $validated['type'] ?? 'adjustment';
|
||||
$typeMapping = [
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
'return_in' => '退貨入庫',
|
||||
'return_out' => '退貨出庫',
|
||||
'transfer_in' => '撥補入庫',
|
||||
'transfer_out' => '撥補出庫',
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
// 如果是編輯頁面來的,可能沒有 type,預設為 "盤點調整" 或 "手動編輯"
|
||||
if (!isset($validated['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
// 寫入異動紀錄
|
||||
// 如果數量沒變,是否要寫紀錄?通常編輯頁面按儲存可能只改了其他欄位(如果有)
|
||||
// 但因為我們目前只存 quantity,如果 quantity 沒變,可以不寫異動,或者寫一筆 0 的異動代表更新屬性
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => ($validated['reason'] ?? '編輯頁面更新') . ($validated['notes'] ?? ''),
|
||||
'actual_time' => now(), // 手動調整設定為當下
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $inventory->warehouse_id)
|
||||
->with('success', '庫存資料已更新');
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
$inventory = \App\Models\Inventory::findOrFail($inventoryId);
|
||||
|
||||
// 歸零異動
|
||||
if ($inventory->quantity > 0) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動編輯',
|
||||
'quantity' => -$inventory->quantity,
|
||||
'balance_before' => $inventory->quantity,
|
||||
'balance_after' => 0,
|
||||
'reason' => '刪除庫存品項',
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
$inventory->delete();
|
||||
|
||||
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
||||
->with('success', '庫存品項已刪除');
|
||||
}
|
||||
|
||||
public function history(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||
|
||||
$transactions = $inventory->transactions->map(function ($tx) {
|
||||
return [
|
||||
'id' => (string) $tx->id,
|
||||
'type' => $tx->type,
|
||||
'quantity' => (float) $tx->quantity,
|
||||
'balanceAfter' => (float) $tx->balance_after,
|
||||
'reason' => $tx->reason,
|
||||
'userName' => $tx->user ? $tx->user->name : '系統',
|
||||
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
||||
];
|
||||
});
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventory' => [
|
||||
'id' => (string) $inventory->id,
|
||||
'productName' => $inventory->product->name,
|
||||
'productCode' => $inventory->product->code,
|
||||
'quantity' => (float) $inventory->quantity,
|
||||
],
|
||||
'transactions' => $transactions
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user