All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m18s
1. 重構機台在線狀態判定機制:移除資料庫 status 欄位,改由 Model 根據心跳時間動態計算。 2. 修正儀表板 (Dashboard) 與機台管理頁面的多語系顯示問題,解決換行導致翻譯失效的 Bug。 3. 修正個人檔案頁面的麵包屑 (Breadcrumbs) 導航,補齊「個人設定」層級。 4. 更新 IoT API (B010, B600) 的認證機制與日誌處理邏輯。 5. 同步更新繁中、英文、日文語言檔,確保 UI 標籤一致性。
574 lines
40 KiB
PHP
574 lines
40 KiB
PHP
@extends('layouts.admin')
|
|
|
|
|
|
@section('content')
|
|
<script>
|
|
window.machineApp = function () {
|
|
return {
|
|
showLogPanel: false,
|
|
showEditModal: false,
|
|
editMachineId: '',
|
|
editMachineName: '',
|
|
activeTab: 'status',
|
|
currentMachineId: '',
|
|
currentMachineSn: '',
|
|
currentMachineName: '',
|
|
logs: [],
|
|
loading: false,
|
|
startDate: '',
|
|
endDate: '',
|
|
tab: 'list',
|
|
viewMode: 'fleet',
|
|
selectedMachine: null,
|
|
slots: [],
|
|
|
|
init() {
|
|
const d = new Date();
|
|
const today = [
|
|
d.getFullYear(),
|
|
String(d.getMonth() + 1).padStart(2, '0'),
|
|
String(d.getDate()).padStart(2, '0')
|
|
].join('-');
|
|
this.startDate = today;
|
|
this.endDate = today;
|
|
this.$watch('activeTab', () => this.fetchLogs());
|
|
},
|
|
|
|
async openLogPanel(id, sn, name) {
|
|
this.currentMachineId = id;
|
|
this.currentMachineSn = sn;
|
|
this.currentMachineName = name;
|
|
this.slots = [];
|
|
this.showLogPanel = true;
|
|
this.activeTab = 'status';
|
|
await this.fetchLogs();
|
|
},
|
|
|
|
openEditModal(id, name) {
|
|
this.editMachineId = id;
|
|
this.editMachineName = name;
|
|
this.showEditModal = true;
|
|
},
|
|
|
|
|
|
async fetchLogs() {
|
|
this.loading = true;
|
|
try {
|
|
let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab;
|
|
if (this.startDate) url += '&start_date=' + this.startDate;
|
|
if (this.endDate) url += '&end_date=' + this.endDate;
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
if (data.success) this.logs = data.data.data || data.data || [];
|
|
} catch (e) { console.error('fetchLogs error:', e); }
|
|
finally { this.loading = false; }
|
|
},
|
|
|
|
async openCabinet(id) {
|
|
this.loading = true;
|
|
this.viewMode = 'cabinet';
|
|
try {
|
|
const res = await fetch('/admin/machines/' + id + '/slots-ajax');
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
this.selectedMachine = data.machine;
|
|
this.slots = data.slots;
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
} catch (e) { console.error('openCabinet error:', e); }
|
|
finally { this.loading = false; }
|
|
},
|
|
|
|
};
|
|
};
|
|
</script>
|
|
|
|
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false">
|
|
<!-- Top Header & Actions -->
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div>
|
|
<h1
|
|
class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
|
|
{{ __('Machine List') }}
|
|
</h1>
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
|
{{ __('Manage your machine fleet and operational data') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Card (Machine List) -->
|
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in overflow-hidden mt-6">
|
|
<!-- Filters Area -->
|
|
<div class="flex items-center justify-between mb-8">
|
|
<form method="GET" action="{{ route('admin.machines.index') }}" class="relative group">
|
|
<input type="hidden" name="tab" value="list">
|
|
<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 / Temp / Sub / Card / Scan') }}</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 Page') }}</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">
|
|
{{ __('APP Version') }}</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 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-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 group"
|
|
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')">
|
|
<div class="flex items-center gap-3">
|
|
<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(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.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<div
|
|
class="text-base font-extrabold 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="flex items-center gap-2 mt-0.5">
|
|
<span
|
|
class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
|
{{ $machine->serial_no ?: '--' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 text-center">
|
|
<div class="flex items-center justify-center gap-2">
|
|
@php
|
|
$cStatus = $machine->calculated_status;
|
|
@endphp
|
|
|
|
@if($cStatus === 'online')
|
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 tooltip"
|
|
title="{{ __('Machine is heartbeat normal') }}">
|
|
<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-xs font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
|
|
__('Online') }}</span>
|
|
</div>
|
|
@elseif($cStatus === 'error')
|
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20 tooltip"
|
|
title="{{ __('Recently reported errors or warnings in logs') }}">
|
|
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
|
|
<span
|
|
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
|
|
__('Error') }}</span>
|
|
</div>
|
|
@else
|
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20 tooltip"
|
|
title="{{ __('No heartbeat for over 30 seconds') }}">
|
|
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
|
|
<span
|
|
class="text-xs font-black text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{
|
|
__('Offline') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
<span class="text-slate-200 dark:text-slate-800 font-light mx-1">|</span>
|
|
<span class="font-mono font-bold text-slate-600 dark:text-slate-300">{{
|
|
$machine->temperature ?? '--' }}°C</span>
|
|
<span class="text-slate-200 dark:text-slate-800 font-light mx-1">|</span>
|
|
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
|
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
|
|
__('Normal') }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
|
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
|
|
__('Normal') }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
|
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
|
|
__('Normal') }}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 font-bold text-slate-600 dark:text-slate-400 text-center">
|
|
{{ $machine->current_page_label ?: '-' }}
|
|
</td>
|
|
<td class="px-6 py-6 font-mono text-xs font-bold text-slate-500 text-center">
|
|
{{ $machine->firmware_version ?: '-' }}
|
|
</td>
|
|
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest text-center">
|
|
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y-m-d H:i:s') : '-' }}
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button type="button"
|
|
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
|
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
|
title="{{ __('Edit Name') }}">
|
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<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>
|
|
<button type="button"
|
|
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
|
|
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
|
|
title="{{ __('View Logs') }}">
|
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
|
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Offcanvas Log Panel -->
|
|
<div x-show="showLogPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
|
|
aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
|
|
|
|
<!-- Background backdrop -->
|
|
<div x-show="showLogPanel" x-transition:enter="ease-in-out duration-300" x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in-out duration-300"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showLogPanel = false">
|
|
</div>
|
|
|
|
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
|
<!-- Sliding panel -->
|
|
<div x-show="showLogPanel"
|
|
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
|
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
|
class="w-screen max-w-4xl">
|
|
|
|
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl">
|
|
<!-- Header -->
|
|
<div
|
|
class="px-5 py-6 sm:px-8 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="min-w-0 flex-1">
|
|
<h2
|
|
class="text-xl sm:text-2xl font-black text-slate-800 dark:text-white font-display flex items-center gap-2 sm:gap-3">
|
|
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-cyan-500 flex-shrink-0"
|
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
stroke-linejoin="round">
|
|
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" />
|
|
<path d="M12 12v9" />
|
|
<path d="m8 17 4 4 4-4" />
|
|
</svg>
|
|
<span class="truncate">{{ __('Machine Logs') }}</span>
|
|
</h2>
|
|
<div
|
|
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[10px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
|
|
<span x-text="currentMachineSn"
|
|
class="font-mono text-cyan-600 dark:text-cyan-400 truncate"></span>
|
|
<span class="hidden sm:inline opacity-50">—</span>
|
|
<span x-text="currentMachineName" class="truncate"></span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-shrink-0 h-7 flex items-center">
|
|
<button type="button" @click="showLogPanel = false"
|
|
class="bg-white dark:bg-slate-800 rounded-full p-2 text-slate-400 hover:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition duration-300 shadow-sm border border-slate-200 dark:border-slate-700">
|
|
<span class="sr-only">{{ __('Close Panel') }}</span>
|
|
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Responsive Search within Panel -->
|
|
<div class="mt-6 flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6">
|
|
<div class="grid grid-cols-2 sm:flex sm:items-center gap-3 sm:gap-4 flex-1">
|
|
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
|
|
<label
|
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
|
__('From') }}</label>
|
|
<input type="date" x-model="startDate" @change="fetchLogs()"
|
|
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-32 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
|
</div>
|
|
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
|
|
<label
|
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
|
|
__('To') }}</label>
|
|
<input type="date" x-model="endDate" @change="fetchLogs()"
|
|
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-32 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-start sm:justify-end">
|
|
<button @click="startDate = ''; endDate = ''; fetchLogs()"
|
|
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
|
|
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
stroke-width="2.5">
|
|
<path d="M18 13.5V19a2 2 0 0 1-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h5.5l7.5 7.5z" />
|
|
<path d="m14 2 2 2 2 2" />
|
|
<path d="m18 2-6 6" />
|
|
</svg>
|
|
{{ __('Clear Filter') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div><!-- /Header -->
|
|
|
|
<!-- Body / Navigation Tabs -->
|
|
<div class="flex-1 flex flex-col min-h-0">
|
|
<div
|
|
class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 sticky top-0 z-10 px-6 sm:px-8 overflow-x-auto hide-scrollbar">
|
|
<nav class="-mb-px flex space-x-6 sm:space-x-8" aria-label="Tabs">
|
|
<button @click="activeTab = 'status'"
|
|
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'status', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'status'}"
|
|
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
|
{{ __('Machine Status') }}
|
|
</button>
|
|
<button @click="activeTab = 'login'"
|
|
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'login', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'login'}"
|
|
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
|
{{ __('Machine Login Logs') }}
|
|
</button>
|
|
<button @click="activeTab = 'submachine'"
|
|
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'submachine', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'submachine'}"
|
|
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
|
{{ __('Sub-machine Status Request') }}
|
|
</button>
|
|
<button @click="activeTab = 'device'"
|
|
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'device', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'device'}"
|
|
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
|
|
{{ __('Device Status Logs') }}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Tab Contents -->
|
|
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
|
|
|
<div class="relative min-h-[400px]">
|
|
<!-- Loading State -->
|
|
<div x-show="loading"
|
|
class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 backdrop-blur-[1px] flex items-center justify-center z-20">
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div
|
|
class="w-8 h-8 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
|
</div>
|
|
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{
|
|
__('Loading...') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Container -->
|
|
<div x-show="activeTab !== 'expiry'"
|
|
class="luxury-card border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
|
|
<!-- Desktop Table -->
|
|
<div class="hidden sm:block overflow-x-auto">
|
|
<table
|
|
class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
|
|
<thead class="bg-slate-50 dark:bg-slate-800/50">
|
|
<tr>
|
|
<th
|
|
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-40">
|
|
{{ __('Timestamp') }}</th>
|
|
<th
|
|
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-24 text-center">
|
|
{{ __('Level') }}</th>
|
|
<th
|
|
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700">
|
|
{{ __('Message') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
<template x-for="log in logs" :key="log.id">
|
|
<tr
|
|
class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-all">
|
|
<td class="px-6 py-4">
|
|
<div class="text-[12px] font-bold text-slate-600 dark:text-slate-300"
|
|
x-text="new Date(log.created_at).toLocaleString()">
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">
|
|
<span :class="{
|
|
'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20': log.level === 'info',
|
|
'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20': log.level === 'warning',
|
|
'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20': log.level === 'error'
|
|
}"
|
|
class="inline-flex items-center px-2 py-0.5 rounded-md border text-[10px] font-black uppercase tracking-wider">
|
|
<span x-text="log.level"></span>
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<p class="text-[13px] font-medium text-slate-700 dark:text-slate-200 truncate max-w-md"
|
|
:title="log.message" x-text="log.message"></p>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Mobile List view -->
|
|
<div class="sm:hidden divide-y divide-slate-100 dark:divide-slate-800">
|
|
<template x-for="log in logs" :key="log.id">
|
|
<div
|
|
class="p-4 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span
|
|
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest"
|
|
x-text="new Date(log.created_at).toLocaleString()"></span>
|
|
<span :class="{
|
|
'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20': log.level === 'info',
|
|
'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20': log.level === 'warning',
|
|
'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20': log.level === 'error'
|
|
}"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded-md border text-[9px] font-black uppercase tracking-tight">
|
|
<span x-text="log.level"></span>
|
|
</span>
|
|
</div>
|
|
<p class="text-[13px] font-bold text-slate-700 dark:text-slate-200 line-clamp-3 leading-relaxed"
|
|
x-text="log.message"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<template x-if="logs.length === 0 && !loading">
|
|
<div class="px-6 py-20 text-center">
|
|
<div class="flex flex-col items-center">
|
|
<div
|
|
class="p-4 rounded-full bg-slate-50 dark:bg-slate-800/50 mb-4 border border-slate-100 dark:border-slate-800/50">
|
|
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600"
|
|
viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
stroke-width="1.5">
|
|
<path
|
|
d="M21 7v10c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V7c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2z" />
|
|
<path d="M12 11l4-4" />
|
|
<path d="M8 15l4-4" />
|
|
</svg>
|
|
</div>
|
|
<span
|
|
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
|
|
__('No matching logs found') }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- /Body -->
|
|
|
|
</div>
|
|
</div><!-- /Sliding panel -->
|
|
</div>
|
|
</div><!-- /Offcanvas -->
|
|
|
|
<!-- Edit Machine Name Modal -->
|
|
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true">
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<!-- Background Backdrop -->
|
|
<div x-show="showEditModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showEditModal = false">
|
|
</div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
|
|
|
<!-- Modal Panel -->
|
|
<div x-show="showEditModal"
|
|
x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] p-10 text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-100 dark:border-slate-800">
|
|
|
|
<div>
|
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2">{{ __('Edit Machine Name') }}</h3>
|
|
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Update identification for your asset') }}</p>
|
|
|
|
<form :action="'/admin/machines/' + editMachineId" method="POST" class="mt-8 space-y-6">
|
|
@csrf
|
|
@method('PUT')
|
|
|
|
<div class="space-y-4">
|
|
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{ __('New Machine Name') }}</label>
|
|
<input type="text" name="name" x-model="editMachineName" required
|
|
class="luxury-input block w-full px-6 py-4 text-base font-bold text-slate-800 dark:text-white bg-slate-50/50 dark:bg-slate-900/50"
|
|
placeholder="{{ __('Enter machine name...') }}">
|
|
</div>
|
|
|
|
<div class="flex items-center gap-4 pt-4">
|
|
<button type="button" @click="showEditModal = false"
|
|
class="px-8 py-4 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black rounded-2xl border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 transition-all">
|
|
{{ __('Cancel') }}
|
|
</button>
|
|
<button type="submit"
|
|
class="flex-1 bg-cyan-500 hover:bg-cyan-600 text-white font-black py-4 rounded-2xl shadow-lg shadow-cyan-500/30 transition-all duration-300 transform hover:-translate-y-0.5 active:scale-95">
|
|
{{ __('Save Changes') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /Edit Modal -->
|
|
|
|
@endsection |