[FEAT] 遠端指令中心 AJAX 化與介面標準化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m10s

1. 將遠端指令中心 (Remote Command Center) 兩大分頁 (操作紀錄、新增指令) 改為 AJAX 異步載入,提升切換速度。
2. 建立抽離的 Blade Partials 結構 (partials/tab-history-index.blade.php, tab-machines-index.blade.php) 以利維護。
3. 實作全域 Loading Bar 與 Luxury Spinner 視覺回饋,確保 AJAX 過程中有明確狀態。
4. 修正庫存管理與指令中心在機台圖片不存在時的 `Undefined array key 0` 錯誤。
5. 標準化操作紀錄搜尋行為:文字搜尋改為 Enter 觸發,日期範圍改為手動按下搜尋按鈕觸發,並新增「重設」功能。
6. 設定 Flatpickr 日期時間選擇器預設時間為 `00:00`。
7. 修正 `stock.blade.php` 中的 PHP 語法錯誤 (括號未閉合)。
8. 同步更新多語系翻譯檔案 (zh_TW, en, ja)。
This commit is contained in:
2026-04-15 13:17:25 +08:00
parent ee985abb2e
commit 24553d9b73
10 changed files with 1680 additions and 1161 deletions

View File

@@ -0,0 +1,260 @@
<!-- Loading Overlay -->
<div x-show="tabLoading === 'history'" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center"
x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
<div class="relative w-8 h-8 flex items-center justify-center">
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
</div>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">
{{ __('Loading Data') }}...</p>
</div>
<!-- Filters Area -->
<div class="mb-8">
<form @submit.prevent="searchInTab('history')" class="flex flex-wrap items-center gap-4">
<!-- Search Box -->
<div class="relative group flex-[1.5] min-w-[200px]">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-full">
</div>
<!-- Date Range -->
<div class="relative group flex-[2] min-w-[340px]" x-data="{
fp: null,
startDate: '{{ request('start_date') }}',
endDate: '{{ request('end_date') }}'
}" x-init="fp = flatpickr($refs.dateRange, {
mode: 'range',
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
defaultHour: 0,
defaultMinute: 0,
locale: 'zh_tw',
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
onChange: function(selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
} else if (selectedDates.length === 0) {
$refs.startDate.value = '';
$refs.endDate.value = '';
}
}
})">
<span
class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</span>
<input type="hidden" name="start_date" x-ref="startDate"
value="{{ request('start_date') }}">
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div>
<!-- Command Type -->
<div class="flex-[0.8] min-w-[160px]">
<x-searchable-select name="command_type" :options="[
'reboot' => __('Machine Reboot'),
'reboot_card' => __('Card Reader Reboot'),
'checkout' => __('Remote Settlement'),
'lock' => __('Lock Page Lock'),
'unlock' => __('Lock Page Unlock'),
'change' => __('Remote Change'),
'dispense' => __('Remote Dispense'),
]" :selected="request('command_type')" :placeholder="__('All Command Types')"
:hasSearch="false"
@change="searchInTab('history')" />
</div>
<!-- Status -->
<div class="flex-[0.8] min-w-[160px]">
<x-searchable-select name="status" :options="[
'pending' => __('Pending'),
'sent' => __('Sent'),
'success' => __('Success'),
'failed' => __('Failed'),
'superseded' => __('Superseded'),
]" :selected="request('status')" :placeholder="__('All Status')" :hasSearch="false" @change="searchInTab('history')" />
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button" @click="searchInTab('history', true)" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Machine Information') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center whitespace-nowrap">
{{ __('Creation Time') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center whitespace-nowrap">
{{ __('Picked up Time') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Command Type') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Operator') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Status') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@foreach ($history as $item)
<tr
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($item->machine) }})">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 shadow-sm overflow-hidden shrink-0">
<template x-if="{{ Js::from(!empty($item->machine->image_urls) && isset($item->machine->image_urls[0])) }}">
<img src="{{ !empty($item->machine->image_urls) ? $item->machine->image_urls[0] : '' }}"
class="w-full h-full object-cover">
</template>
<template x-if="{{ Js::from(empty($item->machine->image_urls) || !isset($item->machine->image_urls[0])) }}">
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
</template>
</div>
<div>
<div
class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $item->machine->name }}</div>
<div
class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $item->machine->serial_no }}</div>
</div>
</div>
</td>
<td
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
<div class="flex flex-col">
<span>{{ $item->created_at->format('Y/m/d') }}</span>
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s')
}}</span>
</div>
</td>
<td
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
@if($item->executed_at)
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s')
}}</span>
</div>
@else
<span class="text-slate-300 dark:text-slate-700">-</span>
@endif
</td>
<td class="px-6 py-6">
<div class="flex flex-col min-w-[200px]">
<span
class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight"
x-text="getCommandName({{ Js::from($item->command_type) }})"></span>
<div class="flex flex-col gap-0.5 mt-1">
<span x-show="getPayloadDetails({{ Js::from($item) }})"
class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
x-text="getPayloadDetails({{ Js::from($item) }})"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic pl-1"
x-text="translateNote({{ Js::from($item->note) }})"></span>
@endif
</div>
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
</div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{
$item->user ? $item->user->name : __('System') }}</span>
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex flex-col items-center gap-1.5">
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
:class="getCommandBadgeClass({{ Js::from($item->status) }})">
<div class="w-1.5 h-1.5 rounded-full mr-2" :class="{
'bg-amber-500 animate-pulse': {{ Js::from($item->status) }} === 'pending',
'bg-cyan-500': {{ Js::from($item->status) }} === 'sent',
'bg-emerald-500': {{ Js::from($item->status) }} === 'success',
'bg-rose-500': {{ Js::from($item->status) }} === 'failed',
'bg-slate-400': {{ Js::from($item->status) }} === 'superseded'
}"></div>
<span x-text="getCommandStatus({{ Js::from($item->status) }})"></span>
</div>
</div>
</td>
</tr>
@endforeach
@if($history->isEmpty())
<tr>
<td colspan="6" class="px-6 py-20 text-center">
<div class="flex flex-col items-center gap-3">
<div
class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
<svg class="w-8 h-8" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{
__('No records found') }}</p>
</div>
</td>
</tr>
@endif
</tbody>
</table>
</div>
<!-- Pagination Area -->
<div class="mt-8">
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>

View File

@@ -0,0 +1,190 @@
{{-- 庫存操作紀錄 Partial (AJAX 可替換) --}}
<!-- Filters Area -->
<div class="mb-8">
<form method="GET" action="{{ route('admin.remote.stock') }}" class="flex flex-wrap items-center gap-4" @submit.prevent="searchInTab()">
<!-- Search Box -->
<div class="relative group flex-[1.5] min-w-[200px]">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" x-model="historySearch"
placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full">
</div>
<!-- Date Range -->
<div class="relative group flex-[2] min-w-[340px]"
x-data="{
fp: null,
startDate: '{{ request('start_date') }}',
endDate: '{{ request('end_date') }}'
}"
x-init="fp = flatpickr($refs.dateRange, {
mode: 'range',
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
defaultHour: 0,
defaultMinute: 0,
locale: 'zh_tw',
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
onChange: function(selectedDates, dateStr, instance) {
if (selectedDates.length === 2) {
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
historyStartDate = $refs.startDate.value;
historyEndDate = $refs.endDate.value;
} else if (selectedDates.length === 0) {
$refs.startDate.value = '';
$refs.endDate.value = '';
historyStartDate = '';
historyEndDate = '';
}
}
})">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</span>
<input type="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div>
<!-- Status -->
<div class="flex-1 min-w-[160px]" @change="
historyStatus = $event.target.value === ' ' ? '' : $event.target.value;
searchInTab();
">
<x-searchable-select
name="status"
:options="[
'pending' => __('Pending'),
'sent' => __('Sent'),
'success' => __('Success'),
'failed' => __('Failed'),
'superseded' => __('Superseded'),
]"
:selected="request('status')"
:placeholder="__('All Status')"
:hasSearch="false"
/>
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</button>
<button type="button" @click="historySearch = ''; historyStartDate = ''; historyEndDate = ''; historyStatus = ''; searchInTab()" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
</div>
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Information') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center whitespace-nowrap">{{ __('Creation Time') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center whitespace-nowrap">{{ __('Picked up Time') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Command Type') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Operator') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@foreach ($history as $item)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(@js($item->machine))">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 shadow-sm overflow-hidden">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
</div>
<div>
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">{{ $item->machine->name }}</div>
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</div>
</div>
</div>
</td>
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
<div class="flex flex-col">
<span>{{ $item->created_at->format('Y/m/d') }}</span>
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
</div>
</td>
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
@if($item->executed_at)
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s') }}</span>
</div>
@else
<span class="text-slate-300 dark:text-slate-700">-</span>
@endif
</td>
<td class="px-6 py-6">
<div class="flex flex-col min-w-[200px]">
<span class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(@js($item->command_type))"></span>
<div class="flex flex-col gap-0.5 mt-1">
<span x-show="getPayloadDetails(@js($item))" class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" x-text="getPayloadDetails(@js($item))"></span>
@if($item->note)
<span class="text-[10px] text-slate-400 italic pl-1" x-text="translateNote(@js($item->note))"></span>
@endif
</div>
</div>
</td>
<td class="px-6 py-6 whitespace-nowrap">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
</div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $item->user ? $item->user->name : __('System') }}</span>
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex flex-col items-center gap-1.5">
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
:class="getCommandBadgeClass(@js($item->status))">
<div class="w-1.5 h-1.5 rounded-full mr-2"
:class="{
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
'bg-cyan-500': @js($item->status) === 'sent',
'bg-emerald-500': @js($item->status) === 'success',
'bg-rose-500': @js($item->status) === 'failed',
'bg-slate-400': @js($item->status) === 'superseded'
}"></div>
<span x-text="getCommandStatus(@js($item->status))"></span>
</div>
</div>
</td>
</tr>
@endforeach
@if($history->isEmpty())
<tr>
<td colspan="6" class="px-6 py-20 text-center">
<div class="flex flex-col items-center gap-3">
<div class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{ __('No records found') }}</p>
</div>
</td>
</tr>
@endif
</tbody>
</table>
</div>
<!-- Pagination Area -->
<div class="mt-8">
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>

View File

@@ -0,0 +1,152 @@
<!-- Loading Overlay -->
<div x-show="tabLoading === 'list'" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center"
x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
<div class="relative w-8 h-8 flex items-center justify-center">
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
</div>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">
{{ __('Loading Data') }}...</p>
</div>
<!-- Filters Area -->
<div class="flex items-center justify-between mb-8">
<form @submit.prevent="searchInTab('list')" class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" placeholder="{{ __('Search machines...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-72">
</form>
</div>
<div class="overflow-x-auto pb-4">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Machine Information') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Status') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Last Communication') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@foreach($machines as $machine)
<tr
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($machine) }})">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
@if(!empty($machine->image_urls) && isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}"
class="w-full h-full object-cover">
@else
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
@endif
</div>
<div>
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $machine->name }}</div>
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $machine->serial_no }}</div>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex items-center justify-center">
@if($machine->status === 'online' || !$machine->status)
<div
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</div>
<span
class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-[0.1em] uppercase">{{
__('Online') }}</span>
</div>
@elseif($machine->status === 'offline')
<div
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span
class="text-[10px] font-black text-slate-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{
__('Offline') }}</span>
</div>
@else
<div
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
</div>
<span
class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{
__('Abnormal') }}</span>
</div>
@endif
</div>
</td>
<td class="px-6 py-6 text-center">
<template x-data="{ heartbeat: {{ Js::from($machine->last_heartbeat_at) }} }">
<div class="flex flex-col items-center">
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
x-text="formatTime(heartbeat)"></span>
<span
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5"
x-text="heartbeat ? heartbeat.split('T')[0] : '--'"></span>
</div>
</template>
</td>
<td class="px-6 py-6 text-right">
<button @click="selectMachine({{ Js::from($machine) }})"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
title="{{ __('Manage') }}">
<svg class="size-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Pagination Area -->
<div class="mt-8">
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>

View File

@@ -0,0 +1,124 @@
<div class="overflow-x-auto pb-4">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Machine Information') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Status') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Alerts') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Last Sync') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($machines as $machine)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($machine) }})">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm">
@if($machine->image_urls && isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg>
@endif
</div>
<div>
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $machine->name }}</div>
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $machine->serial_no }}</div>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex justify-center">
@if($machine->status === 'online' || !$machine->status)
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</div>
<span class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-[0.1em] uppercase">{{ __('Online') }}</span>
</div>
@elseif($machine->status === 'offline')
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span class="text-[10px] font-black text-slate-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{ __('Offline') }}</span>
</div>
@else
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
</div>
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{ __('Abnormal') }}</span>
</div>
@endif
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex flex-col items-center gap-1.5">
@if($machine->low_stock_count > 0)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-500/10 text-rose-500 text-[10px] font-black border border-rose-500/20 uppercase tracking-widest leading-none shadow-sm shadow-rose-500/5">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
{{ $machine->low_stock_count }}&nbsp;{{ __('Low') }}
</span>
@endif
@if($machine->expiring_soon_count > 0)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-500/10 text-amber-500 text-[10px] font-black border border-amber-500/20 uppercase tracking-widest leading-none shadow-sm shadow-amber-500/5">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
{{ $machine->expiring_soon_count }}&nbsp;{{ __('Expiring') }}
</span>
@endif
@if(!$machine->low_stock_count && !$machine->expiring_soon_count)
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em]">{{ __('All Stable') }}</span>
@endif
</div>
</td>
<td class="px-6 py-6 text-center">
<div class="flex flex-col items-center">
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
x-text="formatTime({{ Js::from($machine->last_heartbeat_at) }})"></span>
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
{{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }}
</span>
</div>
</td>
<td class="px-6 py-6 text-right">
<button @click="selectMachine({{ Js::from($machine) }})"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
title="{{ __('Manage') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-20 text-center">
<div class="flex flex-col items-center gap-3">
<div class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{ __('No machines found') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- 標準化分頁底欄 --}}
<div class="mt-8">
{{ $machines->appends(request()->except('machine_page'))->links('vendor.pagination.luxury') }}
</div>