[FEAT] 完善全站多語系支援、角色權限篩選優化及 UI 元件重構
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s

- [DOCS] 補齊 en, ja, zh_TW 語系檔翻譯並完善驗證錯誤訊息 (validation.php)
- [FEAT] 角色權限頁面新增「所屬單位」篩選功能 (僅限系統管理員)
- [STYLE] 優化角色列表顯示,將「類型」變更為具體「所屬單位」名稱
- [STYLE] 修正角色頁面工具列佈局,搜尋框置前並修正下拉箭頭顯示
- [REFACTOR] 統一全站刪除確認視窗,導入新版 <x-delete-confirm-modal /> 組件
- [REFACTOR] 優化 PermissionController 查詢效能 (Eager Loading)
- [FIX] 修正 RoleSeeder 角色命名與資料庫同步邏輯
This commit is contained in:
2026-03-20 13:41:51 +08:00
parent 6588dcd7f7
commit d2cefe3f39
31 changed files with 2431 additions and 1775 deletions

View File

@@ -17,6 +17,12 @@ class PaymentConfigController extends AdminController
{
$per_page = $request->input('per_page', 20);
$configs = PaymentConfig::query()
->when($request->search, function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhereHas('company', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
})
->with(['company', 'creator'])
->latest()
->paginate($per_page)

View File

@@ -32,9 +32,9 @@ class CompanyController extends Controller
$per_page = $request->input('per_page', 10);
$companies = $query->latest()->paginate($per_page)->withQueryString();
// 取得可供選擇的客戶角色範本 (is_system = 0, company_id = null)
$template_roles = \App\Models\System\Role::where('is_system', 0)
->whereNull('company_id')
// 取得可供選擇的客戶角色範本 (系統層級的角色,排除 super-admin)
$template_roles = \App\Models\System\Role::whereNull('company_id')
->where('name', '!=', 'super-admin')
->get();
return view('admin.companies.index', compact('companies', 'template_roles'));
@@ -86,23 +86,23 @@ class CompanyController extends Controller
]);
// 角色初始化與克隆邏輯 (優先使用選擇的角色,否則使用預設)
$selected_role_name = $validated['admin_role'] ?? '通用客戶角色範本';
$role_to_assign = '管理員';
$selected_role_name = $validated['admin_role'] ?? '客戶管理員角色模板';
$role_to_assign = null;
$template_role = \App\Models\System\Role::where('name', $selected_role_name)
->whereNull('company_id')
->where('is_system', 0)
->where('name', '!=', 'super-admin')
->first();
if ($template_role) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
$role_to_assign = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company->id,
'is_system' => false,
]);
$clonedRole->syncPermissions($template_role->permissions);
$role_to_assign->syncPermissions($template_role->getPermissionNames());
} else {
// 如果找不到選定的角色範本,退而求其次嘗試指派現有角色 (通常不應發生)
$role_to_assign = $selected_role_name;

View File

@@ -12,9 +12,9 @@ class PermissionController extends Controller
{
$per_page = request()->input('per_page', 10);
$user = auth()->user();
$query = \App\Models\System\Role::query()->with(['permissions', 'users']);
$query = \App\Models\System\Role::query()->with(['permissions', 'users', 'company']);
// 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null)
// 租戶隔離:租戶只能看到自己公司的角色
if (!$user->isSystemAdmin()) {
$query->where('company_id', $user->company_id);
}
@@ -24,10 +24,27 @@ class PermissionController extends Controller
$query->where('name', 'like', "%{$search}%");
}
// 篩選:所屬單位 (僅限系統管理員)
if ($user->isSystemAdmin() && request()->filled('company_id')) {
if (request()->company_id === 'system') {
$query->where('is_system', true);
} else {
$query->where('company_id', request()->company_id);
}
}
$roles = $query->latest()->paginate($per_page)->withQueryString();
$all_permissions = \Spatie\Permission\Models\Permission::all()
$companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
// 權限遞迴約束:租戶管理員只能看到並指派自己擁有的權限
$permissionQuery = \Spatie\Permission\Models\Permission::query();
if (!$user->isSystemAdmin()) {
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
}
$all_permissions = $permissionQuery->get()
->filter(function($perm) {
// 排除子項目的權限,只顯示主選單權限
// 排除子項目,只顯示主權限
$excluded = [
'menu.basic.machines',
'menu.basic.payment-configs',
@@ -47,7 +64,8 @@ class PermissionController extends Controller
// 根據路由決定標題
$title = request()->routeIs('*.sub-account-roles') ? __('Sub Account Roles') : __('Role Settings');
return view('admin.permission.roles', compact('roles', 'all_permissions', 'title'));
$currentUserRoleIds = $user->roles->pluck('id')->toArray();
return view('admin.permission.roles', compact('roles', 'all_permissions', 'title', 'currentUserRoleIds', 'companies'));
}
/**
@@ -78,6 +96,15 @@ class PermissionController extends Controller
if (!empty($validated['permissions'])) {
$perms = $validated['permissions'];
// 權限遞迴約束驗證:確保指派的權限是操作者權限的子集
if (!auth()->user()->isSystemAdmin()) {
$currentUserPerms = auth()->user()->getAllPermissions()->pluck('name');
if (collect($perms)->diff($currentUserPerms)->isNotEmpty()) {
return redirect()->back()->with('error', __('You cannot assign permissions you do not possess.'));
}
}
// 如果不是系統角色,排除主選單的系統權限
if (!$is_system) {
$perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
@@ -128,6 +155,15 @@ class PermissionController extends Controller
]);
$perms = $validated['permissions'] ?? [];
// 權限遞迴約束驗證
if (!auth()->user()->isSystemAdmin()) {
$currentUserPerms = auth()->user()->getAllPermissions()->pluck('name');
if (collect($perms)->diff($currentUserPerms)->isNotEmpty()) {
return redirect()->back()->with('error', __('You cannot assign permissions you do not possess.'));
}
}
// 如果不是系統角色,排除主選單的系統權限
if (!$is_system) {
$perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
@@ -188,9 +224,9 @@ class PermissionController extends Controller
$per_page = $request->input('per_page', 10);
$users = $query->latest()->paginate($per_page)->withQueryString();
$companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
$roles_query = \App\Models\System\Role::where('name', '!=', 'super-admin');
$roles_query = \App\Models\System\Role::query();
if (!auth()->user()->isSystemAdmin()) {
$roles_query->where('company_id', auth()->user()->company_id);
$roles_query->forCompany(auth()->user()->company_id);
}
$roles = $roles_query->get();
@@ -231,9 +267,9 @@ class PermissionController extends Controller
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($company_id !== null) {
// 如果是租戶帳號,不能選各項系統角色 (is_system = 1)
if ($role->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
// 如果是租戶帳號,不能選超級管理員角色
if ($role->is_system && $role->name === 'super-admin') {
return redirect()->back()->with('error', __('Super-admin role cannot be assigned to tenant accounts.'));
}
// 如果角色有特定的 company_id必須匹配
if ($role->company_id !== null && $role->company_id != $company_id) {
@@ -247,10 +283,9 @@ class PermissionController extends Controller
}
// 角色初始化與克隆邏輯 (只有 super-admin 在幫空白公司開帳號時觸發)
$role_to_assign = $validated['role'];
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($company_id && $role && !$role->is_system && $role->company_id === null) {
if ($company_id && $role && $role->company_id === null && $role->name !== 'super-admin') {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $company_id)
->where('name', '管理員')
@@ -258,17 +293,17 @@ class PermissionController extends Controller
if (!$existingRole) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
$newRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($role->permissions);
$role_to_assign = '管理員';
$newRole->syncPermissions($role->getPermissionNames());
$role = $newRole;
} else {
// 如果已存在名為「管理員」的角色,則直接使用它
$role_to_assign = '管理員';
$role = $existingRole;
}
}
@@ -279,10 +314,10 @@ class PermissionController extends Controller
'password' => \Illuminate\Support\Facades\Hash::make($validated['password']),
'status' => $validated['status'],
'company_id' => $company_id,
'phone' => $validated['phone'],
'phone' => $validated['phone'] ?? null,
]);
$user->assignRole($role_to_assign);
$user->assignRole($role);
return redirect()->back()->with('success', __('Account created successfully.'));
}
@@ -325,8 +360,8 @@ class PermissionController extends Controller
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($user->id !== auth()->id()) { // 排除編輯自己 (super-admin 有特殊邏輯)
if ($target_company_id !== null) {
if ($roleObj->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
if ($roleObj->is_system && $roleObj->name === 'super-admin') {
return redirect()->back()->with('error', __('Super-admin role cannot be assigned to tenant accounts.'));
}
if ($roleObj->company_id !== null && $roleObj->company_id != $target_company_id) {
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
@@ -343,7 +378,7 @@ class PermissionController extends Controller
'username' => $validated['username'],
'email' => $validated['email'],
'status' => $validated['status'],
'phone' => $validated['phone'],
'phone' => $validated['phone'] ?? null,
];
if (auth()->user()->isSystemAdmin()) {
@@ -361,26 +396,25 @@ class PermissionController extends Controller
}
// 角色初始化與克隆邏輯
$role_to_assign = $validated['role'];
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($target_company_id && $roleObj && !$roleObj->is_system && $roleObj->company_id === null) {
if ($target_company_id && $roleObj && $roleObj->company_id === null && $roleObj->name !== 'super-admin') {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $target_company_id)
->where('name', '管理員')
->first();
if (!$existingRole) {
$clonedRole = \App\Models\System\Role::query()->create([
$newRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $target_company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($roleObj->permissions);
$role_to_assign = '管理員';
$newRole->syncPermissions($roleObj->getPermissionNames());
$roleObj = $newRole;
} else {
$role_to_assign = '管理員';
$roleObj = $existingRole;
}
}
@@ -390,7 +424,7 @@ class PermissionController extends Controller
if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
$user->syncRoles(['super-admin']);
} else {
$user->syncRoles([$role_to_assign]);
$user->syncRoles([$roleObj]);
}
return redirect()->back()->with('success', __('Account updated successfully.'));

View File

@@ -24,6 +24,6 @@ class PasswordController extends Controller
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
return back()->with('success', __('Password updated successfully.'));
}
}

View File

@@ -39,7 +39,7 @@ class ProfileController extends Controller
$user->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
return Redirect::route('profile.edit')->with('success', __('Profile updated successfully.'));
}
/**

View File

@@ -13,6 +13,10 @@ class Role extends SpatieRole
'is_system',
];
protected $casts = [
'is_system' => 'boolean',
];
/**
* Get the company that owns the role.
*/