diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 26b677e..78f845f 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -10,6 +10,8 @@ use App\Models\Machine\Machine; use Carbon\Carbon; use Illuminate\Support\Facades\Auth; +use App\Models\System\SystemOperationLog; + class SalesController extends Controller { // 銷售&金流紀錄 @@ -30,34 +32,58 @@ class SalesController extends Controller // 取貨碼設定 public function pickupCodes(Request $request) { - $query = PickupCode::with(['machine.slots.product', 'creator'])->latest(); + $tab = $request->input('tab', 'list'); + $isAjax = $request->ajax(); - if ($request->search) { - $query->where(function ($q) use ($request) { - $q->where('code', 'like', "%{$request->search}%") - ->orWhereHas('machine', function ($mq) use ($request) { - $mq->where('name', 'like', "%{$request->search}%") - ->orWhere('serial_no', 'like', "%{$request->search}%"); - }); - }); - } - - if ($request->status && trim($request->status) !== '') { - $query->where('status', trim($request->status)); - } - - $per_page = $request->input('per_page', 15); - $pickupCodes = $query->paginate($per_page)->withQueryString(); - - // 供新增彈窗使用的機台清單 - $machines = Machine::all(); - - return view('admin.sales.pickup-codes.index', [ + $data = [ 'title' => '取貨碼設定', 'description' => '產生與管理商品取貨驗證碼', - 'pickupCodes' => $pickupCodes, - 'machines' => $machines, - ]); + 'tab' => $tab, + ]; + + // 1. 取貨碼列表 (list) + if (!$isAjax || $tab === 'list') { + $query = PickupCode::with(['machine.slots.product', 'creator'])->latest(); + + if ($request->search) { + $query->where(function ($q) use ($request) { + $q->where('code', 'like', "%{$request->search}%") + ->orWhereHas('machine', function ($mq) use ($request) { + $mq->where('name', 'like', "%{$request->search}%") + ->orWhere('serial_no', 'like', "%{$request->search}%"); + }); + }); + } + + if ($request->status && trim($request->status) !== '') { + $query->where('status', trim($request->status)); + } + + $per_page = $request->input('per_page', 15); + $data['pickupCodes'] = $query->paginate($per_page, ['*'], 'list_page')->withQueryString(); + + // 供新增彈窗使用的機台清單 + $data['machines'] = Machine::all(); + } + + // 2. 操作紀錄 (logs) + if (!$isAjax || $tab === 'logs') { + $data['logs'] = SystemOperationLog::with('user:id,name') + ->where('module', 'pickup_code') + ->latest() + ->paginate($request->input('per_page', 15), ['*'], 'log_page') + ->withQueryString(); + } + + if ($isAjax) { + return response()->json([ + 'success' => true, + 'tab' => $tab, + 'html' => view('admin.sales.pickup-codes.partials.tab-' . $tab, $data)->render() + ]); + } + + return view('admin.sales.pickup-codes.index', $data); } /** @@ -85,6 +111,16 @@ class SalesController extends Controller 'created_by' => Auth::id(), ]); + SystemOperationLog::create([ + 'company_id' => $machine->company_id, + 'user_id' => Auth::id(), + 'module' => 'pickup_code', + 'action' => 'create', + 'target_id' => $pickupCode->id, + 'target_type' => PickupCode::class, + 'new_values' => $pickupCode->toArray(), + ]); + return back()->with('success', __('Pickup code generated: :code', ['code' => $pickupCode->code])); } @@ -97,10 +133,23 @@ class SalesController extends Controller 'expires_at' => 'required|date|after:now', ]); + $oldValues = $pickupCode->toArray(); + $pickupCode->update([ 'expires_at' => $validated['expires_at'], ]); + SystemOperationLog::create([ + 'company_id' => $pickupCode->company_id, + 'user_id' => Auth::id(), + 'module' => 'pickup_code', + 'action' => 'update', + 'target_id' => $pickupCode->id, + 'target_type' => PickupCode::class, + 'old_values' => $oldValues, + 'new_values' => $pickupCode->toArray(), + ]); + return back()->with('success', __('Pickup code updated.')); } @@ -109,7 +158,20 @@ class SalesController extends Controller */ public function destroyPickupCode(PickupCode $pickupCode) { + $oldValues = $pickupCode->toArray(); $pickupCode->update(['status' => 'cancelled']); + + SystemOperationLog::create([ + 'company_id' => $pickupCode->company_id, + 'user_id' => Auth::id(), + 'module' => 'pickup_code', + 'action' => 'cancel', + 'target_id' => $pickupCode->id, + 'target_type' => PickupCode::class, + 'old_values' => $oldValues, + 'new_values' => $pickupCode->toArray(), + ]); + return back()->with('success', __('Pickup code cancelled.')); } diff --git a/app/Models/System/SystemOperationLog.php b/app/Models/System/SystemOperationLog.php new file mode 100644 index 0000000..9970f83 --- /dev/null +++ b/app/Models/System/SystemOperationLog.php @@ -0,0 +1,37 @@ + 'array', + 'new_values' => 'array', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/database/migrations/2026_04_30_094101_create_system_operation_logs_table.php b/database/migrations/2026_04_30_094101_create_system_operation_logs_table.php new file mode 100644 index 0000000..cea1695 --- /dev/null +++ b/database/migrations/2026_04_30_094101_create_system_operation_logs_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade'); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); + $table->string('module', 50)->index(); + $table->string('action', 50); + $table->unsignedBigInteger('target_id')->nullable()->index(); + $table->string('target_type', 100)->nullable(); + $table->json('old_values')->nullable(); + $table->json('new_values')->nullable(); + $table->text('note')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('system_operation_logs'); + } +}; diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php index 4bc3c71..3e3ca30 100644 --- a/resources/views/admin/sales/pickup-codes/index.blade.php +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -6,30 +6,13 @@ $slotSelectConfig = [ "hasSearch" => true, "searchPlaceholder" => __("Search Slot..."), "isHidePlaceholder" => false, -"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg -focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 -placeholder:text-slate-400 dark:placeholder:text-slate-500", +"searchClasses" => "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500", "searchWrapperClasses" => "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10", "toggleClasses" => "hs-select-toggle luxury-select-toggle", -"toggleTemplate" => '', -"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 -rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in", -"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer -hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between -transition-all duration-300", -"optionTemplate" => '
' +"toggleTemplate" => '', +"dropdownClasses" => "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] animate-luxury-in", +"optionClasses" => "hs-select-option py-2.5 px-3 mb-0.5 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg flex items-center justify-between transition-all duration-300", +"optionTemplate" => '
' ]; @endphp @@ -50,18 +33,33 @@ transition-all duration-300", - {{-- Main Content Card --}} -
- + {{-- Tab Navigation --}} +
+ + + + +
-
+ {{-- Content Containers --}} +
+ {{-- List Tab --}} +
-
+ {{-- Main Content Card --}} +
+ + +
+ +
@@ -351,7 +349,26 @@ transition-all duration-300",
{{ $pickupCodes->links('vendor.pagination.luxury') }}
+
{{-- End tab-list-container --}} +
{{-- End luxury-card --}}
+ {{-- End List Tab --}} + + {{-- Logs Tab --}} + + {{-- End Logs Tab --}}
{{-- Create Modal --}} @@ -582,6 +599,8 @@ transition-all duration-300",