Files
star-cloud/resources/views/admin/basic-settings/machines/index.blade.php
sky121113 ee985abb2e
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m44s
[STYLE] 優化機台設定頁面載入提示與 AJAX 互動體驗
1. 新增全站頂部進度條觸發機制,在搜尋或分頁時提供視覺反饋。
2. 為機台設定頁面新增奢華風 Spinner 載入遮罩,替代原本抖動的骨架屏。
3. 優化分頁與搜尋的 AJAX 回傳邏輯,載入時降低背景透明度以維持版面穩定。
4. 新增多語系翻譯字串:Failed to load tab content。
2026-04-15 11:51:23 +08:00

1154 lines
74 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.admin')
@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,
currentMachine: null,
showCreateModelModal: false,
showEditModelModal: false,
currentModel: { name: '' },
modelActionUrl: '',
selectedFileCount: 0,
selectedFiles: [null, null, null],
deletedPhotos: [false, false, false],
showImageLightbox: false,
lightboxImageUrl: '',
showMaintenanceQrModal: false,
maintenanceQrMachineName: '',
maintenanceQrUrl: '',
permissionSearchQuery: '',
openMaintenanceQr(machine) {
this.maintenanceQrMachineName = machine.name;
const baseUrl = '{{ route('admin.maintenance.create', ['serial_no' => 'SERIAL_NO']) }}';
this.maintenanceQrUrl = baseUrl.replace('SERIAL_NO', machine.serial_no);
this.showMaintenanceQrModal = true;
},
openDetail(machine, id, serial) {
this.currentMachine = machine;
window.activeMachineId = id || machine?.id;
window.activeMachineSerial = serial || machine?.serial_no;
this.showDetailDrawer = true;
},
openPhotoModal(machine) {
this.currentMachine = machine;
this.selectedFiles = [null, null, null];
this.deletedPhotos = [false, false, false];
this.showPhotoModal = true;
},
handleFileChange(e) {
this.selectedFileCount = e.target.files.length;
},
handlePhotoFileChange(e, index) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.selectedFiles[index] = e.target.result;
this.deletedPhotos[index] = false;
};
reader.readAsDataURL(file);
}
},
deletePhoto(index) {
this.selectedFiles[index] = null;
this.deletedPhotos[index] = true;
// 同時要把 input file 清掉,避免雖然 marked deleted 但還帶著舊檔案
const input = document.getElementsByName('machine_image_' + index)[0];
if (input) input.value = '';
},
isDeleteConfirmOpen: false,
deleteFormAction: '',
confirmDelete(action) {
this.deleteFormAction = action;
this.isDeleteConfirmOpen = true;
},
// API Token Management
showApiToken: false,
loadingRegenerate: false,
isRegenerateConfirmOpen: false,
copyToken(machine) {
if (!machine?.api_token) return;
navigator.clipboard.writeText(machine.api_token).then(() => {
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('API Token Copied') }}', type: 'success' } }));
});
},
regenerateToken() {
this.isRegenerateConfirmOpen = true;
},
executeRegeneration(id, serial) {
// 僅使用機台序號 (Serial Number) 作為識別碼
const targetSerial = serial || window.activeMachineSerial || id;
if (!targetSerial) {
console.error('ExecuteRegeneration failed: No serial number available');
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: '{{ __('Missing machine identification') }}', type: 'error' }
}));
return;
}
console.log('ExecuteRegeneration using serial:', targetSerial);
this.isRegenerateConfirmOpen = false;
this.loadingRegenerate = true;
fetch(`/admin/basic-settings/machines/${targetSerial}/regenerate-token`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=\'csrf-token\']').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(data => {
this.loadingRegenerate = false;
if(data.success) {
if (this.currentMachine) {
this.currentMachine.api_token = data.api_token;
}
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
}
}).catch(() => {
this.loadingRegenerate = false;
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Error processing request') }}', type: 'error' } }));
});
},
// Permission Management
showPermissionModal: false,
isPermissionsLoading: false,
targetUserId: null,
targetUserName: '',
allMachines: [],
allMachinesCount: 0,
permissions: {},
openPermissionModal(user) {
this.targetUserId = user.id;
this.targetUserName = user.name;
this.showPermissionModal = true;
this.isPermissionsLoading = true;
this.permissions = {};
this.allMachines = [];
this.permissionSearchQuery = '';
fetch(`/admin/machines/permissions/accounts/${user.id}`)
.then(res => res.json())
.then(data => {
if (data.machines) {
this.allMachines = data.machines;
this.allMachinesCount = data.machines.length;
const tempPermissions = {};
data.machines.forEach(m => {
tempPermissions[m.id] = (data.assigned_ids || []).includes(m.id);
});
this.permissions = tempPermissions;
}
})
.catch(e => {
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Failed to load permissions') }}', type: 'error' } }));
})
.finally(() => {
this.isPermissionsLoading = false;
});
},
togglePermission(machineId) {
this.permissions = { ...this.permissions, [machineId]: !this.permissions[machineId] };
},
toggleSelectAll() {
const filtered = this.allMachines.filter(m =>
!this.permissionSearchQuery ||
m.name.toLowerCase().includes(this.permissionSearchQuery.toLowerCase()) ||
m.serial_no.toLowerCase().includes(this.permissionSearchQuery.toLowerCase())
);
if (filtered.length === 0) return;
const allSelected = filtered.every(m => this.permissions[m.id]);
filtered.forEach(m => this.permissions[m.id] = !allSelected);
},
savePermissions() {
const machineIds = Object.keys(this.permissions).filter(id => this.permissions[id]);
fetch(`/admin/machines/permissions/accounts/${this.targetUserId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=\'csrf-token\']').content,
'Accept': 'application/json'
},
body: JSON.stringify({ machine_ids: machineIds })
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.dispatchEvent(new CustomEvent('toast', { detail: { message: data.message, type: 'success' } }));
this.showPermissionModal = false;
// SPA 模式:重新載入 permissions Tab 內容
setTimeout(() => this.searchInTab('permissions'), 300);
} else {
throw new Error(data.error || 'Update failed');
}
})
.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 -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Settings') }}</h1>
<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">
<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>
<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>
</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">
<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') }}
</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') }}
</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') }}
</button>
</div>
<!-- 2. Main Content Card -->
<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>
</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>
<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>
<!-- Models Tab -->
<div x-show="tab === 'models'" x-cloak>
<div x-ref="modelsContent">
@include('admin.basic-settings.machines.partials.tab-models')
</div>
</div>
<!-- Permissions Tab -->
<div x-show="tab === 'permissions'" x-cloak>
<div x-ref="permissionsContent">
@include('admin.basic-settings.machines.partials.tab-permissions')
</div>
</div>
</div>
</div>
<!-- Modals & Drawers -->
<!-- 1. Create Machine Modal -->
<template x-teleport="body">
<div x-show="showCreateMachineModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak
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">
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="showCreateMachineModal = false">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
<div
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add Machine') }}</h3>
<button @click="showCreateMachineModal = false"
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form action="{{ route('admin.basic-settings.machines.store') }}" method="POST"
enctype="multipart/form-data">
@csrf
<div class="px-8 py-8 space-y-6">
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Machine Name') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="name" required class="luxury-input w-full"
placeholder="{{ __('Enter machine name') }}">
</div>
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Serial No') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="serial_no" required class="luxury-input w-full"
placeholder="{{ __('Enter serial number') }}">
</div>
<div class="relative z-20">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Owner') }}
</label>
<x-searchable-select name="company_id" :placeholder="__('Select Owner')">
@foreach($companies as $company)
<option value="{{ $company->id }}" data-title="{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}">
{{ $company->name }}{{ $company->code ? ' (' . $company->code . ')' : '' }}
</option>
@endforeach
</x-searchable-select>
</div>
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Location') }}
</label>
<input type="text" name="location" class="luxury-input w-full"
placeholder="{{ __('Enter machine location') }}">
</div>
<div class="relative z-10">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Model') }} <span class="text-rose-500">*</span>
</label>
<x-searchable-select name="machine_model_id" required :placeholder="__('Select Model')">
@foreach($models as $model)
<option value="{{ $model->id }}">{{ $model->name }}</option>
@endforeach
</x-searchable-select>
</div>
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">
{{ __('Machine Images') }} ({{ __('Max 3') }})
</label>
<label
class="relative flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-2xl cursor-pointer bg-slate-50/50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-800/80 transition-all group">
<template x-if="selectedFileCount === 0">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-8 h-8 mb-3 text-slate-400 group-hover:text-cyan-500 transition-colors"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p
class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
{{ __('Click to upload') }}</p>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 mt-1">
{{ __('PNG, JPG, WEBP up to 10MB') }} ({{ __('Max 3') }})
</p>
</div>
</template>
<template x-if="selectedFileCount > 0">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<div
class="w-10 h-10 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-500 mb-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M5 13l4 4L19 7" />
</svg>
</div>
<p class="text-xs font-black text-emerald-500 uppercase tracking-widest"
x-text="`${selectedFileCount} {{ __('files selected') }}`"></p>
</div>
</template>
<input type="file" name="images[]" multiple accept="image/*" class="hidden"
@change="handleFileChange">
</label>
</div>
</div>
<div
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
<button type="button" @click="showCreateMachineModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<!-- 2. Create Model Modal -->
<template x-teleport="body">
<div x-show="showCreateModelModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak
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">
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="showCreateModelModal = false">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
<div
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add Machine Model') }}</h3>
<button @click="showCreateModelModal = false"
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form action="{{ route('admin.basic-settings.machine-models.store') }}" method="POST">
@csrf
<input type="hidden" name="redirect_to"
value="{{ route('admin.basic-settings.machines.index', ['tab' => 'models']) }}">
<div class="px-8 py-8 space-y-6">
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Model Name') }}</label>
<input type="text" name="name" required class="luxury-input w-full"
placeholder="{{ __('Enter model name') }}">
</div>
</div>
<div
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
<button type="button" @click="showCreateModelModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-8">{{ __('Create') }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<!-- 3. Edit Model Modal -->
<template x-teleport="body">
<div x-show="showEditModelModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak
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">
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="showEditModelModal = false">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
<div
class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Edit Machine Model') }}</h3>
<button @click="showEditModelModal = false"
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form :action="modelActionUrl" method="POST">
@csrf
@method('PUT')
<input type="hidden" name="redirect_to"
value="{{ route('admin.basic-settings.machines.index', ['tab' => 'models']) }}">
<div class="px-8 py-8 space-y-6">
<div>
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Model Name') }}</label>
<input type="text" name="name" x-model="currentModel.name" required
class="luxury-input w-full" placeholder="{{ __('Enter model name') }}">
</div>
</div>
<div
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
<button type="button" @click="showEditModelModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<!-- 4. Machine Photo Management Modal -->
<template x-teleport="body">
<div x-show="showPhotoModal" class="fixed inset-0 z-[150]" x-cloak>
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" x-show="showPhotoModal"
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" @click="showPhotoModal = false">
</div>
<div
class="fixed inset-0 z-[160] overflow-y-auto pointer-events-none p-4 md:p-8 flex items-center justify-center">
<div
class="w-full max-w-2xl bg-white dark:bg-slate-900 rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-slate-800 pointer-events-auto overflow-hidden animate-luxury-in">
<div
class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-slate-900 sticky top-0 z-10">
<div>
<h2 class="text-xl font-black text-slate-800 dark:text-white">{{ __('Machine Images') }}</h2>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-1"
x-text="currentMachine?.name"></p>
</div>
<button @click="showPhotoModal = false"
class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all">
<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.5"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form
:action="'{{ route('admin.basic-settings.machines.photos.update', ':id') }}'.replace(':id', currentMachine?.id)"
method="POST" enctype="multipart/form-data">
@csrf
@method('PATCH')
<div class="p-8 space-y-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<template x-for="i in [0, 1, 2]" :key="i">
<div class="space-y-3">
<label
class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em]"
x-text="'{{ __('Photo Slot') }} ' + (i + 1)"></label>
<div class="relative group aspect-square rounded-[2rem] overflow-hidden border-2 border-dashed border-slate-200 dark:border-slate-800 hover:border-emerald-500/50 transition-all bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center cursor-pointer"
@click="$el.querySelector('input').click()"> <template
x-if="(selectedFiles[i] || (currentMachine?.image_urls && currentMachine.image_urls[i])) && !deletedPhotos[i]">
<div class="absolute inset-0 w-full h-full">
<img :src="selectedFiles[i] || currentMachine.image_urls[i]"
class="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-110">
<div
class="absolute inset-0 bg-slate-900/60 backdrop-blur-[2px] opacity-0 group-hover:opacity-100 transition-all flex flex-col items-center justify-center gap-3">
<button type="button"
class="bg-white text-emerald-600 px-5 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transform hover:scale-105 transition-all"
@click.stop="$el.closest('.group').querySelector('input').click()">
{{ __('Change') }}
</button>
<button type="button"
class="bg-rose-500/90 text-white px-5 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transform hover:scale-105 transition-all"
@click.stop="deletePhoto(i)">
{{ __('Delete') }}
</button>
</div>
</div>
</template>
<template
x-if="!selectedFiles[i] && !(currentMachine?.image_urls && currentMachine.image_urls[i]) || deletedPhotos[i]">
<div class="flex flex-col items-center gap-3">
<div
class="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-emerald-500 group-hover:text-white transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</div>
<span
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest"
x-text="'Slot ' + (i + 1)"></span>
</div>
</template>
<input type="file" :name="'machine_image_' + i" class="hidden" accept="image/*"
@change="handlePhotoFileChange($event, i)">
<input type="hidden" :name="'delete_photo_' + i"
:value="deletedPhotos[i] ? '1' : '0'">
</div>
</div>
</template>
</div>
<div
class="bg-amber-50 dark:bg-amber-900/10 border border-amber-100 dark:border-amber-900/30 rounded-2xl p-4 flex items-center gap-4">
<div class="flex-shrink-0 p-2 rounded-xl bg-amber-100 dark:bg-amber-900/30 text-amber-600">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p
class="text-xs font-bold text-amber-700 dark:text-amber-300 leading-relaxed text-left flex-1">
{{ __('PNG, JPG, WEBP up to 10MB') }}
</p>
</div>
</div>
<div
class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
<button type="button" @click="showPhotoModal = false" class="btn-luxury-ghost">{{ __('Cancel')
}}</button>
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save Changes') }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<!-- 4.1 Image Lightbox Modal -->
<template x-teleport="body">
<div x-show="showImageLightbox"
class="fixed inset-0 z-[200] flex items-center justify-center p-4 md:p-12"
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"
x-cloak>
<!-- Backdrop -->
<div class="absolute inset-0 bg-slate-950/90 backdrop-blur-xl" @click="showImageLightbox = false"></div>
<!-- Close Button -->
<button @click="showImageLightbox = false"
class="absolute top-6 right-6 p-3 rounded-full bg-white/10 hover:bg-white/20 text-white backdrop-blur-md transition-all duration-300 z-10">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Image Container -->
<div class="relative max-w-5xl w-full max-h-full flex items-center justify-center p-4 animate-luxury-in"
@click.away="showImageLightbox = false">
<img :src="lightboxImageUrl"
class="max-w-full max-h-[85vh] rounded-3xl shadow-2xl border border-white/10 ring-1 ring-white/5 object-contain"
x-show="showImageLightbox"
x-transition:enter="transition ease-out duration-500 delay-100"
x-transition:enter-start="scale-95 opacity-0"
x-transition:enter-end="scale-100 opacity-100">
</div>
<!-- Helper text -->
{{ __('Click anywhere to close') }}
</div>
</div>
</template>
<!-- 4.2 Maintenance QR Modal -->
<template x-teleport="body">
<div x-show="showMaintenanceQrModal" class="fixed inset-0 z-[200] overflow-y-auto" x-cloak
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">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="fixed inset-0 transition-opacity" @click="showMaintenanceQrModal = false">
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
</div>
<div class="relative bg-white dark:bg-slate-900 rounded-[2.5rem] shadow-2xl border border-slate-100 dark:border-slate-800 w-full max-w-sm overflow-hidden animate-luxury-in">
<div class="px-8 py-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
<div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Maintenance QR') }}</h3>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1" x-text="maintenanceQrMachineName"></p>
</div>
<button @click="showMaintenanceQrModal = false" class="text-slate-400 hover:text-slate-600 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-10 flex flex-col items-center gap-6">
<div class="p-4 bg-white rounded-3xl shadow-xl border border-slate-100">
<img :src="'{{ route('admin.basic-settings.qr-code') }}?data=' + encodeURIComponent(maintenanceQrUrl)"
class="w-48 h-48"
alt="{{ __('Maintenance QR Code') }}">
</div>
<div class="text-center space-y-2">
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 leading-relaxed px-4">
{{ __('Scan this code to quickly access the maintenance form for this device.') }}
</p>
<div class="mt-4 p-3 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-100 dark:border-slate-700">
<code class="text-[10px] break-all text-cyan-600 dark:text-cyan-400 font-bold" x-text="maintenanceQrUrl"></code>
</div>
</div>
</div>
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-center border-t border-slate-100 dark:border-slate-800">
<button @click="showMaintenanceQrModal = false" class="btn-luxury-primary w-full py-4 rounded-2xl">{{ __('Close') }}</button>
</div>
</div>
</div>
</div>
</template>
<!-- 5. Detail Drawer (Same for both) -->
<template x-teleport="body">
<div x-show="showDetailDrawer" class="fixed inset-0 z-[150]" x-cloak>
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" x-show="showDetailDrawer"
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" @click="showDetailDrawer = false">
</div>
<div class="fixed inset-y-0 right-0 max-w-full flex">
<div class="w-screen max-w-md" x-show="showDetailDrawer"
x-transition:enter="transform transition ease-in-out duration-500"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-500"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full">
<div
class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
<div
class="px-6 py-3 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
<div>
<h2 class="text-xl font-black text-slate-800 dark:text-white">{{ __('Parameters') }}</h2>
<p class="text-xs font-bold text-slate-400 uppercase tracking-[0.2em] mt-1"
x-text="currentMachine?.name"></p>
</div>
<button @click="showDetailDrawer = false"
class="p-2 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto px-6 pt-1 pb-6 space-y-6 custom-scrollbar">
<template x-if="currentMachine?.image_urls && currentMachine.image_urls.length > 0">
<section class="space-y-4">
<h3 class="text-xs font-black text-indigo-500 uppercase tracking-[0.3em]">{{
__('Machine Images') }}</h3>
<div class="grid grid-cols-2 gap-3">
<template x-for="(url, index) in currentMachine.image_urls" :key="index">
<div @click="lightboxImageUrl = url; showImageLightbox = true"
class="relative group aspect-square rounded-2xl overflow-hidden border border-slate-100 dark:border-slate-800 shadow-sm bg-slate-50 dark:bg-slate-800/50 cursor-zoom-in hover:ring-2 hover:ring-cyan-500/50 transition-all duration-300 group/img">
<img :src="url"
class="absolute inset-0 w-full h-full object-cover group-hover/img:scale-105 transition-transform duration-500">
<div class="absolute inset-0 bg-slate-900/0 group-hover/img:bg-slate-900/20 flex items-center justify-center opacity-0 group-hover/img:opacity-100 transition-all duration-300">
<svg class="w-6 h-6 text-white drop-shadow-md" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
</div>
</div>
</template>
</div>
</section>
</template>
<section class="space-y-6">
<h3 class="text-xs font-black text-cyan-500 uppercase tracking-[0.3em]">{{ __('Hardware & Network') }}</h3>
<div class="grid grid-cols-1 gap-4">
<div
class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
<span
class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{
__('Serial & Version') }}</span>
<div class="flex items-center justify-between">
<div class="text-sm font-mono font-bold text-slate-700 dark:text-slate-300"
x-text="currentMachine?.serial_no"></div>
<span
class="px-2 py-0.5 rounded-md bg-white dark:bg-slate-900 text-[10px] font-black text-slate-500 border border-slate-100 dark:border-slate-800"
x-text="'v' + (currentMachine?.firmware_version || '1.0')"></span>
</div>
</div>
<div
class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
<span
class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{
__('Heartbeat') }}</span>
<div class="text-sm font-bold text-slate-700 dark:text-slate-300"
x-text="currentMachine?.last_heartbeat_at ? new Date(currentMachine.last_heartbeat_at).toLocaleString() : '--'">
</div>
</div>
</div>
</section>
<!-- Operational Settings -->
<section class="space-y-6">
<h3 class="text-xs font-black text-amber-500 uppercase tracking-[0.3em]">{{
__('Operations') }}</h3>
<div class="space-y-4">
<div
class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
<span class="text-sm font-bold text-slate-500">{{ __('Heating Range') }}</span>
<span class="text-sm font-black text-slate-700 dark:text-slate-300"
x-text="(currentMachine?.heating_start_time ? currentMachine.heating_start_time.substring(0, 5) : '00:00') + ' ~ ' + (currentMachine?.heating_end_time ? currentMachine.heating_end_time.substring(0, 5) : '00:00')"></span>
</div>
<div
class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
<span class="text-sm font-bold text-slate-500">{{ __('Card Reader No') }}</span>
<span class="text-sm font-black text-slate-700 dark:text-slate-300"
x-text="currentMachine?.card_reader_no || '--'"></span>
</div>
<div class="flex flex-col gap-3 p-3 mt-1 bg-slate-50 dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-700/50 relative">
<div class="flex items-center justify-between">
<span class="text-xs font-black text-slate-500 uppercase tracking-widest">{{ __('API Token') }}</span>
<div class="flex items-center gap-1">
<template x-if="currentMachine?.api_token">
<div class="flex items-center gap-1">
<button @click="showApiToken = !showApiToken"
class="p-1.5 rounded-lg text-slate-400 hover:text-cyan-500 hover:bg-cyan-50 dark:hover:bg-cyan-900/40 transition-all font-bold"
:title="showApiToken ? '{{ __('Hide') }}' : '{{ __('Show') }}'">
<svg x-show="!showApiToken" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
<svg x-show="showApiToken" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>
</button>
<button @click="copyToken(currentMachine)"
class="p-1.5 rounded-lg text-slate-400 hover:text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/40 transition-all font-bold"
title="{{ __('Copy') }}">
<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.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
</button>
</div>
</template>
<button @click="regenerateToken()" :disabled="loadingRegenerate"
class="ml-2 px-2.5 py-1.5 rounded-lg bg-rose-50 dark:bg-rose-500/10 text-rose-500 hover:bg-rose-100 dark:hover:bg-rose-500/20 text-xs font-black uppercase tracking-widest transition-all disabled:opacity-50 flex items-center gap-1.5 border border-rose-100 dark:border-rose-500/20"
title="{{ __('Regenerate') }}">
<svg x-show="loadingRegenerate" class="animate-spin w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
<svg x-show="!loadingRegenerate" class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
<span>{{ __('Regenerate') }}</span>
</button>
</div>
</div>
<div class="bg-white dark:bg-slate-900/50 rounded-lg border border-slate-200 dark:border-slate-700/50 p-2.5 overflow-x-auto custom-scrollbar">
<span class="text-sm font-mono font-bold tracking-[0.1em] text-cyan-600 dark:text-cyan-400 select-all block whitespace-nowrap min-w-full"
x-text="currentMachine?.api_token ? (showApiToken ? currentMachine.api_token : '•'.repeat(16)) : '{{ __('None') }}'"></span>
</div>
</div>
</div>
</section>
<!-- Location -->
<section class="space-y-4">
<h3 class="text-xs font-black text-emerald-500 uppercase tracking-[0.3em]">{{
__('Location') }}</h3>
<div
class="p-4 bg-emerald-50/30 dark:bg-emerald-500/5 rounded-2xl border border-emerald-100/50 dark:border-emerald-500/10">
<p class="text-sm text-emerald-700 dark:text-emerald-400 leading-relaxed font-bold"
x-text="currentMachine?.location || '{{ __('No location set') }}'"></p>
</div>
</section>
</div>
<div
class="p-6 border-t border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
<button @click="showDetailDrawer = false" class="w-full btn-luxury-ghost">{{ __('Close Panel')
}}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Global Delete Confirm Modal -->
<x-delete-confirm-modal />
<x-confirm-modal
alpine-var="isRegenerateConfirmOpen"
confirm-action="isRegenerateConfirmOpen = false; window.dispatchEvent(new CustomEvent('execute-regenerate', { detail: window.activeMachineSerial || window.activeMachineId }))"
icon-type="warning"
confirm-color="sky"
:title="__('Are you sure?')"
:message="__('Regenerating the token will disconnect the physical machine until it is updated. Continue?')"
:confirm-text="__('Yes, regenerate')"
/>
<!-- Machine Permissions Modal -->
<template x-teleport='body'>
<div x-show='showPermissionModal' class='fixed inset-0 z-[160] overflow-y-auto' x-cloak>
<div class='flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0'>
<div x-show='showPermissionModal' @click='showPermissionModal = false'
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'></div>
<span class='hidden sm:inline-block sm:align-middle sm:h-screen'>&#8203;</span>
<div x-show='showPermissionModal' 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 px-8 py-10 text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full overflow-hidden animate-luxury-in'>
<div class='flex justify-between items-center mb-8'>
<div>
<h3 class='text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight'>
{{ __('Authorized Machines Management') }}</h3>
<div class='flex items-center gap-2 mt-1 drop-shadow-sm'>
<span class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>{{
__('Account') }}:</span>
<span class='text-xs font-bold text-cyan-500 uppercase tracking-widest'
x-text='targetUserName'></span>
</div>
</div>
<button @click='showPermissionModal = false'
class='text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors bg-slate-50 dark:bg-slate-800 p-2 rounded-xl'>
<svg class='size-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2.5'
d='M6 18L18 6M6 6l12 12' />
</svg>
</button>
</div>
<div class='relative min-h-[400px]'>
<div class='mb-6 flex flex-col md:flex-row gap-4 items-center'>
<div class='flex-1 relative group w-full text-left'>
<span class='absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10'>
<svg class='size-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors'
viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5'
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='permissionSearchQuery'
placeholder='{{ __("Search machines...") }}'
class='luxury-input py-3 pl-12 pr-6 block w-full text-sm font-extrabold' @click.stop>
</div>
<button @click="toggleSelectAll()"
class="shrink-0 flex items-center gap-2 px-6 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200 dark:border-slate-700 font-black text-xs uppercase tracking-widest shadow-sm">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span
x-text="allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase())).every(m => permissions[m.id]) ? '{{ __('Deselect All') }}' : '{{ __('Select All') }}'"></span>
</button>
</div>
<template x-if='isPermissionsLoading'>
<div
class='absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-slate-900/50 backdrop-blur-sm z-[170] rounded-2xl'>
<div class='flex flex-col items-center gap-3'>
<div
class='w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin'>
</div>
<span class='text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] animate-pulse'>{{ __('Syncing Permissions...') }}</span>
</div>
</div>
</template>
<div
class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[450px] overflow-y-auto pr-2 custom-scrollbar p-1'>
<template
x-for='machine in allMachines.filter(m => !permissionSearchQuery || m.name.toLowerCase().includes(permissionSearchQuery.toLowerCase()) || m.serial_no.toLowerCase().includes(permissionSearchQuery.toLowerCase()))'
:key='machine.id'>
<div @click='togglePermission(machine.id)'
:class='permissions[machine.id] ? "border-cyan-500 bg-cyan-500/5 dark:bg-cyan-500/10 ring-1 ring-cyan-500/20 shadow-md shadow-cyan-500/10" : "border-slate-100 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-600 shadow-sm"'
class='p-4 rounded-2xl border-2 cursor-pointer transition-all duration-300 group relative overflow-hidden'>
<div class='flex flex-col relative z-10 text-left'>
<div class='flex items-center gap-2'>
<div class='size-2 rounded-full'
:class='permissions[machine.id] ? "bg-cyan-500 animate-pulse" : "bg-slate-300 dark:bg-slate-700"'>
</div>
<span class='text-sm font-extrabold truncate drop-shadow-sm'
:class='permissions[machine.id] ? "text-cyan-600 dark:text-cyan-400" : "text-slate-700 dark:text-slate-300"'
x-text='machine.name'></span>
</div>
<span
class='text-[10px] font-mono font-bold text-slate-400 mt-2 tracking-widest uppercase opacity-70'
x-text='machine.serial_no'></span>
</div>
<div
class='absolute -right-2 -bottom-2 opacity-[0.03] text-slate-900 dark:text-white pointer-events-none group-hover:scale-110 transition-transform duration-700'>
<svg class='size-20' fill='currentColor' viewBox='0 0 24 24'>
<path
d='M5 2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm0 2v16h14V4H5zm3 3h8v6H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2z' />
</svg>
</div>
<div class='absolute top-4 right-4 animate-luxury-in'
x-show='permissions[machine.id]'>
<div
class='size-5 rounded-full bg-cyan-500 flex items-center justify-center shadow-lg shadow-cyan-500/30'>
<svg class='size-3 text-white' fill='none' stroke='currentColor'
viewBox='0 0 24 24'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='3'
d='M5 13l4 4L19 7' />
</svg>
</div>
</div>
</div>
</template>
</div>
</div>
<div
class='flex flex-col sm:flex-row justify-between items-center mt-10 pt-8 border-t border-slate-100 dark:border-slate-800 gap-6'>
<div class='flex items-center gap-3'>
<div class='flex -space-x-2'>
<template x-for='i in Math.min(3, Object.values(permissions).filter(v => v).length)'
:key='i'>
<div
class='size-6 rounded-full border-2 border-white dark:border-slate-900 bg-cyan-500 flex items-center justify-center shadow-sm'>
<svg class='size-3 text-white' fill='currentColor' viewBox='0 0 24 24'>
<path
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14.5v-9l6 4.5-6 4.5z' />
</svg>
</div>
</template>
</div>
<p class='text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]'>
{{ __('Selection') }}: <span class='text-cyan-500 text-xs font-extrabold'
x-text='Object.values(permissions).filter(v => v).length'></span> / <span
class="font-extrabold" x-text='allMachines?.length || 0'></span> {{ __('Devices') }}
</p>
</div>
<div class='flex gap-4 w-full sm:w-auto'>
<button @click='showPermissionModal = false'
class='flex-1 sm:flex-none btn-luxury-ghost px-8'>{{ __('Cancel') }}</button>
<button @click='savePermissions()' class='flex-1 sm:flex-none btn-luxury-primary px-12 transition-all duration-300 shadow-lg shadow-cyan-500/20'
:disabled='isPermissionsLoading'>
<span>{{ __('Update Authorization') }}</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
@endsection