[FIX] 取貨碼操作紀錄顯示異常修正
1. 修正 index.blade.php 的 HTML 標籤嵌套錯誤,解決操作紀錄分頁空白問題。 2. 移除暫時的除錯訊息。 3. 整合 SystemOperationLog 機制於取貨碼模組。
This commit is contained in:
parent
84bf7de750
commit
820e1c87db
@ -10,6 +10,8 @@ use App\Models\Machine\Machine;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
use App\Models\System\SystemOperationLog;
|
||||||
|
|
||||||
class SalesController extends Controller
|
class SalesController extends Controller
|
||||||
{
|
{
|
||||||
// 銷售&金流紀錄
|
// 銷售&金流紀錄
|
||||||
@ -30,34 +32,58 @@ class SalesController extends Controller
|
|||||||
// 取貨碼設定
|
// 取貨碼設定
|
||||||
public function pickupCodes(Request $request)
|
public function pickupCodes(Request $request)
|
||||||
{
|
{
|
||||||
$query = PickupCode::with(['machine.slots.product', 'creator'])->latest();
|
$tab = $request->input('tab', 'list');
|
||||||
|
$isAjax = $request->ajax();
|
||||||
|
|
||||||
if ($request->search) {
|
$data = [
|
||||||
$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', [
|
|
||||||
'title' => '取貨碼設定',
|
'title' => '取貨碼設定',
|
||||||
'description' => '產生與管理商品取貨驗證碼',
|
'description' => '產生與管理商品取貨驗證碼',
|
||||||
'pickupCodes' => $pickupCodes,
|
'tab' => $tab,
|
||||||
'machines' => $machines,
|
];
|
||||||
]);
|
|
||||||
|
// 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(),
|
'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]));
|
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',
|
'expires_at' => 'required|date|after:now',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$oldValues = $pickupCode->toArray();
|
||||||
|
|
||||||
$pickupCode->update([
|
$pickupCode->update([
|
||||||
'expires_at' => $validated['expires_at'],
|
'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.'));
|
return back()->with('success', __('Pickup code updated.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +158,20 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroyPickupCode(PickupCode $pickupCode)
|
public function destroyPickupCode(PickupCode $pickupCode)
|
||||||
{
|
{
|
||||||
|
$oldValues = $pickupCode->toArray();
|
||||||
$pickupCode->update(['status' => 'cancelled']);
|
$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.'));
|
return back()->with('success', __('Pickup code cancelled.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
app/Models/System/SystemOperationLog.php
Normal file
37
app/Models/System/SystemOperationLog.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\System;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Traits\TenantScoped;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class SystemOperationLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, TenantScoped;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'company_id',
|
||||||
|
'user_id',
|
||||||
|
'module',
|
||||||
|
'action',
|
||||||
|
'target_id',
|
||||||
|
'target_type',
|
||||||
|
'old_values',
|
||||||
|
'new_values',
|
||||||
|
'note',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'old_values' => 'array',
|
||||||
|
'new_values' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('system_operation_logs', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -6,30 +6,13 @@ $slotSelectConfig = [
|
|||||||
"hasSearch" => true,
|
"hasSearch" => true,
|
||||||
"searchPlaceholder" => __("Search Slot..."),
|
"searchPlaceholder" => __("Search Slot..."),
|
||||||
"isHidePlaceholder" => false,
|
"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
|
"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",
|
||||||
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",
|
"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",
|
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
|
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></div></button>',
|
||||||
class="text-slate-800 dark:text-slate-200" data-title></span>
|
"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",
|
||||||
<div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300"
|
"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",
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="m6 9 6 6 6-6" />
|
|
||||||
</svg></div>
|
|
||||||
</button>',
|
|
||||||
"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" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
|
|
||||||
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg></span></div>'
|
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@ -50,18 +33,33 @@ transition-all duration-300",
|
|||||||
</button>
|
</button>
|
||||||
</x-page-header>
|
</x-page-header>
|
||||||
|
|
||||||
{{-- Main Content Card --}}
|
{{-- Tab Navigation --}}
|
||||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
<div class="mb-4">
|
||||||
<x-luxury-spinner show="isLoadingTable" />
|
<x-tab-nav model="activeTab">
|
||||||
|
<x-tab-nav-item value="list" :label="__('Pickup Codes')" model="activeTab" />
|
||||||
|
<x-tab-nav-item value="logs" :label="__('Operation Records')" model="activeTab" />
|
||||||
|
</x-tab-nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="ajax-content-container"
|
{{-- Content Containers --}}
|
||||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
<div class="relative min-h-[400px]">
|
||||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
{{-- List Tab --}}
|
||||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('per_page='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
<div x-show="activeTab === 'list'" x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
style="display: none;">
|
||||||
|
|
||||||
<form action="{{ route('admin.sales.pickup-codes') }}" method="GET"
|
{{-- Main Content Card --}}
|
||||||
class="flex flex-col md:flex-row md:items-center gap-3 mb-8"
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
<x-luxury-spinner show="isLoadingTable" />
|
||||||
|
|
||||||
|
<div id="tab-list-container"
|
||||||
|
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
||||||
|
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||||
|
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('per_page='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||||
|
|
||||||
|
<form action="{{ route('admin.sales.pickup-codes') }}" method="GET"
|
||||||
|
class="flex flex-col md:flex-row md:items-center gap-3 mb-8"
|
||||||
|
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||||
|
|
||||||
<div class="relative group flex-1 md:flex-none">
|
<div class="relative group flex-1 md:flex-none">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
@ -351,7 +349,26 @@ transition-all duration-300",
|
|||||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||||
{{ $pickupCodes->links('vendor.pagination.luxury') }}
|
{{ $pickupCodes->links('vendor.pagination.luxury') }}
|
||||||
</div>
|
</div>
|
||||||
|
</div> {{-- End tab-list-container --}}
|
||||||
|
</div> {{-- End luxury-card --}}
|
||||||
</div>
|
</div>
|
||||||
|
{{-- End List Tab --}}
|
||||||
|
|
||||||
|
{{-- Logs Tab --}}
|
||||||
|
<div x-show="activeTab === 'logs'" class="relative"
|
||||||
|
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0" style="display: none;">
|
||||||
|
|
||||||
|
{{-- Main Content Card --}}
|
||||||
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
|
||||||
|
<x-luxury-spinner show="tabLoading === 'logs'" />
|
||||||
|
|
||||||
|
<div id="tab-logs-container" class="relative">
|
||||||
|
@include('admin.sales.pickup-codes.partials.tab-logs')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{-- End Logs Tab --}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Create Modal --}}
|
{{-- Create Modal --}}
|
||||||
@ -582,6 +599,8 @@ transition-all duration-300",
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('pickupCodeManager', (config) => ({
|
Alpine.data('pickupCodeManager', (config) => ({
|
||||||
|
activeTab: '{{ request("tab", "list") }}',
|
||||||
|
tabLoading: null,
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
selectedMachine: '',
|
selectedMachine: '',
|
||||||
selectedSlot: '',
|
selectedSlot: '',
|
||||||
@ -598,6 +617,48 @@ transition-all duration-300",
|
|||||||
slotSelectConfig: config,
|
slotSelectConfig: config,
|
||||||
customCode: '',
|
customCode: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async fetchTabData(tab, containerSelector) {
|
||||||
|
if (this.tabLoading === tab) return;
|
||||||
|
this.tabLoading = tab;
|
||||||
|
|
||||||
|
const container = document.querySelector(containerSelector);
|
||||||
|
let url = '{{ route("admin.sales.pickup-codes") }}';
|
||||||
|
|
||||||
|
const urlObj = new URL(url, window.location.origin);
|
||||||
|
urlObj.searchParams.set('tab', tab);
|
||||||
|
urlObj.searchParams.set('_ajax', '1');
|
||||||
|
url = urlObj.toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = data.html;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyUrl = new URL(url, window.location.origin);
|
||||||
|
historyUrl.searchParams.delete('_ajax');
|
||||||
|
window.history.pushState({}, '', historyUrl.toString());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
window.showToast?.('{{ __("Loading failed") }}', 'error');
|
||||||
|
} finally {
|
||||||
|
this.tabLoading = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
generateRandomCode() {
|
generateRandomCode() {
|
||||||
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
|
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
|
||||||
},
|
},
|
||||||
@ -668,14 +729,14 @@ transition-all duration-300",
|
|||||||
copyQrLink() {
|
copyQrLink() {
|
||||||
if (!this.activeTicketUrl) return;
|
if (!this.activeTicketUrl) return;
|
||||||
navigator.clipboard.writeText(this.activeTicketUrl).then(() => {
|
navigator.clipboard.writeText(this.activeTicketUrl).then(() => {
|
||||||
window.showToast('{{ __('Link Copied') }}', 'success');
|
window.showToast("{{ __('Link Copied') }}", 'success');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
copyQrCode() {
|
copyQrCode() {
|
||||||
if (!this.activeQrCode) return;
|
if (!this.activeQrCode) return;
|
||||||
navigator.clipboard.writeText(this.activeQrCode).then(() => {
|
navigator.clipboard.writeText(this.activeQrCode).then(() => {
|
||||||
window.showToast('{{ __('Code Copied') }}', 'success');
|
window.showToast("{{ __('Code Copied') }}", 'success');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -773,6 +834,22 @@ transition-all duration-300",
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.generateRandomCode();
|
this.generateRandomCode();
|
||||||
|
|
||||||
|
// Tab 切換時更新 URL(同庫存管理模式)
|
||||||
|
this.$watch('activeTab', (newTab) => {
|
||||||
|
const url = new URL(window.location.origin + window.location.pathname);
|
||||||
|
url.searchParams.set('tab', newTab);
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
|
||||||
|
if (newTab === 'logs') {
|
||||||
|
this.fetchTabData('logs', '#tab-logs-container');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.$watch('slots', () => this.updateSlotSelect());
|
this.$watch('slots', () => this.updateSlotSelect());
|
||||||
this.$watch('selectedMachine', (val) => {
|
this.$watch('selectedMachine', (val) => {
|
||||||
this.syncSelect('modal-pickup-machine', val);
|
this.syncSelect('modal-pickup-machine', val);
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
<div class="overflow-hidden bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border border-slate-200 dark:border-slate-800 shadow-sm rounded-xl">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-800">
|
||||||
|
<thead class="bg-slate-50/50 dark:bg-slate-800/50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-4 text-left text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
|
{{ __('Time') }}
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-4 text-left text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
|
{{ __('Operator') }}
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-4 text-left text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
|
{{ __('Actions') }}
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-4 text-left text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
|
{{ __('Target') }}
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-4 text-left text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">
|
||||||
|
{{ __('Notes') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
|
||||||
|
@forelse($logs as $log)
|
||||||
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/50 transition-colors duration-200">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
{{ $log->created_at->format('Y-m-d H:i:s') }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-6 h-6 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center text-indigo-600 dark:text-indigo-400 text-xs font-medium">
|
||||||
|
{{ Str::substr($log->user->name ?? 'Sys', 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-slate-700 dark:text-slate-200">
|
||||||
|
{{ $log->user->name ?? __('System') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
@if($log->action === 'create')
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800/50">
|
||||||
|
{{ __('Generate') }}
|
||||||
|
</span>
|
||||||
|
@elseif($log->action === 'update')
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 border border-blue-200 dark:border-blue-800/50">
|
||||||
|
{{ __('Update') }}
|
||||||
|
</span>
|
||||||
|
@elseif($log->action === 'cancel')
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-400 border border-rose-200 dark:border-rose-800/50">
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-slate-50 text-slate-700 dark:bg-slate-500/10 dark:text-slate-400 border border-slate-200 dark:border-slate-800/50">
|
||||||
|
{{ $log->action }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
ID: {{ $log->target_id }}
|
||||||
|
@if($log->new_values && isset($log->new_values['code']))
|
||||||
|
<span class="ml-2 px-2 py-0.5 rounded text-xs bg-slate-100 dark:bg-slate-800 font-mono">
|
||||||
|
{{ $log->new_values['code'] }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
@if($log->action === 'update' && isset($log->old_values['expires_at']) && isset($log->new_values['expires_at']))
|
||||||
|
<div>{{ __('Expires At') }}:
|
||||||
|
<span class="line-through opacity-70">{{ \Carbon\Carbon::parse($log->old_values['expires_at'])->format('Y-m-d H:i') }}</span>
|
||||||
|
<svg class="inline w-3 h-3 mx-1 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" /></svg>
|
||||||
|
<span class="text-amber-600 dark:text-amber-400 font-medium">{{ \Carbon\Carbon::parse($log->new_values['expires_at'])->format('Y-m-d H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-12 text-center">
|
||||||
|
<div class="flex flex-col items-center justify-center text-slate-400 dark:text-slate-500">
|
||||||
|
<svg class="w-12 h-12 mb-4 opacity-50" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm font-medium">{{ __('No records found') }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
@if(isset($logs) && $logs instanceof \Illuminate\Pagination\LengthAwarePaginator && $logs->hasPages())
|
||||||
|
<div class="px-6 py-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50">
|
||||||
|
{{ $logs->links('vendor.pagination.luxury') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@ -19,30 +19,13 @@ $config = [
|
|||||||
"hasSearch" => (bool)$hasSearch,
|
"hasSearch" => (bool)$hasSearch,
|
||||||
"searchPlaceholder" => $placeholder ?: __('Search...'),
|
"searchPlaceholder" => $placeholder ?: __('Search...'),
|
||||||
"isHidePlaceholder" => false,
|
"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
|
"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",
|
||||||
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",
|
"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",
|
"toggleClasses" => "hs-select-toggle luxury-select-toggle",
|
||||||
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span
|
"toggleTemplate" => '<button type="button" aria-expanded="false"><span class="me-2" data-icon></span><span class="text-slate-800 dark:text-slate-200" data-title></span><div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></div></button>',
|
||||||
class="text-slate-800 dark:text-slate-200" data-title></span>
|
"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",
|
||||||
<div class="ms-auto"><svg class="size-4 text-slate-400 transition-transform duration-300"
|
"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",
|
||||||
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
"optionTemplate" => '<div class="flex items-center justify-between w-full"><span data-title></span><span class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></span></div>'
|
||||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="m6 9 6 6 6-6" />
|
|
||||||
</svg></div>
|
|
||||||
</button>',
|
|
||||||
"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" => '<div class="flex items-center justify-between w-full"><span data-title></span><span
|
|
||||||
class="hs-select-active-indicator hidden text-cyan-500"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg></span></div>'
|
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user