feat: phase 4 role management ui matching figma designs
This commit is contained in:
parent
648b6be849
commit
1b70f40e40
2 changed files with 329 additions and 243 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue