nxtgauge-admin-solid/src/routes/admin/roles/index.tsx

144 lines
6.7 KiB
TypeScript
Raw Normal View History

import { A } from '@solidjs/router';
import { createResource, createSignal, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { Eye, Pencil, Trash2 } from 'lucide-solid';
const API = '/api/gateway';
type Role = {
id: string;
name: string;
description?: string;
code?: string;
};
async function loadInternalRoles(): Promise<Role[]> {
try {
const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
const rows = Array.isArray(data) ? data : (data.roles || []);
return rows.map((r: any) => ({
id: r.id,
name: r.name,
description: r.description || '',
code: r.code || r.key || '',
}));
} catch {
return [];
}
}
export default function InternalRolesPage() {
const [roles, { refetch }] = createResource(loadInternalRoles);
const [deleting, setDeleting] = createSignal('');
const [deleteError, setDeleteError] = createSignal('');
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete role "${name}"? This cannot be undone.`)) return;
try {
setDeleting(id);
setDeleteError('');
const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete role');
refetch();
} catch (err: any) {
setDeleteError(err.message || 'Failed to delete role');
} finally {
setDeleting('');
}
};
return (
<AdminShell>
<div class="mb-8">
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1>
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain internal system roles and access privileges.</p>
</div>
<nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="hidden" href="/admin/onboarding-schemas">Onboarding Schemas</A>
</nav>
<Show when={deleteError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
</Show>
<section class="overflow-hidden rounded-[22px] border border-[#d8dbe5] bg-white shadow-[0_14px_28px_-20px_rgba(15,23,42,0.35)]">
<div class="overflow-x-auto">
<table class="w-full min-w-[860px] text-sm">
<thead>
<tr class="bg-[#071b3d] text-white">
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th>
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</th>
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Issue Type</th>
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Edit</th>
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Delete</th>
</tr>
</thead>
<tbody>
<Show when={roles.loading}>
<tr>
<td colspan="5" style="text-align:center;padding:32px;color:#64748b;">Loading internal roles...</td>
</tr>
</Show>
<Show when={!roles.loading && roles.error}>
<tr>
<td colspan="5" style="text-align:center;padding:32px;color:#b91c1c;">Failed to load roles. Is the backend running?</td>
</tr>
</Show>
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
<tr>
<td colspan="5" style="text-align:center;padding:32px;color:#94a3b8;">No internal roles found. Create your first role.</td>
</tr>
</Show>
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
{roles()!.map((role) => (
<tr class="border-b border-[#e4e7ef] text-[17px]">
<td class="px-8 py-7 font-medium text-[#364152]">{role.code || role.id?.slice(0, 6).toUpperCase() || 'ROL247'}</td>
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.name}</td>
<td class="px-8 py-7">
<A class="inline-flex items-center gap-2 font-semibold text-[#fd6216] hover:text-orange-700" href={`/admin/roles/${role.id}`} title="View Role" aria-label={`View ${role.name}`}>
<span>View</span>
<Eye size={16} />
</A>
</td>
<td class="px-8 py-7 text-center">
<A class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#071b3d] hover:bg-slate-100" href={`/admin/roles/${role.id}/edit`} title="Edit Role" aria-label={`Edit ${role.name}`}>
<Pencil size={17} />
</A>
</td>
<td class="px-8 py-7 text-center">
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#c81e1e] hover:bg-red-50 disabled:opacity-60"
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
title="Delete Role"
aria-label={`Delete ${role.name}`}
>
{deleting() === role.id ? '...' : <Trash2 size={17} />}
</button>
</td>
</tr>
))}
</Show>
</tbody>
</table>
</div>
<div class="flex items-center justify-between border-t border-[#e4e7ef] px-8 py-5">
<p class="text-[14px] font-semibold uppercase tracking-[0.1em] text-[#485163]">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p>
<div class="flex items-center gap-2">
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'<'}</button>
<button class="h-11 min-w-11 rounded-xl bg-[#fd6216] px-3 font-bold text-white">1</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">2</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">3</button>
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'>'}</button>
</div>
</div>
</section>
</AdminShell>
);
}