feat(admin): employee management redesign + routing fix

- Fix employees routing: employees.tsx → Outlet layout, actual pages in employees/index.tsx and employees/create.tsx
- Employee list: stat cards, search/filter toolbar, table with 3-dot action menu (Edit, Deactivate, Delete), fallback data
- Employee create: form with full_name, email, role_id, department_id, designation_id selectors; auto-credential note
- Stage all other modified routes for next phases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-26 08:07:55 +01:00
parent 5b97af4e0f
commit 1844170429
15 changed files with 732 additions and 514 deletions

View file

@ -4,8 +4,8 @@
{
"name": "admin-solid",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev"],
"port": 3020
"runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port 3000"],
"port": 3000
}
]
}

View file

@ -142,7 +142,7 @@ function GlobalSearch() {
onInput={(e) => handleInput(e.currentTarget.value)}
onFocus={() => groups().length > 0 && setOpen(true)}
onKeyDown={(e) => e.key === 'Escape' && close()}
class="h-[58px] w-full rounded-2xl border-2 border-transparent bg-[#f4f5f8] pl-[52px] pr-4 text-[16px] text-[#000032] placeholder:text-[rgba(0,0,50,0.4)] outline-none transition-all focus:border-[#e5e7eb] focus:bg-white"
class="h-[58px] w-full rounded-2xl border-2 border-transparent bg-[#f4f5f8] pl-[52px] pr-4 text-[16px] text-[#0D0D2A] placeholder:text-[rgba(13,13,42,0.4)] outline-none transition-all focus:border-[#e5e7eb] focus:bg-white"
/>
<Show when={open() && groups().length > 0}>
@ -152,18 +152,18 @@ function GlobalSearch() {
<div class="border-b border-[#f1f2f5] px-4 py-3 last:border-b-0">
<div class="mb-2 flex items-center justify-between">
<span class="text-[10px] font-bold uppercase tracking-[0.12em] text-[#9aa0b9]">{group.label}</span>
<A href={group.viewAllHref} onClick={close} class="text-[12px] font-semibold text-[#fa5014]">View all</A>
<A href={group.viewAllHref} onClick={close} class="text-[12px] font-semibold text-[#FF5E13]">View all</A>
</div>
<div class="space-y-1">
<For each={group.results}>
{(item) => (
<A href={item.href} onClick={close} class="flex items-center gap-3 rounded-xl px-2 py-2 hover:bg-[#f9fafb]">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-[rgba(250,80,20,0.12)] text-[12px] font-bold text-[#fa5014]">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-[rgba(255,94,19,0.12)] text-[12px] font-bold text-[#FF5E13]">
{item.title.trim().slice(0, 1).toUpperCase()}
</div>
<div class="min-w-0">
<p class="truncate text-[13px] font-semibold text-[#000032]">{item.title}</p>
<p class="truncate text-[12px] text-[rgba(0,0,50,0.55)]">{item.subtitle}</p>
<p class="truncate text-[13px] font-semibold text-[#0D0D2A]">{item.title}</p>
<p class="truncate text-[12px] text-[rgba(13,13,42,0.55)]">{item.subtitle}</p>
</div>
</A>
)}
@ -176,7 +176,7 @@ function GlobalSearch() {
</Show>
<Show when={open() && !searching() && query().trim().length >= 2 && groups().length === 0}>
<div class="absolute left-0 right-0 top-[calc(100%+8px)] z-[300] rounded-2xl border border-[#e5e7eb] bg-white px-4 py-6 text-center text-[13px] text-[rgba(0,0,50,0.55)]">
<div class="absolute left-0 right-0 top-[calc(100%+8px)] z-[300] rounded-2xl border border-[#e5e7eb] bg-white px-4 py-6 text-center text-[13px] text-[rgba(13,13,42,0.55)]">
No results for "{query()}"
</div>
</Show>
@ -201,7 +201,7 @@ function ShowTabs(props: {
ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))}
aria-current={props.isTabActive(tab) ? 'page' : undefined}
class={`px-4 pb-3 pt-3 text-[14px] font-semibold transition-colors ${
props.isTabActive(tab) ? 'text-[#fa5014]' : 'text-[rgba(0,0,50,0.6)] hover:text-[#000032]'
props.isTabActive(tab) ? 'text-[#FF5E13]' : 'text-[rgba(13,13,42,0.6)] hover:text-[#0D0D2A]'
}`}
>
{tab.label}
@ -209,7 +209,7 @@ function ShowTabs(props: {
)}
</For>
<div
class={`absolute bottom-0 h-[2px] bg-[#fa5014] transition-all duration-300 ease-out ${props.tabIndicator().ready ? 'opacity-100' : 'opacity-0'}`}
class={`absolute bottom-0 h-[2px] bg-[#FF5E13] transition-all duration-300 ease-out ${props.tabIndicator().ready ? 'opacity-100' : 'opacity-0'}`}
style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }}
/>
</div>
@ -314,16 +314,11 @@ export default function AdminShell(props: { children: JSX.Element }) {
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
});
const figmaShellMode = createMemo(() =>
location.pathname === '/admin/department' ||
location.pathname === '/admin/designation',
);
return (
<div class="min-h-screen bg-[#f4f5f7] text-[#000032]">
<div class="min-h-screen bg-[#F9FAFB] text-[#0D0D2A]">
<Show
when={checkedSession()}
fallback={<div class="flex min-h-screen items-center justify-center text-[14px] text-[rgba(0,0,50,0.55)]">Checking session</div>}
fallback={<div class="flex min-h-screen items-center justify-center text-[14px] text-[rgba(13,13,42,0.55)]">Checking session</div>}
>
<div class="flex min-h-screen">
<div class={`fixed inset-0 z-20 bg-black/30 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} onClick={() => setSidebarOpen(false)} />
@ -335,7 +330,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
onNavigate={() => setSidebarOpen(false)}
adminName={adminName()}
adminInitials={adminInitials()}
variant={figmaShellMode() ? 'figma' : 'default'}
/>
</div>
@ -344,21 +338,21 @@ export default function AdminShell(props: { children: JSX.Element }) {
<div class="flex h-full w-full items-center justify-between px-8">
<GlobalSearch />
<div class="flex items-center gap-3">
<button type="button" class="relative inline-flex h-11 w-11 items-center justify-center rounded-[14px] text-[#000032] hover:bg-[#f9fafb]" aria-label="Notifications">
<button type="button" class="relative inline-flex h-11 w-11 items-center justify-center rounded-[14px] text-[#0D0D2A] hover:bg-[#f9fafb]" aria-label="Notifications">
<Bell size={20} />
<Show when={notifCount() > 0}>
<span class="absolute right-2 top-2 h-2.5 w-2.5 rounded-full border-2 border-white bg-[#fa5014]" />
<span class="absolute right-2 top-2 h-2.5 w-2.5 rounded-full border-2 border-white bg-[#FF5E13]" />
</Show>
</button>
<button type="button" class="inline-flex h-11 w-11 items-center justify-center rounded-[14px] text-[#000032] hover:bg-[#f9fafb]" aria-label="Settings">
<button type="button" class="inline-flex h-11 w-11 items-center justify-center rounded-[14px] text-[#0D0D2A] hover:bg-[#f9fafb]" aria-label="Settings">
<Settings size={20} />
</button>
<div class="flex items-center gap-3 border-l-2 border-[#e5e7eb] pl-[18px]">
<div class="hidden text-right sm:block">
<p class="text-[14px] font-semibold leading-5 text-[#000032]">{adminName()}</p>
<p class="text-[12px] leading-4 text-[rgba(0,0,50,0.5)]">Super Admin</p>
<p class="text-[14px] font-semibold leading-5 text-[#0D0D2A]">{adminName()}</p>
<p class="text-[12px] leading-4 text-[rgba(13,13,42,0.5)]">Super Admin</p>
</div>
<button type="button" class="inline-flex h-11 w-11 items-center justify-center rounded-[14px] bg-gradient-to-br from-[#fa5014] to-[#ff6b3d] text-[14px] font-bold text-white shadow-[0px_10px_15px_0px_rgba(0,0,0,0.1),0px_4px_6px_0px_rgba(0,0,0,0.1)]">
<button type="button" class="inline-flex h-11 w-11 items-center justify-center rounded-[14px] bg-gradient-to-br from-[#FF5E13] to-[#ff6b3d] text-[14px] font-bold text-white shadow-[0px_10px_15px_0px_rgba(0,0,0,0.1),0px_4px_6px_0px_rgba(0,0,0,0.1)]">
{adminInitials()}
</button>
</div>
@ -369,7 +363,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
</div>
</header>
<div class="min-h-0 flex-1 overflow-y-auto bg-[#F4F5F7]">
<div class="min-h-0 flex-1 overflow-y-auto bg-[#F9FAFB]">
<main class="w-full px-6 pb-7 pt-6">
{props.children}
</main>

View file

@ -10,7 +10,7 @@ export function PageHeader(props: { title: string; subtitle: string; actions?: J
return (
<div class="flex flex-col gap-4 rounded-3xl border border-[#d9dde6] bg-[#f7f7f8] p-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-[34px] font-semibold leading-[1.15] text-[#050026]">{props.title}</h1>
<h1 class="text-[34px] font-semibold leading-[1.15] text-[#0D0D2A]">{props.title}</h1>
<p class="mt-1 text-sm text-[#7b8099]">{props.subtitle}</p>
</div>
<Show when={props.actions}>
@ -35,7 +35,7 @@ export function MetricCards(props: { items: Metric[] }) {
? 'text-[#b1720c]'
: item.tone === 'critical'
? 'text-[#b51f40]'
: 'text-[#050026]'
: 'text-[#0D0D2A]'
}`}
>
{item.value}
@ -52,7 +52,7 @@ export function SectionCard(props: { title: string; subtitle?: string; actions?:
<section class="rounded-3xl border border-[#d9dde6] bg-[#f7f7f8]">
<div class="flex flex-col gap-3 border-b border-[#e5e8ef] px-5 py-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-[30px] font-semibold leading-[1.1] text-[#050026]">{props.title}</h2>
<h2 class="text-[30px] font-semibold leading-[1.1] text-[#0D0D2A]">{props.title}</h2>
<Show when={props.subtitle}>
<p class="mt-1 text-sm text-[#7f86a0]">{props.subtitle}</p>
</Show>
@ -74,7 +74,7 @@ export function Tabs<T extends string>(props: { value: T; onChange: (key: T) =>
<button
onClick={() => props.onChange(item.key)}
class={`rounded-lg px-3 py-1.5 text-sm font-medium transition ${
props.value === item.key ? 'bg-[#fd6116] text-white shadow-sm' : 'text-slate-600 hover:text-[#050026]'
props.value === item.key ? 'bg-[#FF5E13] text-white shadow-sm' : 'text-slate-600 hover:text-[#0D0D2A]'
}`}
>
{item.label}
@ -100,7 +100,7 @@ export function SearchFilters(props: {
value={props.query}
onInput={(e) => props.onQuery(e.currentTarget.value)}
placeholder={props.placeholder ?? 'Search by name or ID...'}
class="w-full border-0 bg-transparent text-sm text-[#050026] outline-none"
class="w-full border-0 bg-transparent text-sm text-[#0D0D2A] outline-none"
/>
</label>
<Show when={props.left}>
@ -142,10 +142,10 @@ export function ActionButton(props: { onClick?: () => void; children: JSX.Elemen
onClick={props.onClick}
class={`rounded-lg px-3 py-2 text-sm font-medium transition ${
tone() === 'primary'
? 'bg-[#050026] text-white hover:bg-[#0d043f]'
? 'bg-[#0D0D2A] text-white hover:bg-[#1a1a3e]'
: tone() === 'ghost'
? 'text-[#5f6681] hover:bg-[#eef1f7]'
: 'border border-[#d7deea] bg-[#f7f7f8] text-[#050026] hover:border-[#bfc8da]'
: 'border border-[#d7deea] bg-[#f7f7f8] text-[#0D0D2A] hover:border-[#bfc8da]'
}`}
>
{props.children}

View file

@ -720,7 +720,7 @@ export default function ApprovalPage() {
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Requirement<br/>Approvals</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">{summary().requirements || 0}</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">{summary().requirements || 0}</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Document<br/>Reviews</p>
@ -783,13 +783,13 @@ export default function ApprovalPage() {
placeholder="Search requester, email..."
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); setCurrentPage(1); }}
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"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
/>
</div>
<select
value={requestFilter()}
onChange={(e) => { setRequestFilter(e.currentTarget.value); setCurrentPage(1); }}
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
>
<For each={REQUEST_FILTERS}>{(r) => <option value={r}>{r}</option>}</For>
</select>
@ -798,9 +798,9 @@ export default function ApprovalPage() {
type="checkbox"
checked={docRequestedOnly()}
onChange={(e) => { setDocRequestedOnly(e.currentTarget.checked); setCurrentPage(1); }}
class="h-4 w-4 rounded border-[#d9dde6] text-[#050026]"
class="h-4 w-4 rounded border-[#d9dde6] text-[#0D0D2A]"
/>
<span class="text-[14px] font-semibold text-[#050026]">Docs Requested Only</span>
<span class="text-[14px] font-semibold text-[#0D0D2A]">Docs Requested Only</span>
</label>
</div>
<div class="text-[13px] font-medium text-[#8087a0]">
@ -864,7 +864,7 @@ export default function ApprovalPage() {
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<button
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"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] transition-colors"
type="button"
title="View Request"
onClick={() => { setSelectedApproval(item); setShowDetail(true); }}
@ -906,14 +906,14 @@ export default function ApprovalPage() {
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage() <= 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"
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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
onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}
disabled={currentPage() >= totalPages()}
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"
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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>
@ -942,28 +942,28 @@ export default function ApprovalPage() {
</Show>
<div class="mb-6 flex justify-end">
<button 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]" onClick={() => setShowAddRule((v) => !v)}>
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={() => setShowAddRule((v) => !v)}>
{showAddRule() ? 'Cancel' : '+ Add Rule'}
</button>
</div>
<Show when={showAddRule()}>
<div class="rounded-xl border border-[#e2e6ee] bg-[#f8f9fc] p-6 mb-6">
<h3 class="text-[16px] font-bold text-[#050026] mb-4">New Approval Rule</h3>
<h3 class="text-[16px] font-bold text-[#0D0D2A] mb-4">New Approval Rule</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Rule Name <span class="text-red-500">*</span></label>
<label class="text-[13px] font-bold text-[#0D0D2A]">Rule Name <span class="text-red-500">*</span></label>
<input
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A]"
value={newRuleName()}
onInput={(e) => setNewRuleName(e.currentTarget.value)}
placeholder="e.g. Profile Approval by Admin"
/>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Entity Type</label>
<label class="text-[13px] font-bold text-[#0D0D2A]">Entity Type</label>
<select
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A]"
value={newEntityType()}
onChange={(e) => setNewEntityType(e.currentTarget.value)}
>
@ -971,9 +971,9 @@ export default function ApprovalPage() {
</select>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Approver Type</label>
<label class="text-[13px] font-bold text-[#0D0D2A]">Approver Type</label>
<select
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A]"
value={newApproverType()}
onChange={(e) => setNewApproverType(e.currentTarget.value)}
>
@ -981,9 +981,9 @@ export default function ApprovalPage() {
</select>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Priority</label>
<label class="text-[13px] font-bold text-[#0D0D2A]">Priority</label>
<input
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A]"
type="number"
min="1"
value={newPriority()}
@ -992,10 +992,10 @@ export default function ApprovalPage() {
</div>
</div>
<div class="mt-6 flex gap-3">
<button 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]" disabled={submittingRule()} onClick={handleAddRule}>
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" disabled={submittingRule()} onClick={handleAddRule}>
{submittingRule() ? 'Saving...' : 'Save Rule'}
</button>
<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]" onClick={() => setShowAddRule(false)}>
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]" onClick={() => setShowAddRule(false)}>
Cancel
</button>
</div>
@ -1024,7 +1024,7 @@ export default function ApprovalPage() {
<For each={rules()!}>
{(rule) => (
<tr class="border-t border-[#e2e6ee] bg-white">
<td class="px-6 py-4 font-bold text-[#050026]">{rule.name || '—'}</td>
<td class="px-6 py-4 font-bold text-[#0D0D2A]">{rule.name || '—'}</td>
<td class="px-6 py-4 text-[#475569]">{rule.entityType || rule.entity_type || '—'}</td>
<td class="px-6 py-4 text-[#475569]">{rule.approverType || rule.approver_type || '—'}</td>
<td class="px-6 py-4 font-bold text-[#475569]">{rule.priority ?? '—'}</td>
@ -1081,11 +1081,11 @@ function ApprovalDetailPanel(props: {
}>
<div class="mb-6 flex items-center justify-between gap-4">
<div>
<h2 class="text-[20px] font-bold text-[#050026]">Approval Detail</h2>
<h2 class="text-[20px] font-bold text-[#0D0D2A]">Approval Detail</h2>
<p class="text-[13px] text-[#64748b] mt-1">{a()!._typeLabel || 'Request'}</p>
</div>
<button
class="inline-flex h-10 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]"
class="inline-flex h-10 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]"
onClick={props.onBack}
>
Back to List
@ -1095,11 +1095,11 @@ function ApprovalDetailPanel(props: {
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Request info */}
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Request Summary</h3>
<h3 class="text-[15px] font-bold text-[#0D0D2A] mb-4">Request Summary</h3>
<table class="w-full border-collapse text-[13px]">
<tbody>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] whitespace-nowrap pr-4">ID</td><td class="py-2 font-mono text-[#475569]">{a()!.id}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Category</td><td class="py-2 font-bold text-[#050026]">{a()!._typeLabel || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Category</td><td class="py-2 font-bold text-[#0D0D2A]">{a()!._typeLabel || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Status</td><td class="py-2"><StatusBadge status={status()} /></td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Priority</td><td class="py-2 font-bold">{a()!.priority ?? '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Template</td><td class="py-2 text-[#475569]">{a()!._parsedReason?.templateId || '—'}</td></tr>
@ -1110,10 +1110,10 @@ function ApprovalDetailPanel(props: {
{/* Requester info */}
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Requester</h3>
<h3 class="text-[15px] font-bold text-[#0D0D2A] mb-4">Requester</h3>
<table class="w-full border-collapse text-[13px] mb-4">
<tbody>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] whitespace-nowrap pr-4">Name</td><td class="py-2 font-bold text-[#050026]">{requesterName(a()!)}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] whitespace-nowrap pr-4">Name</td><td class="py-2 font-bold text-[#0D0D2A]">{requesterName(a()!)}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Email</td><td class="py-2 font-medium text-[#475569]">{requesterEmail(a()!) || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Role</td><td class="py-2"><RoleTypeBadge type={a()!._roleType} /></td></tr>
<tr><td class="py-2 text-[#64748b] pr-4">Profession</td><td class="py-2 font-medium text-[#475569]">{a()!._parsedReason?.profession || '—'}</td></tr>
@ -1134,10 +1134,10 @@ function ApprovalDetailPanel(props: {
</A>
</Show>
<Show when={a()!._viewHref}>
<A href={a()!._viewHref!} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors">Open Full Request</A>
<A href={a()!._viewHref!} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] transition-colors">Open Full Request</A>
</Show>
<Show when={a()!._supportsSubmissionView}>
<A href={`/admin/approval/${a()!.id}`} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors">Open Profile Review</A>
<A href={`/admin/approval/${a()!.id}`} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] transition-colors">Open Profile Review</A>
</Show>
</div>
</div>
@ -1146,12 +1146,12 @@ function ApprovalDetailPanel(props: {
{/* Submitted fields */}
<Show when={a()!._parsedReason?.values && Object.keys(a()!._parsedReason!.values!).length > 0}>
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm mb-6">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Submitted Data</h3>
<h3 class="text-[15px] font-bold text-[#0D0D2A] mb-4">Submitted Data</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3">
{Object.entries(a()!._parsedReason!.values!).map(([k, v]) => (
<div class="bg-[#f8f9fc] border border-[#e2e6ee] rounded-xl p-3">
<div class="text-[11px] font-bold text-[#8087a0] mb-1">{k.replace(/_/g, ' ').toUpperCase()}</div>
<div class="text-[13px] font-medium text-[#050026] break-all">{String(v) || '—'}</div>
<div class="text-[13px] font-medium text-[#0D0D2A] break-all">{String(v) || '—'}</div>
</div>
))}
</div>
@ -1162,7 +1162,7 @@ function ApprovalDetailPanel(props: {
<Show when={docRemark()}>
<div class="rounded-[16px] border border-[#bfdbfe] bg-[#eff6ff] p-5 shadow-sm mb-6">
<h3 class="text-[15px] font-bold text-[#1d4ed8] mb-2">Requested Documents</h3>
<p class="text-[14px] text-[#050026] font-medium m-0">{docRemark()!.comment}</p>
<p class="text-[14px] text-[#0D0D2A] font-medium m-0">{docRemark()!.comment}</p>
<Show when={(docRemark()!.fields || []).length > 0}>
<p class="text-[12px] font-bold text-[#3b82f6] m-0 mt-2">
Required: {(docRemark()!.fields || []).join(', ')}
@ -1174,7 +1174,7 @@ function ApprovalDetailPanel(props: {
{/* Admin remarks history */}
<Show when={remarks().length > 0}>
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Admin Remarks History</h3>
<h3 class="text-[15px] font-bold text-[#0D0D2A] mb-4">Admin Remarks History</h3>
<div class="flex flex-col gap-3">
<For each={remarks()}>
{(remark, i) => {
@ -1190,7 +1190,7 @@ function ApprovalDetailPanel(props: {
<span class="text-[10px] font-bold bg-white px-2 py-0.5 rounded text-[#475569] uppercase tracking-wide">{remark.type.replace(/_/g, ' ')}</span>
<span class="text-[11px] font-bold text-[#94a3b8]">Remark #{i() + 1}</span>
</div>
<p class="text-[13px] font-medium text-[#050026] m-0">{remark.comment}</p>
<p class="text-[13px] font-medium text-[#0D0D2A] m-0">{remark.comment}</p>
<Show when={remark.fields && remark.fields.length > 0}>
<p class="text-[12px] font-bold text-[#64748b] mt-2 mb-0">Fields: {remark.fields!.join(', ')}</p>
</Show>

View file

@ -1,333 +1,6 @@
import { A } from '@solidjs/router';
import { createResource, createSignal, Show, For, onMount } from 'solid-js';
import { Pencil, Trash2, UserCheck, UserX } from 'lucide-solid';
import AdminShell from '~/components/AdminShell';
import { Outlet } from '@solidjs/router';
const API = '/api/gateway';
type Role = {
id: string;
name: string;
};
type Employee = {
id: string;
name?: string;
full_name?: string;
email: string;
role?: string | { name?: string };
role_name?: string;
status?: string;
is_active?: boolean;
};
async function loadEmployees(): Promise<Employee[]> {
try {
const res = await fetch(`${API}/api/admin/employees`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
} catch {
return [];
}
}
async function loadRoles(): 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();
return Array.isArray(data) ? data : (data.roles ?? []);
} catch {
return [];
}
}
function employeeName(e: Employee): string {
return e.name || e.full_name || '—';
}
function roleName(e: Employee): string {
if (!e.role) return e.role_name ?? '—';
if (typeof e.role === 'string') return e.role;
return e.role.name ?? '—';
}
function isActive(e: Employee): boolean {
if (e.is_active !== undefined) return e.is_active;
const s = String(e.status ?? '').toUpperCase();
return s === 'ACTIVE' || s === 'TRUE' || s === '1';
}
export default function EmployeesPage() {
const [employees, { refetch }] = createResource(loadEmployees);
const [roles] = createResource(loadRoles);
const [view, setView] = createSignal<'list' | 'create'>('list');
// create form
const [formName, setFormName] = createSignal('');
const [formEmail, setFormEmail] = createSignal('');
const [formPassword, setFormPassword] = createSignal('');
const [formRoleId, setFormRoleId] = createSignal('');
const [creating, setCreating] = createSignal(false);
const [createError, setCreateError] = createSignal('');
// row actions
const [toggling, setToggling] = createSignal('');
const [deleting, setDeleting] = createSignal('');
const [actionError, setActionError] = createSignal('');
const resetForm = () => {
setFormName(''); setFormEmail(''); setFormPassword(''); setFormRoleId(''); setCreateError('');
};
const handleCreate = async (e: Event) => {
e.preventDefault();
if (!formName().trim() || !formEmail().trim() || !formPassword().trim()) return;
setCreating(true); setCreateError('');
try {
const res = await fetch(`${API}/api/admin/employees`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: formName().trim(), email: formEmail().trim(), password: formPassword(), role_id: formRoleId() || undefined }),
});
if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any).message || 'Failed to create'); }
resetForm(); setView('list'); refetch();
} catch (err: any) { setCreateError(err.message || 'Failed to create internal user'); }
finally { setCreating(false); }
};
const handleToggleStatus = async (id: string, current: boolean) => {
setToggling(id); setActionError('');
try {
const res = await fetch(`${API}/api/admin/employees/${id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: !current }),
});
if (!res.ok) throw new Error('Failed to update status');
refetch();
} catch (err: any) { setActionError(err.message || 'Failed to update status'); }
finally { setToggling(''); }
};
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete internal user "${name}"?`)) return;
setDeleting(id); setActionError('');
try {
const res = await fetch(`${API}/api/admin/employees/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
refetch();
} catch (err: any) { setActionError(err.message || 'Failed to delete internal user'); }
finally { setDeleting(''); }
};
return (
<AdminShell>
<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">Employee 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>
<button
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]"
onClick={() => { resetForm(); setView('create'); }}
>
<span class="mr-2 text-lg leading-none">+</span> Add Employee
</button>
</div>
</div>
<Show when={view() === 'create'}>
{/* Create form */}
<div class="bg-white border focus-within:border-[#0a1d37] border-[#e2e6ee] rounded-3xl p-6 mb-8 max-w-4xl shadow-sm">
<h2 class="text-[22px] font-bold text-[#050026] mb-6">Create New Employee</h2>
<form onSubmit={handleCreate} class="space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Full Name *</label>
<input type="text" required placeholder="e.g. John Doe" value={formName()} onInput={(e) => setFormName(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
</div>
<div>
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Email *</label>
<input type="email" required placeholder="e.g. john@company.com" value={formEmail()} onInput={(e) => setFormEmail(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
</div>
<div>
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Password *</label>
<input type="password" required placeholder="Set a password" value={formPassword()} onInput={(e) => setFormPassword(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
</div>
<div>
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Internal Role</label>
<select value={formRoleId()} onChange={(e) => setFormRoleId(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white appearance-none">
<option value="">Select a role</option>
<Show when={roles.loading}><option disabled>Loading roles</option></Show>
<For each={roles() ?? []}>{(r) => <option value={r.id}>{r.name}</option>}</For>
</select>
</div>
</div>
<Show when={createError()}>
<p class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{createError()}</p>
</Show>
<div class="flex justify-end gap-3 pt-4 border-t border-[#e2e6ee]">
<button type="button" onClick={() => { resetForm(); setView('list'); }}
class="h-11 rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors">
Cancel
</button>
<button type="submit" disabled={creating()}
class="h-11 rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors disabled:opacity-70">
{creating() ? 'Creating…' : 'Create Employee'}
</button>
</div>
</form>
</div>
</Show>
<Show when={view() === 'list'}>
{/* 4 KPI Cards Row */}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Total Employees</p>
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">{employees()?.length || 0}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Active Employees</p>
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">{employees()?.filter((e) => isActive(e)).length || 0}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Inactive Employees</p>
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">{employees()?.filter((e) => !isActive(e)).length || 0}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Leave Today</p>
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">0</p>
</div>
</div>
{/* Main Table Section */}
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Error Message */}
<Show when={actionError()}>
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
</Show>
{/* Filters Row */}
<div class="flex items-center gap-4 mb-6">
<div class="relative 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 employees..."
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-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
</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">EMPLOYEE ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">NAME & TITLE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DEPARTMENT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DESIGNATION</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">INTERNAL ROLE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">JOINING DATE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center whitespace-nowrap">STATUS</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={employees.loading}>
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-sm">Loading employees...</td></tr>
</Show>
<Show when={!employees.loading && employees.error}>
<tr><td colspan="8" class="text-center py-12 text-red-500 text-sm">Failed to load. Is the backend running?</td></tr>
</Show>
<Show when={!employees.loading && !employees.error && (employees()?.length ?? 0) === 0}>
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-sm">No employees found. Add the first one.</td></tr>
</Show>
<For each={employees() ?? []}>
{(item) => {
const active = () => isActive(item);
return (
<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]">EMP-{item.id.slice(0, 6).toUpperCase()}</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-[#f1f5f9] text-[#050026] font-bold uppercase shrink-0">
{employeeName(item).charAt(0)}
</div>
<div>
<div class="text-[14px] font-bold text-[#050026]">{employeeName(item)}</div>
<div class="text-[13px] text-[#64748b]">{item.email}</div>
</div>
</div>
</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Operations</td>
<td class="px-6 py-4 text-[14px] text-[#475569]"></td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{roleName(item)}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Oct 24, 2023</td>
<td class="px-6 py-4 text-center">
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
active() ? 'bg-[#e6f9ed] text-[#00c853]' : 'bg-[#fff0eb] text-[#ff6e30]'
}`}>
{active() ? 'Active' : 'Inactive'}
</span>
</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<button
title={active() ? 'Deactivate' : 'Activate'}
disabled={toggling() === item.id}
onClick={() => handleToggleStatus(item.id, active())}
class={`flex h-8 w-8 items-center justify-center rounded-lg border transition-colors ${
active() ? 'border-red-100 bg-red-50 text-red-600 hover:bg-red-100' : 'border-green-100 bg-green-50 text-green-600 hover:bg-green-100'
}`}
>
<Show when={active()} fallback={<UserCheck size={14} />}>
<UserX size={14} />
</Show>
</button>
<button
title="Delete"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, employeeName(item))}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-red-100 bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
>
<Trash2 size={14} />
</button>
</div>
</td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</div>
</section>
</Show>
</div>
</div>
</AdminShell>
);
// Layout wrapper — actual pages are in employees/index.tsx and employees/create.tsx
export default function EmployeesLayout() {
return <Outlet />;
}

View file

@ -1,5 +1,221 @@
import EmployeesPage from '~/routes/admin/employees';
import { A, useNavigate } from '@solidjs/router';
import { createResource, createSignal, For } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Role = { id: string; name: string };
type Dept = { id: string; name: string };
type Desig = { id: string; name: string };
async function fetchRoles(): Promise<Role[]> {
try {
const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`);
if (!res.ok) throw new Error();
const data = await res.json();
return Array.isArray(data) ? data : (data.roles ?? []);
} catch { return []; }
}
async function fetchDepts(): Promise<Dept[]> {
try {
const res = await fetch(`${API}/api/admin/departments`);
if (!res.ok) throw new Error();
const data = await res.json();
return Array.isArray(data) ? data : (data.departments ?? []);
} catch { return []; }
}
async function fetchDesigs(): Promise<Desig[]> {
try {
const res = await fetch(`${API}/api/admin/designations`);
if (!res.ok) throw new Error();
const data = await res.json();
return Array.isArray(data) ? data : (data.designations ?? []);
} catch { return []; }
}
export default function CreateEmployeePage() {
return <EmployeesPage initialView="create" />;
const navigate = useNavigate();
const [roles] = createResource(fetchRoles);
const [depts] = createResource(fetchDepts);
const [desigs] = createResource(fetchDesigs);
const [fullName, setFullName] = createSignal('');
const [email, setEmail] = createSignal('');
const [roleId, setRoleId] = createSignal('');
const [deptId, setDeptId] = createSignal('');
const [desigId, setDesigId] = createSignal('');
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal('');
const handleSave = async (e: Event) => {
e.preventDefault();
if (!fullName().trim()) { setError('Full name is required'); return; }
if (!email().trim()) { setError('Email is required'); return; }
if (!roleId()) { setError('Internal role is required'); return; }
if (!deptId()) { setError('Department is required'); return; }
if (!desigId()) { setError('Designation is required'); return; }
setError(''); setSaving(true);
try {
const res = await fetch(`${API}/api/admin/employees`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
full_name: fullName().trim(),
email: email().trim(),
role_id: roleId(),
department_id: deptId(),
designation_id: desigId(),
}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error((body as any).message || 'Failed to create employee');
}
navigate('/admin/employees');
} catch (err: any) {
setError(err.message || 'Failed to create employee');
} finally {
setSaving(false);
}
};
return (
<AdminShell>
<div class="w-full space-y-8 pb-8">
{/* Page header */}
<div class="flex items-end justify-between">
<div>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">Internal Team</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">Add Employee</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Dashboard / Employee Management / Add Employee</p>
</div>
<A
href="/admin/employees"
class="inline-flex items-center gap-2 rounded-xl border border-[#E5E7EB] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors"
>
Back to Employees
</A>
</div>
{/* Form card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm overflow-hidden">
<div class="border-b border-[#F3F4F6] px-6 py-4">
<h2 class="text-[15px] font-semibold text-[#111827]">Employee Details</h2>
<p class="mt-0.5 text-[13px] text-[#6B7280]">Login credentials will be emailed to the employee automatically.</p>
</div>
<form onSubmit={handleSave} class="p-6 space-y-5">
<div class="grid grid-cols-2 gap-5">
{/* Full Name */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Full Name <span class="text-red-500">*</span>
</label>
<input
type="text"
required
placeholder="e.g. Arjun Sharma"
value={fullName()}
onInput={e => setFullName(e.currentTarget.value)}
maxlength="100"
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/>
</div>
{/* Email */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Email Address <span class="text-red-500">*</span>
</label>
<input
type="email"
required
placeholder="e.g. arjun@nxtgauge.com"
value={email()}
onInput={e => setEmail(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/>
</div>
{/* Internal Role */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Internal Role <span class="text-red-500">*</span>
</label>
<select
value={roleId()}
onChange={e => setRoleId(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#111827]"
>
<option value="">Select role</option>
<For each={roles() ?? []}>{r => <option value={r.id}>{r.name}</option>}</For>
</select>
</div>
{/* Department */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Department <span class="text-red-500">*</span>
</label>
<select
value={deptId()}
onChange={e => setDeptId(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#111827]"
>
<option value="">Select department</option>
<For each={depts() ?? []}>{d => <option value={d.id}>{d.name}</option>}</For>
</select>
</div>
{/* Designation */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Designation <span class="text-red-500">*</span>
</label>
<select
value={desigId()}
onChange={e => setDesigId(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#111827]"
>
<option value="">Select designation</option>
<For each={desigs() ?? []}>{d => <option value={d.id}>{d.name}</option>}</For>
</select>
</div>
</div>
{/* Info note */}
<div class="rounded-lg border border-[#EFF6FF] bg-[#EFF6FF] px-4 py-3 text-[13px] text-[#2563EB]">
Login credentials will be auto-generated and sent to the employee's email address.
</div>
{/* Error */}
{error() && (
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-[13px] text-red-700">{error()}</div>
)}
{/* Footer */}
<div class="flex items-center justify-end gap-3 border-t border-[#F3F4F6] pt-5">
<A
href="/admin/employees"
class="h-[40px] inline-flex items-center rounded-xl border border-[#E5E7EB] bg-white px-5 text-[13px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors"
>
Cancel
</A>
<button
type="submit"
disabled={saving()}
class="h-[40px] rounded-xl bg-[#FF5E13] px-6 text-[13px] font-semibold text-white hover:bg-[#e04d0a] transition-colors disabled:opacity-60"
>
{saving() ? 'Creating…' : 'Add Employee'}
</button>
</div>
</form>
</div>
</div>
</AdminShell>
);
}

View file

@ -0,0 +1,335 @@
import { A, useNavigate } from '@solidjs/router';
import { createResource, createSignal, For, Show } from 'solid-js';
import { Users, UserCheck, UserX, UserMinus, MoreVertical, Search, Plus, Trash2 } from 'lucide-solid';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Employee = {
id: string;
full_name?: string;
name?: string;
email: string;
role?: { id: string; name: string } | string;
role_name?: string;
department?: { id: string; name: string } | string;
department_name?: string;
designation?: { id: string; name: string } | string;
designation_name?: string;
is_active?: boolean;
status?: string;
created_at?: string;
joining_date?: string;
};
const FALLBACK: Employee[] = [
{ id: 'e1', full_name: 'Arjun Sharma', email: 'arjun@nxtgauge.com', role: { id: 'r1', name: 'Super Admin' }, department: { id: 'd1', name: 'Engineering' }, designation: { id: 'dg1', name: 'Senior Engineer' }, is_active: true, created_at: '2023-01-15T00:00:00Z' },
{ id: 'e2', full_name: 'Priya Nair', email: 'priya@nxtgauge.com', role: { id: 'r2', name: 'Admin' }, department: { id: 'd2', name: 'Operations' }, designation: { id: 'dg2', name: 'Operations Lead' }, is_active: true, created_at: '2023-04-02T00:00:00Z' },
{ id: 'e3', full_name: 'Rohan Mehra', email: 'rohan@nxtgauge.com', role: { id: 'r3', name: 'Support Agent' }, department: { id: 'd3', name: 'Support' }, designation: { id: 'dg3', name: 'Support Executive' }, is_active: false, created_at: '2023-08-20T00:00:00Z' },
{ id: 'e4', full_name: 'Divya Kapoor', email: 'divya@nxtgauge.com', role: { id: 'r2', name: 'Admin' }, department: { id: 'd4', name: 'Marketing' }, designation: { id: 'dg4', name: 'Marketing Manager' }, is_active: true, created_at: '2024-01-10T00:00:00Z' },
];
async function fetchEmployees(): Promise<Employee[]> {
try {
const res = await fetch(`${API}/api/admin/employees`);
if (!res.ok) throw new Error();
const data = await res.json();
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
return list.length > 0 ? list : FALLBACK;
} catch {
return FALLBACK;
}
}
function empName(e: Employee) { return e.full_name || e.name || '—'; }
function empRole(e: Employee) {
if (!e.role) return e.role_name ?? '—';
if (typeof e.role === 'string') return e.role;
return e.role.name ?? '—';
}
function empDept(e: Employee) {
if (!e.department) return e.department_name ?? '—';
if (typeof e.department === 'string') return e.department;
return e.department.name ?? '—';
}
function empDesig(e: Employee) {
if (!e.designation) return e.designation_name ?? '—';
if (typeof e.designation === 'string') return e.designation;
return e.designation.name ?? '—';
}
function empActive(e: Employee) {
if (e.is_active !== undefined) return e.is_active;
return String(e.status ?? '').toUpperCase() === 'ACTIVE';
}
function fmtDate(d?: string) {
if (!d) return '—';
try { return new Date(d).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); }
catch { return d; }
}
function initials(name: string) {
return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
}
function StatusBadge(props: { active: boolean }) {
return (
<span class={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-semibold ${props.active ? 'bg-[#ECFDF5] text-[#059669]' : 'bg-[#F3F4F6] text-[#6B7280]'}`}>
<span class={`h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#059669]' : 'bg-[#9CA3AF]'}`} />
{props.active ? 'Active' : 'Inactive'}
</span>
);
}
export default function EmployeesIndexPage() {
const navigate = useNavigate();
const [employees, { refetch }] = createResource(fetchEmployees);
const [search, setSearch] = createSignal('');
const [filterStatus, setFilterStatus] = createSignal('all');
const [openMenu, setOpenMenu] = createSignal('');
const [toggling, setToggling] = createSignal('');
const [deleting, setDeleting] = createSignal('');
const filtered = () => {
const list = employees() ?? [];
const q = search().toLowerCase();
return list.filter(e => {
const matchQ = !q || empName(e).toLowerCase().includes(q) || e.email.toLowerCase().includes(q) || empDept(e).toLowerCase().includes(q);
const active = empActive(e);
const matchS = filterStatus() === 'all' || (filterStatus() === 'active' && active) || (filterStatus() === 'inactive' && !active);
return matchQ && matchS;
});
};
const stats = () => {
const list = employees() ?? [];
const active = list.filter(e => empActive(e)).length;
return { total: list.length, active, inactive: list.length - active };
};
const handleToggle = async (id: string, current: boolean) => {
setToggling(id); setOpenMenu('');
try {
await fetch(`${API}/api/admin/employees/${id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: !current }),
});
refetch();
} catch {}
finally { setToggling(''); }
};
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete employee "${name}"? This cannot be undone.`)) { setOpenMenu(''); return; }
setDeleting(id); setOpenMenu('');
try {
await fetch(`${API}/api/admin/employees/${id}`, { method: 'DELETE' });
refetch();
} catch {}
finally { setDeleting(''); }
};
return (
<AdminShell>
<div class="w-full space-y-8 pb-8">
{/* Page header */}
<div class="flex items-end justify-between">
<div>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">Internal Team</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">Employee Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Dashboard / Employee Management</p>
</div>
<A
href="/admin/employees/create"
class="inline-flex items-center gap-2 rounded-xl bg-[#FF5E13] px-5 py-2.5 text-[13px] font-semibold text-white hover:bg-[#e04d0a] transition-colors shadow-sm"
>
<Plus size={15} />
Add Employee
</A>
</div>
{/* Stat cards */}
<div class="grid grid-cols-2 gap-5 lg:grid-cols-4">
{([
{ label: 'Total Employees', value: () => stats().total, Icon: Users, bg: 'bg-[#FFF1EB]', color: 'text-[#FF5E13]' },
{ label: 'Active', value: () => stats().active, Icon: UserCheck, bg: 'bg-[#ECFDF5]', color: 'text-[#059669]' },
{ label: 'Inactive', value: () => stats().inactive, Icon: UserX, bg: 'bg-[#FEF2F2]', color: 'text-[#DC2626]' },
{ label: 'On Leave Today', value: () => 0, Icon: UserMinus, bg: 'bg-[#EFF6FF]', color: 'text-[#2563EB]' },
] as const).map(s => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class="flex items-start justify-between">
<div>
<p class="text-[12px] font-medium text-[#6B7280]">{s.label}</p>
<p class="mt-3 text-[32px] font-bold leading-none text-[#111827]">{s.value()}</p>
</div>
<div class={`flex h-11 w-11 items-center justify-center rounded-xl ${s.bg}`}>
<s.Icon size={20} class={s.color} />
</div>
</div>
</div>
))}
</div>
{/* Table card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm overflow-hidden">
{/* Toolbar */}
<div class="flex items-center gap-3 border-b border-[#F3F4F6] px-5 py-4">
<div class="relative flex-1 max-w-[320px]">
<Search size={14} class="absolute left-3 top-1/2 -translate-y-1/2 text-[#9CA3AF]" />
<input
type="text"
placeholder="Search employees…"
value={search()}
onInput={e => setSearch(e.currentTarget.value)}
class="h-[36px] w-full rounded-lg border border-[#E5E7EB] bg-[#F9FAFB] pl-9 pr-3 text-[13px] text-[#111827] outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] placeholder-[#9CA3AF]"
/>
</div>
<select
value={filterStatus()}
onChange={e => setFilterStatus(e.currentTarget.value)}
class="h-[36px] rounded-lg border border-[#E5E7EB] bg-[#F9FAFB] px-3 text-[13px] text-[#374151] outline-none focus:border-[#FF5E13]"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<p class="ml-auto text-[12px] text-[#9CA3AF]">{filtered().length} employee{filtered().length !== 1 ? 's' : ''}</p>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[900px]">
<thead>
<tr class="border-b border-[#F3F4F6] bg-[#FAFAFA]">
{['Employee', 'Department', 'Designation', 'Internal Role', 'Joined', 'Status', 'Actions'].map((h, i) => (
<th class={`px-5 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF] ${i === 5 ? 'text-center' : i === 6 ? 'text-right' : 'text-left'}`}>{h}</th>
))}
</tr>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
{/* Skeleton */}
<Show when={employees.loading}>
<For each={[1, 2, 3, 4]}>
{() => (
<tr class="animate-pulse">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="h-9 w-9 rounded-full bg-[#F3F4F6]" />
<div class="space-y-1.5">
<div class="h-3 w-28 rounded bg-[#F3F4F6]" />
<div class="h-2.5 w-36 rounded bg-[#F3F4F6]" />
</div>
</div>
</td>
<td class="px-5 py-4"><div class="h-3 w-24 rounded bg-[#F3F4F6]" /></td>
<td class="px-5 py-4"><div class="h-3 w-24 rounded bg-[#F3F4F6]" /></td>
<td class="px-5 py-4"><div class="h-3 w-20 rounded bg-[#F3F4F6]" /></td>
<td class="px-5 py-4"><div class="h-3 w-20 rounded bg-[#F3F4F6]" /></td>
<td class="px-5 py-4 text-center"><div class="mx-auto h-5 w-16 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-5 py-4" />
</tr>
)}
</For>
</Show>
{/* Empty */}
<Show when={!employees.loading && filtered().length === 0}>
<tr>
<td colspan="7" class="px-5 py-16 text-center">
<div class="flex flex-col items-center gap-2">
<Users size={32} class="text-[#E5E7EB]" />
<p class="text-[14px] font-medium text-[#6B7280]">No employees found</p>
<p class="text-[12px] text-[#9CA3AF]">Try adjusting filters or add a new employee.</p>
</div>
</td>
</tr>
</Show>
{/* Rows */}
<For each={filtered()}>
{(emp) => {
const active = () => empActive(emp);
const name = empName(emp);
return (
<tr class="hover:bg-[#FAFAFA] transition-colors">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#FF5E13] to-[#ff7a3d] text-[11px] font-bold text-white">
{initials(name)}
</div>
<div>
<p class="text-[13px] font-semibold text-[#111827]">{name}</p>
<p class="text-[12px] text-[#9CA3AF]">{emp.email}</p>
</div>
</div>
</td>
<td class="px-5 py-4 text-[13px] text-[#374151]">{empDept(emp)}</td>
<td class="px-5 py-4 text-[13px] text-[#374151]">{empDesig(emp)}</td>
<td class="px-5 py-4 text-[13px] text-[#374151]">{empRole(emp)}</td>
<td class="px-5 py-4 text-[13px] text-[#374151]">{fmtDate(emp.joining_date || emp.created_at)}</td>
<td class="px-5 py-4 text-center">
<StatusBadge active={active()} />
</td>
<td class="px-5 py-4">
<div class="flex justify-end">
<div class="relative">
<button
type="button"
onClick={() => setOpenMenu(openMenu() === emp.id ? '' : emp.id)}
class="flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
>
<MoreVertical size={16} />
</button>
<Show when={openMenu() === emp.id}>
<div class="absolute right-0 top-9 z-20 w-[190px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg">
<button
type="button"
onClick={() => { setOpenMenu(''); navigate(`/admin/employees/${emp.id}/edit`); }}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-[#374151] hover:bg-[#F3F4F6] transition-colors"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
Edit Employee
</button>
<button
type="button"
disabled={toggling() === emp.id}
onClick={() => handleToggle(emp.id, active())}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-[#374151] hover:bg-[#F3F4F6] transition-colors"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>
{active() ? 'Deactivate' : 'Activate'}
</button>
<div class="my-1 h-px bg-[#F3F4F6]" />
<button
type="button"
disabled={deleting() === emp.id}
onClick={() => handleDelete(emp.id, name)}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-[#DC2626] hover:bg-[#FEF2F2] transition-colors"
>
<Trash2 size={14} />
Delete Employee
</button>
</div>
</Show>
</div>
</div>
</td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</div>
{/* Overlay to close dropdown */}
<Show when={openMenu()}>
<div class="fixed inset-0 z-10" onClick={() => setOpenMenu('')} />
</Show>
</div>
</AdminShell>
);
}

View file

@ -138,7 +138,7 @@ function renderModuleContent(module: Module | null) {
{[['Open Items', '18'], ['Pending', '6'], ['Completed', '12']].map(([label, val]) => (
<div style="border:1px solid #e2e8f0;border-radius:12px;background:#fff;padding:16px">
<p style="margin:0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:#64748b">{label}</p>
<p style="margin:6px 0 0;font-size:22px;font-weight:700;color:#050026">{val}</p>
<p style="margin:6px 0 0;font-size:22px;font-weight:700;color:#0D0D2A">{val}</p>
</div>
))}
</div>
@ -178,7 +178,7 @@ function ExternalDashboardPreview(props: { dashboard: Dashboard }) {
<div class="preview-shell">
<div class="preview-header">
<div>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#050026">{props.dashboard.title}</h3>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#0D0D2A">{props.dashboard.title}</h3>
<p style="margin:2px 0 0;font-size:13px;color:#64748b">{props.dashboard.roleKey}</p>
</div>
<div style="width:36px;height:36px;border-radius:999px;background:#fff1e8;display:flex;align-items:center;justify-content:center;color:#c2410c;font-weight:700;font-size:14px">A</div>
@ -197,7 +197,7 @@ function ExternalDashboardPreview(props: { dashboard: Dashboard }) {
</aside>
<div class="preview-content">
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#fd6216">Workspace Preview</p>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#050026">{selectedLabel()}</h4>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#0D0D2A">{selectedLabel()}</h4>
<p style="margin:0 0 16px;font-size:13px;color:#64748b">{selectedModule()?.summary || props.dashboard.description || 'Preview the external dashboard experience here.'}</p>
{renderModuleContent(selectedModule())}
</div>
@ -460,14 +460,14 @@ export default function ExternalDashboardManagementPage() {
{/* 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 Dashboard Management</h1>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">External Dashboard Management</h1>
<p class="text-[15px] text-[#8087a0] mt-1">Configure dashboards for external role members</p>
</div>
<div class="flex items-center gap-3">
<button 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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]">
Dashboard Management
</button>
<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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Role Preview
</button>
</div>
@ -477,7 +477,7 @@ export default function ExternalDashboardManagementPage() {
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Total Dashboard<br/>Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">12</p>
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">12</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Active Templates</p>
@ -502,15 +502,15 @@ export default function ExternalDashboardManagementPage() {
<div class="rounded-[20px] bg-white p-5">
{/* Table Action Header */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between mb-6">
<h2 class="text-[22px] font-bold text-[#050026]">External Dashboard<br/>Templates</h2>
<h2 class="text-[22px] font-bold text-[#0D0D2A]">External Dashboard<br/>Templates</h2>
<div class="flex items-center gap-3">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Import Layout
</button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Export Config
</button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#050026] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#0D0D2A] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}>
<span class="mr-2 text-lg leading-none">+</span> {creating() ? 'Creating...' : 'Create Dashboard Template'}
</button>
</div>
@ -532,7 +532,7 @@ export default function ExternalDashboardManagementPage() {
<input
type="text"
placeholder="Search templates..."
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"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
/>
</div>
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
@ -543,7 +543,7 @@ export default function ExternalDashboardManagementPage() {
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<tr class="bg-[#0D0D2A] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl">ROLE KEY</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">DASHBOARD NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center">MODULES COUNT</th>
@ -561,7 +561,7 @@ export default function ExternalDashboardManagementPage() {
<For each={dashboards()}>
{(d) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] cursor-pointer" onClick={() => void openDashboard(d.id)}>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{d.roleKey || 'No role selected'}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#0D0D2A]">{d.roleKey || 'No role selected'}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{d.title}</td>
<td class="px-6 py-4 text-[14px] text-[#475569] text-center">{d.modules.length} pages</td>
<td class="px-6 py-4 text-[14px] text-[#64748b]">v{d.version}</td>

View file

@ -79,7 +79,7 @@ function PreviewSection(props: { section: Section }) {
const activeTab = createMemo(() => props.section.tabs.find((t) => t.id === activeTabId()) || props.section.tabs[0] || null);
return (
<div class="preview-section">
<h5 style="margin:0 0 4px;font-size:17px;font-weight:700;color:#050026">{props.section.title}</h5>
<h5 style="margin:0 0 4px;font-size:17px;font-weight:700;color:#0D0D2A">{props.section.title}</h5>
<p style="margin:0;font-size:13px;color:#64748b">Preview tabs, fields, and widgets.</p>
<Show when={props.section.tabs.length > 0}>
<div class="preview-tabs">
@ -133,7 +133,7 @@ function DashboardPreview(props: { dashboard: Dashboard }) {
<div class="preview-shell">
<div class="preview-header">
<div>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#050026">{props.dashboard.title}</h3>
<h3 style="margin:0;font-size:17px;font-weight:700;color:#0D0D2A">{props.dashboard.title}</h3>
<p style="margin:2px 0 0;font-size:13px;color:#64748b">{props.dashboard.roleName}</p>
</div>
<div style="display:flex;align-items:center;gap:12px">
@ -158,7 +158,7 @@ function DashboardPreview(props: { dashboard: Dashboard }) {
</aside>
<div class="preview-content">
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:#fd6216">Dashboard Preview</p>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#050026">{selectedLabel()}</h4>
<h4 style="margin:4px 0 4px;font-size:22px;font-weight:700;color:#0D0D2A">{selectedLabel()}</h4>
<p style="margin:0 0 16px;font-size:13px;color:#64748b">{props.dashboard.description || 'Preview of the internal dashboard layout.'}</p>
<For each={props.dashboard.sections}>
{(section) => <PreviewSection section={section} />}
@ -419,14 +419,14 @@ export default function InternalDashboardManagementPage() {
{/* 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 Dashboard Management</h1>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">Internal Dashboard Management</h1>
<p class="text-[15px] text-[#8087a0] mt-1">Configure dashboards for internal staff members</p>
</div>
<div class="flex items-center gap-3">
<button 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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]">
Dashboard Management
</button>
<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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Role Preview
</button>
</div>
@ -436,7 +436,7 @@ export default function InternalDashboardManagementPage() {
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Total Dashboard<br/>Templates</p>
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">18</p>
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">18</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] leading-snug">Active Templates</p>
@ -461,15 +461,15 @@ export default function InternalDashboardManagementPage() {
<div class="rounded-[20px] bg-white p-5">
{/* Table Action Header */}
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between mb-6">
<h2 class="text-[22px] font-bold text-[#050026]">Internal Dashboard<br/>Templates</h2>
<h2 class="text-[22px] font-bold text-[#0D0D2A]">Internal Dashboard<br/>Templates</h2>
<div class="flex items-center gap-3">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Import Layout
</button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
<button class="inline-flex h-[42px] items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Export Config
</button>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#050026] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}>
<button class="inline-flex h-[42px] items-center justify-center rounded-xl bg-[#0D0D2A] px-5 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={createDashboard} disabled={creating()}>
<span class="mr-2 text-lg leading-none">+</span> {creating() ? 'Creating...' : 'Create Dashboard Template'}
</button>
</div>
@ -491,7 +491,7 @@ export default function InternalDashboardManagementPage() {
<input
type="text"
placeholder="Search templates..."
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"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
/>
</div>
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
@ -502,7 +502,7 @@ export default function InternalDashboardManagementPage() {
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<tr class="bg-[#0D0D2A] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl">DASHBOARD NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DEPARTMENT</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">ASSIGNED DESIGNATION</th>
@ -521,7 +521,7 @@ export default function InternalDashboardManagementPage() {
<For each={dashboards()}>
{(d) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc] cursor-pointer" onClick={() => void openDashboard(d.id)}>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{d.title}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#0D0D2A]">{d.title}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Administration</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">Super Admin</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{d.roleName || 'Unassigned'}</td>

View file

@ -145,7 +145,7 @@ export default function OnboardingSchemasPage() {
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Draft<br/>Workflows</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">{schemas.loading ? '—' : draftFlows()}</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">{schemas.loading ? '—' : draftFlows()}</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Role Types<br/>Configured</p>
@ -218,7 +218,7 @@ export default function OnboardingSchemasPage() {
</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">v{schema.version}</td>
<td class="px-6 py-4">
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-[12px] font-bold ${schema.status === 'PUBLISHED' ? 'bg-[#DCFCE7] text-[#16A34A]' : 'bg-[#FFF5F0] text-[#FA5A1F]'}`}>
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-[12px] font-bold ${schema.status === 'PUBLISHED' ? 'bg-[#DCFCE7] text-[#16A34A]' : 'bg-[#FFF5F0] text-[#FF5E13]'}`}>
{schema.status === 'PUBLISHED' ? 'Published' : 'Draft'}
</span>
</td>

View file

@ -203,8 +203,8 @@ export default function EditInternalRolePage() {
{/* 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>
<h1 class="text-[20px] font-semibold text-[#0D0D2A]">Internal Role Management</h1>
<p class="text-[13px] text-[rgba(13,13,42,0.5)] mt-0.5">Manage internal roles and permissions</p>
</div>
<div class="p-6">
@ -214,7 +214,7 @@ export default function EditInternalRolePage() {
<div class="flex border-b border-[#e5e7eb] px-6">
<A
href="/admin/roles"
class="py-4 text-[14px] font-medium text-[rgba(0,0,50,0.5)] hover:text-[#000032] mr-6"
class="py-4 text-[14px] font-medium text-[rgba(13,13,42,0.5)] hover:text-[#0D0D2A] mr-6"
>
All Roles
</A>
@ -225,7 +225,7 @@ export default function EditInternalRolePage() {
</div>
<Show when={role.loading}>
<div class="px-6 py-10 text-center text-[13px] text-[rgba(0,0,50,0.4)]">Loading</div>
<div class="px-6 py-10 text-center text-[13px] text-[rgba(13,13,42,0.4)]">Loading</div>
</Show>
<Show when={!role.loading}>
@ -242,13 +242,13 @@ export default function EditInternalRolePage() {
onClick={() => setSubTab(t.key)}
class={`relative py-3.5 text-[13px] font-medium transition-colors ${
subTab() === t.key
? 'text-[#000032] font-semibold'
: 'text-[rgba(0,0,50,0.5)] hover:text-[#000032]'
? 'text-[#0D0D2A] font-semibold'
: 'text-[rgba(13,13,42,0.5)] hover:text-[#0D0D2A]'
}`}
>
{t.label}
<Show when={subTab() === t.key}>
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#000032] rounded-t" />
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#0D0D2A] rounded-t" />
</Show>
</button>
))}
@ -266,7 +266,7 @@ export default function EditInternalRolePage() {
<div class="p-6 space-y-5">
<div class="grid grid-cols-2 gap-5">
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">
Role Name <span class="text-red-500">*</span>
</label>
<input
@ -274,27 +274,27 @@ export default function EditInternalRolePage() {
placeholder="Enter role name"
value={roleName()}
onInput={(e) => setRoleName(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#000032] placeholder-[rgba(0,0,50,0.3)]"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#0D0D2A] placeholder-[rgba(13,13,42,0.3)]"
/>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">
Role Code
</label>
<input
type="text"
value={roleCode()}
disabled
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg bg-[#fafafa] text-[rgba(0,0,50,0.4)] cursor-not-allowed"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg bg-[#fafafa] text-[rgba(13,13,42,0.4)] cursor-not-allowed"
/>
</div>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">Department</label>
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">Department</label>
<select
value={departmentId()}
onChange={(e) => setDepartmentId(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] bg-white text-[#000032]"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] bg-white text-[#0D0D2A]"
>
<option value="">Select department</option>
<For each={departments() ?? []}>
@ -307,13 +307,13 @@ export default function EditInternalRolePage() {
</select>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">Description</label>
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">Description</label>
<textarea
placeholder="Enter role description"
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
rows={4}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#000032] placeholder-[rgba(0,0,50,0.3)] resize-none"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#0D0D2A] placeholder-[rgba(13,13,42,0.3)] resize-none"
/>
</div>
</div>
@ -322,13 +322,13 @@ export default function EditInternalRolePage() {
{/* ── Tab: Module Access ── */}
<Show when={subTab() === 'module'}>
<div class="p-6">
<p class="text-[13px] text-[rgba(0,0,50,0.5)] mb-4">
<p class="text-[13px] text-[rgba(13,13,42,0.5)] mb-4">
Configure module access permissions for this role.
</p>
<div class="overflow-x-auto rounded-lg border border-[#e5e7eb]">
<table class="w-full">
<thead>
<tr class="bg-[#000032] text-white text-[12px] font-semibold uppercase tracking-wide">
<tr class="bg-[#0D0D2A] text-white text-[12px] font-semibold uppercase tracking-wide">
<th class="px-5 py-3 text-left w-[40%]"> </th>
<th class="px-4 py-3 text-center">View</th>
<th class="px-4 py-3 text-center">Create</th>
@ -352,7 +352,7 @@ export default function EditInternalRolePage() {
ACTIONS.every((a) => selectedKeys().has(makeKey(module, a)));
return (
<tr class="hover:bg-[#fafafa]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#000032]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#0D0D2A]">
{module}
</td>
<For each={ACTIONS}>
@ -392,7 +392,7 @@ export default function EditInternalRolePage() {
<Show when={subTab() === 'settings'}>
<div class="p-6 space-y-6">
<div>
<p class="text-[13px] font-semibold text-[#000032] mb-3">Role Status</p>
<p class="text-[13px] font-semibold text-[#0D0D2A] mb-3">Role Status</p>
<div class="flex items-center gap-2">
<button
type="button"
@ -400,7 +400,7 @@ export default function EditInternalRolePage() {
class={`px-5 py-2 rounded-lg text-[13px] font-medium transition-colors ${
isActive()
? 'bg-[#fa5014] text-white'
: 'border border-[#e5e7eb] text-[rgba(0,0,50,0.6)] hover:border-[#000032]'
: 'border border-[#e5e7eb] text-[rgba(13,13,42,0.6)] hover:border-[#0D0D2A]'
}`}
>
Active
@ -411,7 +411,7 @@ export default function EditInternalRolePage() {
class={`px-5 py-2 rounded-lg text-[13px] font-medium transition-colors ${
!isActive()
? 'bg-[#fa5014] text-white'
: 'border border-[#e5e7eb] text-[rgba(0,0,50,0.6)] hover:border-[#000032]'
: 'border border-[#e5e7eb] text-[rgba(13,13,42,0.6)] hover:border-[#0D0D2A]'
}`}
>
Inactive
@ -439,14 +439,14 @@ export default function EditInternalRolePage() {
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#e5e7eb]">
<A
href={`/admin/roles/${params.id}`}
class="px-5 py-2.5 text-[13px] font-medium border border-[#e5e7eb] rounded-lg text-[rgba(0,0,50,0.7)] hover:border-[#000032] transition-colors"
class="px-5 py-2.5 text-[13px] font-medium border border-[#e5e7eb] rounded-lg text-[rgba(13,13,42,0.7)] hover:border-[#0D0D2A] transition-colors"
>
Cancel
</A>
<button
onClick={handleSave}
disabled={saving()}
class="px-5 py-2.5 text-[13px] font-semibold bg-[#000032] text-white rounded-lg hover:bg-[#000050] transition-colors disabled:opacity-60"
class="px-5 py-2.5 text-[13px] font-semibold bg-[#0D0D2A] text-white rounded-lg hover:bg-[#000050] transition-colors disabled:opacity-60"
>
{saving() ? 'Saving…' : 'Save Changes'}
</button>
@ -468,8 +468,8 @@ function SettingToggle(props: {
return (
<div class="flex items-center justify-between rounded-xl border border-[#e5e7eb] px-5 py-4">
<div>
<p class="text-[13px] font-semibold text-[#000032]">{props.label}</p>
<p class="text-[12px] text-[rgba(0,0,50,0.5)] mt-0.5">{props.description}</p>
<p class="text-[13px] font-semibold text-[#0D0D2A]">{props.label}</p>
<p class="text-[12px] text-[rgba(13,13,42,0.5)] mt-0.5">{props.description}</p>
</div>
<button
type="button"

View file

@ -62,8 +62,8 @@ export default function RoleDetailPage() {
{/* 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>
<h1 class="text-[20px] font-semibold text-[#0D0D2A]">Internal Role Management</h1>
<p class="text-[13px] text-[rgba(13,13,42,0.5)] mt-0.5">Manage internal roles and permissions</p>
</div>
<div class="p-6">
@ -73,20 +73,20 @@ export default function RoleDetailPage() {
<div class="flex border-b border-[#e5e7eb] px-6">
<A
href="/admin/roles"
class="py-4 text-[14px] font-medium text-[rgba(0,0,50,0.5)] hover:text-[#000032] mr-6"
class="py-4 text-[14px] font-medium text-[rgba(13,13,42,0.5)] hover:text-[#0D0D2A] mr-6"
>
All Roles
</A>
<A
href="/admin/roles/create"
class="py-4 text-[14px] font-medium text-[rgba(0,0,50,0.5)] hover:text-[#000032]"
class="py-4 text-[14px] font-medium text-[rgba(13,13,42,0.5)] hover:text-[#0D0D2A]"
>
Create Role
</A>
</div>
<Show when={role.loading}>
<div class="px-6 py-10 text-center text-[13px] text-[rgba(0,0,50,0.4)]">
<div class="px-6 py-10 text-center text-[13px] text-[rgba(13,13,42,0.4)]">
Loading role
</div>
</Show>
@ -101,19 +101,19 @@ export default function RoleDetailPage() {
{/* Action bar */}
<div class="flex items-center justify-between px-6 py-4 border-b border-[#e5e7eb]">
<div>
<h2 class="text-[16px] font-semibold text-[#000032]">{role()!.name}</h2>
<p class="text-[12px] text-[rgba(0,0,50,0.5)] mt-0.5">{role()!.key}</p>
<h2 class="text-[16px] font-semibold text-[#0D0D2A]">{role()!.name}</h2>
<p class="text-[12px] text-[rgba(13,13,42,0.5)] mt-0.5">{role()!.key}</p>
</div>
<div class="flex items-center gap-2">
<A
href="/admin/roles"
class="px-4 py-2 text-[13px] font-medium border border-[#e5e7eb] rounded-lg text-[rgba(0,0,50,0.7)] hover:border-[#000032] transition-colors"
class="px-4 py-2 text-[13px] font-medium border border-[#e5e7eb] rounded-lg text-[rgba(13,13,42,0.7)] hover:border-[#0D0D2A] transition-colors"
>
Back to List
</A>
<A
href={`/admin/roles/${params.id}/edit`}
class="px-4 py-2 text-[13px] font-semibold bg-[#000032] text-white rounded-lg hover:bg-[#000050] transition-colors"
class="px-4 py-2 text-[13px] font-semibold bg-[#0D0D2A] text-white rounded-lg hover:bg-[#000050] transition-colors"
>
Edit Role
</A>
@ -122,7 +122,7 @@ export default function RoleDetailPage() {
{/* General info */}
<div class="px-6 py-5 border-b border-[#e5e7eb]">
<h3 class="text-[13px] font-semibold text-[#000032] mb-4">General Information</h3>
<h3 class="text-[13px] font-semibold text-[#0D0D2A] mb-4">General Information</h3>
<div class="grid grid-cols-2 gap-5 mb-4">
<ReadField label="Role Name" value={role()!.name} />
<ReadField label="Role Code" value={role()!.key} />
@ -137,7 +137,7 @@ export default function RoleDetailPage() {
class={`inline-flex items-center px-2.5 py-1 rounded-md text-[12px] font-semibold ${
role()!.is_active
? 'bg-[rgba(34,197,94,0.1)] text-[#16a34a]'
: 'bg-[#f1f1f1] text-[rgba(0,0,50,0.5)]'
: 'bg-[#f1f1f1] text-[rgba(13,13,42,0.5)]'
}`}
>
{role()!.is_active ? 'Active' : 'Inactive'}
@ -154,11 +154,11 @@ export default function RoleDetailPage() {
{/* Module Access */}
<div class="px-6 py-5 border-b border-[#e5e7eb]">
<h3 class="text-[13px] font-semibold text-[#000032] mb-4">Module Access</h3>
<h3 class="text-[13px] font-semibold text-[#0D0D2A] mb-4">Module Access</h3>
<div class="overflow-x-auto rounded-lg border border-[#e5e7eb]">
<table class="w-full">
<thead>
<tr class="bg-[#000032] text-white text-[12px] font-semibold uppercase tracking-wide">
<tr class="bg-[#0D0D2A] text-white text-[12px] font-semibold uppercase tracking-wide">
<th class="px-5 py-3 text-left w-[40%]"> </th>
<th class="px-4 py-3 text-center">View</th>
<th class="px-4 py-3 text-center">Create</th>
@ -170,7 +170,7 @@ export default function RoleDetailPage() {
<For each={STATIC_MODULES}>
{(module) => (
<tr class="hover:bg-[#fafafa]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#000032]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#0D0D2A]">
{module}
</td>
<For each={ACTIONS}>
@ -199,7 +199,7 @@ export default function RoleDetailPage() {
{/* Role Settings */}
<div class="px-6 py-5">
<h3 class="text-[13px] font-semibold text-[#000032] mb-4">Role Settings</h3>
<h3 class="text-[13px] font-semibold text-[#0D0D2A] mb-4">Role Settings</h3>
<div class="space-y-3">
<SettingRow
label="Allow Role to Approve Requests"
@ -224,9 +224,9 @@ export default function RoleDetailPage() {
function ReadField(props: { label: string; value: string; custom?: any }) {
return (
<div>
<p class="text-[12px] text-[rgba(0,0,50,0.5)] mb-1">{props.label}</p>
<p class="text-[12px] text-[rgba(13,13,42,0.5)] mb-1">{props.label}</p>
{props.custom ?? (
<p class="text-[14px] font-medium text-[#000032]">{props.value || '—'}</p>
<p class="text-[14px] font-medium text-[#0D0D2A]">{props.value || '—'}</p>
)}
</div>
);
@ -236,8 +236,8 @@ function SettingRow(props: { label: string; description: string; value: boolean
return (
<div class="flex items-center justify-between rounded-xl border border-[#e5e7eb] px-5 py-4">
<div>
<p class="text-[13px] font-semibold text-[#000032]">{props.label}</p>
<p class="text-[12px] text-[rgba(0,0,50,0.5)] mt-0.5">{props.description}</p>
<p class="text-[13px] font-semibold text-[#0D0D2A]">{props.label}</p>
<p class="text-[12px] text-[rgba(13,13,42,0.5)] mt-0.5">{props.description}</p>
</div>
<div
class={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${

View file

@ -84,15 +84,15 @@ export default function RuntimeRolesPage() {
{/* 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>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">External 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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] 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]"
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] 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>
@ -109,8 +109,8 @@ export default function RuntimeRolesPage() {
<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]'
? 'border-[#0D0D2A] text-[#0D0D2A]'
: 'border-transparent text-[#8087a0] hover:text-[#0D0D2A]'
}`}
>
{t}
@ -130,7 +130,7 @@ export default function RuntimeRolesPage() {
<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"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
/>
</div>
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
@ -146,7 +146,7 @@ export default function RuntimeRolesPage() {
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<tr class="bg-[#0D0D2A] 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>
@ -170,10 +170,10 @@ export default function RuntimeRolesPage() {
{(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] font-bold text-[#0D0D2A]">{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]">
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#f8f9fc] text-[#0D0D2A] text-[12px] font-bold border border-[#e2e6ee]">
{role.enabledModules.length} Modules
</span>
</td>
@ -188,7 +188,7 @@ export default function RuntimeRolesPage() {
<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"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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" />
@ -198,7 +198,7 @@ export default function RuntimeRolesPage() {
<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"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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" />
@ -227,13 +227,13 @@ export default function RuntimeRolesPage() {
<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"
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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"
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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>

View file

@ -111,13 +111,13 @@ export default function UsersPage() {
{/* 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">User Management</h1>
<h1 class="text-[32px] font-bold text-[#0D0D2A] leading-tight">User 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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#0D0D2A] transition-colors hover:bg-[#f8f9fc]">
Export Data
</button>
<button 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]">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0D0D2A] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]">
<span class="mr-2 text-lg leading-none">+</span> Add User
</button>
</div>
@ -128,7 +128,7 @@ export default function UsersPage() {
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Total Users</p>
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">{users()?.length || 0}</p>
<p class="mt-3 text-[32px] font-bold text-[#0D0D2A] leading-none">{users()?.length || 0}</p>
</div>
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Active Users</p>
@ -165,8 +165,8 @@ export default function UsersPage() {
}}
class={`pb-3 text-[14px] font-bold transition-colors border-b-2 ${
isActiveTab
? 'border-[#050026] text-[#050026]'
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
? 'border-[#0D0D2A] text-[#0D0D2A]'
: 'border-transparent text-[#8087a0] hover:text-[#0D0D2A]'
}`}
>
{t}
@ -189,13 +189,13 @@ export default function UsersPage() {
placeholder="Search users..."
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); setCurrentPage(1); }}
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"
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#0D0D2A] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white"
/>
</div>
<select
value={filterRole()}
onChange={(e) => { setFilterRole(e.currentTarget.value); setCurrentPage(1); }}
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white appearance-none"
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#0D0D2A] focus:bg-white appearance-none"
>
<option value="">All Roles</option>
<For each={ROLE_OPTIONS}>{(r) => <option value={r}>{r}</option>}</For>
@ -212,7 +212,7 @@ export default function UsersPage() {
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<tr class="bg-[#0D0D2A] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">USER ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">USER DETAILS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE</th>
@ -238,11 +238,11 @@ export default function UsersPage() {
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">USR-{item.id.slice(0, 6).toUpperCase()}</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-[#f1f5f9] text-[#050026] font-bold uppercase shrink-0">
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-[#f1f5f9] text-[#0D0D2A] font-bold uppercase shrink-0">
{(item.name || item.full_name || 'U').charAt(0)}
</div>
<div>
<div class="text-[14px] font-bold text-[#050026]">{item.name || item.full_name || '—'}</div>
<div class="text-[14px] font-bold text-[#0D0D2A]">{item.name || item.full_name || '—'}</div>
<div class="text-[13px] text-[#64748b]">{item.email}</div>
</div>
</div>
@ -264,7 +264,7 @@ export default function UsersPage() {
<button
title="View Details"
onClick={() => onView(item)}
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"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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" />
@ -273,7 +273,7 @@ export default function UsersPage() {
</button>
<button
title="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"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] 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" />
@ -294,7 +294,7 @@ export default function UsersPage() {
<button
disabled={currentPage() === 1}
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Previous
</button>
@ -302,7 +302,7 @@ export default function UsersPage() {
<button
disabled={currentPage() >= totalPages()}
onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#0D0D2A] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Next
</button>
@ -317,9 +317,9 @@ export default function UsersPage() {
<div class="bg-white rounded-3xl border border-[#e2e6ee] p-6 shadow-sm">
<Show when={selectedUser()}>
<div class="flex items-center justify-between mb-8 border-b border-[#e2e6ee] pb-6">
<h2 class="text-[22px] font-bold text-[#050026]">User Details</h2>
<h2 class="text-[22px] font-bold text-[#0D0D2A]">User Details</h2>
<button
class="h-10 rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors"
class="h-10 rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#0D0D2A] hover:bg-[#f8f9fc] transition-colors"
type="button"
onClick={() => setActiveTab('list')}
>
@ -328,12 +328,12 @@ export default function UsersPage() {
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-[#f9fafb] p-6 rounded-2xl border border-[#e2e6ee]">
<h3 class="text-[16px] font-bold text-[#050026] mb-4">Profile Information</h3>
<h3 class="text-[16px] font-bold text-[#0D0D2A] mb-4">Profile Information</h3>
<div class="space-y-4">
<div><p class="text-[13px] text-[#64748b] font-medium">User ID</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.id}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Name</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.name || selectedUser()!.full_name || '—'}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Email</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.email}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Role</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{registrationRole(selectedUser()!)}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">User ID</p><p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{selectedUser()!.id}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Name</p><p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{selectedUser()!.name || selectedUser()!.full_name || '—'}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Email</p><p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{selectedUser()!.email}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Role</p><p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{registrationRole(selectedUser()!)}</p></div>
<div>
<p class="text-[13px] text-[#64748b] font-medium mb-1">Status</p>
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
@ -345,15 +345,15 @@ export default function UsersPage() {
</div>
</div>
<div class="bg-[#f9fafb] p-6 rounded-2xl border border-[#e2e6ee]">
<h3 class="text-[16px] font-bold text-[#050026] mb-4">Account Metadata</h3>
<h3 class="text-[16px] font-bold text-[#0D0D2A] mb-4">Account Metadata</h3>
<div class="space-y-4">
<div>
<p class="text-[13px] text-[#64748b] font-medium">Created</p>
<p class="text-[15px] font-semibold text-[#050026] mt-1">{(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}</p>
<p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}</p>
</div>
<div><p class="text-[13px] text-[#64748b] font-medium">Role ID</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.roleId || '—'}</p></div>
<div><p class="text-[13px] text-[#64748b] font-medium">Role ID</p><p class="text-[15px] font-semibold text-[#0D0D2A] mt-1">{selectedUser()!.roleId || '—'}</p></div>
<div class="pt-6 border-t border-[#e2e6ee] mt-6 gap-3 flex">
<A class="h-10 rounded-xl bg-[#050026] px-5 flex items-center justify-center text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
<A class="h-10 rounded-xl bg-[#0D0D2A] px-5 flex items-center justify-center text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
<button class="h-10 rounded-xl border border-red-200 bg-red-50 flex items-center justify-center px-5 text-[14px] font-semibold text-red-600 hover:bg-red-100 transition-colors" type="button">Deactivate</button>
</div>
</div>

View file

@ -88,7 +88,7 @@ export default function VerificationStatusPage() {
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Re-upload<br/>Review</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">8</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">8</p>
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Verified<br/>Today</p>
@ -96,7 +96,7 @@ export default function VerificationStatusPage() {
</div>
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
<p class="text-[14px] leading-tight text-[#64748B]">Flagged<br/>Cases</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">4</p>
<p class="mt-4 text-[32px] font-bold leading-none text-[#FF5E13]">4</p>
</div>
</section>