diff --git a/app/Http/Controllers/Admin/RemoteController.php b/app/Http/Controllers/Admin/RemoteController.php index f10aee0..2a17873 100644 --- a/app/Http/Controllers/Admin/RemoteController.php +++ b/app/Http/Controllers/Admin/RemoteController.php @@ -15,19 +15,32 @@ class RemoteController extends Controller */ public function index(Request $request) { - $machines = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get(); $selectedMachine = null; + // --- 1. 機台列表處理 (New Command Tab) --- + $machineQuery = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc'); + + if ($request->filled('search') && $request->input('tab') === 'list') { + $search = $request->input('search'); + $machineQuery->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('serial_no', 'like', "%{$search}%"); + }); + } + + $machines = $machineQuery->paginate($request->input('per_page', 10), ['*'], 'machine_page'); + + // --- 2. 歷史紀錄處理 (Operation Records Tab) --- $historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock') ->with(['machine', 'user']); - if ($request->filled('search')) { + if ($request->filled('search') && ($request->input('tab') === 'history' || !$request->has('tab'))) { $search = $request->input('search'); - $historyQuery->where(function($q) use ($search) { - $q->whereHas('machine', function($mq) use ($search) { + $historyQuery->where(function ($q) use ($search) { + $q->whereHas('machine', function ($mq) use ($search) { $mq->where('name', 'like', "%{$search}%") - ->orWhere('serial_no', 'like', "%{$search}%"); - })->orWhereHas('user', function($uq) use ($search) { + ->orWhere('serial_no', 'like', "%{$search}%"); + })->orWhereHas('user', function ($uq) use ($search) { $uq->where('name', 'like', "%{$search}%"); }); }); @@ -59,17 +72,35 @@ class RemoteController extends Controller $historyQuery->where('status', $request->input('status')); } - $history = $historyQuery->latest()->paginate($request->input('per_page', 10)); + $history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page'); + // --- 3. 特定機台詳情處理 --- if ($request->has('machine_id')) { - $selectedMachine = Machine::with(['slots.product', 'commands' => function($query) { - $query->where('command_type', '!=', 'reload_stock') - ->latest() - ->limit(5); - }])->find($request->machine_id); + $selectedMachine = Machine::with([ + 'slots.product', + 'commands' => function ($query) { + $query->where('command_type', '!=', 'reload_stock') + ->latest() + ->limit(5); + } + ])->find($request->machine_id); } + // --- 4. AJAX 回應處理 --- if ($request->ajax()) { + if ($request->has('tab')) { + $tab = $request->input('tab'); + $viewPath = $tab === 'list' ? 'admin.remote.partials.tab-machines-index' : 'admin.remote.partials.tab-history-index'; + + return response()->json([ + 'success' => true, + 'html' => view($viewPath, [ + 'machines' => $machines, + 'history' => $history, + ])->render() + ]); + } + return response()->json([ 'success' => true, 'machine' => $selectedMachine, @@ -142,29 +173,42 @@ class RemoteController extends Controller */ public function stock(Request $request) { - $machines = Machine::withCount([ + // 1. 機台查詢與分頁 + $machineQuery = Machine::withCount([ 'slots as slots_count', 'slots as low_stock_count' => function ($query) { - $query->where('stock', '<=', 5); + $query->where('stock', '<=', 5); }, 'slots as expiring_soon_count' => function ($query) { $query->whereNotNull('expiry_date') - ->where('expiry_date', '<=', now()->addDays(7)) - ->where('expiry_date', '>=', now()->startOfDay()); + ->where('expiry_date', '<=', now()->addDays(7)) + ->where('expiry_date', '>=', now()->startOfDay()); } - ])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get(); - - $historyQuery = RemoteCommand::with(['machine', 'user']); + ]); + if ($request->filled('machine_search')) { + $ms = $request->input('machine_search'); + $machineQuery->where(function ($q) use ($ms) { + $q->where('name', 'like', "%{$ms}%") + ->orWhere('serial_no', 'like', "%{$ms}%"); + }); + } + + $machines = $machineQuery->orderBy('last_heartbeat_at', 'desc') + ->orderBy('id', 'desc') + ->paginate($request->input('per_page', 10), ['*'], 'machine_page'); + + // 2. 歷史紀錄查詢與分頁 + $historyQuery = RemoteCommand::with(['machine', 'user']); $historyQuery->where('command_type', 'reload_stock'); if ($request->filled('search')) { $search = $request->input('search'); - $historyQuery->where(function($q) use ($search) { - $q->whereHas('machine', function($mq) use ($search) { + $historyQuery->where(function ($q) use ($search) { + $q->whereHas('machine', function ($mq) use ($search) { $mq->where('name', 'like', "%{$search}%") - ->orWhere('serial_no', 'like', "%{$search}%"); - })->orWhereHas('user', function($uq) use ($search) { + ->orWhere('serial_no', 'like', "%{$search}%"); + })->orWhereHas('user', function ($uq) use ($search) { $uq->where('name', 'like', "%{$search}%"); }); }); @@ -191,15 +235,31 @@ class RemoteController extends Controller $historyQuery->where('status', $request->input('status')); } - $history = $historyQuery->latest()->paginate($request->input('per_page', 10)); + $history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page'); + + // 3. AJAX 回傳處理 + if ($request->boolean('_ajax')) { + $tab = $request->input('tab', 'history'); + if ($tab === 'machines') { + return response()->view('admin.remote.partials.tab-machines', [ + 'machines' => $machines, + ]); + } + return response()->view('admin.remote.partials.tab-history', [ + 'history' => $history, + ]); + } $selectedMachine = null; if ($request->has('machine_id')) { - $selectedMachine = Machine::with(['slots.product', 'commands' => function($query) { - $query->where('command_type', 'reload_stock') - ->latest() - ->limit(50); - }])->find($request->machine_id); + $selectedMachine = Machine::with([ + 'slots.product', + 'commands' => function ($query) { + $query->where('command_type', 'reload_stock') + ->latest() + ->limit(50); + } + ])->find($request->machine_id); } return view('admin.remote.stock', [ diff --git a/lang/en.json b/lang/en.json index b7dc3be..7fa4133 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1154,5 +1154,6 @@ "Publish Time": "Publish Time", "Expired Time": "Expired Time", "Inventory synced with machine": "Inventory synced with machine", - "Failed to load tab content": "Failed to load tab content" + "Failed to load tab content": "Failed to load tab content", + "No machines found": "No machines found" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 3831844..7f6c714 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1153,5 +1153,6 @@ "Publish Time": "公開時間", "Expired Time": "終了時間", "Inventory synced with machine": "在庫が機体と同期されました", - "Failed to load tab content": "タブコンテンツの読み込みに失敗しました" + "Failed to load tab content": "タブコンテンツの読み込みに失敗しました", + "No machines found": "マシンが見つかりません" } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index fbced3a..44f6840 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1159,5 +1159,6 @@ "Publish Time": "發布時間", "Expired Time": "下架時間", "Inventory synced with machine": "庫存已與機台同步", - "Failed to load tab content": "載入分頁內容失敗" + "Failed to load tab content": "載入分頁內容失敗", + "No machines found": "未找到機台" } \ No newline at end of file diff --git a/resources/views/admin/remote/index.blade.php b/resources/views/admin/remote/index.blade.php index d6fb835..31a2b41 100644 --- a/resources/views/admin/remote/index.blade.php +++ b/resources/views/admin/remote/index.blade.php @@ -15,6 +15,7 @@ // 預設為 history,篩選條件存在時也維持 history history: @js($history), loading: false, + tabLoading: null, submitting: false, // App Config & Meta @@ -67,17 +68,153 @@ note: '', async init() { - if (initialMachineId) { - const machine = this.machines.find(m => m.id == initialMachineId); - if (machine) { - await this.selectMachine(machine); - } - } - // Watch for machine data changes to rebuild slot select this.$watch('selectedMachine.slots', () => { this.$nextTick(() => this.updateSlotSelect()); }); + + // 首次載入時綁定分頁連結 + this.$nextTick(() => { + this.bindPaginationLinks(this.$refs.historyContent, 'history'); + this.bindPaginationLinks(this.$refs.listContent, 'list'); + }); + + // 同步頂部進度條 + this.$watch('tabLoading', (val) => { + const bar = document.getElementById('top-loading-bar'); + if (bar) { + if (val) bar.classList.add('loading'); + else bar.classList.remove('loading'); + } + }); + + if (initialMachineId) { + const machine = this.machines.data.find(m => m.id == initialMachineId); + if (machine) { + await this.selectMachine(machine); + } + } + }, + + // 攔截分頁連結與下拉切換 (與庫存模組一致的強健版本) + bindPaginationLinks(container, tab) { + if (!container) return; + + // 處理連結 + container.querySelectorAll('a[href]').forEach(a => { + const href = a.getAttribute('href'); + if (!href || href.startsWith('#') || href.startsWith('javascript:')) return; + + try { + const url = new URL(href, window.location.origin); + const pageKey = (tab === 'list') ? 'machine_page' : 'history_page'; + + // 確保只有分頁連結被攔截 + if (!url.searchParams.has(pageKey) || a.closest('td.px-6')) return; + + a.addEventListener('click', (e) => { + if (a.title) return; // 略過有 title 的連結 (可能是其他功能) + e.preventDefault(); + const page = url.searchParams.get(pageKey) || 1; + const perPage = url.searchParams.get('per_page') || ''; + let extra = `&${pageKey}=${page}`; + if (perPage) extra += `&per_page=${perPage}`; + + const newUrl = new URL(this.appConfig.indexUrl); + newUrl.searchParams.set('tab', tab); + newUrl.searchParams.set('_ajax', '1'); + extra.split('&').filter(Boolean).forEach(p => { + const [k, v] = p.split('='); + newUrl.searchParams.set(k, v); + }); + + this.fetchTabData(tab, newUrl.toString()); + }); + } catch (err) { } + }); + + // 處理下拉切換 (每頁筆數或跳頁) + container.querySelectorAll('select[onchange]').forEach(sel => { + const origOnchange = sel.getAttribute('onchange'); + sel.removeAttribute('onchange'); + sel.addEventListener('change', () => { + const val = sel.value; + const pageKey = (tab === 'list') ? 'machine_page' : 'history_page'; + try { + if (val.startsWith('http') || val.startsWith('/')) { + const url = new URL(val, window.location.origin); + const page = url.searchParams.get(pageKey) || 1; + const perPage = url.searchParams.get('per_page') || ''; + let extra = `&${pageKey}=${page}`; + if (perPage) extra += `&per_page=${perPage}`; + + const newUrl = new URL(this.appConfig.indexUrl); + newUrl.searchParams.set('tab', tab); + newUrl.searchParams.set('_ajax', '1'); + extra.split('&').filter(Boolean).forEach(p => { + const [k, v] = p.split('='); + newUrl.searchParams.set(k, v); + }); + this.fetchTabData(tab, newUrl.toString()); + } else if (origOnchange && origOnchange.includes('per_page')) { + const newUrl = new URL(this.appConfig.indexUrl); + newUrl.searchParams.set('tab', tab); + newUrl.searchParams.set('_ajax', '1'); + newUrl.searchParams.set('per_page', val); + this.fetchTabData(tab, newUrl.toString()); + } + } catch (err) { } + }); + }); + }, + + async searchInTab(tab, clear = false) { + const form = event?.target?.closest('form'); + const url = new URL(this.appConfig.indexUrl); + url.searchParams.set('tab', tab); + url.searchParams.set('_ajax', '1'); + + if (!clear && form) { + const formData = new FormData(form); + for (let [key, value] of formData.entries()) { + if (value) url.searchParams.set(key, value); + } + } + + await this.fetchTabData(tab, url.toString()); + }, + + async fetchTabData(tab, url) { + this.tabLoading = tab; + try { + const response = await fetch(url, { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + const data = await response.json(); + if (data.success) { + const ref = (tab === 'history') ? this.$refs.historyContent : this.$refs.listContent; + if (ref) { + ref.innerHTML = data.html; + + this.$nextTick(() => { + Alpine.initTree(ref); + this.bindPaginationLinks(ref, tab); + if (window.HSStaticMethods && window.HSStaticMethods.autoInit) { + setTimeout(() => window.HSStaticMethods.autoInit(), 100); + } + }); + } + + // Update browser URL + const browserUrl = new URL(url); + browserUrl.searchParams.delete('_ajax'); + window.history.pushState({}, '', browserUrl.toString()); + } + } catch (e) { + console.error('Fetch error:', e); + } finally { + this.tabLoading = null; + } }, updateSlotSelect() { @@ -400,244 +537,9 @@
-
- -
-
- -
- - - - - - - -
- - -
- - - - - - - - -
- - -
- -
- - -
- -
- - - -
- - - - - - -
-
-
-
- - - - - - - - - - - - - @foreach ($history as $item) - - - - - - - - - @endforeach - @if($history->isEmpty()) - - - - @endif - -
- {{ __('Machine Information') }} - {{ __('Creation Time') }} - {{ __('Picked up Time') }} - {{ __('Command Type') }} - {{ __('Operator') }} - {{ __('Status') }}
-
-
- - - -
-
-
- {{ $item->machine->name }}
-
- {{ $item->machine->serial_no }}
-
-
-
-
- {{ $item->created_at->format('Y/m/d') }} - {{ $item->created_at->format('H:i:s') - }} -
-
- @if($item->executed_at) -
- {{ $item->executed_at->format('Y/m/d') }} - {{ $item->executed_at->format('H:i:s') - }} -
- @else - - - @endif -
-
- -
- - @if($item->note) - - @endif -
-
-
-
-
- {{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }} -
- {{ - $item->user ? $item->user->name : __('System') }} -
-
-
-
-
- -
-
-
-
-
- - - -
-

{{ - __('No records found') }}

-
-
-
- - -
- {{ $history->appends(request()->query())->links('vendor.pagination.luxury') }} +
+
+ @include('admin.remote.partials.tab-history-index')
@@ -647,138 +549,9 @@
-
- -
-
- - - - - - - -
-
- -
- - - - - - - - - - - - -
- {{ __('Machine Information') }} - {{ __('Status') }} - {{ __('Last Communication') }} - {{ __('Actions') }}
+
+
+ @include('admin.remote.partials.tab-machines-index')
diff --git a/resources/views/admin/remote/partials/tab-history-index.blade.php b/resources/views/admin/remote/partials/tab-history-index.blade.php new file mode 100644 index 0000000..4af2008 --- /dev/null +++ b/resources/views/admin/remote/partials/tab-history-index.blade.php @@ -0,0 +1,260 @@ + +
+
+
+
+
+ + + +
+
+

+ {{ __('Loading Data') }}...

+
+ + +
+
+ +
+ + + + + + + +
+ + +
+ + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + + + + + + + + + + + + @foreach ($history as $item) + + + + + + + + + @endforeach + @if($history->isEmpty()) + + + + @endif + +
+ {{ __('Machine Information') }} + {{ __('Creation Time') }} + {{ __('Picked up Time') }} + {{ __('Command Type') }} + {{ __('Operator') }} + {{ __('Status') }}
+
+
+ + +
+
+
+ {{ $item->machine->name }}
+
+ {{ $item->machine->serial_no }}
+
+
+
+
+ {{ $item->created_at->format('Y/m/d') }} + {{ $item->created_at->format('H:i:s') + }} +
+
+ @if($item->executed_at) +
+ {{ $item->executed_at->format('Y/m/d') }} + {{ $item->executed_at->format('H:i:s') + }} +
+ @else + - + @endif +
+
+ +
+ + @if($item->note) + + @endif +
+
+
+
+
+ {{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }} +
+ {{ + $item->user ? $item->user->name : __('System') }} +
+
+
+
+
+ +
+
+
+
+
+ + + +
+

{{ + __('No records found') }}

+
+
+
+ + +
+ {{ $history->appends(request()->query())->links('vendor.pagination.luxury') }} +
diff --git a/resources/views/admin/remote/partials/tab-history.blade.php b/resources/views/admin/remote/partials/tab-history.blade.php new file mode 100644 index 0000000..fd48f90 --- /dev/null +++ b/resources/views/admin/remote/partials/tab-history.blade.php @@ -0,0 +1,190 @@ +{{-- 庫存操作紀錄 Partial (AJAX 可替換) --}} + +
+
+ +
+ + + + + + + +
+ + +
+ + + + + + +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + + + + + + + + + + + + @foreach ($history as $item) + + + + + + + + + @endforeach + @if($history->isEmpty()) + + + + @endif + +
{{ __('Machine Information') }}{{ __('Creation Time') }}{{ __('Picked up Time') }}{{ __('Command Type') }}{{ __('Operator') }}{{ __('Status') }}
+
+
+ + + +
+
+
{{ $item->machine->name }}
+
{{ $item->machine->serial_no }}
+
+
+
+
+ {{ $item->created_at->format('Y/m/d') }} + {{ $item->created_at->format('H:i:s') }} +
+
+ @if($item->executed_at) +
+ {{ $item->executed_at->format('Y/m/d') }} + {{ $item->executed_at->format('H:i:s') }} +
+ @else + - + @endif +
+
+ +
+ + @if($item->note) + + @endif +
+
+
+
+
+ {{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }} +
+ {{ $item->user ? $item->user->name : __('System') }} +
+
+
+
+
+ +
+
+
+
+
+ + + +
+

{{ __('No records found') }}

+
+
+
+ + +
+ {{ $history->appends(request()->query())->links('vendor.pagination.luxury') }} +
diff --git a/resources/views/admin/remote/partials/tab-machines-index.blade.php b/resources/views/admin/remote/partials/tab-machines-index.blade.php new file mode 100644 index 0000000..45cb651 --- /dev/null +++ b/resources/views/admin/remote/partials/tab-machines-index.blade.php @@ -0,0 +1,152 @@ + +
+
+
+
+
+ + + +
+
+

+ {{ __('Loading Data') }}...

+
+ + +
+
+ + + + + + + +
+
+ +
+ + + + + + + + + + + @foreach($machines as $machine) + + + + + + + @endforeach + +
+ {{ __('Machine Information') }} + {{ __('Status') }} + {{ __('Last Communication') }} + {{ __('Actions') }}
+
+
+ @if(!empty($machine->image_urls) && isset($machine->image_urls[0])) + + @else + + + + @endif +
+
+
+ {{ $machine->name }}
+
+ {{ $machine->serial_no }}
+
+
+
+
+ @if($machine->status === 'online' || !$machine->status) +
+
+ + +
+ {{ + __('Online') }} +
+ @elseif($machine->status === 'offline') +
+
+ {{ + __('Offline') }} +
+ @else +
+
+ + +
+ {{ + __('Abnormal') }} +
+ @endif +
+
+ + + +
+
+ + +
+ {{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }} +
diff --git a/resources/views/admin/remote/partials/tab-machines.blade.php b/resources/views/admin/remote/partials/tab-machines.blade.php new file mode 100644 index 0000000..7aa4f88 --- /dev/null +++ b/resources/views/admin/remote/partials/tab-machines.blade.php @@ -0,0 +1,124 @@ +
+ + + + + + + + + + + + @forelse($machines as $machine) + + + + + + + + @empty + + + + @endforelse + +
+ {{ __('Machine Information') }} + {{ __('Status') }} + {{ __('Alerts') }} + {{ __('Last Sync') }} + {{ __('Actions') }}
+
+
+ @if($machine->image_urls && isset($machine->image_urls[0])) + + @else + + + + @endif +
+
+
+ {{ $machine->name }}
+
+ {{ $machine->serial_no }}
+
+
+
+
+ @if($machine->status === 'online' || !$machine->status) +
+
+ + +
+ {{ __('Online') }} +
+ @elseif($machine->status === 'offline') +
+
+ {{ __('Offline') }} +
+ @else +
+
+ + +
+ {{ __('Abnormal') }} +
+ @endif +
+
+
+ @if($machine->low_stock_count > 0) + + + {{ $machine->low_stock_count }} {{ __('Low') }} + + @endif + @if($machine->expiring_soon_count > 0) + + + {{ $machine->expiring_soon_count }} {{ __('Expiring') }} + + @endif + @if(!$machine->low_stock_count && !$machine->expiring_soon_count) + {{ __('All Stable') }} + @endif +
+
+
+ + + {{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }} + +
+
+ +
+
+
+ + + +
+

{{ __('No machines found') }}

+
+
+
+ +{{-- 標準化分頁底欄 --}} +
+ {{ $machines->appends(request()->except('machine_page'))->links('vendor.pagination.luxury') }} +
diff --git a/resources/views/admin/remote/stock.blade.php b/resources/views/admin/remote/stock.blade.php index 803714f..369b213 100644 --- a/resources/views/admin/remote/stock.blade.php +++ b/resources/views/admin/remote/stock.blade.php @@ -2,242 +2,351 @@ @section('content') -
- +
+