All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m18s
1. 重構機台在線狀態判定機制:移除資料庫 status 欄位,改由 Model 根據心跳時間動態計算。 2. 修正儀表板 (Dashboard) 與機台管理頁面的多語系顯示問題,解決換行導致翻譯失效的 Bug。 3. 修正個人檔案頁面的麵包屑 (Breadcrumbs) 導航,補齊「個人設定」層級。 4. 更新 IoT API (B010, B600) 的認證機制與日誌處理邏輯。 5. 同步更新繁中、英文、日文語言檔,確保 UI 標籤一致性。
1371 lines
94 KiB
PHP
1371 lines
94 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@section('content')
|
|
<div class="space-y-2 pb-20" x-data="{
|
|
tab: '{{ $tab }}',
|
|
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' } }));
|
|
setTimeout(() => window.location.reload(), 500);
|
|
} else {
|
|
throw new Error(data.error || 'Update failed');
|
|
}
|
|
})
|
|
.catch(e => {
|
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message: e.message, type: 'error' } }));
|
|
});
|
|
}
|
|
}" @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">
|
|
@if($tab === 'machines')
|
|
<button @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">
|
|
<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' }}">
|
|
{{ __('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' }}">
|
|
{{ __('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' }}">
|
|
{{ __('Machine Permissions') }}
|
|
</a>
|
|
</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>
|
|
</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>
|
|
</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
|
|
</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>
|
|
</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 -->
|
|
|
|
<!-- 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">​</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">​</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">​</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'>​</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 |