feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages

- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-27 01:56:55 +01:00
parent c65f32d45c
commit ab487ce884
6 changed files with 958 additions and 1083 deletions

View file

@ -12,6 +12,48 @@ type Tab = { href: string; label: string; exact?: boolean };
type SearchResult = { id: string; title: string; subtitle: string; href: string };
type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] };
const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [
{ prefix: '/admin', label: 'Dashboard', exact: true },
{ prefix: '/admin/department', label: 'Department Management' },
{ prefix: '/admin/designation', label: 'Designation Management' },
{ prefix: '/admin/roles', label: 'Internal Role Management' },
{ prefix: '/admin/employees', label: 'Employee Management' },
{ prefix: '/admin/runtime-roles', label: 'External Role Management' },
{ prefix: '/admin/onboarding-management', label: 'Onboarding Management' },
{ prefix: '/admin/onboarding-schemas', label: 'Onboarding Management' },
{ prefix: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management' },
{ prefix: '/admin/external-dashboard-management', label: 'External Dashboard Management' },
{ prefix: '/admin/role-ui-configs', label: 'External Dashboard Management' },
{ prefix: '/admin/verification-status', label: 'Verification Management' },
{ prefix: '/admin/approval', label: 'Approval Management' },
{ prefix: '/admin/users', label: 'Users Management' },
{ prefix: '/admin/company', label: 'Company Management' },
{ prefix: '/admin/candidate', label: 'Candidate Management' },
{ prefix: '/admin/customer', label: 'Customer Management' },
{ prefix: '/admin/photographer', label: 'Photographer Management' },
{ prefix: '/admin/makeup-artist', label: 'Makeup Artist Management' },
{ prefix: '/admin/tutors', label: 'Tutors Management' },
{ prefix: '/admin/developers', label: 'Developers Management' },
{ prefix: '/admin/video-editors', label: 'Video Editor Management' },
{ prefix: '/admin/fitness-trainers', label: 'Fitness Trainer Management' },
{ prefix: '/admin/catering-services', label: 'Catering Services Management' },
{ prefix: '/admin/graphic-designers', label: 'Graphic Designer Management' },
{ prefix: '/admin/social-media-managers', label: 'Social Media Manager Management' },
{ prefix: '/admin/jobs', label: 'Jobs Management' },
{ prefix: '/admin/leads', label: 'Leads Management' },
{ prefix: '/admin/pricing', label: 'Pricing Management' },
{ prefix: '/admin/credit', label: 'Credit Management' },
{ prefix: '/admin/coupon', label: 'Coupon Management' },
{ prefix: '/admin/discount', label: 'Discount Management' },
{ prefix: '/admin/tax', label: 'Tax Management' },
{ prefix: '/admin/order', label: 'Order Management' },
{ prefix: '/admin/invoice', label: 'Invoice Management' },
{ prefix: '/admin/review', label: 'Review Management' },
{ prefix: '/admin/support', label: 'Support Management' },
{ prefix: '/admin/report', label: 'Report Management' },
{ prefix: '/admin/ledger', label: 'Ledger Management' },
];
const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [
{
prefixes: ['/admin/runtime-roles'],
@ -138,7 +180,7 @@ function GlobalSearch() {
<input
type="text"
value={query()}
placeholder="Search for anything..."
placeholder="Search system resources..."
onInput={(e) => handleInput(e.currentTarget.value)}
onFocus={() => groups().length > 0 && setOpen(true)}
onKeyDown={(e) => e.key === 'Escape' && close()}
@ -306,6 +348,16 @@ export default function AdminShell(props: { children: JSX.Element }) {
void verify();
});
const pageTitle = createMemo(() => {
const path = location.pathname;
for (const entry of PAGE_TITLES) {
if (entry.exact ? path === entry.prefix : (path === entry.prefix || path.startsWith(`${entry.prefix}/`))) {
return entry.label;
}
}
return 'Admin';
});
const adminInitials = createMemo(() => {
if (adminName().trim().toLowerCase() === 'admin user') return 'AD';
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean);
@ -320,10 +372,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
when={checkedSession()}
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="flex h-screen overflow-hidden">
<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)} />
<div class={`fixed inset-y-0 left-0 z-30 -translate-x-full transition-transform duration-200 lg:relative lg:translate-x-0 ${sidebarOpen() ? 'translate-x-0' : ''}`}>
<div class={`fixed inset-y-0 left-0 z-30 h-full -translate-x-full transition-transform duration-200 lg:relative lg:translate-x-0 ${sidebarOpen() ? 'translate-x-0' : ''}`}>
<AdminSidebar
collapsed={sidebarCollapsed()}
onToggle={() => setSidebarCollapsed((v) => !v)}
@ -334,37 +386,38 @@ export default function AdminShell(props: { children: JSX.Element }) {
</div>
<div class="flex min-w-0 flex-1 flex-col">
<header class="h-[101px] border-b border-[#e5e7eb] bg-white shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_0px_rgba(0,0,0,0.1)]">
<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-[#0D0D2A] hover:bg-[#f9fafb]" aria-label="Notifications">
<Bell size={20} />
<header style="height:64px;border-bottom:1px solid #E5E7EB;background:white;flex-shrink:0">
<div style="display:flex;height:100%;width:100%;align-items:center;justify-content:flex-end;padding:0 32px">
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="position:relative;display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:#6B7280;background:none;border:none;cursor:pointer" aria-label="Notifications">
<Bell size={18} />
<Show when={notifCount() > 0}>
<span class="absolute right-2 top-2 h-2.5 w-2.5 rounded-full border-2 border-white bg-[#FF5E13]" />
<span style="position:absolute;right:8px;top:8px;width:7px;height:7px;border-radius:50%;border:2px solid white;background:#FF5E13" />
</Show>
</button>
<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 type="button" style="display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:#6B7280;background:none;border:none;cursor:pointer" aria-label="Settings">
<Settings size={18} />
</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-[#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-[#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)]">
<div style="width:1px;height:24px;background:#E5E7EB;margin:0 8px" />
<button
type="button"
style="display:inline-flex;align-items:center;gap:8px;border-radius:8px;padding:4px 8px 4px 4px;background:none;border:none;cursor:pointer"
aria-label="User menu"
>
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#FF5E13,#ff7a3d);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:white;flex-shrink:0">
{adminInitials()}
</button>
</div>
<button type="button" class="inline-flex h-11 w-11 items-center justify-center rounded-[14px] text-[#71759a] lg:hidden" onClick={() => setSidebarOpen((v) => !v)}>
<User size={20} />
</div>
<div style="text-align:left">
<p style="font-size:13px;font-weight:600;color:#111827;line-height:1.3">{adminName()}</p>
<p style="font-size:11px;color:#6B7280;line-height:1.3">Super Admin</p>
</div>
</button>
</div>
</div>
</header>
<div class="min-h-0 flex-1 overflow-y-auto bg-[#F9FAFB]">
<main class="w-full px-10 pb-9 pt-8">
<main style="width:100%;padding:28px 24px 36px 24px">
{props.children}
</main>
</div>

View file

@ -83,10 +83,6 @@ export default function AdminSidebar(props: {
adminInitials: string;
}) {
const location = useLocation();
const isPreview = () => location.search.includes('_preview=1');
const isDepartmentView = () =>
location.pathname === '/admin/department' || location.pathname === '/admin/department-management';
const visibleGroups = () => ((isPreview() || isDepartmentView()) ? GROUPS.slice(0, 3) : GROUPS);
const isActive = (item: NavItem) => {
if (location.pathname === '/admin') return item.href === '/admin';
@ -97,50 +93,41 @@ export default function AdminSidebar(props: {
return (
<aside
class={`flex h-full flex-col bg-white border-r border-[#E5E7EB] transition-all duration-300 ${
props.collapsed ? 'w-[72px]' : (isDepartmentView() ? 'w-[300px]' : 'w-[272px]')
}`}
style="overflow:hidden;display:flex;flex-direction:column;background:white;border-right:1px solid #E5E7EB;transition:width 0.3s;flex-shrink:0"
style:width={props.collapsed ? '64px' : '220px'}
>
{/* Logo area */}
<div class={`relative flex h-[101px] shrink-0 items-center border-b border-[#E5E7EB] ${props.collapsed ? 'justify-center px-3' : 'px-6'}`}>
<A href="/admin" onClick={props.onNavigate} class="flex items-center">
<img
src={props.collapsed ? '/nxtgauge-icon.png' : '/nxtgauge-logo.png'}
alt="Nxtgauge"
class={`${props.collapsed ? 'h-8' : 'h-[22px]'} w-auto object-contain`}
/>
<div style="position:relative;height:64px;display:flex;align-items:center;border-bottom:1px solid #E5E7EB;flex-shrink:0;padding:0 14px">
<A href="/admin" onClick={props.onNavigate} style="display:flex;align-items:center;gap:10px;text-decoration:none;overflow:hidden">
<div style="width:32px;height:32px;border-radius:8px;background:#FF5E13;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<img src="/nxtgauge-icon.png" alt="Nxtgauge" style="width:18px;height:18px;object-fit:contain;filter:brightness(0) invert(1)" />
</div>
<Show when={!props.collapsed}>
<div style="overflow:hidden">
<p style="font-size:15px;font-weight:700;color:#0D0D2A;line-height:1.2;white-space:nowrap">Nxtgauge</p>
<p style="font-size:8px;font-weight:600;text-transform:uppercase;letter-spacing:0.12em;color:#9CA3AF;white-space:nowrap">Admin Console</p>
</div>
</Show>
</A>
<Show when={!props.collapsed}>
<button
type="button"
onClick={props.onToggle}
class="absolute right-4 top-1/2 -translate-y-1/2 inline-flex h-7 w-7 items-center justify-center rounded-lg text-[#9CA3AF] transition-colors hover:text-[#374151]"
aria-label="Collapse sidebar"
>
<ChevronLeft size={16} />
</button>
</Show>
<Show when={props.collapsed}>
<button
type="button"
onClick={props.onToggle}
class="absolute -right-3 top-1/2 z-10 -translate-y-1/2 inline-flex h-6 w-6 items-center justify-center rounded-full border border-[#E5E7EB] bg-white text-[#9CA3AF] transition-colors hover:text-[#374151]"
aria-label="Expand sidebar"
>
<ChevronLeft size={12} class="rotate-180" />
</button>
</Show>
<button
type="button"
onClick={props.onToggle}
style="position:absolute;right:-10px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.1);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10;color:#6B7280"
aria-label={props.collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
<ChevronLeft size={11} style={`transition:transform 0.3s;${props.collapsed ? 'transform:rotate(180deg)' : ''}`} />
</button>
</div>
{/* Navigation */}
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-4 py-5">
<For each={visibleGroups()}>
<nav style="flex:1;min-height:0;overflow-y:auto;padding:10px 8px">
<For each={GROUPS}>
{(group, gi) => (
<>
<Show when={gi() > 0}>
<div class="my-3 h-px bg-[#E5E7EB]" />
<div style="height:1px;background:#F3F4F6;margin:6px 4px" />
</Show>
<div class="space-y-1">
<div style="display:flex;flex-direction:column;gap:1px">
<For each={group}>
{(item) => {
const active = () => isActive(item);
@ -150,26 +137,16 @@ export default function AdminSidebar(props: {
href={item.href}
onClick={props.onNavigate}
title={props.collapsed ? item.label : undefined}
class={`relative flex h-[48px] w-full items-center rounded-2xl text-[16px] font-medium transition-colors ${
props.collapsed ? 'justify-center px-0' : 'px-3'
} ${
active()
? 'bg-[#FFF1EB] text-[#FF5E13]'
: 'text-[#6B7280] hover:bg-[#F3F4F6] hover:text-[#111827]'
}`}
style={`display:flex;align-items:center;height:36px;border-radius:8px;text-decoration:none;padding:0 ${props.collapsed ? '0' : '10px'};${props.collapsed ? 'justify-content:center;' : ''}${active() ? 'background:#FFF3EE;color:#FF5E13;' : 'color:#6B7280;'}`}
aria-current={active() ? 'page' : undefined}
>
{/* Active left indicator */}
<Show when={active() && !props.collapsed}>
<span class="absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-r-full bg-[#FF5E13]" />
</Show>
<Icon
size={isDepartmentView() ? 22 : 18}
class={`shrink-0 ${active() ? 'text-[#FF5E13]' : 'text-[#9CA3AF]'}`}
strokeWidth={2}
size={16}
style={`flex-shrink:0;${active() ? 'color:#FF5E13' : 'color:#9CA3AF'}`}
strokeWidth={active() ? 2.5 : 2}
/>
<Show when={!props.collapsed}>
<span class="ml-3 truncate">{item.label}</span>
<span style="margin-left:9px;font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{item.label}</span>
</Show>
</A>
);
@ -180,21 +157,6 @@ export default function AdminSidebar(props: {
)}
</For>
</nav>
{/* User card */}
<div class="shrink-0 border-t border-[#E5E7EB] p-3">
<div class={`flex items-center rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-3 py-2.5 ${props.collapsed ? 'justify-center' : 'gap-3'}`}>
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#FF5E13] to-[#ff7a3d] text-[12px] font-bold text-white">
{props.adminInitials}
</div>
<Show when={!props.collapsed}>
<div class="min-w-0 flex-1">
<p class="truncate text-[13px] font-semibold leading-5 text-[#111827]">{props.adminName}</p>
<p class="text-[11px] leading-4 text-[#6B7280]">Super Admin</p>
</div>
</Show>
</div>
</div>
</aside>
);
}

View file

@ -66,10 +66,8 @@ function normalizeDepartment(item: any, idx: number): DepartmentRecord {
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span class={`inline-flex items-center rounded-full border px-3 py-1.5 text-[13px] font-semibold ${
active() ? 'border-[#FFD8C2] bg-[#FFF1EB] text-[#FF5E13]' : 'border-[#D1D5DB] bg-[#F3F4F6] text-[#4B5563]'
}`}>
<span class={`mr-1.5 h-2 w-2 rounded-full ${active() ? 'bg-[#FF5E13]' : 'bg-[#9CA3AF]'}`} />
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
@ -77,16 +75,16 @@ function StatusBadge(props: { status: string }) {
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label class="block">
<span class="text-[14px] font-semibold text-[#374151]">
{props.label}{props.required && <span class="ml-0.5 text-[#FF5E13]">*</span>}
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<input
type={props.type ?? 'text'}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
class="mt-2 h-[48px] w-full rounded-2xl border border-[#E5E7EB] bg-white px-4 text-[15px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors"
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
);
@ -258,200 +256,113 @@ export default function DepartmentManagementPage() {
return (
<AdminShell>
<div class="mx-auto w-full max-w-[1480px] space-y-5 pb-8 pt-1">
<div class="w-full space-y-6 pb-8">
{/* Page header */}
<Show when={!isPreview() || view() === 'form'}>
<div>
<h1 class="text-[42px] font-bold leading-[1.08] tracking-[-0.01em] text-[#111827]">Department Management</h1>
<p class="mt-1.5 text-[14px] leading-[1.45] text-[#6B7280]">Manage all departments and organizational structure</p>
</div>
</Show>
<div style="margin-bottom: 1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Department Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Manage all departments and organizational structure</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<div>
<Show when={error()}>
<div class="rounded-xl border border-[#FECACA] bg-[#FEF2F2] px-4 py-3 text-[13px] text-[#B91C1C]">
{error()}
</div>
</Show>
<Show when={!isPreview()}>
<div class="flex items-center justify-between">
<div class="inline-flex items-center gap-3 text-[20px] font-semibold tracking-[0.2em] uppercase text-[#8B93AB]">
<span class="text-[20px] text-[#A0A7BC]">Dashboard</span>
<span></span>
<span class="text-[20px] text-[#A0A7BC]">Organization</span>
<span></span>
<span class="text-[20px] tracking-[0.16em] text-[#D44B22]">Department Management</span>
</div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ key: 'all', label: 'All Departments', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } },
{ key: 'create', label: 'Create Department', action: () => { setListTab('create'); openCreate(); } },
{ key: 'view', label: 'View Department', action: () => setListTab('view') },
] as const).map((tab) => (
<button
type="button"
class="inline-flex h-[56px] items-center gap-3 rounded-[14px] border border-[#E2E7F1] bg-white px-6 text-[15px] font-medium text-[#4E587C]"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
>
<svg class="h-5 w-5 text-[#58638A]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
Oct 01, 2023 - Oct 31, 2023
<svg class="h-4 w-4 text-[#58638A]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
{tab.label}
</button>
</div>
</Show>
))}
</div>
{/* Table card */}
<div class="mt-2 overflow-hidden rounded-[22px] border border-[#DDE2EA] bg-white shadow-[0_8px_22px_rgba(13,13,42,0.05)]">
<Show when={!isPreview()}>
<div class="flex items-center gap-10 border-b border-[#E8ECF2] px-9 pt-0.5">
<button
type="button"
onClick={() => { setListTab('all'); setStatusFilter('all'); void load(); }}
class={`relative inline-flex items-center gap-2 px-0 py-4 text-[18px] font-semibold ${listTab() === 'all' ? 'text-[#111827]' : 'text-[#5F6780] hover:text-[#111827]'}`}
>
All Departments
<span class="inline-flex min-w-[30px] justify-center rounded-full bg-[#E8EAF3] px-2 py-0.5 text-[12px] font-semibold text-[#4F5877]">
{filteredRows().length}
</span>
<Show when={listTab() === 'all'}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#1E2235]" />
</Show>
</button>
<button
type="button"
onClick={() => { setListTab('create'); openCreate(); }}
class={`relative px-0 py-4 text-[18px] font-medium transition-colors ${listTab() === 'create' ? 'text-[#111827]' : 'text-[#5F6780] hover:text-[#111827]'}`}
>
Create Department
<Show when={listTab() === 'create'}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#1E2235]" />
</Show>
</button>
<button
type="button"
onClick={() => { setListTab('view'); setStatusFilter('all'); void load(); }}
class={`relative px-0 py-4 text-[18px] font-medium transition-colors ${listTab() === 'view' ? 'text-[#111827]' : 'text-[#5F6780] hover:text-[#111827]'}`}
>
View Department
<Show when={listTab() === 'view'}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#1E2235]" />
</Show>
</button>
<button
type="button"
onClick={() => { setListTab('inactive'); setStatusFilter('inactive'); void load(); }}
class={`relative px-0 py-4 text-[18px] font-medium transition-colors ${listTab() === 'inactive' ? 'text-[#111827]' : 'text-[#5F6780] hover:text-[#111827]'}`}
>
Inactive
<Show when={listTab() === 'inactive'}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#1E2235]" />
</Show>
</button>
</div>
</Show>
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none" class="overflow-hidden border-t border-b border-[#E5E7EB] bg-white shadow-sm">
{/* Filters */}
<Show when={!isPreview()}>
<div class="flex items-center justify-between gap-6 border-b border-[#E8ECF2] px-9 py-5">
<div class="flex min-w-0 flex-1 items-center gap-5">
{/* Search */}
<div class="relative w-full max-w-[860px]">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-5">
<svg class="h-5 w-5 text-[#A2A9BD]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
</span>
<input
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search by name, ID or department..."
class="h-[58px] w-full rounded-[12px] border border-[#EEF1F6] bg-[#F4F6FA] pl-[54px] pr-5 text-[15px] text-[#2A3150] outline-none placeholder:text-[#949DB4] focus:border-[#CED4E2] focus:bg-white transition-colors"
/>
</div>
{/* Sorting */}
<div class="relative">
{/* Filter bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search departments..."
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
<div style="position:relative">
<button
type="button"
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
class="inline-flex h-[58px] min-w-[138px] items-center justify-center gap-2 rounded-[12px] border border-[#DFE5EF] bg-[#F8FAFD] px-5 text-[15px] font-medium text-[#4E587C] transition-colors hover:bg-white"
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg class="h-4 w-4 text-[#66708A]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13" /><path d="m3 13 4 4 4-4" /><path d="M17 20V7" /><path d="m21 11-4-4-4 4" /></svg>
Sorting
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
Sort
</button>
<Show when={sortMenuOpen()}>
<div class="absolute right-0 top-[62px] z-30 min-w-[220px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg shadow-black/10">
<button type="button" onClick={() => { setSortBy('name_asc'); setSortMenuOpen(false); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${sortBy() === 'name_asc' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Name (A-Z)</button>
<button type="button" onClick={() => { setSortBy('name_desc'); setSortMenuOpen(false); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${sortBy() === 'name_desc' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Name (Z-A)</button>
<button type="button" onClick={() => { setSortBy('employees_desc'); setSortMenuOpen(false); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${sortBy() === 'employees_desc' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Employees (High-Low)</button>
<button type="button" onClick={() => { setSortBy('employees_asc'); setSortMenuOpen(false); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${sortBy() === 'employees_asc' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Employees (Low-High)</button>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['name_asc', 'name_desc', 'employees_desc', 'employees_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Employees (High-Low)', 'Employees (Low-High)'][i]}
</button>
))}
</div>
</Show>
</div>
{/* Filter */}
<div class="relative">
<div style="position:relative">
<button
type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
class="inline-flex h-[58px] min-w-[112px] items-center justify-center gap-2 rounded-[12px] border border-[#DFE5EF] bg-[#F8FAFD] px-5 text-[15px] font-medium text-[#4E587C] transition-colors hover:bg-white"
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg class="h-4 w-4 text-[#66708A]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4" /></svg>
Filter
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
Filters
</button>
<Show when={filterMenuOpen()}>
<div class="absolute right-0 top-[62px] z-30 min-w-[170px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg shadow-black/10">
<button type="button" onClick={() => { setListTab('all'); setStatusFilter('all'); setFilterMenuOpen(false); void load(); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${statusFilter() === 'all' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>All Status</button>
<button type="button" onClick={() => { setListTab('view'); setStatusFilter('active'); setFilterMenuOpen(false); void load(); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${statusFilter() === 'active' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Active</button>
<button type="button" onClick={() => { setListTab('inactive'); setStatusFilter('inactive'); setFilterMenuOpen(false); void load(); }} class={`block w-full rounded-lg px-3 py-2 text-left text-[14px] ${statusFilter() === 'inactive' ? 'bg-[#FFF1EB] text-[#FF5E13]' : 'text-[#374151] hover:bg-[#F9FAFB]'}`}>Inactive</button>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'active', 'inactive'] as const).map((s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); void load(); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : 'Inactive'}
</button>
))}
</div>
</Show>
</div>
</div>
<div class="ml-2 flex shrink-0 items-center gap-3">
<button
type="button"
class="inline-flex h-[58px] items-center rounded-[12px] border-2 border-[#1C2238] bg-white px-8 text-[15px] font-semibold text-[#1D243A] transition-colors hover:bg-[#F8FAFC]"
>
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export
</button>
<button
type="button"
onClick={openCreate}
class="inline-flex h-[58px] items-center gap-2 rounded-[12px] bg-[#00033D] px-8 text-[15px] font-semibold text-white shadow-[0_7px_16px_rgba(0,3,61,0.26)] transition-colors hover:bg-[#0C0F52]"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Add Department
</button>
</div>
</div>
</Show>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="min-w-full">
<Show when={!isPreview()}>
<thead>
<tr class="text-left bg-[#00033D]">
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Department Name</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Department Code</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Description</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Total Employees</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Status</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Created Date</th>
<th class="px-8 py-5 text-[13px] font-semibold uppercase tracking-[0.08em] text-white">Actions</th>
<tr style="background:#0D0D2A;text-align:left">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department Code</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Description</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Total Employees</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Created Date</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
</tr>
</thead>
</Show>
<tbody class="divide-y divide-[#E9EDF4]">
<tbody>
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="7" class="px-6 py-16 text-center">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-[#F9FAFB]">
<svg class="h-7 w-7 text-[#9CA3AF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3" /><path d="M9 9h6M9 12h6M9 15h4" /></svg>
</div>
<p class="mt-3 text-[15px] font-semibold text-[#111827]">No departments found</p>
<p class="text-[15px] font-semibold text-[#111827]">No departments found</p>
<p class="mt-1 text-[13px] text-[#6B7280]">Create your first department to get started.</p>
<button type="button" onClick={openCreate} class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Create Department
</button>
</td>
@ -460,61 +371,51 @@ export default function DepartmentManagementPage() {
>
<For each={filteredRows()}>
{(row) => (
<tr class="group hover:bg-[#FBFCFE] transition-colors">
<td class="px-8 py-6">
<p class="text-[16px] font-semibold text-[#111827]">{row.name}</p>
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td>
<td class="px-8 py-6">
<span class="rounded-lg bg-[#F3F4F6] px-3 py-1.5 text-[15px] font-mono font-semibold text-[#374151]">
{String(row.code || '—')}
</span>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
</td>
<td class="max-w-[520px] px-8 py-6 text-[15px] text-[#66708A]">
<p class="line-clamp-1">{String(row.description || '—')}</p>
<td style="padding:12px 20px;max-width:340px">
<p style="font-size:13px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{String(row.description || '—')}</p>
</td>
<td class="px-8 py-6 text-[16px] font-semibold text-[#111827]">{Number(row.totalEmployees || 0)}</td>
<td class="px-8 py-6"><StatusBadge status={row.status} /></td>
<td class="px-8 py-6 text-[15px] text-[#6B7280]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td class="relative px-8 py-6">
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
aria-label="More actions"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="19" r="1.5" /></svg>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
</button>
<Show when={openMenuId() === row.id}>
<div class="absolute right-5 top-11 z-20 w-[200px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg shadow-black/10">
<button
type="button"
onClick={() => openEdit(row)}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]"
>
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 20h9" /><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z" /></svg>
<div class="absolute right-5 top-11 z-20 w-[200px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg">
<button type="button" onClick={() => { setOpenMenuId(null); setListTab('view'); }} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
View Department
</button>
<button type="button" onClick={() => openEdit(row)} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
Edit Department
</button>
<button
type="button"
onClick={async () => {
try {
const res = await fetch(`${API}/api/admin/departments/${row.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }),
});
const res = await fetch(`${API}/api/admin/departments/${row.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }) });
if (!res.ok) throw new Error(`Request failed (${res.status})`);
} catch (err: any) {
setError(err?.message || 'Failed to update status.');
} finally {
setOpenMenuId(null);
await load();
}
} catch (err: any) { setError(err?.message || 'Failed to update status.'); }
finally { setOpenMenuId(null); await load(); }
}}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]"
>
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M9 12l2 2 4-4" /></svg>
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
{row.status === 'ACTIVE' ? 'Deactivate Department' : 'Activate Department'}
</button>
<div class="my-1 h-px bg-[#F3F4F6]" />
<button
@ -524,17 +425,13 @@ export default function DepartmentManagementPage() {
try {
const res = await fetch(`${API}/api/admin/departments/${row.id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Request failed (${res.status})`);
} catch (err: any) {
setError(err?.message || 'Failed to delete department.');
} finally {
setOpenMenuId(null);
await load();
}
} catch (err: any) { setError(err?.message || 'Failed to delete department.'); }
finally { setOpenMenuId(null); await load(); }
}}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#DC2626] hover:bg-[#FEF2F2]"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" /></svg>
Delete
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
Delete Department
</button>
</div>
</Show>
@ -549,88 +446,81 @@ export default function DepartmentManagementPage() {
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div class="flex items-center justify-between border-t border-[#E8ECF2] px-7 py-4">
<p class="text-[14px] text-[#6B7280]">
Showing <span class="font-semibold text-[#111827]">1{filteredRows().length}</span> of <span class="font-semibold text-[#111827]">{filteredRows().length}</span> departments
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing <strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> departments
</p>
<div class="flex items-center gap-1.5">
<button type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] transition-colors"></button>
<button type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-xl bg-[#FF5E13] text-[15px] font-semibold text-white">1</button>
<button type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#E5E7EB] text-[15px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">2</button>
<button type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#E5E7EB] text-[15px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">3</button>
<button type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] transition-colors"></button>
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
</div>
</div>
</Show>
</div>
</div>
</Show>
{/* ── FORM VIEW (Create / Edit) ── */}
<Show when={view() === 'form'}>
<div class="mt-2 overflow-hidden rounded-[24px] border border-[#DDE2EA] bg-white shadow-[0_8px_22px_rgba(13,13,42,0.05)]">
<div class="flex items-center gap-1 border-b border-[#E8ECF2] px-7 pt-0.5">
<button
type="button"
onClick={() => setView('list')}
class="px-6 py-4 text-[18px] font-semibold text-[#6B7280] transition-colors hover:text-[#111827]"
>
All Departments
</button>
<button
type="button"
class="relative px-6 py-4 text-[18px] font-semibold text-[#111827]"
>
{editingId() ? 'Edit Department' : 'Create Department'}
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</button>
</div>
{/* Top tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button type="button" onClick={() => setView('list')} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
All Departments
</button>
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
{editingId() ? 'Edit Department' : 'Create Department'}
</button>
</div>
{/* Tab nav */}
<div class="flex items-center gap-2 border-b border-[#E8ECF2] px-7">
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Department Settings', 'Permissions'];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
class={`relative px-6 py-5 text-[17px] font-semibold transition-colors ${
formTab() === tab ? 'text-[#111827]' : 'text-[#9CA3AF] hover:text-[#6B7280]'
}`}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
>
{labels[i]}
<Show when={formTab() === tab}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
</div>
<div class="p-7">
<div style="padding:24px">
<Show when={error()}>
<div class="mb-5 rounded-xl border border-[#FECACA] bg-[#FEF2F2] px-4 py-3 text-[13px] text-[#B91C1C]">
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
{error()}
</div>
</Show>
{/* General Information */}
<Show when={formTab() === 'general'}>
<div class="space-y-5">
<div class="grid grid-cols-2 gap-6">
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Department Name" required value={name()} onInput={setName} placeholder="e.g. Engineering" />
<FormInput label="Department Code" required value={code()} onInput={setCode} placeholder="e.g. ENG-001" />
</div>
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Description</span>
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this department's purpose..."
rows="4"
class="mt-1.5 w-full rounded-2xl border border-[#E5E7EB] bg-white px-4 py-3.5 text-[15px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors resize-none"
rows="3"
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
/>
</label>
<div class="grid grid-cols-2 gap-6">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Department Head" value={departmentHead()} onInput={setDepartmentHead} placeholder="e.g. Arun Kumar" />
<FormInput label="Department Email" type="email" value={departmentEmail()} onInput={setDepartmentEmail} placeholder="dept@nxtgauge.com" />
</div>
@ -639,21 +529,16 @@ export default function DepartmentManagementPage() {
{/* Department Settings */}
<Show when={formTab() === 'settings'}>
<div class="space-y-6">
<div style="display:flex;flex-direction:column;gap:32px">
<div>
<p class="text-[14px] font-semibold text-[#111827]">Department Status</p>
<p class="mt-0.5 text-[13px] text-[#6B7280]">Set whether this department is currently active</p>
<div class="mt-4 flex gap-3">
<p style="font-size:14px;font-weight:600;color:#111827">Department Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this department is currently active</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
<button
type="button"
onClick={() => setStatus(s)}
class={`h-[44px] rounded-xl border px-6 text-[14px] font-semibold transition-colors ${
status() === s
? s === 'ACTIVE' ? 'border-[#059669] bg-[#ECFDF5] text-[#059669]' : 'border-[#6B7280] bg-[#F3F4F6] text-[#374151]'
: 'border-[#E5E7EB] bg-white text-[#6B7280] hover:bg-[#F9FAFB]'
}`}
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
>
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
</button>
@ -662,9 +547,9 @@ export default function DepartmentManagementPage() {
</div>
<div>
<p class="text-[14px] font-semibold text-[#111827]">Department Visibility</p>
<p class="mt-0.5 text-[13px] text-[#6B7280]">Choose who can see this department</p>
<div class="mt-4 grid grid-cols-2 gap-4">
<p style="font-size:14px;font-weight:600;color:#111827">Department Visibility</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Choose who can see this department</p>
<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px">
{[
{ key: 'INTERNAL', label: 'Internal', desc: 'Visible to internal employees only' },
{ key: 'EXTERNAL', label: 'External', desc: 'Visible to external users and partners' },
@ -672,37 +557,33 @@ export default function DepartmentManagementPage() {
<button
type="button"
onClick={() => setVisibility(opt.key as 'INTERNAL' | 'EXTERNAL')}
class={`flex w-full items-start gap-3 rounded-2xl border p-5 text-left transition-colors ${
visibility() === opt.key
? 'border-[#FF5E13] bg-[#FFF7ED]'
: 'border-[#E5E7EB] bg-[#F9FAFB]'
}`}
style={`display:flex;align-items:flex-start;gap:10px;border-radius:12px;border:1px solid ${visibility() === opt.key ? '#FF5E13' : '#E5E7EB'};background:${visibility() === opt.key ? '#FFF7ED' : '#F9FAFB'};padding:14px 16px;text-align:left;cursor:pointer`}
>
<div class={`mt-0.5 h-4 w-4 shrink-0 rounded-full border-2 ${
visibility() === opt.key ? 'border-[#FF5E13]' : 'border-[#E5E7EB]'
}`}>
<div class={`m-[3px] h-[6px] w-[6px] rounded-full ${visibility() === opt.key ? 'bg-[#FF5E13]' : 'bg-transparent'}`} />
<div style={`margin-top:2px;width:16px;height:16px;border-radius:50%;border:2px solid ${visibility() === opt.key ? '#FF5E13' : '#D1D5DB'};display:flex;align-items:center;justify-content:center;flex-shrink:0`}>
<Show when={visibility() === opt.key}>
<div style="width:6px;height:6px;border-radius:50%;background:#FF5E13" />
</Show>
</div>
<div>
<p class="text-[13px] font-semibold text-[#111827]">{opt.label}</p>
<p class="mt-0.5 text-[12px] text-[#6B7280]">{opt.desc}</p>
<p style="font-size:13px;font-weight:600;color:#111827">{opt.label}</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">{opt.desc}</p>
</div>
</button>
))}
</div>
</div>
<div class="flex items-center justify-between rounded-2xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p class="text-[13px] font-semibold text-[#111827]">Allow Employee Transfers</p>
<p class="mt-0.5 text-[12px] text-[#6B7280]">Employees can request to transfer into this department</p>
<p style="font-size:13px;font-weight:600;color:#111827">Allow Employee Transfers</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">Employees can request to transfer into this department</p>
</div>
<button
type="button"
onClick={() => setTransfersEnabled((v) => !v)}
class={`relative h-6 w-11 rounded-full transition-colors ${transfersEnabled() ? 'bg-[#FF5E13]' : 'bg-[#E5E7EB]'}`}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${transfersEnabled() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
>
<span class={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${transfersEnabled() ? 'translate-x-5' : 'translate-x-0.5'}`} />
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${transfersEnabled() ? '22px' : '2px'}`} />
</button>
</div>
</div>
@ -710,18 +591,18 @@ export default function DepartmentManagementPage() {
{/* Permissions */}
<Show when={formTab() === 'permissions'}>
<div class="space-y-6">
<p class="text-[14px] text-[#6B7280]">Select the permissions available to employees in this department.</p>
<div style="display:flex;flex-direction:column;gap:24px">
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees in this department.</p>
<For each={permissionGroups}>
{(group) => (
<div>
<p class="mb-3 text-[13px] font-bold uppercase tracking-wider text-[#9CA3AF]">{group.title}</p>
<div class="grid grid-cols-2 gap-3.5">
<p style="margin-bottom:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#9CA3AF">{group.title}</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<For each={group.items}>
{(item) => (
<label class="flex cursor-pointer items-center gap-3 rounded-2xl border border-[#E5E7EB] bg-[#F9FAFB] px-5 py-3.5 hover:border-[#FF5E13] hover:bg-[#FFF1EB] transition-colors">
<input type="checkbox" class="h-4 w-4 rounded accent-[#FF5E13]" />
<span class="text-[14px] font-medium text-[#374151]">{item}</span>
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" />
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
</label>
)}
</For>
@ -734,11 +615,11 @@ export default function DepartmentManagementPage() {
</div>
{/* Form actions */}
<div class="flex items-center justify-end gap-3 border-t border-[#E8ECF2] px-7 py-4">
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
<button
type="button"
onClick={() => { setView('list'); resetForm(); }}
class="h-[44px] rounded-xl border border-[#E5E7EB] bg-white px-6 text-[14px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors"
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
@ -746,7 +627,7 @@ export default function DepartmentManagementPage() {
type="button"
onClick={() => void save()}
disabled={isSaving()}
class="h-[44px] rounded-xl bg-[#0D0D2A] px-7 text-[14px] font-semibold text-white hover:bg-[#1a1a3e] disabled:cursor-not-allowed disabled:opacity-60 transition-colors"
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:1"
>
{isSaving() ? 'Saving...' : editingId() ? 'Update Department' : 'Create Department'}
</button>

View file

@ -1,6 +1,6 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import { updateModuleRecord, deleteModuleRecord } from '~/lib/admin/client';
import type { CrudRecord } from '~/lib/admin/types';
type DesignationRecord = CrudRecord & {
@ -23,61 +23,62 @@ const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
{ id: 'z6', name: 'Finance Analyst', code: 'FA-006', department: 'Finance', level: 'Analyst', totalEmployees: 9, status: 'INACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-15' },
];
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level'];
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst'];
const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Success'];
const permissionItems = [
'View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees',
'Assign Roles', 'Approve Requests', 'Manage Team Members',
];
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span class={`inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-semibold ${
active() ? 'bg-[#ECFDF5] text-[#059669]' : 'bg-[#F3F4F6] text-[#6B7280]'
}`}>
<span class={`mr-1.5 h-1.5 w-1.5 rounded-full ${active() ? 'bg-[#059669]' : 'bg-[#9CA3AF]'}`} />
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function LevelBadge(props: { level: string }) {
const colors: Record<string, string> = {
Senior: 'bg-[#EFF6FF] text-[#2563EB]',
Manager: 'bg-[#FDF4FF] text-[#9333EA]',
Director: 'bg-[#FFF7ED] text-[#EA580C]',
Lead: 'bg-[#F0FDF4] text-[#16A34A]',
};
const cls = colors[props.level] ?? 'bg-[#F3F4F6] text-[#6B7280]';
return <span class={`inline-flex rounded-full px-2.5 py-1 text-[11px] font-semibold ${cls}`}>{props.level || '—'}</span>;
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<input
type={props.type ?? 'text'}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
);
}
function Toggle(props: { value: boolean; onChange: () => void; label: string; desc: string }) {
function FormSelect(props: { label: string; required?: boolean; value: string; onChange: (v: string) => void; children: any }) {
return (
<div class="flex items-center justify-between rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div>
<p class="text-[13px] font-semibold text-[#111827]">{props.label}</p>
<p class="mt-0.5 text-[12px] text-[#6B7280]">{props.desc}</p>
</div>
<button
type="button"
onClick={props.onChange}
class={`relative h-6 w-11 rounded-full transition-colors ${props.value ? 'bg-[#FF5E13]' : 'bg-[#E5E7EB]'}`}
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<select
value={props.value}
onChange={(e) => props.onChange(e.currentTarget.value)}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;appearance:none"
>
<span class={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${props.value ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button>
</div>
{props.children}
</select>
</label>
);
}
export default function DesignationManagementPage() {
const [view, setView] = createSignal<'list' | 'form'>('list');
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
const [search, setSearch] = createSignal('');
const [deptFilter, setDeptFilter] = createSignal('all');
const [statusFilter, setStatusFilter] = createSignal('all');
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
@ -90,6 +91,8 @@ export default function DesignationManagementPage() {
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [canManageTeam, setCanManageTeam] = createSignal(false);
const [canApprove, setCanApprove] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false);
const [error, setError] = createSignal('');
const load = async () => {
try {
@ -114,12 +117,7 @@ export default function DesignationManagementPage() {
}
}
} catch {}
try {
const data = await listModuleRecords<DesignationRecord>('designation', { q: search().trim() || undefined });
setRows(Array.isArray(data) && data.length > 0 ? data : FALLBACK_DESIGNATIONS);
} catch {
setRows(FALLBACK_DESIGNATIONS);
}
setRows(FALLBACK_DESIGNATIONS);
};
onMount(() => void load());
@ -133,17 +131,10 @@ export default function DesignationManagementPage() {
return r;
});
const stats = createMemo(() => ({
total: rows().length,
active: rows().filter((d) => d.status === 'ACTIVE').length,
inactive: rows().filter((d) => d.status === 'INACTIVE').length,
totalEmployees: rows().reduce((acc, d) => acc + Number(d.totalEmployees || 0), 0),
}));
const resetForm = () => {
setEditingId(null); setName(''); setCode(''); setDepartment('');
setLevel(''); setDescription(''); setStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false); setFormTab('general');
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError('');
};
const openCreate = () => { resetForm(); setView('form'); };
@ -159,18 +150,29 @@ export default function DesignationManagementPage() {
};
const save = async () => {
const payload: Partial<DesignationRecord> = {
name: name().trim() || 'New Designation', code: code().trim() || undefined,
department: department().trim(), level: level().trim(),
description: description().trim(), status: status(),
canManageTeam: canManageTeam(), canApprove: canApprove(),
};
if (editingId()) {
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
} else {
await createModuleRecord<DesignationRecord>('designation', payload);
if (!name().trim()) { setError('Designation name is required.'); setFormTab('general'); return; }
setIsSaving(true); setError('');
try {
const payload: Partial<DesignationRecord> = {
name: name().trim(), code: code().trim() || undefined,
department: department().trim(), level: level().trim(),
description: description().trim(), status: status(),
canManageTeam: canManageTeam(), canApprove: canApprove(),
};
if (editingId()) {
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
} else {
const res = await fetch('/api/gateway/api/admin/designations', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
}
setView('list'); resetForm(); await load();
} catch (err: any) {
setError(err?.message || 'Failed to save designation.');
} finally {
setIsSaving(false);
}
setView('list'); resetForm(); await load();
};
const formatDate = (v?: string) => {
@ -181,317 +183,328 @@ export default function DesignationManagementPage() {
return (
<AdminShell>
<div class="w-full space-y-8 pb-8">
<div style="width:100%;padding-bottom:32px">
{/* Page header */}
<div class="flex items-end justify-between">
<div>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">Organisation</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">
{view() === 'form' ? (editingId() ? 'Edit Designation' : 'Create Designation') : 'Designation Management'}
</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">
{view() === 'form'
? 'Dashboard / Designation Management / ' + (editingId() ? 'Edit Designation' : 'Create Designation')
: 'Manage all job designations and position levels'}
</p>
</div>
<Show when={view() === 'list'}>
<button
type="button"
onClick={openCreate}
class="inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-5 py-2.5 text-[13px] font-semibold text-white shadow-sm hover:bg-[#1a1a3e] transition-colors"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Create Designation
</button>
</Show>
<div style="margin-bottom:24px">
<h1 style="font-size:28px;font-weight:700;color:#111827;line-height:1.2">Designation Management</h1>
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage all job designations and position levels</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<div>
{/* Tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ key: 'all', label: 'All Designations', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } },
{ key: 'create', label: 'Create Designation', action: () => { setListTab('create'); openCreate(); } },
{ key: 'view', label: 'View Designation', action: () => setListTab('view') },
] as const).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
{/* Summary cards */}
<div class="grid grid-cols-4 gap-5">
{[
{ label: 'Total Designations', value: stats().total, color: 'text-[#FF5E13]', bg: 'bg-[#FFF1EB]' },
{ label: 'Active', value: stats().active, color: 'text-[#059669]', bg: 'bg-[#ECFDF5]' },
{ label: 'Inactive', value: stats().inactive, color: 'text-[#6B7280]', bg: 'bg-[#F3F4F6]' },
{ label: 'Total Employees', value: stats().totalEmployees, color: 'text-[#2563EB]', bg: 'bg-[#EFF6FF]' },
].map((s) => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class={`inline-flex h-10 w-10 items-center justify-center rounded-xl ${s.bg} ${s.color} text-[16px] font-bold`}>
{s.value}
</div>
<p class="mt-3 text-[13px] font-medium text-[#6B7280]">{s.label}</p>
<p class={`mt-0.5 text-[22px] font-bold tracking-tight ${s.color}`}>{s.value}</p>
</div>
))}
</div>
{/* Table card */}
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
{/* Table card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Filters */}
<div class="flex items-center gap-3 border-b border-[#F3F4F6] p-5">
<div class="relative flex-1 max-w-sm">
<svg class="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[#9CA3AF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
{/* Filter bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search designations..."
class="h-[40px] w-full rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] pl-10 pr-4 text-[13px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:bg-white focus:ring-2 focus:ring-[rgba(255,94,19,0.08)] transition-colors"
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
</div>
<select
value={deptFilter()}
onChange={(e) => setDeptFilter(e.currentTarget.value)}
class="h-[40px] rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-3 pr-8 text-[13px] text-[#374151] outline-none focus:border-[#FF5E13] transition-colors"
>
<option value="all">All Departments</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value)}
class="h-[40px] rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-3 pr-8 text-[13px] text-[#374151] outline-none focus:border-[#FF5E13] transition-colors"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<p class="ml-auto text-[13px] text-[#6B7280]">
<span class="font-semibold text-[#111827]">{filteredRows().length}</span> designations
</p>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-[#F3F4F6] bg-[#FAFAFA] text-left">
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Designation Name</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Code</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Department</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Level</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Employees</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Status</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Created</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="8" class="px-6 py-16 text-center">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-[#F9FAFB]">
<svg class="h-7 w-7 text-[#9CA3AF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M20 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2Z" /><path d="M12 12h.01" /></svg>
</div>
<p class="mt-3 text-[15px] font-semibold text-[#111827]">No designations found</p>
<p class="mt-1 text-[13px] text-[#6B7280]">Create your first designation to get started.</p>
<button type="button" onClick={openCreate} class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Create Designation
</button>
</td>
</tr>
}
<select
value={deptFilter()}
onChange={(e) => setDeptFilter(e.currentTarget.value)}
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
>
<option value="all">All Departments</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
<div style="position:relative">
<button
type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<For each={filteredRows()}>
{(row) => (
<tr class="group hover:bg-[#FAFAFA] transition-colors">
<td class="px-6 py-4">
<p class="text-[14px] font-semibold text-[#111827]">{row.name}</p>
</td>
<td class="px-6 py-4">
<span class="rounded-lg bg-[#F3F4F6] px-2.5 py-1 text-[12px] font-mono font-semibold text-[#374151]">
{String(row.code || '—')}
</span>
</td>
<td class="px-6 py-4 text-[13px] text-[#374151]">{String(row.department || '—')}</td>
<td class="px-6 py-4"><LevelBadge level={String(row.level || '')} /></td>
<td class="px-6 py-4 text-[13px] font-semibold text-[#111827]">{Number(row.totalEmployees || 0)}</td>
<td class="px-6 py-4"><StatusBadge status={row.status} /></td>
<td class="px-6 py-4 text-[13px] text-[#6B7280]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td class="relative px-6 py-4">
<button
type="button"
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
aria-label="More actions"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="19" r="1.5" /></svg>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
Filters
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'ACTIVE', 'INACTIVE'] as const).map((s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'ACTIVE' ? 'Active' : 'Inactive'}
</button>
))}
</div>
</Show>
</div>
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export
</button>
</div>
{/* Table */}
<div style="overflow-x:auto">
<table style="min-width:100%">
<thead>
<tr style="background:#0D0D2A;text-align:left">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Designation Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Code</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Level</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Employees</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Created Date</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
</tr>
</thead>
<tbody>
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="8" style="padding:64px 24px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No designations found</p>
<p style="margin-top:4px;font-size:13px;color:#6B7280">Create your first designation to get started.</p>
<button type="button" onClick={openCreate} style="margin-top:16px;display:inline-flex;align-items:center;gap:8px;border-radius:12px;background:#0D0D2A;padding:8px 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">
Create Designation
</button>
<Show when={openMenuId() === row.id}>
<div class="absolute right-6 top-12 z-20 w-[200px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg shadow-black/10">
<button type="button" onClick={() => openEdit(row)} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 20h9" /><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z" /></svg>
Edit Designation
</button>
<button type="button" onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M9 12l2 2 4-4" /></svg>
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
</button>
<div class="my-1 h-px bg-[#F3F4F6]" />
<button type="button" onClick={async () => { await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#DC2626] hover:bg-[#FEF2F2]">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" /></svg>
Delete
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div class="flex items-center justify-between border-t border-[#F3F4F6] px-6 py-4">
<p class="text-[13px] text-[#6B7280]">
Showing <span class="font-semibold text-[#111827]">1{filteredRows().length}</span> of <span class="font-semibold text-[#111827]">{filteredRows().length}</span> designations
</p>
<div class="flex items-center gap-1.5">
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] transition-colors"></button>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-[#0D0D2A] text-[13px] font-semibold text-white">1</button>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">2</button>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] transition-colors"></button>
</div>
}
>
<For each={filteredRows()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td>
<td style="padding:12px 20px">
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">{String(row.level || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
style="display:inline-flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:8px;border:none;background:none;color:#9CA3AF;cursor:pointer"
aria-label="More actions"
>
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
</button>
<Show when={openMenuId() === row.id}>
<div style="position:absolute;right:20px;top:44px;z-index:20;width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
Edit Designation
</button>
<button type="button" onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
</button>
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
<button type="button" onClick={async () => { if (!window.confirm(`Delete designation "${row.name}"?`)) return; await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer">
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
Delete
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</Show>
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing <strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations
</p>
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
</div>
</div>
</Show>
</div>
</div>
</Show>
{/* ── FORM VIEW ── */}
<Show when={view() === 'form'}>
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Top tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
All Designations
</button>
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
{editingId() ? 'Edit Designation' : 'Create Designation'}
</button>
</div>
{/* Tab nav */}
<div class="flex items-center gap-1 border-b border-[#F3F4F6] px-6">
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Designation Settings', 'Permissions'];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
class={`relative px-4 py-4 text-[13px] font-semibold transition-colors ${
formTab() === tab ? 'text-[#111827]' : 'text-[#9CA3AF] hover:text-[#6B7280]'
}`}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
>
{labels[i]}
<Show when={formTab() === tab}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
</div>
<div class="p-6">
<div style="padding:24px">
<Show when={error()}>
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
{error()}
</div>
</Show>
{/* General Information */}
<Show when={formTab() === 'general'}>
<div class="space-y-5">
<div class="grid grid-cols-2 gap-5">
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Designation Name <span class="text-[#FF5E13]">*</span></span>
<input value={name()} onInput={(e) => setName(e.currentTarget.value)} placeholder="e.g. Senior Software Engineer" class="mt-1.5 h-[42px] w-full rounded-xl border border-[#E5E7EB] bg-white px-3.5 text-[14px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors" />
</label>
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Designation Code <span class="text-[#FF5E13]">*</span></span>
<input value={code()} onInput={(e) => setCode(e.currentTarget.value)} placeholder="e.g. SSE-001" class="mt-1.5 h-[42px] w-full rounded-xl border border-[#E5E7EB] bg-white px-3.5 text-[14px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors" />
</label>
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Designation Name" required value={name()} onInput={setName} placeholder="e.g. Senior Software Engineer" />
<FormInput label="Designation Code" required value={code()} onInput={setCode} placeholder="e.g. SSE-001" />
</div>
<div class="grid grid-cols-2 gap-5">
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Department <span class="text-[#FF5E13]">*</span></span>
<select value={department()} onChange={(e) => setDepartment(e.currentTarget.value)} class="mt-1.5 h-[42px] w-full rounded-xl border border-[#E5E7EB] bg-white px-3.5 text-[14px] text-[#111827] outline-none focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors">
<option value="">Select department</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</select>
</label>
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Designation Level <span class="text-[#FF5E13]">*</span></span>
<select value={level()} onChange={(e) => setLevel(e.currentTarget.value)} class="mt-1.5 h-[42px] w-full rounded-xl border border-[#E5E7EB] bg-white px-3.5 text-[14px] text-[#111827] outline-none focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors">
<option value="">Select level</option>
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
</select>
</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormSelect label="Department" required value={department()} onChange={setDepartment}>
<option value="">Select department</option>
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
</FormSelect>
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}>
<option value="">Select level</option>
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
</FormSelect>
</div>
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">Description</span>
<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} placeholder="Brief description of this designation's responsibilities..." rows="3" class="mt-1.5 w-full rounded-xl border border-[#E5E7EB] bg-white px-3.5 py-3 text-[14px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.1)] transition-colors resize-none" />
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this designation's responsibilities..."
rows="3"
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
/>
</label>
</div>
</Show>
{/* Designation Settings */}
<Show when={formTab() === 'settings'}>
<div class="space-y-5">
<div style="display:flex;flex-direction:column;gap:32px">
<div>
<p class="text-[14px] font-semibold text-[#111827]">Designation Status</p>
<p class="mt-0.5 text-[13px] text-[#6B7280]">Set whether this designation is currently active</p>
<div class="mt-3 flex gap-2">
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
<button
type="button"
onClick={() => setStatus(s)}
class={`h-[38px] rounded-xl border px-5 text-[13px] font-semibold transition-colors ${
status() === s
? s === 'ACTIVE' ? 'border-[#059669] bg-[#ECFDF5] text-[#059669]' : 'border-[#6B7280] bg-[#F3F4F6] text-[#374151]'
: 'border-[#E5E7EB] bg-white text-[#6B7280] hover:bg-[#F9FAFB]'
}`}
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
>
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
</button>
))}
</div>
</div>
<Toggle
value={canManageTeam()}
onChange={() => setCanManageTeam((v) => !v)}
label="Allow Managing Team Members"
desc="This designation can manage and oversee team members"
/>
<Toggle
value={canApprove()}
onChange={() => setCanApprove((v) => !v)}
label="Allow Approval Permissions"
desc="This designation can approve requests and submissions"
/>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</p>
</div>
<button
type="button"
onClick={() => setCanManageTeam((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? '22px' : '2px'}`} />
</button>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p>
</div>
<button
type="button"
onClick={() => setCanApprove((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? '22px' : '2px'}`} />
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === 'permissions'}>
<div class="space-y-4">
<p class="text-[13px] text-[#6B7280]">Select the permissions available to employees with this designation.</p>
<div class="grid grid-cols-2 gap-2">
<For each={permissionItems}>
{(item) => (
<label class="flex cursor-pointer items-center gap-3 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-4 py-3 hover:border-[#FF5E13] hover:bg-[#FFF1EB] transition-colors">
<input type="checkbox" class="h-4 w-4 rounded accent-[#FF5E13]" />
<span class="text-[13px] font-medium text-[#374151]">{item}</span>
</label>
)}
</For>
<div style="display:flex;flex-direction:column;gap:16px">
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
{['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles', 'Approve Requests', 'Manage Team Members'].map((item) => (
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" />
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
</label>
))}
</div>
</div>
</Show>
</div>
{/* Form actions */}
<div class="flex items-center justify-end gap-3 border-t border-[#F3F4F6] px-6 py-4">
<button type="button" onClick={() => { setView('list'); resetForm(); }} class="h-[40px] rounded-xl border border-[#E5E7EB] bg-white px-5 text-[13px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors">
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
<button
type="button"
onClick={() => { setView('list'); resetForm(); }}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
<button type="button" onClick={() => void save()} class="h-[40px] rounded-xl bg-[#0D0D2A] px-6 text-[13px] font-semibold text-white hover:bg-[#1a1a3e] transition-colors">
{editingId() ? 'Update Designation' : 'Create Designation'}
<button
type="button"
onClick={() => void save()}
disabled={isSaving()}
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
>
{isSaving() ? 'Saving...' : editingId() ? 'Update Designation' : 'Create Designation'}
</button>
</div>
</div>

View file

@ -1,6 +1,5 @@
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 { Eye, SquarePen, Search, Trash2 } from 'lucide-solid';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
@ -18,15 +17,55 @@ type Employee = {
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' },
{
id: 'EMP001',
full_name: 'John Smith',
email: 'john.smith@nxtgauge.com',
department: { id: 'd1', name: 'Engineering' },
designation: { id: 'dg1', name: 'Senior Software Engineer' },
role: { id: 'r1', name: 'Engineering Lead' },
is_active: true,
},
{
id: 'EMP002',
full_name: 'Sarah Johnson',
email: 'sarah.j@nxtgauge.com',
department: { id: 'd2', name: 'Marketing' },
designation: { id: 'dg2', name: 'Marketing Manager' },
role: { id: 'r2', name: 'Marketing Manager' },
is_active: true,
},
{
id: 'EMP003',
full_name: 'Michael Brown',
email: 'michael.b@nxtgauge.com',
department: { id: 'd3', name: 'Sales' },
designation: { id: 'dg3', name: 'Sales Executive' },
role: { id: 'r3', name: 'Sales Director' },
status: 'PROBATION',
is_active: false,
},
{
id: 'EMP004',
full_name: 'Emily Davis',
email: 'emily.d@nxtgauge.com',
department: { id: 'd4', name: 'Human Resources' },
designation: { id: 'dg4', name: 'HR Specialist' },
role: { id: 'r4', name: 'HR Admin' },
is_active: true,
},
{
id: 'EMP005',
full_name: 'David Wilson',
email: 'david.w@nxtgauge.com',
department: { id: 'd5', name: 'Finance' },
designation: { id: 'dg5', name: 'Financial Analyst' },
role: { id: 'r5', name: 'Finance Controller' },
is_active: true,
},
];
async function fetchEmployees(): Promise<Employee[]> {
@ -41,7 +80,9 @@ async function fetchEmployees(): Promise<Employee[]> {
}
}
function empName(e: Employee) { return e.full_name || e.name || '—'; }
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;
@ -57,278 +98,172 @@ function empDesig(e: Employee) {
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 empStatus(e: Employee) {
const raw = String(e.status || '').toUpperCase();
if (raw === 'PROBATION') return 'Probation';
if (e.is_active === false || raw === 'INACTIVE') return 'Inactive';
return 'Active';
}
function StatusBadge(props: { active: boolean }) {
function StatusBadge(props: { status: 'Active' | 'Inactive' | 'Probation' }) {
const classes = () => {
if (props.status === 'Active') return 'border-[#B7E4C7] bg-[#DEF7E8] text-[#0B8A4A]';
if (props.status === 'Probation') return 'border-[#F6D78F] bg-[#FFF3D6] text-[#B7791F]';
return 'border-[#D1D5DB] bg-[#F3F4F6] text-[#6B7280]';
};
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 class={`inline-flex min-w-[68px] items-center justify-center border px-2 py-1 text-[11px] font-semibold ${classes()}`}>
{props.status}
</span>
);
}
export default function EmployeesIndexPage() {
const navigate = useNavigate();
const [employees, { refetch }] = createResource(fetchEmployees);
const [employees] = createResource(fetchEmployees);
const [search, setSearch] = createSignal('');
const [filterStatus, setFilterStatus] = createSignal('all');
const [openMenu, setOpenMenu] = createSignal('');
const [toggling, setToggling] = createSignal('');
const [deleting, setDeleting] = createSignal('');
const [activeTab, setActiveTab] = createSignal<'list' | 'create'>('list');
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 q = search().trim().toLowerCase();
if (!q) return list;
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 list.filter((e) =>
[e.id, empName(e), e.email, empDept(e), empDesig(e), empRole(e)]
.join(' ')
.toLowerCase()
.includes(q),
);
};
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 class="w-full space-y-4 pb-8">
<div>
<h1 class="text-[48px] font-bold leading-[1.1] tracking-[-0.02em] text-[#0B1246]">Employee Management</h1>
<p class="mt-1 text-[15px] text-[#7E849F]">Manage internal employees and their information</p>
</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 class="rounded-none border border-[#D9DDE6] bg-white">
<div class="border-b border-[#D9DDE6] px-6">
<div class="flex items-end gap-10">
<button
type="button"
onClick={() => setActiveTab('list')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'list' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
All Employees
<Show when={activeTab() === 'list'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button>
<button
type="button"
onClick={() => setActiveTab('create')}
class={`relative pb-3 pt-4 text-[36px] font-semibold ${
activeTab() === 'create' ? 'text-[#0B1246]' : 'text-[#7E849F]'
}`}
>
Create Employee
<Show when={activeTab() === 'create'}>
<span class="absolute bottom-0 left-0 h-[4px] w-full bg-[#FF5E13]" />
</Show>
</button>
</div>
</div>
<Show
when={activeTab() === 'list'}
fallback={
<div class="px-6 py-12 text-[14px] text-[#6B7280]">
Create Employee form will follow this new base design next.
</div>
}
>
<div class="border-b border-[#D9DDE6] px-6 py-5">
<div class="grid grid-cols-[1fr_160px_120px] gap-4">
<label class="flex h-[58px] items-center border border-[#D9DDE6] bg-[#F7F8FA] px-4 text-[#9CA3AF]">
<Search size={19} class="mr-3 text-[#9CA3AF]" />
<input
type="text"
placeholder="Search employees..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
class="w-full border-0 bg-transparent text-[34px] text-[#0B1246] placeholder:text-[#9CA3AF] outline-none"
/>
</label>
<div class="h-[58px] border border-[#D9DDE6] bg-white" />
<button
type="button"
class="h-[58px] border border-[#D9DDE6] bg-white text-[34px] font-medium text-[#0B1246] transition-colors hover:bg-[#F9FAFB]"
>
Export
</button>
</div>
</div>
))}
</div>
{/* Table card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-[1400px] table-fixed border-collapse">
<thead>
<tr class="bg-[#02033B] text-left">
{['ID', 'Name', 'Email', 'Department', 'Designation', 'Role', 'Status', 'Actions'].map((h) => (
<th class="px-6 py-5 text-[31px] font-semibold text-white">{h}</th>
))}
</tr>
</thead>
{/* 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>
<tbody>
<Show when={employees.loading}>
<tr>
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">Loading employees...</td>
</tr>
</Show>
{/* 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]">
<Show when={!employees.loading && filtered().length === 0}>
<tr>
<td colspan="8" class="px-6 py-10 text-center text-[24px] text-[#9CA3AF]">No employees found.</td>
</tr>
</Show>
{/* 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>
<For each={filtered()}>
{(emp) => (
<tr class="border-b border-[#D9DDE6] bg-white align-middle">
<td class="px-6 py-4 text-[35px] italic text-[#303A67]">{emp.id}</td>
<td class="px-6 py-4 text-[41px] font-semibold leading-[1.15] text-[#0B1246]">{empName(emp)}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{emp.email}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDept(emp)}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empDesig(emp)}</td>
<td class="px-6 py-4 text-[35px] text-[#59608A]">{empRole(emp)}</td>
<td class="px-6 py-4">
<StatusBadge status={empStatus(emp) as 'Active' | 'Inactive' | 'Probation'} />
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-4">
<button type="button" class="text-[#FF5E13] transition-colors hover:text-[#E04D0A]" aria-label="View">
<Eye size={23} />
</button>
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Edit">
<SquarePen size={23} />
</button>
<button type="button" class="text-[#5B628D] transition-colors hover:text-[#303A67]" aria-label="Delete">
<Trash2 size={23} />
</button>
</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>
</tbody>
</table>
</div>
</Show>
</div>
{/* Overlay to close dropdown */}
<Show when={openMenu()}>
<div class="fixed inset-0 z-10" onClick={() => setOpenMenu('')} />
</Show>
</div>
</AdminShell>
);

View file

@ -1,6 +1,7 @@
import { A } from '@solidjs/router';
import { A, useNavigate } from '@solidjs/router';
import { createResource, createSignal, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { ArrowUpDown, Filter, Download, Eye, Pencil } from 'lucide-solid';
const API = '/api/gateway';
@ -20,12 +21,11 @@ type Role = {
type ListResponse = { roles: Role[]; total: number; page: number; per_page: number };
const FALLBACK_ROLES: Role[] = [
{ id: 'r1', key: 'SUPER_ADMIN', name: 'Super Admin', audience: 'INTERNAL', department_name: 'Engineering', is_active: true, users_assigned: 3, permissions_count: 42, created_at: '2026-01-10' },
{ id: 'r2', key: 'OPS_MANAGER', name: 'Operations Manager', audience: 'INTERNAL', department_name: 'Operations', is_active: true, users_assigned: 7, permissions_count: 28, created_at: '2026-01-15' },
{ id: 'r3', key: 'HR_ADMIN', name: 'HR Admin', audience: 'INTERNAL', department_name: 'Human Resources', is_active: true, users_assigned: 4, permissions_count: 16, created_at: '2026-01-20' },
{ id: 'r4', key: 'FINANCE_VIEWER', name: 'Finance Viewer', audience: 'INTERNAL', department_name: 'Finance', is_active: true, users_assigned: 6, permissions_count: 8, created_at: '2026-02-01' },
{ id: 'r5', key: 'SUPPORT_AGENT', name: 'Support Agent', audience: 'INTERNAL', department_name: 'Customer Success', is_active: false, users_assigned: 12, permissions_count: 10, created_at: '2026-02-10' },
{ id: 'r6', key: 'CONTENT_MOD', name: 'Content Moderator', audience: 'INTERNAL', department_name: 'Marketing', is_active: true, users_assigned: 5, permissions_count: 14, created_at: '2026-02-15' },
{ id: 'r1', key: 'ADM-SYS-001', name: 'System Administrator', audience: 'INTERNAL', department_name: 'Information Technology', is_active: true, users_assigned: 12, permissions_count: 0, created_at: '2024-01-12' },
{ id: 'r2', key: 'FIN-MGR-002', name: 'Finance Manager', audience: 'INTERNAL', department_name: 'Accounting & Finance', is_active: true, users_assigned: 4, permissions_count: 42, created_at: '2024-02-05' },
{ id: 'r3', key: 'HR-COOR-003', name: 'HR Coordinator', audience: 'INTERNAL', department_name: 'Human Resources', is_active: false, users_assigned: 8, permissions_count: 28, created_at: '2024-03-18' },
{ id: 'r4', key: 'ENG-LEAD-004', name: 'Lead Developer', audience: 'INTERNAL', department_name: 'Engineering', is_active: true, users_assigned: 25, permissions_count: 64, created_at: '2024-04-02' },
{ id: 'r5', key: 'SLS-ASSC-005', name: 'Sales Associate', audience: 'INTERNAL', department_name: 'Growth & Sales', is_active: true, users_assigned: 48, permissions_count: 15, created_at: '2024-05-11' },
];
async function loadRoles(params: { q: string; page: number }): Promise<ListResponse> {
@ -44,26 +44,45 @@ async function loadRoles(params: { q: string; page: number }): Promise<ListRespo
}
function formatDate(iso: string) {
try { return new Date(iso).toISOString().slice(0, 10); } catch { return '—'; }
try {
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
} catch { return '—'; }
}
function StatusBadge(props: { active: boolean }) {
return (
<span class={`inline-flex items-center rounded-full px-2.5 py-1 text-[11px] font-semibold ${
props.active ? 'bg-[#ECFDF5] text-[#059669]' : 'bg-[#F3F4F6] text-[#6B7280]'
<span class={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[12px] font-medium ${
props.active
? 'border-[#FF5E13] bg-[#FFF3EE] text-[#FF5E13]'
: 'border-[#D1D5DB] bg-[#F9FAFB] text-[#6B7280]'
}`}>
<span class={`mr-1.5 h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#059669]' : 'bg-[#9CA3AF]'}`} />
<span class={`h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#FF5E13]' : 'bg-[#9CA3AF]'}`} />
{props.active ? 'Active' : 'Inactive'}
</span>
);
}
function UsersBadge(props: { count: number }) {
return (
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-[#EEF2FF] text-[12px] font-semibold text-[#4F46E5]">
{props.count > 99 ? '99+' : String(props.count).padStart(2, '0')}
</span>
);
}
const TABS = [
{ label: 'All Roles', href: '/admin/roles' },
{ label: 'Create Role', href: '/admin/roles/create' },
{ label: 'View Role', href: '/admin/roles/view' },
{ label: 'Edit Role', href: '/admin/roles/edit' },
];
export default function InternalRolesListPage() {
const navigate = useNavigate();
const [search, setSearch] = createSignal('');
const [debouncedSearch, setDebouncedSearch] = createSignal('');
const [page, setPage] = createSignal(1);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [deleting, setDeleting] = createSignal('');
let debounceTimer: ReturnType<typeof setTimeout>;
const handleSearch = (val: string) => {
@ -72,7 +91,7 @@ export default function InternalRolesListPage() {
debounceTimer = setTimeout(() => { setDebouncedSearch(val); setPage(1); }, 300);
};
const [data, { refetch }] = createResource(
const [data] = createResource(
() => ({ q: debouncedSearch(), page: page() }),
loadRoles,
);
@ -84,111 +103,77 @@ export default function InternalRolesListPage() {
};
const roles = () => data()?.roles ?? [];
const stats = () => ({
total: data()?.total ?? 0,
active: roles().filter((r) => r.is_active).length,
inactive: roles().filter((r) => !r.is_active).length,
totalUsers: roles().reduce((acc, r) => acc + r.users_assigned, 0),
});
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete role "${name}"? This cannot be undone.`)) return;
setDeleting(id);
try {
const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed');
refetch();
} finally {
setDeleting(''); setOpenMenuId(null);
}
};
return (
<AdminShell>
<div class="w-full space-y-8 pb-8">
<div class="w-full space-y-6 pb-8">
{/* Header */}
<div class="flex items-end justify-between">
<div>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">Access Control</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage roles for internal admin users</p>
</div>
<A
href="/admin/roles/create"
class="inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-5 py-2.5 text-[13px] font-semibold text-white shadow-sm hover:bg-[#1a1a3e] transition-colors"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Create Role
</A>
{/* Page header */}
<div>
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage organizational access levels with granular permission control.</p>
</div>
{/* Summary cards */}
<Show when={!data.loading}>
<div class="grid grid-cols-4 gap-5">
{[
{ label: 'Total Roles', value: stats().total, color: 'text-[#FF5E13]', bg: 'bg-[#FFF1EB]' },
{ label: 'Active Roles', value: stats().active, color: 'text-[#059669]', bg: 'bg-[#ECFDF5]' },
{ label: 'Inactive Roles', value: stats().inactive, color: 'text-[#6B7280]', bg: 'bg-[#F3F4F6]' },
{ label: 'Users Assigned', value: stats().totalUsers, color: 'text-[#2563EB]', bg: 'bg-[#EFF6FF]' },
].map((s) => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class={`inline-flex h-10 w-10 items-center justify-center rounded-xl ${s.bg} ${s.color} text-[16px] font-bold`}>
{s.value}
</div>
<p class="mt-3 text-[13px] font-medium text-[#6B7280]">{s.label}</p>
<p class={`mt-0.5 text-[22px] font-bold tracking-tight ${s.color}`}>{s.value}</p>
</div>
))}
</div>
</Show>
{/* Tabs */}
<div class="flex items-center gap-6 border-b border-[#E5E7EB]">
<For each={TABS}>
{(tab) => {
const active = () => tab.href === '/admin/roles';
return (
<A
href={tab.href}
class={`pb-3 text-[14px] font-medium transition-colors ${
active()
? 'border-b-2 border-[#FF5E13] text-[#FF5E13]'
: 'text-[#6B7280] hover:text-[#111827]'
}`}
>
{tab.label}
</A>
);
}}
</For>
</div>
{/* Table card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
<div class="overflow-hidden rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Filters */}
<div class="flex items-center gap-3 border-b border-[#F3F4F6] p-5">
<div class="relative flex-1 max-w-sm">
<svg class="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[#9CA3AF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
<input
type="text"
placeholder="Search roles..."
value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)}
class="h-[40px] w-full rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] pl-10 pr-4 text-[13px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:bg-white focus:ring-2 focus:ring-[rgba(255,94,19,0.08)] transition-colors"
/>
</div>
<select class="h-[40px] rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-3 pr-8 text-[13px] text-[#374151] outline-none focus:border-[#FF5E13] transition-colors">
<option>All Departments</option>
<option>Engineering</option>
<option>Operations</option>
<option>Finance</option>
</select>
<select class="h-[40px] rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] px-3 pr-8 text-[13px] text-[#374151] outline-none focus:border-[#FF5E13] transition-colors">
<option>All Status</option>
<option>Active</option>
<option>Inactive</option>
</select>
<p class="ml-auto text-[13px] text-[#6B7280]">
<Show when={!data.loading}>
<span class="font-semibold text-[#111827]">{data()?.total ?? 0}</span> roles
</Show>
</p>
{/* Filter bar */}
<div class="flex items-center gap-2 px-5 py-4">
<input
type="text"
placeholder="Filter by role name or code..."
value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)}
class="h-[34px] flex-1 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] text-[#111827] outline-none placeholder:text-[#9CA3AF] focus:border-[#FF5E13] focus:ring-2 focus:ring-[rgba(255,94,19,0.08)] transition-colors"
/>
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">
<ArrowUpDown size={13} />
Sort
</button>
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[12px] font-medium text-[#374151] hover:bg-[#F9FAFB] transition-colors">
<Filter size={13} />
Filters
</button>
<button type="button" class="inline-flex h-[34px] items-center gap-1.5 rounded-lg bg-[#0D0D2A] px-3 text-[12px] font-semibold text-white hover:bg-[#1a1a3e] transition-colors">
<Download size={13} />
Export
</button>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-[#F3F4F6] bg-[#FAFAFA] text-left">
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Role Name</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Role Key</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Department</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Users Assigned</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Permissions</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Status</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Created</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Actions</th>
<tr class="bg-[#0D0D2A] text-left">
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Name</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Role Code</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Department</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Users Assigned</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Permissions</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Status</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">Created Date</th>
<th class="px-5 py-3 text-[11px] font-semibold uppercase tracking-wider text-[#8AACC8]">AC</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
@ -196,14 +181,14 @@ export default function InternalRolesListPage() {
<For each={[0, 1, 2, 3, 4]}>
{() => (
<tr class="animate-pulse">
<td class="px-6 py-4"><div class="h-4 w-36 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-4 w-24 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-4 w-28 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-4 w-12 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-4 w-16 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-6 w-16 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-4 w-24 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-6 py-4"><div class="h-8 w-8 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-36 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-28 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-7 w-7 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-20 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-6 w-20 rounded-full bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-24 rounded-lg bg-[#F3F4F6]" /></td>
<td class="px-5 py-3.5"><div class="h-4 w-12 rounded-lg bg-[#F3F4F6]" /></td>
</tr>
)}
</For>
@ -211,13 +196,9 @@ export default function InternalRolesListPage() {
<Show when={!data.loading && roles().length === 0}>
<tr>
<td colspan="8" class="px-6 py-16 text-center">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-[#F9FAFB]">
<svg class="h-7 w-7 text-[#9CA3AF]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M12 15v2m0 0v2m0-2h2m-2 0H10M9 11V7a3 3 0 0 1 6 0v4" /><path d="M5 11h14a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z" /></svg>
</div>
<p class="mt-3 text-[15px] font-semibold text-[#111827]">No internal roles found</p>
<p class="text-[15px] font-semibold text-[#111827]">No roles found</p>
<p class="mt-1 text-[13px] text-[#6B7280]">Create your first internal role to control admin access.</p>
<A href="/admin/roles/create" class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
Create Role
</A>
</td>
@ -226,58 +207,52 @@ export default function InternalRolesListPage() {
<Show when={!data.loading}>
<For each={roles()}>
{(role) => (
<tr class="group hover:bg-[#FAFAFA] transition-colors">
<td class="px-6 py-4">
<p class="text-[14px] font-semibold text-[#111827]">{role.name}</p>
<p class="mt-0.5 text-[12px] text-[#9CA3AF] line-clamp-1">{role.description || ''}</p>
<tr class="hover:bg-[#FAFAFA] transition-colors">
<td class="px-5 py-3.5">
<p class="text-[14px] font-bold text-[#111827]">{role.name}</p>
</td>
<td class="px-6 py-4">
<span class="rounded-lg bg-[#F3F4F6] px-2.5 py-1 text-[11px] font-mono font-semibold text-[#374151]">
{role.key}
</span>
<td class="px-5 py-3.5">
<div class="text-[11px] font-mono leading-[1.6] text-[#9CA3AF]">
{role.key.split('-').map((seg, i, arr) => (
<span class="block">{seg}{i < arr.length - 1 ? '-' : ''}</span>
))}
</div>
</td>
<td class="px-6 py-4 text-[13px] text-[#374151]">{role.department_name || '—'}</td>
<td class="px-6 py-4 text-[13px] font-semibold text-[#111827]">{role.users_assigned}</td>
<td class="px-6 py-4">
<span class="inline-flex items-center gap-1 rounded-lg bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-semibold text-[#2563EB]">
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M9 12l2 2 4-4" /><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
{role.permissions_count}
</span>
<td class="px-5 py-3.5 text-[13px] text-[#374151]">{role.department_name || '—'}</td>
<td class="px-5 py-3.5">
<UsersBadge count={role.users_assigned} />
</td>
<td class="px-6 py-4"><StatusBadge active={role.is_active} /></td>
<td class="px-6 py-4 text-[13px] text-[#6B7280]">{formatDate(role.created_at)}</td>
<td class="relative px-6 py-4">
<button
type="button"
onClick={() => setOpenMenuId(openMenuId() === role.id ? null : role.id)}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
aria-label="More actions"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="19" r="1.5" /></svg>
</button>
<Show when={openMenuId() === role.id}>
<div class="absolute right-6 top-12 z-20 w-[200px] rounded-xl border border-[#E5E7EB] bg-white p-1.5 shadow-lg shadow-black/10">
<A href={`/admin/roles/${role.id}`} onClick={() => setOpenMenuId(null)} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="3" /><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
View Role
</A>
<A href={`/admin/roles/${role.id}/edit`} onClick={() => setOpenMenuId(null)} class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#374151] hover:bg-[#F9FAFB]">
<svg class="h-4 w-4 text-[#FF5E13]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 20h9" /><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z" /></svg>
Edit Role
</A>
<div class="my-1 h-px bg-[#F3F4F6]" />
<button
type="button"
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
class="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-[#DC2626] hover:bg-[#FEF2F2]"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" /></svg>
{deleting() === role.id ? 'Deleting…' : 'Delete Role'}
</button>
</div>
<td class="px-5 py-3.5">
<Show when={role.permissions_count === 0} fallback={
<span class="text-[13px] text-[#374151]">{role.permissions_count} Controls</span>
}>
<span class="text-[13px] font-semibold text-[#FF5E13]">All Access</span>
</Show>
</td>
<td class="px-5 py-3.5">
<StatusBadge active={role.is_active} />
</td>
<td class="px-5 py-3.5 text-[13px] text-[#6B7280]">{formatDate(role.created_at)}</td>
<td class="px-5 py-3.5">
<div class="flex items-center gap-3">
<button
type="button"
onClick={() => navigate(`/admin/roles/${role.id}`)}
class="text-[#9CA3AF] hover:text-[#374151] transition-colors"
aria-label="View role"
>
<Eye size={17} />
</button>
<button
type="button"
onClick={() => navigate(`/admin/roles/${role.id}/edit`)}
class="text-[#9CA3AF] hover:text-[#FF5E13] transition-colors"
aria-label="Edit role"
>
<Pencil size={16} />
</button>
</div>
</td>
</tr>
)}
</For>
@ -290,14 +265,14 @@ export default function InternalRolesListPage() {
<Show when={!data.loading && roles().length > 0}>
<div class="flex items-center justify-between border-t border-[#F3F4F6] px-6 py-4">
<p class="text-[13px] text-[#6B7280]">
Page <span class="font-semibold text-[#111827]">{page()}</span> of <span class="font-semibold text-[#111827]">{totalPages()}</span>
Showing <span class="font-semibold text-[#111827]">{roles().length}</span> of <span class="font-semibold text-[#111827]">{data()?.total ?? 0}</span> roles
</p>
<div class="flex items-center gap-1.5">
<button
type="button"
disabled={page() === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
></button>
<For each={Array.from({ length: Math.min(totalPages(), 5) }, (_, i) => i + 1)}>
{(p) => (
@ -305,7 +280,9 @@ export default function InternalRolesListPage() {
type="button"
onClick={() => setPage(p)}
class={`inline-flex h-8 w-8 items-center justify-center rounded-lg text-[13px] font-medium transition-colors ${
page() === p ? 'bg-[#0D0D2A] text-white' : 'border border-[#E5E7EB] text-[#374151] hover:bg-[#F9FAFB]'
page() === p
? 'bg-[#FF5E13] text-white'
: 'border border-[#E5E7EB] text-[#374151] hover:bg-[#F9FAFB]'
}`}
>{p}</button>
)}
@ -314,13 +291,67 @@ export default function InternalRolesListPage() {
type="button"
disabled={page() >= totalPages()}
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[#E5E7EB] text-[#6B7280] hover:bg-[#F9FAFB] disabled:opacity-40 disabled:cursor-not-allowed transition-colors text-[16px]"
></button>
</div>
</div>
</Show>
</div>
{/* Bottom stats row */}
<div class="grid grid-cols-2 gap-5">
{/* Distribution card */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
<div class="flex items-center justify-between mb-5">
<p class="text-[15px] font-semibold text-[#111827]">Distribution</p>
<button type="button" class="text-[#9CA3AF] hover:text-[#374151]">
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
</button>
</div>
<div class="flex items-end gap-3 h-[100px]">
{[
{ h: 55, color: '#FFD4C2' },
{ h: 80, color: '#FF5E13' },
{ h: 45, color: '#FFD4C2' },
{ h: 65, color: '#FFD4C2' },
{ h: 38, color: '#FFD4C2' },
].map((bar) => (
<div class="flex-1 rounded-t-md" style={{ height: `${bar.h}%`, background: bar.color }} />
))}
</div>
<p class="mt-4 text-[12px] text-[#6B7280]">
Most users are concentrated in <span class="font-semibold text-[#FF5E13]">Sales</span> and{' '}
<span class="font-semibold text-[#FF5E13]">Engineering</span> departments.
</p>
</div>
{/* Audit Readiness Score card */}
<div class="flex items-center justify-between rounded-2xl bg-[#0D0D2A] p-8 shadow-sm">
<div class="flex-1 pr-8">
<p class="text-[18px] font-bold text-white">Audit Readiness Score</p>
<p class="mt-2 text-[13px] leading-relaxed text-[#8AACC8]">
Your organizational permissions currently align with 94% of compliance standards. Review inactive roles to reach 100%.
</p>
<button type="button" class="mt-5 inline-flex h-10 items-center rounded-xl bg-[#FF5E13] px-5 text-[13px] font-semibold text-white hover:bg-[#e54d0a] transition-colors">
Review Audit Log
</button>
</div>
<div class="relative flex h-[100px] w-[100px] shrink-0 items-center justify-center">
<svg class="h-full w-full -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" fill="none" stroke="#1E2D3D" stroke-width="10" />
<circle cx="50" cy="50" r="42" fill="none" stroke="#FF5E13" stroke-width="10"
stroke-dasharray={`${2 * Math.PI * 42 * 0.94} ${2 * Math.PI * 42}`}
stroke-linecap="round" />
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-[22px] font-bold text-white">94%</span>
</div>
</div>
</div>
</div>
</div>
</AdminShell>
);