feat: phase 4 role management ui matching figma designs

This commit is contained in:
Ashwin Kumar 2026-03-26 00:06:47 +01:00
parent 648b6be849
commit 1b70f40e40
2 changed files with 329 additions and 243 deletions

View file

@ -95,195 +95,178 @@ export default function InternalRolesListPage() {
return (
<AdminShell>
<div class="flex flex-col gap-0 -mx-6 -mt-6 min-h-full">
{/* Page title */}
<div class="bg-white border-b border-[#e5e7eb] px-6 py-5">
<h1 class="text-[20px] font-semibold text-[#000032]">Internal Role Management</h1>
<p class="text-[13px] text-[rgba(0,0,50,0.5)] mt-0.5">Manage internal roles and permissions</p>
</div>
{/* Card */}
<div class="p-6">
<div class="rounded-xl border border-[#e5e7eb] bg-white shadow-sm overflow-hidden">
{/* Tabs */}
<div class="flex border-b border-[#e5e7eb] px-6">
<button
class="relative py-4 text-[14px] font-semibold text-[#fa5014] mr-6"
>
All Roles
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#fa5014] rounded-t" />
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header & Title */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Internal Role Management</h1>
</div>
<div class="flex items-center gap-3">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
Export Data
</button>
<A
href="/admin/roles/create"
class="py-4 text-[14px] font-medium text-[rgba(0,0,50,0.5)] hover:text-[#000032]"
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
>
Create Role
<span class="mr-2 text-lg leading-none">+</span> Add Role
</A>
</div>
{/* Toolbar */}
<div class="flex items-center gap-3 px-6 py-4 border-b border-[#e5e7eb]">
<div class="relative flex-1 max-w-[280px]">
<Search size={15} class="absolute left-3 top-1/2 -translate-y-1/2 text-[#9195ad]" />
<input
type="text"
placeholder="Search roles..."
value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)}
class="w-full pl-9 pr-3 py-2 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] bg-white"
/>
</div>
<select class="text-[13px] border border-[#e5e7eb] rounded-lg px-3 py-2 outline-none bg-white text-[rgba(0,0,50,0.6)] focus:border-[#fa5014]">
<option value="">All Departments</option>
</select>
<select class="text-[13px] border border-[#e5e7eb] rounded-lg px-3 py-2 outline-none bg-white text-[rgba(0,0,50,0.6)] focus:border-[#fa5014]">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="bg-[#000032] text-white text-[12px] font-semibold uppercase tracking-wide">
<th class="px-6 py-3 text-left">Role Name</th>
<th class="px-6 py-3 text-left">Department</th>
<th class="px-6 py-3 text-left">Users Assigned</th>
<th class="px-6 py-3 text-left">Permissions Count</th>
<th class="px-6 py-3 text-left">Status</th>
<th class="px-6 py-3 text-left">Created Date</th>
<th class="px-6 py-3 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-[#e5e7eb]">
<Show when={data.loading}>
<tr>
<td colspan="7" class="px-6 py-10 text-center text-[13px] text-[rgba(0,0,50,0.4)]">
Loading roles
</td>
</tr>
</Show>
<Show when={!data.loading && (data()?.roles?.length ?? 0) === 0}>
<tr>
<td colspan="7" class="px-6 py-10 text-center text-[13px] text-[rgba(0,0,50,0.4)]">
No internal roles found.{' '}
<A href="/admin/roles/create" class="text-[#fa5014] hover:underline">
Create your first role.
</A>
</td>
</tr>
</Show>
<For each={data()?.roles ?? []}>
{(role) => (
<tr class="hover:bg-[#fafafa] transition-colors">
<td class="px-6 py-4">
<p class="text-[14px] font-semibold text-[#000032]">{role.name}</p>
<p class="text-[12px] text-[rgba(0,0,50,0.4)] mt-0.5">{role.key}</p>
</td>
<td class="px-6 py-4 text-[13px] text-[rgba(0,0,50,0.7)]">
{role.department_name || '—'}
</td>
<td class="px-6 py-4 text-[14px] font-semibold text-[#000032]">
{role.users_assigned}
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-1 rounded-md bg-[rgba(250,80,20,0.1)] text-[#fa5014] text-[12px] font-semibold">
{role.permissions_count} Permissions
</span>
</td>
<td class="px-6 py-4">
<Show
when={role.is_active}
fallback={
<span class="inline-flex items-center px-2.5 py-1 rounded-md bg-[#f1f1f1] text-[rgba(0,0,50,0.5)] text-[12px] font-semibold">
Inactive
</span>
}
>
<span class="inline-flex items-center px-2.5 py-1 rounded-md bg-[rgba(34,197,94,0.1)] text-[#16a34a] text-[12px] font-semibold">
Active
</span>
</Show>
</td>
<td class="px-6 py-4 text-[13px] text-[rgba(0,0,50,0.6)]">
{formatDate(role.created_at)}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<A
href={`/admin/roles/${role.id}`}
class="text-[12px] font-medium text-[#000032] hover:text-[#fa5014] transition-colors"
>
View
</A>
<span class="text-[#e5e7eb]">|</span>
<A
href={`/admin/roles/${role.id}/edit`}
class="text-[12px] font-medium text-[#000032] hover:text-[#fa5014] transition-colors"
>
Edit
</A>
<span class="text-[#e5e7eb]">|</span>
<button
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
class="text-[12px] font-medium text-red-500 hover:text-red-700 transition-colors disabled:opacity-50"
>
{deleting() === role.id ? '…' : 'Delete'}
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={!data.loading && (data()?.total ?? 0) > 0}>
<div class="flex items-center justify-between px-6 py-4 border-t border-[#e5e7eb]">
<p class="text-[13px] text-[rgba(0,0,50,0.5)]">
Showing {((page() - 1) * (data()?.per_page ?? 8)) + 1}
{Math.min(page() * (data()?.per_page ?? 8), data()?.total ?? 0)} of {data()?.total ?? 0} roles
</p>
<div class="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page() <= 1}
class="h-8 w-8 flex items-center justify-center rounded-lg border border-[#e5e7eb] text-[rgba(0,0,50,0.5)] hover:border-[#000032] disabled:opacity-40 transition-colors text-[13px]"
>
</button>
<For each={Array.from({ length: totalPages() }, (_, i) => i + 1)}>
{(p) => (
<button
onClick={() => setPage(p)}
class={`h-8 w-8 flex items-center justify-center rounded-lg text-[13px] font-medium transition-colors ${
p === page()
? 'bg-[#fa5014] text-white'
: 'border border-[#e5e7eb] text-[rgba(0,0,50,0.6)] hover:border-[#000032]'
}`}
>
{p}
</button>
)}
</For>
<button
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
disabled={page() >= totalPages()}
class="h-8 w-8 flex items-center justify-center rounded-lg border border-[#e5e7eb] text-[rgba(0,0,50,0.5)] hover:border-[#000032] disabled:opacity-40 transition-colors text-[13px]"
>
</button>
</div>
</div>
</Show>
</div>
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Tabs */}
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
<For each={['Active Roles', 'Archived Roles']}>
{(t) => (
<button
class={`pb-3 text-[14px] font-bold transition-colors border-b-2 ${
t === 'Active Roles'
? 'border-[#050026] text-[#050026]'
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
}`}
>
{t}
</button>
)}
</For>
</div>
{/* Filters Row */}
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
<div class="relative w-full md:w-[320px]">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Search roles..."
value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
/>
</div>
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<div class="flex-1"></div>
<Show when={!data.loading}>
<span class="text-[13px] text-[#8087a0] font-medium">
Showing {((page() - 1) * (data()?.per_page ?? 8)) + 1}{Math.min(page() * (data()?.per_page ?? 8), data()?.total ?? 0)} of {data()?.total ?? 0} roles
</span>
</Show>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">ROLE ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CATEGORY</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ASSOCIATED USERS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ACTIVE PERMISSIONS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
</tr>
</thead>
<tbody>
<Show when={data.loading}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">Loading roles...</td></tr>
</Show>
<Show when={!data.loading && (data()?.roles?.length ?? 0) === 0}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">No internal roles found.</td></tr>
</Show>
<For each={data()?.roles ?? []}>
{(role) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">{role.key.toUpperCase()}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{role.name}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{role.department_name || '—'}</td>
<td class="px-6 py-4 text-[14px] font-semibold text-[#050026]">
<div class="flex -space-x-2 mr-2 inline-flex items-center">
{/* Placeholder for avatar group if users > 0 */}
<Show when={role.users_assigned > 0} fallback={<span class="text-[#c1c7d0] font-normal">0 users</span>}>
<div class="h-6 w-6 rounded-full bg-[#e2e8f0] border-2 border-white flex items-center justify-center text-[10px] font-bold text-[#475569]">U</div>
<div class="h-6 w-6 rounded-full bg-[#f1f5f9] border-2 border-white flex items-center justify-center text-[10px] font-bold text-[#64748b]">+{role.users_assigned - 1}</div>
</Show>
</div>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#f8f9fc] text-[#050026] text-[12px] font-bold border border-[#e2e6ee]">
{role.permissions_count} Permissions
</span>
</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{formatDate(role.created_at)}</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<A
title="View Details"
href={`/admin/roles/${role.id}`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</A>
<A
title="Edit"
href={`/admin/roles/${role.id}/edit`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</A>
<button
title="Archive"
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={totalPages() > 1}>
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
<span class="text-[13px] font-medium text-[#8087a0]">Page {page()} of {totalPages()}</span>
<div class="flex items-center gap-2">
<button
disabled={page() === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<button
disabled={page() >= totalPages()}
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
</Show>
</div>
</section>
</div>
</div>
</AdminShell>

View file

@ -1,5 +1,5 @@
import { A, useSearchParams } from '@solidjs/router';
import { createMemo, createResource, Show } from 'solid-js';
import { createMemo, createResource, Show, For } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
@ -72,73 +72,176 @@ async function loadExternalRoles(): Promise<ExternalRole[]> {
export default function RuntimeRolesPage() {
const [searchParams] = useSearchParams();
const [roles] = createResource(loadExternalRoles);
const selectedRoleKey = createMemo(() => (searchParams.roleKey || '').toLowerCase());
const selectedRoleKey = createMemo(() => {
const rk = searchParams.roleKey;
return (Array.isArray(rk) ? rk[0] : rk || '').toLowerCase();
});
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">External Role Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Configure and maintain external system roles and access privileges.</p>
</div>
<div class="flex-1 p-6">
<div class="table-card">
<div class="overflow-x-auto">
<table data-table class="w-full min-w-[860px] text-sm">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Issue Type</th>
<th class="text-center">Edit</th>
<th class="text-center">Delete</th>
</tr>
</thead>
<tbody>
<Show when={roles.loading}>
<tr><td colspan="5" class="text-center px-8 py-8 text-slate-500">Loading external roles...</td></tr>
</Show>
<Show when={!roles.loading && roles.error}>
<tr><td colspan="5" class="text-center px-8 py-8 text-red-600">Failed to load external roles. Is the backend running?</td></tr>
</Show>
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
<tr><td colspan="5" class="text-center px-8 py-8 text-slate-400">No external roles configured yet.</td></tr>
</Show>
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
{roles()!.map((role) => (
<tr class={`hover:bg-slate-50 ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-slate-50' : ''}`}>
<td class="text-slate-500">{role.roleKey || role.id?.slice(0, 6).toUpperCase()}</td>
<td class="font-semibold text-slate-900">{role.displayName}</td>
<td class="text-slate-500">
<A class="inline-flex items-center gap-1 font-medium text-[#0a1d37] hover:text-[#0f2a4e]" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer">
<span>View</span>
<span></span>
</A>
</td>
<td class="text-center">
<A class="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-600 hover:bg-slate-100" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role"></A>
</td>
<td class="text-center">
<button class="inline-flex h-8 w-8 items-center justify-center rounded-md text-red-600 hover:bg-red-50" title="Delete External Role" aria-label={`Delete ${role.displayName}`}>🗑</button>
</td>
</tr>
))}
</Show>
</tbody>
</table>
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header & Title */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">External Role Management</h1>
</div>
<div class="flex items-center justify-between border-t border-gray-200 px-6 py-4">
<p class="text-sm text-slate-500">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p>
<div class="flex items-center gap-2">
<button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-700 hover:bg-gray-50">{'<'}</button>
<button class="h-9 min-w-9 rounded-lg bg-[#0a1d37] px-3 text-sm font-medium text-white">1</button>
<button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50">2</button>
<button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50">3</button>
<button class="h-9 min-w-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-700 hover:bg-gray-50">{'>'}</button>
</div>
<div class="flex items-center gap-3">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
Export Data
</button>
<A
href="/admin/runtime-roles/new"
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
>
<span class="mr-2 text-lg leading-none">+</span> Add Role
</A>
</div>
</div>
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Tabs */}
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
<For each={['Active Roles', 'Archived Roles']}>
{(t: string) => (
<button
class={`pb-3 text-[14px] font-bold transition-colors border-b-2 ${
t === 'Active Roles'
? 'border-[#050026] text-[#050026]'
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
}`}
>
{t}
</button>
)}
</For>
</div>
{/* Filters Row */}
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
<div class="relative w-full md:w-[320px]">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Search roles..."
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
/>
</div>
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<div class="flex-1"></div>
<Show when={!roles.loading}>
<span class="text-[13px] text-[#8087a0] font-medium">
Showing 1{roles()?.length || 0} of {roles()?.length || 0} roles
</span>
</Show>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">ROLE ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CATEGORY</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ENABLED MODULES</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">STATUS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ONBOARDING SCHEMA</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
</tr>
</thead>
<tbody>
<Show when={roles.loading}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">Loading external roles...</td></tr>
</Show>
<Show when={!roles.loading && roles.error}>
<tr><td colspan="7" class="text-center py-12 text-red-500 text-[14px]">Failed to load external roles. Is the backend running?</td></tr>
</Show>
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">No external roles configured yet.</td></tr>
</Show>
<For each={(!roles.loading && !roles.error ? roles() : []) as ExternalRole[]}>
{(role: ExternalRole) => (
<tr class={`border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-[#f8f9fc]' : ''}`}>
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">{(role.roleKey || role.id?.slice(0, 6)).toUpperCase()}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{role.displayName}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{role.vertical || '—'}</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#f8f9fc] text-[#050026] text-[12px] font-bold border border-[#e2e6ee]">
{role.enabledModules.length} Modules
</span>
</td>
<td class="px-6 py-4 text-[14px]">
<Show when={role.isActive} fallback={<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium bg-red-100 text-red-800">Inactive</span>}>
<span class="inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium bg-green-100 text-green-800">Active</span>
</Show>
</td>
<td class="px-6 py-4 text-[14px] text-[#0ea5e9] hover:underline cursor-pointer">{role.onboardingSchemaId || '—'}</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<A
title="View Details"
href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</A>
<A
title="Edit"
href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</A>
<button
title="Delete External Role"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-red-50 hover:text-red-500 hover:border-red-200 transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{/* Pagination */}
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
<span class="text-[13px] font-medium text-[#8087a0]">Page 1 of 1</span>
<div class="flex items-center gap-2">
<button
disabled
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<button
disabled
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</div>
</div>
</AdminShell>