Files
star-erp/app/Modules/Inventory/Controllers/CountDocController.php
sky121113 702af0a259 feat(inventory): 重構庫存盤點流程與優化操作日誌
1. 重構盤點流程:實作自動狀態轉換(盤點中/盤點完成)、整合按鈕為「儲存盤點結果」、更名 UI 狀態標籤。
2. 優化操作日誌:
   - 實作全域 ID 轉名稱邏輯(倉庫、使用者)。
   - 合併單次操作的日誌記錄,避免重複產生。
   - 修復日誌產生過程中的 Collection 修改錯誤。
3. 修正 TypeScript lint 錯誤(Index, Show 頁面)。
2026-02-04 15:12:10 +08:00

224 lines
8.2 KiB
PHP

<?php
namespace App\Modules\Inventory\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Inventory\Models\InventoryCountDoc;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Services\CountService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CountDocController extends Controller
{
protected $countService;
public function __construct(CountService $countService)
{
$this->countService = $countService;
}
public function index(Request $request)
{
$query = InventoryCountDoc::query()
->with(['createdBy', 'completedBy', 'warehouse']);
if ($request->filled('warehouse_id')) {
$query->where('warehouse_id', $request->warehouse_id);
}
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('doc_no', 'like', "%{$search}%")
->orWhere('remarks', 'like', "%{$search}%");
});
}
$perPage = $request->input('per_page', 10);
if (!in_array($perPage, [10, 20, 50, 100])) {
$perPage = 10;
}
$countQuery = function ($query) {
$query->whereNotNull('counted_qty');
};
$docs = $query->withCount(['items', 'items as counted_items_count' => $countQuery])
->orderByDesc('created_at')
->paginate($perPage)
->withQueryString()
->through(function ($doc) {
return [
'id' => (string) $doc->id,
'doc_no' => $doc->doc_no,
'status' => $doc->status,
'warehouse_name' => $doc->warehouse->name,
'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d H:i') : '-',
'completed_at' => $doc->completed_at ? $doc->completed_at->format('Y-m-d H:i') : '-',
'created_by' => $doc->createdBy?->name,
'remarks' => $doc->remarks,
'total_items' => $doc->items_count,
'counted_items' => $doc->counted_items_count,
];
});
return Inertia::render('Inventory/Count/Index', [
'docs' => $docs,
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'filters' => $request->only(['warehouse_id', 'search', 'per_page']),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'warehouse_id' => 'required|exists:warehouses,id',
'remarks' => 'nullable|string|max:255',
]);
$doc = $this->countService->createDoc(
$validated['warehouse_id'],
$validated['remarks'] ?? null,
auth()->id()
);
// 自動執行快照
$this->countService->snapshot($doc, false);
return redirect()->route('inventory.count.show', [$doc->id])
->with('success', '已建立盤點單並完成庫存快照');
}
public function show(InventoryCountDoc $doc)
{
$doc->load(['items.product.baseUnit', 'createdBy', 'completedBy', 'warehouse']);
$docData = [
'id' => (string) $doc->id,
'doc_no' => $doc->doc_no,
'warehouse_id' => (string) $doc->warehouse_id,
'warehouse_name' => $doc->warehouse->name,
'status' => $doc->status,
'remarks' => $doc->remarks,
'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d H:i') : null,
'created_by' => $doc->createdBy?->name,
'items' => $doc->items->map(function ($item) {
return [
'id' => (string) $item->id,
'product_name' => $item->product->name,
'product_code' => $item->product->code,
'batch_number' => $item->batch_number,
'unit' => $item->product->baseUnit?->name,
'system_qty' => (float) $item->system_qty,
'counted_qty' => is_null($item->counted_qty) ? '' : (float) $item->counted_qty,
'diff_qty' => (float) $item->diff_qty,
'notes' => $item->notes,
];
}),
];
return Inertia::render('Inventory/Count/Show', [
'doc' => $docData,
]);
}
public function print(InventoryCountDoc $doc)
{
$doc->load(['items.product.baseUnit', 'createdBy', 'completedBy', 'warehouse']);
$docData = [
'id' => (string) $doc->id,
'doc_no' => $doc->doc_no,
'warehouse_name' => $doc->warehouse->name,
'snapshot_date' => $doc->snapshot_date ? $doc->snapshot_date->format('Y-m-d') : date('Y-m-d'), // Use date only
'created_at' => $doc->created_at->format('Y-m-d'),
'print_date' => date('Y-m-d'),
'created_by' => $doc->createdBy?->name,
'items' => $doc->items->map(function ($item) {
return [
'id' => (string) $item->id,
'product_name' => $item->product->name,
'product_code' => $item->product->code,
'specification' => $item->product->specification,
'unit' => $item->product->baseUnit?->name,
'quantity' => (float) ($item->counted_qty ?? $item->system_qty), // Default to system qty if counted is null, or just counted? User wants "Count Sheet" -> maybe blank if not counted?
// Actually, if it's "Completed", we show counted. If it's "Pending", we usually show blank or system.
// The 'Show' page logic suggests we show counted_qty.
'counted_qty' => $item->counted_qty,
'notes' => $item->notes,
];
}),
];
return Inertia::render('Inventory/Count/Print', [
'doc' => $docData,
]);
}
public function update(Request $request, InventoryCountDoc $doc)
{
if ($doc->status === 'completed') {
return redirect()->back()->with('error', '此盤點單已完成,無法修改');
}
$validated = $request->validate([
'items' => 'array',
'items.*.id' => 'required|exists:inventory_count_items,id',
'items.*.counted_qty' => 'nullable|numeric|min:0',
'items.*.notes' => 'nullable|string',
]);
if (isset($validated['items'])) {
$this->countService->updateCount($doc, $validated['items']);
}
// 重新讀取以獲取最新狀態
$doc->refresh();
if ($doc->status === 'completed') {
return redirect()->route('inventory.count.index')
->with('success', '盤點完成,單據已自動存檔並完成。');
}
return redirect()->back()->with('success', '盤點資料已暫存');
}
public function reopen(InventoryCountDoc $doc)
{
// 權限檢查 (通常僅允許有權限者執行,例如 inventory.adjust)
// 注意:前端已經用 <Can> 保護按鈕,後端這裡最好也加上檢查
if (!auth()->user()->can('inventory.adjust')) {
abort(403);
}
if ($doc->status !== 'completed') {
return redirect()->back()->with('error', '僅能針對已完成的盤點單重新開啟盤點');
}
// 執行取消核准邏輯
$doc->update([
'status' => 'counting', // 回復為盤點中
'completed_at' => null, // 清除完成時間
'completed_by' => null, // 清除完成者
]);
return redirect()->back()->with('success', '已重新開啟盤點,單據回復為盤點中狀態');
}
public function destroy(InventoryCountDoc $doc)
{
if ($doc->status === 'completed') {
return redirect()->back()->with('error', '已完成的盤點單無法刪除');
}
// Activity Log handled by Model Trait
$doc->items()->delete();
$doc->delete();
return redirect()->route('inventory.count.index')
->with('success', '盤點單已刪除');
}
}