[FEAT] 實作角色權限分類、租戶角控管理與介面多語系優化
1. [FEAT] 權限劃分為「系統層級」與「客戶層級」,並在後端強制過濾跨權限分配。 2. [FEAT] 整合選單權限至主選單層級 (基本設定、權限設定),簡化角色管理 UI。 3. [STYLE] 側邊欄優化:補齊多語系翻譯,並為基本設定子選單增加視覺圖示。 4. [REFACTOR] 更新 RoleSeeder,將 tenant-admin 重新分類為客戶層級角色。
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
@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: false,
|
||||
@@ -7,12 +12,14 @@
|
||||
roleId: '',
|
||||
roleName: '',
|
||||
rolePermissions: [],
|
||||
isSystem: false,
|
||||
modalTitle: '{{ __('Create Role') }}',
|
||||
openModal(edit = false, id = '', name = '', permissions = []) {
|
||||
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;
|
||||
}
|
||||
@@ -20,7 +27,7 @@
|
||||
<!-- 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 tracking-tight font-display">{{ __('Roles') }}</h1>
|
||||
<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>
|
||||
<button @click="openModal()" class="btn-luxury-primary text-sm">
|
||||
@@ -33,8 +40,8 @@
|
||||
<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('admin.permission.roles') }}" method="GET" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<form action="{{ route($baseRoute) }}" method="GET" 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>
|
||||
@@ -49,11 +56,11 @@
|
||||
<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 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Role Name') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Type') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Permissions') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Users') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</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">{{ __('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">{{ __('Type') }}</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">
|
||||
@@ -61,7 +68,7 @@
|
||||
<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/10 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-all duration-500">
|
||||
<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>
|
||||
@@ -74,40 +81,40 @@
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@if($role->is_system)
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-widest">
|
||||
{{ __('System') }}
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-800 uppercase tracking-wider">
|
||||
{{ __('System Level') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
|
||||
{{ __('Custom') }}
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-wider uppercase">
|
||||
{{ __('Company Level') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<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-[10px] 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-tight">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
|
||||
<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-tight">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
|
||||
@empty
|
||||
<span class="text-[11px] font-bold text-slate-400 italic tracking-tight">{{ __('No permissions') }}</span>
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 italic tracking-tight">{{ __('No permissions') }}</span>
|
||||
@endforelse
|
||||
@if($role->permissions->count() > 6)
|
||||
<span class="px-2 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-tight">+{{ $role->permissions->count() - 6 }}</span>
|
||||
<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-tight">+{{ $role->permissions->count() - 6 }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="text-sm font-black text-slate-600 dark:text-slate-400">{{ $role->users()->count() }}</span>
|
||||
<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">
|
||||
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}', {{ $role->permissions->pluck('name') }})" class="p-2 rounded-xl bg-slate-50/50 dark:bg-slate-900/30 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/10 transition-all border border-transparent hover:border-cyan-500/20 shadow-sm tooltip" title="{{ __('Edit') }}">
|
||||
<button @click="openModal(true, @js($role->id), @js($role->name), @js($role->permissions->pluck('name')), {{ $role->is_system ? 'true' : 'false' }})" 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>
|
||||
</button>
|
||||
@if(!$role->is_system)
|
||||
<form action="{{ route('admin.permission.roles.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline text-slate-400">
|
||||
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
|
||||
<form action="{{ route($baseRoute . '.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline text-slate-400">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 rounded-xl bg-slate-50/50 dark:bg-slate-900/30 text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all border border-transparent hover:border-rose-500/20 shadow-sm tooltip" title="{{ __('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>
|
||||
@@ -150,7 +157,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form :action="isEdit ? '{{ route('admin.permission.roles') }}/' + roleId : '{{ route('admin.permission.roles.store') }}'" method="POST">
|
||||
<form :action="isEdit ? '{{ route($baseRoute) }}/' + roleId : '{{ route($baseRoute . '.store') }}'" method="POST">
|
||||
@csrf
|
||||
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
|
||||
|
||||
@@ -160,14 +167,30 @@
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Name') }}</label>
|
||||
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && {{ json_encode($roles->pluck('is_system', 'id')) }}[roleId]">
|
||||
<template x-if="isEdit && {{ json_encode($roles->pluck('is_system', 'id')) }}[roleId]">
|
||||
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && roleName === 'super-admin'">
|
||||
<template x-if="isEdit && roleName === 'super-admin'">
|
||||
<p class="text-[10px] text-amber-500 font-bold mt-1 px-1 flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
{{ __('System role name cannot be modified.') }}
|
||||
{{ __('The Super Admin role name cannot be modified.') }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="space-y-4">
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Type') }}</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
|
||||
<input type="radio" name="is_system" value="1" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('System Level') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
|
||||
<input type="radio" name="is_system" value="0" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('Company Level') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Right: Permissions -->
|
||||
@@ -188,16 +211,17 @@
|
||||
'line' => 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
|
||||
'reservation' => 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
'special-permission' => 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
||||
'companies' => 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
|
||||
'accounts' => 'M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'roles' => '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',
|
||||
'basic-settings' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
||||
'permissions' => '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',
|
||||
];
|
||||
@endphp
|
||||
@foreach($all_permissions->get('menu', []) as $permission)
|
||||
@php
|
||||
$pure_name = str_replace('menu.', '', $permission->name);
|
||||
$is_restricted = in_array($permission->name, ['menu.basic-settings', 'menu.permissions']);
|
||||
@endphp
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-transparent hover:border-slate-200 dark:hover:border-slate-700 group">
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-transparent hover:border-slate-200 dark:hover:border-slate-700 group"
|
||||
@if($is_restricted) x-show="isSystem == '1'" x-transition @endif>
|
||||
<div class="relative flex items-center">
|
||||
<input type="checkbox" name="permissions[]" value="{{ $permission->name }}"
|
||||
x-model="rolePermissions"
|
||||
|
||||
Reference in New Issue
Block a user