[STYLE] 商品管理與分類管理 UI 標準化,補全多語系翻譯
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 50s

This commit is contained in:
2026-04-01 09:50:57 +08:00
parent e27eee78f5
commit 08fc86d3f8
14 changed files with 567 additions and 279 deletions

View File

@@ -131,7 +131,7 @@ class MachineController extends AdminController
*/ */
public function slotsAjax(Machine $machine) public function slotsAjax(Machine $machine)
{ {
$slots = $machine->slots()->with('product:id,name,image')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')->get(); $slots = $machine->slots()->with('product:id,name,image_url')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')->get();
return response()->json([ return response()->json([
'success' => true, 'success' => true,

View File

@@ -27,7 +27,7 @@ class PermissionController extends Controller
// 篩選:公司名稱 (僅限系統管理員) // 篩選:公司名稱 (僅限系統管理員)
if ($user->isSystemAdmin() && request()->filled('company_id')) { if ($user->isSystemAdmin() && request()->filled('company_id')) {
if (request()->company_id === 'system') { if (request()->company_id === 'system') {
$query->where('is_system', true); $query->whereNull('company_id');
} else { } else {
$query->where('company_id', request()->company_id); $query->where('company_id', request()->company_id);
} }
@@ -36,7 +36,9 @@ class PermissionController extends Controller
$roles = $query->latest()->paginate($per_page)->withQueryString(); $roles = $query->latest()->paginate($per_page)->withQueryString();
$companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect(); $companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
// 權限遞迴約束:租戶管理員只能看到並指派自己擁有的權限 // 權限分組邏輯中的標題與過濾
$isSubAccountRoles = request()->routeIs('*.sub-account-roles');
$title = $isSubAccountRoles ? __('Sub Account Roles') : __('Role Settings');
$permissionQuery = \Spatie\Permission\Models\Permission::query(); $permissionQuery = \Spatie\Permission\Models\Permission::query();
if (!$user->isSystemAdmin()) { if (!$user->isSystemAdmin()) {
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name')); $permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
@@ -44,18 +46,14 @@ class PermissionController extends Controller
// 權限分組邏輯 // 權限分組邏輯
$all_permissions = $permissionQuery->get() $all_permissions = $permissionQuery->get()
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
->groupBy(function($perm) { ->groupBy(function($perm) {
if (str_starts_with($perm->name, 'menu.')) { if (str_starts_with($perm->name, 'menu.')) {
// 主選單權限menu.xxx (兩段)
// 子選單權限menu.xxx.yyy (三段)
return 'menu'; return 'menu';
} }
return 'other'; return 'other';
}); });
// 根據路由決定標題
$title = request()->routeIs('*.sub-account-roles') ? __('Sub Account Roles') : __('Role Settings');
$currentUserRoleIds = $user->roles->pluck('id')->toArray(); $currentUserRoleIds = $user->roles->pluck('id')->toArray();
return view('admin.permission.roles', compact('roles', 'all_permissions', 'title', 'currentUserRoleIds', 'companies')); return view('admin.permission.roles', compact('roles', 'all_permissions', 'title', 'currentUserRoleIds', 'companies'));
} }
@@ -77,7 +75,9 @@ class PermissionController extends Controller
$all_permissions = $permissionQuery->get()->groupBy(fn($p) => str_starts_with($p->name, 'menu.') ? 'menu' : 'other'); $all_permissions = $permissionQuery->get()->groupBy(fn($p) => str_starts_with($p->name, 'menu.') ? 'menu' : 'other');
$title = request()->routeIs('*.sub-account-roles.create') ? __('Create Sub Account Role') : __('Create New Role'); $title = request()->routeIs('*.sub-account-roles.create') ? __('Create Sub Account Role') : __('Create New Role');
$back_url = request()->routeIs('*.sub-account-roles.create') ? route('admin.data-config.sub-account-roles') : route('admin.permission.roles'); $back_url = request()->routeIs('*.sub-account-roles.create')
? route('admin.data-config.sub-accounts', ['tab' => 'roles'])
: route('admin.permission.roles');
return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url')); return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url'));
} }
@@ -98,6 +98,7 @@ class PermissionController extends Controller
// 權限分組邏輯 // 權限分組邏輯
$all_permissions = $permissionQuery->get() $all_permissions = $permissionQuery->get()
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
->groupBy(function($perm) { ->groupBy(function($perm) {
if (str_starts_with($perm->name, 'menu.')) { if (str_starts_with($perm->name, 'menu.')) {
return 'menu'; return 'menu';
@@ -109,7 +110,9 @@ class PermissionController extends Controller
$title = request()->routeIs('*.sub-account-roles.edit') ? __('Edit Sub Account Role') : __('Edit Role Permissions'); $title = request()->routeIs('*.sub-account-roles.edit') ? __('Edit Sub Account Role') : __('Edit Role Permissions');
// 麵包屑/返回路徑 // 麵包屑/返回路徑
$back_url = request()->routeIs('*.sub-account-roles.edit') ? route('admin.data-config.sub-account-roles') : route('admin.permission.roles'); $back_url = request()->routeIs('*.sub-account-roles.edit')
? route('admin.data-config.sub-accounts', ['tab' => 'roles'])
: route('admin.permission.roles');
return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url')); return view('admin.permission.roles-edit', compact('role', 'all_permissions', 'title', 'back_url'));
} }
@@ -158,8 +161,8 @@ class PermissionController extends Controller
$role->syncPermissions($perms); $role->syncPermissions($perms);
} }
$target_route = request()->routeIs('*.sub-account-roles.*') ? 'admin.data-config.sub-account-roles' : 'admin.permission.roles'; $target_route = request()->routeIs('*.sub-account-roles.*') ? route('admin.data-config.sub-accounts', ['tab' => 'roles']) : route('admin.permission.roles');
return redirect()->route($target_route)->with('success', __('Role created successfully.')); return redirect()->to($target_route)->with('success', __('Role created successfully.'));
} }
/** /**
@@ -219,8 +222,8 @@ class PermissionController extends Controller
} }
$role->syncPermissions($perms); $role->syncPermissions($perms);
$target_route = request()->routeIs('*.sub-account-roles.*') ? 'admin.data-config.sub-account-roles' : 'admin.permission.roles'; $target_route = request()->routeIs('*.sub-account-roles.*') ? route('admin.data-config.sub-accounts', ['tab' => 'roles']) : route('admin.permission.roles');
return redirect()->route($target_route)->with('success', __('Role updated successfully.')); return redirect()->to($target_route)->with('success', __('Role updated successfully.'));
} }
/** /**
@@ -244,46 +247,95 @@ class PermissionController extends Controller
$role->delete(); $role->delete();
if (request()->routeIs('*.sub-account-roles.*')) {
return redirect()->route('admin.data-config.sub-accounts', ['tab' => 'roles'])->with('success', __('Role deleted successfully.'));
}
return redirect()->back()->with('success', __('Role deleted successfully.')); return redirect()->back()->with('success', __('Role deleted successfully.'));
} }
// 帳號管理 // 帳號管理
public function accounts(Request $request) public function accounts(Request $request)
{ {
$query = \App\Models\System\User::query()->with(['company', 'roles', 'machines']); $user = auth()->user();
$isSubAccountRoute = $request->routeIs('admin.data-config.sub-accounts');
$tab = $request->input('tab', 'accounts');
// 租戶隔離:如果不是系統管理員,則只看自己公司的成員 // 初始化變數
if (!auth()->user()->isSystemAdmin()) { $users = collect();
$query->where('company_id', auth()->user()->company_id); $roles = collect();
$paginated_roles = collect();
$all_permissions = collect();
$currentUserRoleIds = $user->roles->pluck('id')->toArray();
$companies = $user->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
if ($isSubAccountRoute && $tab === 'roles') {
// 處理角色分頁邏輯 (移植自 roles())
$per_page = $request->input('per_page', 10);
$roles_query = \App\Models\System\Role::query()->with(['permissions', 'users', 'company']);
if (!$user->isSystemAdmin()) {
$roles_query->where('company_id', $user->company_id);
}
if ($search = $request->input('search')) {
$roles_query->where('name', 'like', "%{$search}%");
}
if ($user->isSystemAdmin() && $request->filled('company_id')) {
if ($request->company_id === 'system') {
$roles_query->where('is_system', true);
} else {
$roles_query->where('company_id', $request->company_id);
}
}
$paginated_roles = $roles_query->latest()->paginate($per_page)->withQueryString();
// 權限分組邏輯
$permissionQuery = \Spatie\Permission\Models\Permission::query();
if (!$user->isSystemAdmin()) {
$permissionQuery->whereIn('name', $user->getAllPermissions()->pluck('name'));
}
$all_permissions = $permissionQuery->get()
->reject(fn($p) => $p->name === 'menu.data-config.sub-account-roles')
->groupBy(fn($p) => str_starts_with($p->name, 'menu.') ? 'menu' : 'other');
} else {
// 處理帳號名單邏輯
$query = \App\Models\System\User::query()->with(['company', 'roles', 'machines']);
if (!$user->isSystemAdmin()) {
$query->where('company_id', $user->company_id);
}
if ($search = $request->input('search')) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('username', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
if ($user->isSystemAdmin() && $request->filled('company_id')) {
$query->where('company_id', $request->company_id);
}
$per_page = $request->input('per_page', 10);
$users = $query->latest()->paginate($per_page)->withQueryString();
$roles_query = \App\Models\System\Role::query();
if (!$user->isSystemAdmin()) {
$roles_query->forCompany($user->company_id);
}
$roles = $roles_query->get();
} }
// 搜尋 $title = $isSubAccountRoute ? __('Sub Account Management') : __('Account Management');
if ($search = $request->input('search')) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('username', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
// 公司篩選 (僅限 super-admin) return view('admin.data-config.accounts', compact(
if (auth()->user()->isSystemAdmin() && $request->filled('company_id')) { 'users', 'companies', 'roles', 'paginated_roles', 'all_permissions', 'title', 'tab', 'currentUserRoleIds'
$query->where('company_id', $request->company_id); ));
}
$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::query();
if (!auth()->user()->isSystemAdmin()) {
$roles_query->forCompany(auth()->user()->company_id);
}
$roles = $roles_query->get();
// 根據路由決定標題
$title = request()->routeIs('*.sub-accounts') ? __('Sub Account Management') : __('Account Management');
return view('admin.data-config.accounts', compact('users', 'companies', 'roles', 'title'));
} }
/** /**

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. 取得權限 ID
$permission = DB::table('permissions')
->where('name', 'menu.data-config.sub-account-roles')
->first();
if ($permission) {
// 2. 移除角色與該權限的關聯 (雖然 Spatie 通常會處理,但手動確保清理乾淨)
DB::table('role_has_permissions')
->where('permission_id', $permission->id)
->delete();
// 3. 移除權限本身
DB::table('permissions')
->where('id', $permission->id)
->delete();
}
// 4. 清理權限快取 (如果有的話)
try {
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
} catch (\Exception $e) {
// 忽略快取清理失敗(例如在沒有 Redis 的環境中)
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// 由於是要永久拿掉down 邏輯通常不需要重建,
// 若真要復原,應透過重跑 Seeder 或手動新增。
}
};

View File

@@ -34,7 +34,6 @@ class RoleSeeder extends Seeder
'menu.data-config.products', 'menu.data-config.products',
'menu.data-config.advertisements', 'menu.data-config.advertisements',
'menu.data-config.sub-accounts', 'menu.data-config.sub-accounts',
'menu.data-config.sub-account-roles',
'menu.data-config.points', 'menu.data-config.points',
'menu.data-config.badges', 'menu.data-config.badges',
'menu.remote', 'menu.remote',
@@ -81,7 +80,6 @@ class RoleSeeder extends Seeder
'menu.data-config.products', 'menu.data-config.products',
'menu.data-config.advertisements', 'menu.data-config.advertisements',
'menu.data-config.sub-accounts', 'menu.data-config.sub-accounts',
'menu.data-config.sub-account-roles',
'menu.data-config.points', 'menu.data-config.points',
'menu.data-config.badges', 'menu.data-config.badges',
'menu.remote', 'menu.remote',

View File

@@ -13,6 +13,7 @@
"Account": "Account", "Account": "Account",
"Account :name status has been changed to :status.": "Account :name status has been changed to :status.", "Account :name status has been changed to :status.": "Account :name status has been changed to :status.",
"Account Info": "Account Info", "Account Info": "Account Info",
"Account List": "Account List",
"Account Management": "Account Management", "Account Management": "Account Management",
"Account Name": "Account Name", "Account Name": "Account Name",
"Account Settings": "Account Settings", "Account Settings": "Account Settings",
@@ -429,6 +430,7 @@
"No matching machines": "No matching machines", "No matching machines": "No matching machines",
"No permissions": "No permissions", "No permissions": "No permissions",
"No roles found.": "No roles found.", "No roles found.": "No roles found.",
"No roles available": "No roles available",
"No slots found": "No slots found", "No slots found": "No slots found",
"No users found": "No users found", "No users found": "No users found",
"None": "None", "None": "None",
@@ -559,7 +561,7 @@
"Risk": "Risk", "Risk": "Risk",
"Role": "Role", "Role": "Role",
"Role Identification": "Role Identification", "Role Identification": "Role Identification",
"Role Management": "Role Permission Management", "Role Management": "Role Management",
"Role Name": "Role Name", "Role Name": "Role Name",
"Role Permissions": "Role Permissions", "Role Permissions": "Role Permissions",
"Role Settings": "Role Permissions", "Role Settings": "Role Permissions",
@@ -777,7 +779,6 @@
"menu.data-config.badges": "Badge Settings", "menu.data-config.badges": "Badge Settings",
"menu.data-config.points": "Point Settings", "menu.data-config.points": "Point Settings",
"menu.data-config.products": "Product Management", "menu.data-config.products": "Product Management",
"menu.data-config.sub-account-roles": "Sub Account Roles",
"menu.data-config.sub-accounts": "Sub Account Management", "menu.data-config.sub-accounts": "Sub Account Management",
"menu.line": "Line Management", "menu.line": "Line Management",
"menu.machines": "Machine Management", "menu.machines": "Machine Management",

View File

@@ -13,6 +13,7 @@
"Account": "帳號", "Account": "帳號",
"Account :name status has been changed to :status.": "アカウント :name のステータスが :status に変更されました。", "Account :name status has been changed to :status.": "アカウント :name のステータスが :status に変更されました。",
"Account Management": "アカウント管理", "Account Management": "アカウント管理",
"Account List": "アカウント一覧",
"Account Name": "帳號姓名", "Account Name": "帳號姓名",
"Account Settings": "アカウント設定", "Account Settings": "アカウント設定",
"Account Status": "アカウント狀態", "Account Status": "アカウント狀態",
@@ -424,6 +425,7 @@
"No matching machines": "一致する機台がありません", "No matching machines": "一致する機台がありません",
"No permissions": "権限項目なし", "No permissions": "権限項目なし",
"No roles found.": "ロールが見つかりませんでした。", "No roles found.": "ロールが見つかりませんでした。",
"No roles available": "利用可能なロールがありません",
"No slots found": "未找到貨道資訊", "No slots found": "未找到貨道資訊",
"No users found": "ユーザーが見つかりません", "No users found": "ユーザーが見つかりません",
"None": "なし", "None": "なし",
@@ -557,7 +559,7 @@
"Risk": "リスク", "Risk": "リスク",
"Role": "ロール", "Role": "ロール",
"Role Identification": "ロール識別情報", "Role Identification": "ロール識別情報",
"Role Management": "ロール權限管理", "Role Management": "ロール管理",
"Role Name": "ロール名", "Role Name": "ロール名",
"Role Permissions": "ロール權限", "Role Permissions": "ロール權限",
"Role Settings": "ロール權限", "Role Settings": "ロール權限",
@@ -777,7 +779,6 @@
"menu.data-config.badges": "バッジ設定", "menu.data-config.badges": "バッジ設定",
"menu.data-config.points": "ポイント設定", "menu.data-config.points": "ポイント設定",
"menu.data-config.products": "商品管理", "menu.data-config.products": "商品管理",
"menu.data-config.sub-account-roles": "サブアカウントロール",
"menu.data-config.sub-accounts": "サブアカウント管理", "menu.data-config.sub-accounts": "サブアカウント管理",
"menu.line": "LINE 設定", "menu.line": "LINE 設定",
"menu.machines": "機台管理", "menu.machines": "機台管理",

View File

@@ -13,6 +13,7 @@
"Account": "帳號", "Account": "帳號",
"Account :name status has been changed to :status.": "帳號 :name 的狀態已變更為 :status。", "Account :name status has been changed to :status.": "帳號 :name 的狀態已變更為 :status。",
"Account Info": "帳號資訊", "Account Info": "帳號資訊",
"Account List": "帳號列表",
"Account Management": "帳號管理", "Account Management": "帳號管理",
"Account Name": "帳號名稱", "Account Name": "帳號名稱",
"Account Settings": "帳戶設定", "Account Settings": "帳戶設定",
@@ -439,6 +440,7 @@
"No matching machines": "查無匹配機台", "No matching machines": "查無匹配機台",
"No permissions": "無權限項目", "No permissions": "無權限項目",
"No roles found.": "找不到角色資料。", "No roles found.": "找不到角色資料。",
"No roles available": "目前沒有角色資料。",
"No slots found": "未找到貨道資訊", "No slots found": "未找到貨道資訊",
"No users found": "找不到用戶資料", "No users found": "找不到用戶資料",
"None": "無", "None": "無",
@@ -806,7 +808,6 @@
"menu.data-config.badges": "徽章設定", "menu.data-config.badges": "徽章設定",
"menu.data-config.points": "點數設定", "menu.data-config.points": "點數設定",
"menu.data-config.products": "商品管理", "menu.data-config.products": "商品管理",
"menu.data-config.sub-account-roles": "子帳號角色",
"menu.data-config.sub-accounts": "子帳號管理", "menu.data-config.sub-accounts": "子帳號管理",
"menu.line": "LINE 配置", "menu.line": "LINE 配置",
"menu.machines": "機台管理", "menu.machines": "機台管理",
@@ -912,5 +913,6 @@
"System Default (All Companies)": "系統預設 (所有公司)", "System Default (All Companies)": "系統預設 (所有公司)",
"No materials available": "沒有可用的素材", "No materials available": "沒有可用的素材",
"Search...": "搜尋...", "Search...": "搜尋...",
"Category Name": "分類名稱",
"Category Management": "分類管理" "Category Management": "分類管理"
} }

View File

@@ -297,7 +297,7 @@
<td class="px-6 py-6 cursor-pointer" @click='openDetail({{ $machine->toJson() }}, {{ $machine->id }}, "{{ $machine->serial_no }}")'> <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="flex items-center gap-4">
<div <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 transition-all duration-300 overflow-hidden"> 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])) @if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover"> <img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else @else
@@ -437,7 +437,7 @@
<td class="px-6 py-6 font-display text-left"> <td class="px-6 py-6 font-display text-left">
<div class="flex items-center gap-4 text-left"> <div class="flex items-center gap-4 text-left">
<div <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"> 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"> <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" <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" /> 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" />
@@ -540,7 +540,7 @@
<td class="px-6 py-6"> <td class="px-6 py-6">
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<div <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 transition-all duration-300"> 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" <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2"> stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"

View File

@@ -20,7 +20,7 @@ $roleSelectConfig = [
@endphp @endphp
@section('content') @section('content')
<div class="space-y-6 pb-20" x-data="accountManager({ <div class="space-y-2 pb-20" x-data="accountManager({
roles: @js($roles), roles: @js($roles),
errors: @js($errors->any()), errors: @js($errors->any()),
oldValues: { oldValues: {
@@ -42,212 +42,338 @@ $roleSelectConfig = [
<div> <div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</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"> <p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
{{ __('Manage administrative and tenant accounts') }} {{ $tab === 'roles' ? __('Define and manage security roles and permissions.') : __('Manage administrative and tenant accounts') }}
</p> </p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button @click="openCreateModal()" class="btn-luxury-primary"> @if($tab === 'roles')
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <a href="{{ route('admin.data-config.sub-account-roles.create') }}" class="btn-luxury-primary">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
<span>{{ __('Add Account') }}</span> </svg>
</button> <span>{{ __('Add Role') }}</span>
</a>
@else
<button @click="openCreateModal()" class="btn-luxury-primary">
<svg class="size-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 Account') }}</span>
</button>
@endif
</div> </div>
</div> </div>
@if(request()->routeIs('admin.data-config.sub-accounts') && auth()->user()->can('menu.data-config.sub-accounts'))
<!-- Tabs Navigation (Pills Style) -->
<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" aria-label="Tabs">
<button type="button"
@click="switchTab('accounts')"
:class="tab === 'accounts' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
{{ __('Account List') }}
</button>
<button type="button"
@click="switchTab('roles')"
:class="tab === 'roles' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
{{ __('Role Management') }}
</button>
</div>
@endif
<!-- Accounts Content (Integrated Card) --> <!-- Accounts Content (Integrated Card) -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in"> <div class="luxury-card rounded-3xl p-8 animate-luxury-in mt-6">
<!-- Filters & Search --> @if($tab === 'roles')
<form action="{{ route($baseRoute) }}" method="GET" <!-- Role Filters & Search -->
class="flex flex-col md:flex-row md:items-center gap-4 mb-10"> <form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
<div class="relative group"> <input type="hidden" name="tab" value="roles">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"> <div class="relative group">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" <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">
stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle>
<circle cx="11" cy="11" r="8"></circle> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> </svg>
</svg> </span>
</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...') }}">
<input type="text" name="search" value="{{ request('search') }}" </div>
class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input"
placeholder="{{ __('Search users...') }}">
</div>
@if(auth()->user()->isSystemAdmin()) @if(auth()->user()->isSystemAdmin())
<div class="relative"> <div class="relative">
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')" <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()">
:placeholder="__('All Affiliations')" class="w-full md:w-auto min-w-[280px]" <option value="system" {{ request('company_id') === 'system' ? 'selected' : '' }} data-title="{{ __('System Level') }}">{{ __('System Level') }}</option>
onchange="this.form.submit()" /> </x-searchable-select>
</div> </div>
@endif @endif
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}"> <input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</form> </form>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0"> <table class="w-full text-left border-separate border-spacing-y-0">
<thead> <thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10"> <tr class="bg-slate-50/50 dark:bg-slate-900/10">
<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>
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"> <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>
{{ __('User 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">{{ __('Permissions') }}</th>
<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>
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"> <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>
{{ __('Contact Info') }}</th> </tr>
@if(auth()->user()->isSystemAdmin()) </thead>
<th <tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
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"> @forelse($paginated_roles as $role)
{{ __('Affiliation') }}</th> <tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
@endif <td class="px-6 py-6">
<th <div class="flex items-center gap-3">
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"> <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">
{{ __('Role') }}</th> <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>
<th </div>
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"> <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>
{{ __('Status') }}</th> @if($role->is_system)
<th <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') }}">
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"> <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>
{{ __('Actions') }}</th> </span>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($users 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">
<div class="flex items-center gap-x-4">
<div
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm">
@if($user->avatar)
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
@else
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
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>
@endif @endif
</div> </div>
<div class="flex flex-col"> </td>
<span <td class="px-6 py-6">
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> @if($role->is_system)
<span <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">
class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase"><span {{ __('System Level') }}
class="font-mono">{{ $user->username }}</span></span> </span>
</div>
</div>
</td>
<td class="px-6 py-6 font-display">
<div class="flex flex-col">
@if($user->phone)
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ $user->phone }}</span>
@endif
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 @if($user->phone) mt-1 @endif tracking-widest">{{ $user->email ?? '-' }}</span>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($user->company)
<span
class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $user->company->name }}</span>
@else
<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') }}</span>
@endif
</td>
@endif
<td class="px-6 py-6 text-center">
@foreach($user->roles as $role)
<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-700 uppercase tracking-widest">
{{ $role->name }}
</span>
@endforeach
</td>
<td class="px-6 py-6 text-center">
@if($user->status)
<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-widest uppercase">
{{ __('Active') }}
</span>
@else
<span
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20 tracking-widest uppercase">
{{ __('Disabled') }}
</span>
@endif
</td>
<td class="px-6 py-6 text-right">
<div class="flex justify-end items-center gap-2">
@if(!$user->hasRole('super-admin') || auth()->user()->hasRole('super-admin'))
@if($user->status)
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
title="{{ __('Disable') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
</button>
@else
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
title="{{ __('Enable') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
</svg>
</button>
@endif
<button @click="openEditModal(@js($user))"
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"
title="{{ __('Edit') }}">
<svg class="size-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>
<form action="{{ route($baseRoute . '.destroy', $user->id) }}" method="POST"
class="inline-block">
@csrf
@method('DELETE')
<button type="button"
@click="confirmDelete('{{ route($baseRoute . '.destroy', $user->id) }}')"
class="p-2 rounded-xl 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 group/btn"
title="{{ __('Delete Account') }}">
<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="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>
@else @else
<span <span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">
class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">{{ __('Protected') }}</span> {{ $role->company->name ?? __('Company Level') }}
</span>
@endif @endif
</div> </td>
</td> <td class="px-6 py-6" width="30%">
</tr> <div class="flex flex-wrap gap-1 max-w-xs">
@empty @forelse($role->permissions->take(6) as $permission)
<tr> <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>
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-24 text-center"> @empty
<div class="flex flex-col items-center gap-3 opacity-20"> <span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ __('No permissions') }}</span>
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> @endforelse
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" @if($role->permissions->count() > 6)
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" /> <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>
</svg> @endif
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p> </div>
</div> </td>
</td> <td class="px-6 py-6 text-center">
</tr> <span class="text-sm font-extrabold text-slate-700 dark:text-slate-300">{{ $role->users()->count() }}</span>
@endforelse </td>
</tbody> <td class="px-6 py-6 text-right">
</table> <div class="flex items-center justify-end gap-2">
</div> <a href="{{ route('admin.data-config.sub-account-roles.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('admin.data-config.sub-account-roles.destroy', $role->id) }}" method="POST" @submit.prevent="if({{ $role->users()->count() }} > 0) { triggerDeleteWarning('{{ __('Cannot delete role with active users.') }}'); return; } confirmDelete('{{ route('admin.data-config.sub-account-roles.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/50 dark:border-slate-800/50 pt-6"> <div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $users->links('vendor.pagination.luxury') }} {{ $paginated_roles->links('vendor.pagination.luxury') }}
</div> </div>
@else
<!-- Accounts Filters & Search -->
<form action="{{ route($baseRoute) }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-10">
<input type="hidden" name="tab" value="accounts">
<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 users...') }}">
</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()" />
</div>
@endif
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</form>
<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">
{{ __('User 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">
{{ __('Contact Info') }}</th>
@if(auth()->user()->isSystemAdmin())
<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>
@endif
<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">
{{ __('Role') }}</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">
{{ __('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 text-right">
{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($users 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">
<div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm">
@if($user->avatar)
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
@else
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
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>
@endif
</div>
<div class="flex flex-col">
<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-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase"><span class="font-mono">{{ $user->username }}</span></span>
</div>
</div>
</td>
<td class="px-6 py-6 font-display">
<div class="flex flex-col">
@if($user->phone)
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ $user->phone }}</span>
@endif
<span class="text-xs font-bold text-slate-400 dark:text-slate-500 @if($user->phone) mt-1 @endif tracking-widest">{{ $user->email ?? '-' }}</span>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($user->company)
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $user->company->name }}</span>
@else
<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') }}</span>
@endif
</td>
@endif
<td class="px-6 py-6 text-center">
@foreach($user->roles as $role)
<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-700 uppercase tracking-widest">
{{ $role->name }}
</span>
@endforeach
</td>
<td class="px-6 py-6 text-center">
@if($user->status)
<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-widest uppercase">
{{ __('Active') }}
</span>
@else
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20 tracking-widest uppercase">
{{ __('Disabled') }}
</span>
@endif
</td>
<td class="px-6 py-6 text-right">
<div class="flex justify-end items-center gap-2">
@if(!$user->hasRole('super-admin') || auth()->user()->hasRole('super-admin'))
@if($user->status)
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; statusToggleSource = 'list'; isStatusConfirmOpen = true"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 hover:bg-amber-500/5 transition-all border border-transparent hover:border-amber-500/20"
title="{{ __('Disable') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
</button>
@else
<button type="button"
@click="toggleFormAction = '{{ route($baseRoute . '.status.toggle', $user->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
title="{{ __('Enable') }}">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
</svg>
</button>
@endif
<button @click="openEditModal(@js($user))"
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"
title="{{ __('Edit') }}">
<svg class="size-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>
<form action="{{ route($baseRoute . '.destroy', $user->id) }}" method="POST"
class="inline-block">
@csrf
@method('DELETE')
<button type="button"
@click="confirmDelete('{{ route($baseRoute . '.destroy', $user->id) }}')"
class="p-2 rounded-xl 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 group/btn"
title="{{ __('Delete Account') }}">
<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="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>
@else
<span
class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">{{ __('Protected') }}</span>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" 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">
{{ $users->links('vendor.pagination.luxury') }}
</div>
@endif
</div> </div>
<!-- User Modal --> <!-- User Modal -->
@@ -403,6 +529,49 @@ $roleSelectConfig = [
@csrf @csrf
@method('PATCH') @method('PATCH')
</form> </form>
<!-- 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">&#8203;</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>
</div> </div>
@endsection @endsection
@@ -423,6 +592,7 @@ $roleSelectConfig = [
role: initData.oldValues.role || '', role: initData.oldValues.role || '',
status: initData.oldValues.status || 1 status: initData.oldValues.status || 1
}, },
tab: initData.tab || 'accounts',
roleSelectConfig: initData.roleSelectConfig, roleSelectConfig: initData.roleSelectConfig,
isDeleteConfirmOpen: false, isDeleteConfirmOpen: false,
deleteFormAction: '', deleteFormAction: '',
@@ -430,6 +600,20 @@ $roleSelectConfig = [
toggleFormAction: '', toggleFormAction: '',
statusToggleSource: 'list', statusToggleSource: 'list',
isWarningModalOpen: false,
deleteWarningMsg: '',
triggerDeleteWarning(msg) {
this.deleteWarningMsg = msg;
this.isWarningModalOpen = true;
},
switchTab(newTab) {
const url = new URL(window.location.href);
url.searchParams.set('tab', newTab);
// Clear search and filters when switching tabs if desired, but here we just go to the URL
window.location.href = url.toString();
},
confirmDelete(action) { confirmDelete(action) {
this.deleteFormAction = action; this.deleteFormAction = action;
this.isDeleteConfirmOpen = true; this.isDeleteConfirmOpen = true;

View File

@@ -398,20 +398,17 @@ window.machineApp = function(initialTab) {
<div class="flex items-center gap-4 cursor-pointer group/info" <div class="flex items-center gap-4 cursor-pointer group/info"
@click="openCabinet('{{ $machine->id }}')"> @click="openCabinet('{{ $machine->id }}')">
<div <div
class="w-12 h-12 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 overflow-hidden group-hover/info:border-cyan-500/50 group-hover/info:shadow-lg group-hover/info:shadow-cyan-500/10 transition-all duration-300 shadow-sm relative"> class="w-12 h-12 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 overflow-hidden group-hover/info:bg-cyan-500 group-hover/info:text-white transition-all duration-300 shadow-sm relative">
@if(isset($machine->image_urls[0])) @if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" <img src="{{ $machine->image_urls[0] }}"
class="w-full h-full object-cover group-hover/info:scale-110 transition-transform duration-500"> class="w-full h-full object-cover group-hover/info:scale-110 transition-transform duration-500">
@else @else
<svg class="w-6 h-6 stroke-[1.5]" fill="none" stroke="currentColor" <svg class="w-6 h-6 stroke-[2.5]" fill="none" stroke="currentColor"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" /> d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
</svg> </svg>
@endif @endif
<div
class="absolute inset-0 bg-cyan-500/0 group-hover/info:bg-cyan-500/10 transition-colors duration-300">
</div>
</div> </div>
<div> <div>
<div <div
@@ -548,10 +545,10 @@ window.machineApp = function(initialTab) {
<!-- Product Image/Icon --> <!-- Product Image/Icon -->
<div <div
class="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-white/10 flex items-center justify-center p-2 mb-2 overflow-hidden backdrop-blur-md"> class="w-12 h-12 sm:w-16 sm:h-16 rounded-2xl bg-white/10 flex items-center justify-center p-2 mb-2 overflow-hidden backdrop-blur-md">
<template x-if="slot.product && slot.product.image"> <template x-if="slot.product && slot.product.image_url">
<img :src="slot.product.image" class="w-full h-full object-contain"> <img :src="slot.product.image_url" class="w-full h-full object-contain">
</template> </template>
<template x-if="!slot.product || !slot.product.image"> <template x-if="!slot.product || !slot.product.image_url">
<svg class="w-6 h-6 stroke-[1.5]" fill="none" stroke="currentColor" <svg class="w-6 h-6 stroke-[1.5]" fill="none" stroke="currentColor"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
@@ -918,10 +915,12 @@ window.machineApp = function(initialTab) {
<!-- Slot Header Info --> <!-- Slot Header Info -->
<div <div
class="flex items-center gap-4 p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800"> class="flex items-center gap-4 p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-800">
<div <div class="flex-shrink-0 w-16 h-16 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center p-2 overflow-hidden">
class="w-16 h-16 rounded-xl bg-white dark:bg-slate-800 p-2 border border-slate-200 dark:border-slate-700 shadow-sm overflow-hidden"> <template x-if="selectedSlot.product && selectedSlot.product.image_url">
<template x-if="selectedSlot.product && selectedSlot.product.image"> <img :src="selectedSlot.product.image_url" class="w-full h-full object-contain">
<img :src="selectedSlot.product.image" class="w-full h-full object-contain"> </template>
<template x-if="!selectedSlot.product || !selectedSlot.product.image_url">
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</template> </template>
</div> </div>
<div> <div>

View File

@@ -140,7 +140,7 @@
<td class="px-6 py-6 font-display"> <td class="px-6 py-6 font-display">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div <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"> 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 transition-all duration-300 shadow-sm overflow-hidden">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" <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" /> 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" />

View File

@@ -112,7 +112,7 @@ $roleSelectConfig = [
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300"> <tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6"> <td class="px-6 py-6">
<div class="flex items-center gap-x-4"> <div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm"> <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($product->image_url) @if($product->image_url)
<img src="{{ $product->image_url }}" class="w-full h-full object-cover"> <img src="{{ $product->image_url }}" class="w-full h-full object-cover">
@else @else
@@ -125,7 +125,7 @@ $roleSelectConfig = [
@php @php
$catName = $product->category->localized_name ?? __('Uncategorized'); $catName = $product->category->localized_name ?? __('Uncategorized');
@endphp @endphp
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded transition-colors group-hover:text-slate-600 dark:group-hover:text-slate-300">{{ $catName }}</span> <span class="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded transition-colors group-hover:text-slate-600 dark:group-hover:text-slate-300">{{ $catName }}</span>
@if(($companySettings['enable_material_code'] ?? false) && isset($product->metadata['material_code'])) @if(($companySettings['enable_material_code'] ?? false) && isset($product->metadata['material_code']))
<span class="text-[10px] font-bold text-emerald-500/80 uppercase tracking-widest bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">#{{ $product->metadata['material_code'] }}</span> <span class="text-[10px] font-bold text-emerald-500/80 uppercase tracking-widest bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">#{{ $product->metadata['material_code'] }}</span>
@endif @endif
@@ -217,13 +217,21 @@ $roleSelectConfig = [
@forelse($categories as $category) @forelse($categories as $category)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300"> <tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6"> <td class="px-6 py-6">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 transition-colors group-hover:text-cyan-600"> <div class="flex items-center gap-x-4">
{{ $category->localized_name }} <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">
</span> <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v13.5A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V9.432a2.25 2.25 0 00-.659-1.591l-4.182-4.182A2.25 2.25 0 0014.568 3h-5z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12h-6m6 4h-6m6-8h-6" />
</svg>
</div>
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 transition-colors group-hover:text-cyan-600 dark:group-hover:text-cyan-400">
{{ $category->localized_name }}
</span>
</div>
</td> </td>
@if(auth()->user()->isSystemAdmin()) @if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6 text-center"> <td class="px-6 py-6 text-center">
<span class="text-xs font-bold text-slate-600 dark:text-slate-400">{{ $category->company->name ?? __('System Default') }}</span> <span class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ $category->company->name ?? __('System Default') }}</span>
</td> </td>
@endif @endif
<td class="px-6 py-6 text-right"> <td class="px-6 py-6 text-right">

View File

@@ -229,13 +229,6 @@
</a></li> </a></li>
@endcan @endcan
@can('menu.data-config.sub-account-roles')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" 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>
{{ __('Sub Account Roles') }}
</a></li>
@endcan
@can('menu.data-config.points') @can('menu.data-config.points')
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}"> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>

View File

@@ -130,12 +130,14 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class, 'storeAccount'])->name('sub-accounts.store')->middleware('can:menu.data-config.sub-accounts'); Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class, 'storeAccount'])->name('sub-accounts.store')->middleware('can:menu.data-config.sub-accounts');
Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateAccount'])->name('sub-accounts.update')->middleware('can:menu.data-config.sub-accounts'); Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateAccount'])->name('sub-accounts.update')->middleware('can:menu.data-config.sub-accounts');
Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyAccount'])->name('sub-accounts.destroy')->middleware('can:menu.data-config.sub-accounts'); Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyAccount'])->name('sub-accounts.destroy')->middleware('can:menu.data-config.sub-accounts');
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class, 'roles'])->name('sub-account-roles')->middleware('can:menu.data-config.sub-account-roles'); Route::get('/sub-account-roles', function() {
Route::get('/sub-account-roles/create', [App\Http\Controllers\Admin\PermissionController::class, 'createRole'])->name('sub-account-roles.create')->middleware('can:menu.data-config.sub-account-roles'); return redirect()->route('admin.data-config.sub-accounts', ['tab' => 'roles']);
Route::get('/sub-account-roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class, 'editRole'])->name('sub-account-roles.edit')->middleware('can:menu.data-config.sub-account-roles'); })->name('sub-account-roles')->middleware('can:menu.data-config.sub-accounts');
Route::post('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class, 'storeRole'])->name('sub-account-roles.store')->middleware('can:menu.data-config.sub-account-roles'); Route::get('/sub-account-roles/create', [App\Http\Controllers\Admin\PermissionController::class, 'createRole'])->name('sub-account-roles.create')->middleware('can:menu.data-config.sub-accounts');
Route::put('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateRole'])->name('sub-account-roles.update')->middleware('can:menu.data-config.sub-account-roles'); Route::get('/sub-account-roles/{id}/edit', [App\Http\Controllers\Admin\PermissionController::class, 'editRole'])->name('sub-account-roles.edit')->middleware('can:menu.data-config.sub-accounts');
Route::delete('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyRole'])->name('sub-account-roles.destroy')->middleware('can:menu.data-config.sub-account-roles'); Route::post('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class, 'storeRole'])->name('sub-account-roles.store')->middleware('can:menu.data-config.sub-accounts');
Route::put('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'updateRole'])->name('sub-account-roles.update')->middleware('can:menu.data-config.sub-accounts');
Route::delete('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class, 'destroyRole'])->name('sub-account-roles.destroy')->middleware('can:menu.data-config.sub-accounts');
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class, 'points'])->name('points'); Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class, 'points'])->name('points');
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class, 'badges'])->name('badges'); Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class, 'badges'])->name('badges');
}); });