2026-01-28 18:04:45 +08:00
< ? 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 ])) {
2026-02-03 17:24:34 +08:00
$perPage = 10 ;
2026-01-28 18:04:45 +08:00
}
2026-01-29 13:04:54 +08:00
$countQuery = function ( $query ) {
$query -> whereNotNull ( 'counted_qty' );
};
$docs = $query -> withCount ([ 'items' , 'items as counted_items_count' => $countQuery ])
-> orderByDesc ( 'created_at' )
2026-01-28 18:04:45 +08:00
-> 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 ,
2026-01-29 13:04:54 +08:00
'total_items' => $doc -> items_count ,
'counted_items' => $doc -> counted_items_count ,
2026-01-28 18:04:45 +08:00
];
});
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 ()
);
// 自動執行快照
2026-02-04 15:12:10 +08:00
$this -> countService -> snapshot ( $doc , false );
2026-01-28 18:04:45 +08:00
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 ,
]);
}
2026-01-29 13:04:54 +08:00
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 ,
]);
}
2026-01-28 18:04:45 +08:00
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' ]);
}
2026-02-04 15:12:10 +08:00
// 重新讀取以獲取最新狀態
$doc -> refresh ();
if ( $doc -> status === 'completed' ) {
2026-01-28 18:04:45 +08:00
return redirect () -> route ( 'inventory.count.index' )
2026-02-04 15:12:10 +08:00
-> with ( 'success' , '盤點完成,單據已自動存檔並完成。' );
2026-01-28 18:04:45 +08:00
}
2026-02-04 15:12:10 +08:00
return redirect () -> back () -> with ( 'success' , '盤點資料已暫存' );
}
public function reopen ( InventoryCountDoc $doc )
{
// 權限檢查 (通常僅允許有權限者執行,例如 inventory.adjust)
// 注意:前端已經用 <Can> 保護按鈕,後端這裡最好也加上檢查
if ( ! auth () -> user () -> can ( 'inventory.adjust' )) {
abort ( 403 );
}
2026-02-04 16:56:08 +08:00
if ( ! in_array ( $doc -> status , [ 'completed' , 'no_adjust' ])) {
return redirect () -> back () -> with ( 'error' , '僅能針對已完成或無需盤調的盤點單重新開啟盤點' );
2026-02-04 15:12:10 +08:00
}
// 執行取消核准邏輯
$doc -> update ([
'status' => 'counting' , // 回復為盤點中
'completed_at' => null , // 清除完成時間
'completed_by' => null , // 清除完成者
]);
return redirect () -> back () -> with ( 'success' , '已重新開啟盤點,單據回復為盤點中狀態' );
2026-01-28 18:04:45 +08:00
}
2026-01-29 13:04:54 +08:00
2026-02-04 13:25:49 +08:00
public function destroy ( InventoryCountDoc $doc )
2026-01-29 13:04:54 +08:00
{
2026-02-04 13:25:49 +08:00
if ( $doc -> status === 'completed' ) {
return redirect () -> back () -> with ( 'error' , '已完成的盤點單無法刪除' );
2026-01-29 13:04:54 +08:00
}
2026-02-04 15:12:10 +08:00
// Activity Log handled by Model Trait
2026-02-04 13:25:49 +08:00
2026-01-28 18:04:45 +08:00
$doc -> items () -> delete ();
$doc -> delete ();
2026-02-04 13:25:49 +08:00
2026-01-28 18:04:45 +08:00
return redirect () -> route ( 'inventory.count.index' )
-> with ( 'success' , '盤點單已刪除' );
}
}