All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m6s
1. 修復帳號管理與角色權限頁面搜尋功能,支援 Enter 鍵快捷提交。 2. 完成 B013 (機台故障上報) API 實作,改用非同步隊列 (ProcessMachineError) 處理日誌上報。 3. 精簡 B013 API 參數,移除冗餘的 message 欄位,統一由雲端對照表翻譯。 4. 更新技術規格文件 (SKILL.md) 與系統 API 文件配置 (api-docs.php)。 5. 修正平台管理員帳號在搜尋過濾時的資料隔離邏輯。
226 lines
16 KiB
PHP
226 lines
16 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@php
|
|
$routeName = request()->route()->getName();
|
|
$baseRoute = str_contains($routeName, 'sub-account-roles') ? 'admin.data-config.sub-account-roles' : 'admin.permission.roles';
|
|
@endphp
|
|
|
|
@section('content')
|
|
<div class="space-y-6" x-data="{
|
|
showModal: {{ $errors->any() ? 'true' : 'false' }},
|
|
isEdit: {{ (old('_method') == 'PUT' || request()->has('edit')) ? 'true' : 'false' }},
|
|
roleId: '{{ old('roleId', '') }}',
|
|
roleName: '{{ old('name', '') }}',
|
|
rolePermissions: @js(old('permissions', [])),
|
|
currentUserRoleIds: @js($currentUserRoleIds ?? []),
|
|
isSystem: {{ old('is_system', '0') }},
|
|
modalTitle: '{{ $errors->any() && old('_method') == 'PUT' ? __('Edit Role') : ($errors->any() ? __('Create Role') : __('Create Role')) }}',
|
|
openModal(edit = false, id = '', name = '', permissions = [], isSys = false) {
|
|
this.isEdit = edit;
|
|
this.roleId = id;
|
|
this.roleName = name;
|
|
this.rolePermissions = Array.isArray(permissions) ? permissions : (typeof permissions === 'string' ? JSON.parse(permissions) : []);
|
|
this.isSystem = isSys;
|
|
this.modalTitle = edit ? '{{ __('Edit Role') }}' : '{{ __('Create Role') }}';
|
|
this.showModal = true;
|
|
},
|
|
isWarningModalOpen: false,
|
|
deleteWarningMsg: '',
|
|
triggerDeleteWarning(msg) {
|
|
this.deleteWarningMsg = msg;
|
|
this.isWarningModalOpen = true;
|
|
},
|
|
isDeleteConfirmOpen: false,
|
|
deleteFormAction: '',
|
|
confirmDelete(action) {
|
|
this.deleteFormAction = action;
|
|
this.isDeleteConfirmOpen = true;
|
|
}
|
|
}">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div>
|
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</h1>
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles and permissions.') }}</p>
|
|
</div>
|
|
<a href="{{ route($baseRoute . '.create') }}" class="btn-luxury-primary text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
|
<span>{{ __('Add Role') }}</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Roles Content (Integrated Card) -->
|
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
|
<!-- Toolbar -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
|
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4">
|
|
<div class="relative group">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
</span>
|
|
<input type="text" name="search" value="{{ request('search') }}"
|
|
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
|
|
placeholder="{{ __('Search roles...') }}"
|
|
@keydown.enter="$el.form.submit()">
|
|
</div>
|
|
|
|
@if(auth()->user()->isSystemAdmin())
|
|
<div class="relative">
|
|
<x-searchable-select
|
|
name="company_id"
|
|
:options="$companies"
|
|
:selected="request('company_id')"
|
|
placeholder="{{ __('All Affiliations') }}"
|
|
class="w-full md:w-auto min-w-[280px]"
|
|
onchange="this.form.submit()"
|
|
>
|
|
<option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
|
|
</x-searchable-select>
|
|
</div>
|
|
@endif
|
|
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
|
<button type="submit" class="hidden"></button>
|
|
</form>
|
|
</div>
|
|
|
|
<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">{{ __('Role 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">{{ __('Affiliation') }}</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">{{ __('Permissions') }}</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">{{ __('Users') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
@forelse($roles as $role)
|
|
<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-3">
|
|
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
|
</div>
|
|
<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">{{ $role->name }}</span>
|
|
@if($role->is_system)
|
|
<span class="p-1.5 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-600 dark:text-cyan-400 rounded-lg tooltip" title="{{ __('System Role') }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6">
|
|
@if($role->is_system)
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">
|
|
{{ __('System Level') }}
|
|
</span>
|
|
@else
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">
|
|
{{ $role->company->name ?? __('Company Level') }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-6" width="30%">
|
|
<div class="flex flex-wrap gap-1 max-w-xs">
|
|
@forelse($role->permissions->take(6) as $permission)
|
|
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">{{ __($permission->name) }}</span>
|
|
@empty
|
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ __('No permissions') }}</span>
|
|
@endforelse
|
|
@if($role->permissions->count() > 6)
|
|
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">+{{ $role->permissions->count() - 6 }}</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-6 text-center">
|
|
<span class="text-sm font-extrabold text-slate-700 dark:text-slate-300">{{ $role->users()->count() }}</span>
|
|
</td>
|
|
<td class="px-6 py-6 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<a href="{{ route($baseRoute . '.edit', $role->id) }}" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
|
|
<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="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>
|
|
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
|
|
<form action="{{ route($baseRoute . '.destroy', $role->id) }}" method="POST" @submit.prevent="if({{ $role->users()->count() }} > 0) { triggerDeleteWarning('{{ __('Cannot delete role with active users.') }}'); return; } confirmDelete('{{ route($baseRoute . '.destroy', $role->id) }}')" class="inline text-slate-400">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20 tooltip" title="{{ __('Delete') }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="5" class="px-6 py-20 text-center">
|
|
<div class="flex flex-col items-center justify-center gap-4 text-slate-400">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>
|
|
<p class="text-lg font-bold">{{ __('No roles found.') }}</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
|
{{ $roles->links('vendor.pagination.luxury') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Delete Warning Modal -->
|
|
<div x-show="isWarningModalOpen" class="fixed inset-0 z-[200] 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="isWarningModalOpen" 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 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
|
@click="isWarningModalOpen = false"></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
|
|
|
<div x-show="isWarningModalOpen" 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-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-slate-800">
|
|
|
|
<div class="sm:flex sm:items-start">
|
|
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-rose-100 dark:bg-rose-500/10 rounded-2xl sm:mx-0 sm:h-12 sm:w-12 text-rose-600 dark:text-rose-400">
|
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-6 sm:text-left">
|
|
<h3 class="text-xl font-black text-slate-800 dark:text-white leading-6 tracking-tight outfit-font">
|
|
{{ __('Cannot Delete Role') }}
|
|
</h3>
|
|
<div class="mt-4">
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed" x-text="deleteWarningMsg"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse">
|
|
<button type="button" @click="isWarningModalOpen = false"
|
|
class="inline-flex justify-center w-full px-8 py-3 text-sm font-black text-white transition-all bg-slate-800 dark:bg-slate-700 rounded-xl hover:bg-slate-900 dark:hover:bg-slate-600 sm:w-auto tracking-widest uppercase">
|
|
{{ __('Got it') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Delete Confirm Modal -->
|
|
<x-delete-confirm-modal :message="__('Are you sure you want to delete this role? This action cannot be undone.')" />
|
|
|
|
</div>
|
|
@endsection
|