[STYLE] 優化機台設定頁面載入提示與 AJAX 互動體驗
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m44s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m44s
1. 新增全站頂部進度條觸發機制,在搜尋或分頁時提供視覺反饋。 2. 為機台設定頁面新增奢華風 Spinner 載入遮罩,替代原本抖動的骨架屏。 3. 優化分頁與搜尋的 AJAX 回傳邏輯,載入時降低背景透明度以維持版面穩定。 4. 新增多語系翻譯字串:Failed to load tab content。
This commit is contained in:
@@ -22,51 +22,83 @@ class MachineSettingController extends AdminController
|
||||
/**
|
||||
* 顯示機台與型號設定列表 (採用標籤頁整合)
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
$tab = $request->input('tab', 'machines');
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$search = $request->input('search');
|
||||
$isAjax = $request->boolean('_ajax');
|
||||
|
||||
// 1. 處理機台清單 (Machines Tab)
|
||||
// AJAX 模式:只查當前 Tab 的搜尋/分頁結果
|
||||
if ($isAjax) {
|
||||
$machines = null;
|
||||
$models_list = null;
|
||||
$users_list = null;
|
||||
|
||||
switch ($tab) {
|
||||
case 'machines':
|
||||
$machineQuery = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
|
||||
if ($tab === 'machines' && $search) {
|
||||
if ($search) {
|
||||
$machineQuery->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
$machines = $machineQuery->latest()->paginate($per_page)->withQueryString();
|
||||
break;
|
||||
|
||||
// 2. 處理型號清單 (Models Tab)
|
||||
case 'models':
|
||||
$modelQuery = MachineModel::query()->withCount('machines');
|
||||
if ($tab === 'models' && $search) {
|
||||
if ($search) {
|
||||
$modelQuery->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
$models_list = $modelQuery->latest()->paginate($per_page)->withQueryString();
|
||||
break;
|
||||
|
||||
// 3. 處理機台權限 (Permissions Tab) - 僅顯示 is_admin 帳號
|
||||
$users_list = null;
|
||||
if ($tab === 'permissions') {
|
||||
case 'permissions':
|
||||
$userQuery = \App\Models\System\User::query()
|
||||
->where('is_admin', true)
|
||||
->with(['company', 'machines']);
|
||||
|
||||
if ($search) {
|
||||
$userQuery->where(function($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('username', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$userQuery->where('company_id', $request->company_id);
|
||||
}
|
||||
|
||||
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
||||
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
|
||||
|
||||
$tabView = match($tab) {
|
||||
'models' => 'admin.basic-settings.machines.partials.tab-models',
|
||||
'permissions' => 'admin.basic-settings.machines.partials.tab-permissions',
|
||||
default => 'admin.basic-settings.machines.partials.tab-machines',
|
||||
};
|
||||
return response()->view($tabView, compact(
|
||||
'machines', 'models_list', 'users_list', 'companies', 'tab'
|
||||
));
|
||||
}
|
||||
|
||||
// SSR 模式:一次查好全部三個 Tab 的首頁資料(供 x-show 即時切換)
|
||||
$machines = Machine::query()
|
||||
->with(['machineModel', 'paymentConfig', 'company'])
|
||||
->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
$models_list = MachineModel::query()
|
||||
->withCount('machines')
|
||||
->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
$userQuery = \App\Models\System\User::query()
|
||||
->where('is_admin', true)
|
||||
->with(['company', 'machines']);
|
||||
$users_list = $userQuery->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
// 基礎下拉資料 (用於新增/編輯機台的彈窗)
|
||||
$models = MachineModel::select('id', 'name')->get();
|
||||
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
|
||||
$companies = \App\Models\System\Company::select('id', 'name', 'code')->get();
|
||||
|
||||
@@ -1153,5 +1153,6 @@
|
||||
"Waiting": "Waiting",
|
||||
"Publish Time": "Publish Time",
|
||||
"Expired Time": "Expired Time",
|
||||
"Inventory synced with machine": "Inventory synced with machine"
|
||||
"Inventory synced with machine": "Inventory synced with machine",
|
||||
"Failed to load tab content": "Failed to load tab content"
|
||||
}
|
||||
@@ -1152,5 +1152,6 @@
|
||||
"Waiting": "待機中",
|
||||
"Publish Time": "公開時間",
|
||||
"Expired Time": "終了時間",
|
||||
"Inventory synced with machine": "在庫が機体と同期されました"
|
||||
"Inventory synced with machine": "在庫が機体と同期されました",
|
||||
"Failed to load tab content": "タブコンテンツの読み込みに失敗しました"
|
||||
}
|
||||
@@ -1158,5 +1158,6 @@
|
||||
"Waiting": "等待中",
|
||||
"Publish Time": "發布時間",
|
||||
"Expired Time": "下架時間",
|
||||
"Inventory synced with machine": "庫存已與機台同步"
|
||||
"Inventory synced with machine": "庫存已與機台同步",
|
||||
"Failed to load tab content": "載入分頁內容失敗"
|
||||
}
|
||||
@@ -3,6 +3,11 @@
|
||||
@section('content')
|
||||
<div class="space-y-2 pb-20" x-data="{
|
||||
tab: '{{ $tab }}',
|
||||
tabLoading: false,
|
||||
machineSearch: '',
|
||||
modelSearch: '',
|
||||
permissionSearch: '',
|
||||
permissionCompanyId: '{{ request('company_id') }}',
|
||||
showCreateMachineModal: false,
|
||||
showPhotoModal: false,
|
||||
showDetailDrawer: false,
|
||||
@@ -180,7 +185,9 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
this.showPermissionModal = false;
|
||||
// SPA 模式:重新載入 permissions Tab 內容
|
||||
setTimeout(() => this.searchInTab('permissions'), 300);
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
@@ -188,6 +195,113 @@
|
||||
.catch(e => {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: e.message, type: 'error' } }));
|
||||
});
|
||||
},
|
||||
// === 搜尋/分頁 AJAX(僅在搜尋或換頁時觸發,Tab 切換不走此路) ===
|
||||
async searchInTab(tabName, extraQuery = '') {
|
||||
this.tabLoading = true;
|
||||
const searchMap = { machines: this.machineSearch, models: this.modelSearch, permissions: this.permissionSearch };
|
||||
const search = searchMap[tabName] || '';
|
||||
let qs = `tab=${tabName}&_ajax=1`;
|
||||
if (search) qs += `&search=${encodeURIComponent(search)}`;
|
||||
if (tabName === 'permissions' && this.permissionCompanyId) qs += `&company_id=${this.permissionCompanyId}`;
|
||||
if (extraQuery) qs += extraQuery;
|
||||
|
||||
// 同步 URL(不含 _ajax)
|
||||
const visibleQs = qs.replace(/&?_ajax=1/, '');
|
||||
history.pushState({}, '', `{{ route('admin.basic-settings.machines.index') }}?${visibleQs}`);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`{{ route('admin.basic-settings.machines.index') }}?${qs}`,
|
||||
{ headers: { 'X-Requested-With': 'XMLHttpRequest' } }
|
||||
);
|
||||
const html = await res.text();
|
||||
const ref = this.$refs[tabName + 'Content'];
|
||||
if (ref) {
|
||||
ref.innerHTML = html;
|
||||
this.$nextTick(() => {
|
||||
Alpine.initTree(ref);
|
||||
this.bindPaginationLinks(ref, tabName);
|
||||
if (window.HSStaticMethods) {
|
||||
setTimeout(() => window.HSStaticMethods.autoInit(), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Search failed:', e);
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Failed to load tab content') }}', type: 'error' } }));
|
||||
} finally {
|
||||
this.tabLoading = false;
|
||||
}
|
||||
},
|
||||
// 攔截分頁連結,改為 AJAX 請求
|
||||
bindPaginationLinks(container, tabName) {
|
||||
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);
|
||||
if (!url.searchParams.has('page') || a.closest('td.px-6')) return;
|
||||
a.addEventListener('click', (e) => {
|
||||
if (a.title) return; // 排除 action 按鈕
|
||||
e.preventDefault();
|
||||
const page = url.searchParams.get('page') || 1;
|
||||
const perPage = url.searchParams.get('per_page') || '';
|
||||
let extra = `&page=${page}`;
|
||||
if (perPage) extra += `&per_page=${perPage}`;
|
||||
this.searchInTab(tabName, extra);
|
||||
});
|
||||
} catch(err) {}
|
||||
});
|
||||
// 攔截分頁 <select> (快速跳頁 & 每頁筆數)
|
||||
container.querySelectorAll('select[onchange]').forEach(sel => {
|
||||
const origOnchange = sel.getAttribute('onchange');
|
||||
sel.removeAttribute('onchange');
|
||||
sel.addEventListener('change', () => {
|
||||
const val = sel.value;
|
||||
try {
|
||||
if (val.startsWith('http') || val.startsWith('/')) {
|
||||
const url = new URL(val, window.location.origin);
|
||||
const page = url.searchParams.get('page') || 1;
|
||||
const perPage = url.searchParams.get('per_page') || '';
|
||||
let extra = `&page=${page}`;
|
||||
if (perPage) extra += `&per_page=${perPage}`;
|
||||
this.searchInTab(tabName, extra);
|
||||
} else if (origOnchange && origOnchange.includes('per_page')) {
|
||||
this.searchInTab(tabName, `&per_page=${val}`);
|
||||
}
|
||||
} catch(err) {
|
||||
if (origOnchange) new Function(origOnchange).call(sel);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
init() {
|
||||
// 觸發頂部進度條
|
||||
this.$watch('tabLoading', (val) => {
|
||||
const bar = document.getElementById('top-loading-bar');
|
||||
if (bar) {
|
||||
if (val) bar.classList.add('loading');
|
||||
else bar.classList.remove('loading');
|
||||
}
|
||||
});
|
||||
// 首次載入時綁定每個 Tab 的分頁連結
|
||||
this.$nextTick(() => {
|
||||
['machines', 'models', 'permissions'].forEach(t => {
|
||||
const ref = this.$refs[t + 'Content'];
|
||||
if (ref) this.bindPaginationLinks(ref, t);
|
||||
});
|
||||
});
|
||||
// Tab 切換時同步 URL
|
||||
this.$watch('tab', (newTab) => {
|
||||
history.pushState({}, '', `{{ route('admin.basic-settings.machines.index') }}?tab=${newTab}`);
|
||||
});
|
||||
// 瀏覽器上一頁/下一頁
|
||||
window.addEventListener('popstate', () => {
|
||||
const url = new URL(window.location.href);
|
||||
this.tab = url.searchParams.get('tab') || 'machines';
|
||||
});
|
||||
}
|
||||
}" @execute-regenerate.window="executeRegeneration($event.detail)">
|
||||
<!-- 1. Header Area -->
|
||||
@@ -197,416 +311,85 @@
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Management of operational parameters and models') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if($tab === 'machines')
|
||||
<button @click="showCreateMachineModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<button x-show="tab === 'machines'" x-cloak @click="showCreateMachineModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>{{ __('Add Machine') }}</span>
|
||||
</button>
|
||||
@elseif($tab === 'models')
|
||||
<button @click="showCreateModelModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<button x-show="tab === 'models'" x-cloak @click="showCreateModelModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>{{ __('Add Machine Model') }}</span>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50">
|
||||
<a href="{{ route('admin.basic-settings.machines.index', ['tab' => 'machines']) }}"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'machines' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
<button @click="tab = 'machines'"
|
||||
:class="tab === 'machines' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all">
|
||||
{{ __('Machines') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.basic-settings.machines.index', ['tab' => 'models']) }}"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'models' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
</button>
|
||||
<button @click="tab = 'models'"
|
||||
:class="tab === 'models' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all">
|
||||
{{ __('Models') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.basic-settings.machines.index', ['tab' => 'permissions']) }}"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all {{ $tab === 'permissions' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200' }}">
|
||||
</button>
|
||||
<button @click="tab = 'permissions'"
|
||||
:class="tab === 'permissions' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all">
|
||||
{{ __('Machine Permissions') }}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 2. Main Content Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}" class="relative group">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
<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>
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6 relative overflow-hidden">
|
||||
<!-- Spinner Overlay -->
|
||||
<div x-show="tabLoading"
|
||||
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>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
placeholder="{{ $tab === 'machines' ? __('Search machines...') : ($tab === 'models' ? __('Search models...') : __('Search accounts...')) }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
|
||||
@if($tab === 'permissions' && auth()->user()->isSystemAdmin())
|
||||
<div class="w-72">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}">
|
||||
<input type="hidden" name="tab" value="permissions">
|
||||
<input type="hidden" name="search" value="{{ request('search') }}">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" onchange="this.form.submit()" />
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<div :class="tabLoading ? 'opacity-30 pointer-events-none transition-opacity duration-300' : 'transition-opacity duration-300'">
|
||||
<!-- Machines Tab -->
|
||||
<div x-show="tab === 'machines'" x-cloak>
|
||||
<div x-ref="machinesContent">
|
||||
@include('admin.basic-settings.machines.partials.tab-machines')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($tab === 'machines')
|
||||
<!-- Machine Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<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 Info') }}</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">
|
||||
{{ __('Machine Model') }}</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">
|
||||
{{ __('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">
|
||||
{{ __('Card Reader') }}</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">
|
||||
{{ __('Owner') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</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='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300 overflow-hidden">
|
||||
@if(isset($machine->image_urls[0]))
|
||||
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="w-5 h-5" 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
|
||||
<!-- Models Tab -->
|
||||
<div x-show="tab === 'models'" x-cloak>
|
||||
<div x-ref="modelsContent">
|
||||
@include('admin.basic-settings.machines.partials.tab-models')
|
||||
</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">
|
||||
{{ $machine->name }}</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{
|
||||
$machine->serial_no }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
|
||||
<span
|
||||
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
|
||||
{{ $machine->machineModel->name ?? '--' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@php
|
||||
$isOnline = $machine->last_heartbeat_at && $machine->last_heartbeat_at->diffInSeconds() < 30;
|
||||
@endphp <div class="flex items-center gap-2.5">
|
||||
<div class="relative flex h-2.5 w-2.5">
|
||||
@if($isOnline)
|
||||
<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.5 w-2.5 bg-emerald-500"></span>
|
||||
@else
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-2.5 w-2.5 bg-slate-300 dark:bg-slate-600"></span>
|
||||
@endif
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider {{ $isOnline ? 'text-emerald-500' : 'text-slate-500 dark:text-slate-400' }}">
|
||||
{{ $isOnline ? __('Online') : __('Offline') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
|
||||
{{ $machine->card_reader_seconds ?? 0 }}s <span
|
||||
class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span
|
||||
class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{
|
||||
$machine->card_reader_no ?? '--' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $machine->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right flex items-center justify-end gap-2">
|
||||
<button @click="openMaintenanceQr(@js($machine->only(['name', 'serial_no'])))"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 dark:hover:bg-emerald-500/10 border border-transparent hover:border-emerald-500/20 transition-all inline-flex group/btn"
|
||||
title="{{ __('Maintenance QR Code') }}">
|
||||
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="openPhotoModal(@js($machine->only(['id', 'name', 'image_urls'])))"
|
||||
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"
|
||||
title="{{ __('Machine Images') }}">
|
||||
<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="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
|
||||
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"
|
||||
title="{{ __('Edit Settings') }}">
|
||||
<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>
|
||||
</a>
|
||||
<button
|
||||
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
|
||||
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"
|
||||
title="{{ __('View Details') }}">
|
||||
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5"
|
||||
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
|
||||
{{ __('No data available') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $machines->appends(['tab' => 'machines'])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
|
||||
@elseif($tab === 'permissions')
|
||||
<!-- Permissions Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<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">
|
||||
{{ __('Account Info') }}</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-left">
|
||||
{{ __('Company Name') }}</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">
|
||||
{{ __('Authorized Machines') }}</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">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($users_list as $user)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 font-display text-left">
|
||||
<div class="flex items-center gap-4 text-left">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span
|
||||
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">{{
|
||||
$user->name }}</span>
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{
|
||||
$user->username }}</span>
|
||||
<!-- Permissions Tab -->
|
||||
<div x-show="tab === 'permissions'" x-cloak>
|
||||
<div x-ref="permissionsContent">
|
||||
@include('admin.basic-settings.machines.partials.tab-permissions')
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-left">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
|
||||
{{ $user->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<div
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1 text-left">
|
||||
@forelse($user->machines as $m)
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 text-left">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
<span
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">--
|
||||
{{ __('None') }} --</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button
|
||||
@click='openPermissionModal({{ json_encode(["id" => $user->id, "name" => $user->name]) }})'
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span>{{ __('Authorize') }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6 text-left mb-6">
|
||||
@if($users_list)
|
||||
{{ $users_list->appends(['tab' => 'permissions'])->links('vendor.pagination.luxury') }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@else
|
||||
<!-- Model Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Model Name') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Machine Count') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Last Updated') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($models_list as $model)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</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">
|
||||
{{ $model->name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $model->machines_count ?? 0 }} {{ __('Items') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div
|
||||
class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
|
||||
{{ $model->updated_at->format('Y/m/d H:i') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right space-x-2">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button @click="currentModel = @js($model->only(['name'])); modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 dark:hover:text-cyan-400 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all group/btn"
|
||||
title="{{ __('Edit') }}">
|
||||
<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.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<form :id="'delete-model-form-' + {{ $model->id }}"
|
||||
action="{{ route('admin.basic-settings.machine-models.destroy', $model) }}"
|
||||
method="POST" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button"
|
||||
@click="confirmDelete('{{ route('admin.basic-settings.machine-models.destroy', $model) }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 hover:bg-rose-500/5 dark:hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition-all"
|
||||
title="{{ __('Delete') }}">
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4"
|
||||
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
|
||||
{{ __('No data available') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $models_list->appends(['tab' => 'models'])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Modals & Drawers -->
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
{{-- Machines Tab Content (Partial) --}}
|
||||
{{-- 此檔案被 index.blade.php 的 @include 和 AJAX 模式共用 --}}
|
||||
|
||||
{{-- Toolbar 區:搜尋框 --}}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<form @submit.prevent="searchInTab('machines')" 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" x-model="machineSearch"
|
||||
placeholder="{{ __('Search machines...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Machine Table --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<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 Info') }}</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">
|
||||
{{ __('Machine Model') }}</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">
|
||||
{{ __('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">
|
||||
{{ __('Card Reader') }}</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">
|
||||
{{ __('Owner') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</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='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300 overflow-hidden">
|
||||
@if(isset($machine->image_urls[0]))
|
||||
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="w-5 h-5" 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">
|
||||
{{ $machine->name }}</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{
|
||||
$machine->serial_no }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'>
|
||||
<span
|
||||
class="text-xs font-bold text-slate-600 dark:text-slate-300 uppercase tracking-widest">
|
||||
{{ $machine->machineModel->name ?? '--' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@php
|
||||
$isOnline = $machine->last_heartbeat_at && $machine->last_heartbeat_at->diffInSeconds() < 30;
|
||||
@endphp <div class="flex items-center gap-2.5">
|
||||
<div class="relative flex h-2.5 w-2.5">
|
||||
@if($isOnline)
|
||||
<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.5 w-2.5 bg-emerald-500"></span>
|
||||
@else
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-2.5 w-2.5 bg-slate-300 dark:bg-slate-600"></span>
|
||||
@endif
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider {{ $isOnline ? 'text-emerald-500' : 'text-slate-500 dark:text-slate-400' }}">
|
||||
{{ $isOnline ? __('Online') : __('Offline') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
|
||||
{{ $machine->card_reader_seconds ?? 0 }}s <span
|
||||
class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span
|
||||
class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{
|
||||
$machine->card_reader_no ?? '--' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $machine->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right flex items-center justify-end gap-2">
|
||||
<button @click="openMaintenanceQr(@js($machine->only(['name', 'serial_no'])))"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 dark:hover:bg-emerald-500/10 border border-transparent hover:border-emerald-500/20 transition-all inline-flex group/btn"
|
||||
title="{{ __('Maintenance QR Code') }}">
|
||||
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="openPhotoModal(@js($machine->only(['id', 'name', 'image_urls'])))"
|
||||
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"
|
||||
title="{{ __('Machine Images') }}">
|
||||
<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="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
|
||||
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"
|
||||
title="{{ __('Edit Settings') }}">
|
||||
<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>
|
||||
</a>
|
||||
<button
|
||||
@click="openDetail(@js($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])))"
|
||||
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"
|
||||
title="{{ __('View Details') }}">
|
||||
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5"
|
||||
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
|
||||
{{ __('No data available') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $machines->appends(['tab' => 'machines'])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
{{-- Models Tab Content (Partial) --}}
|
||||
{{-- 此檔案被 index.blade.php 的 @include 和 AJAX 模式共用 --}}
|
||||
|
||||
{{-- Toolbar 區:搜尋框 --}}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<form @submit.prevent="searchInTab('models')" 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" x-model="modelSearch"
|
||||
placeholder="{{ __('Search models...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Model Table --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Model Name') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Machine Count') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Last Updated') }}</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($models_list as $model)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</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">
|
||||
{{ $model->name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $model->machines_count ?? 0 }} {{ __('Items') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div
|
||||
class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
|
||||
{{ $model->updated_at->format('Y/m/d H:i') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right space-x-2">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button @click="currentModel = @js($model->only(['name'])); modelActionUrl = '{{ route('admin.basic-settings.machine-models.update', $model) }}'; showEditModelModal = true"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 dark:hover:text-cyan-400 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all group/btn"
|
||||
title="{{ __('Edit') }}">
|
||||
<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.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<form :id="'delete-model-form-' + {{ $model->id }}"
|
||||
action="{{ route('admin.basic-settings.machine-models.destroy', $model) }}"
|
||||
method="POST" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button"
|
||||
@click="confirmDelete('{{ route('admin.basic-settings.machine-models.destroy', $model) }}')"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 hover:bg-rose-500/5 dark:hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition-all"
|
||||
title="{{ __('Delete') }}">
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4"
|
||||
class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase">
|
||||
{{ __('No data available') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $models_list->appends(['tab' => 'models'])->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@@ -0,0 +1,130 @@
|
||||
{{-- Permissions Tab Content (Partial) --}}
|
||||
{{-- 此檔案被 index.blade.php 的 @include 和 AJAX 模式共用 --}}
|
||||
|
||||
{{-- Toolbar 區:搜尋框 + 公司篩選 --}}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<form @submit.prevent="searchInTab('permissions')" 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" x-model="permissionSearch"
|
||||
placeholder="{{ __('Search accounts...') }}"
|
||||
class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-72">
|
||||
<x-searchable-select name="company_filter" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="permissionCompanyId = $event.target.value; searchInTab('permissions')" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Permissions Table --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<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">
|
||||
{{ __('Account Info') }}</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-left">
|
||||
{{ __('Company Name') }}</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">
|
||||
{{ __('Authorized Machines') }}</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">
|
||||
{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($users_list ?? [] as $user)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 font-display text-left">
|
||||
<div class="flex items-center gap-4 text-left">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl 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 group-hover:border-cyan-500 shadow-sm group-hover:shadow-cyan-500/50 transition-all duration-300">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col text-left">
|
||||
<span
|
||||
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">{{
|
||||
$user->name }}</span>
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-slate-500 tracking-widest uppercase">{{
|
||||
$user->username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-left">
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
|
||||
{{ $user->company->name ?? __('System') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<div
|
||||
class="flex flex-wrap gap-2 justify-center lg:justify-start max-w-[420px] mx-auto lg:mx-0 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar py-1 text-left">
|
||||
@forelse($user->machines as $m)
|
||||
<div
|
||||
class="flex flex-col px-3 py-1.5 rounded-xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-white/5 hover:border-cyan-500/30 transition-all duration-300 text-left">
|
||||
<span class="text-[11px] font-black text-slate-700 dark:text-slate-200 leading-tight">{{
|
||||
$m->name }}</span>
|
||||
<span class="text-[9px] font-mono font-bold text-cyan-500 tracking-tighter mt-0.5 opacity-80">{{
|
||||
$m->serial_no }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="w-full text-center lg:text-left">
|
||||
<span
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">--
|
||||
{{ __('None') }} --</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button
|
||||
@click='openPermissionModal({{ json_encode(["id" => $user->id, "name" => $user->name]) }})'
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white transition-all duration-300 text-xs font-black uppercase tracking-widest shadow-sm shadow-cyan-500/5 group/auth">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span>{{ __('Authorize') }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6 text-left mb-6">
|
||||
@if($users_list)
|
||||
{{ $users_list->appends(['tab' => 'permissions'])->links('vendor.pagination.luxury') }}
|
||||
@endif
|
||||
</div>
|
||||
Reference in New Issue
Block a user