feat(admin): redesign sidebar, dashboard, dept, designation & roles UI

- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-26 08:01:23 +01:00
parent 244895b241
commit 5b97af4e0f
6 changed files with 1428 additions and 928 deletions

View file

@ -5,9 +5,9 @@ import {
LayoutDashboard, ClipboardList, UserRoundSearch, UserCircle,
Camera, Palette, BookOpen, Code2, BriefcaseBusiness, HandHelping,
WalletCards, CreditCard, Tag, Percent, Receipt, ShoppingCart,
FileCheck, Star, HeadphonesIcon, BarChart3, BookMarked, Bell,
FileCheck, Star, HeadphonesIcon, BarChart3,
ChevronLeft, BadgeCheck, Activity, Film, Utensils, PenTool,
MessageSquare, Megaphone,
Megaphone,
} from 'lucide-solid';
type NavItem = {
@ -20,6 +20,8 @@ type NavItem = {
const GROUPS: NavItem[][] = [
[
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
],
[
{ href: '/admin/department', label: 'Department Management', icon: Building2 },
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
{ href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck },
@ -40,22 +42,21 @@ const GROUPS: NavItem[][] = [
{ href: '/admin/company', label: 'Company Management', icon: Building2 },
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle },
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle },
],
[
{ href: '/admin/photographer', label: 'Photographer Management', icon: Camera },
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette },
{ href: '/admin/tutors', label: 'Tutor Management', icon: BookOpen },
{ href: '/admin/developers', label: 'Developer Management', icon: Code2 },
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity },
{ href: '/admin/graphic-designers', label: 'Graphic Designer Management', icon: PenTool },
{ href: '/admin/social-media-managers', label: 'Social Media Management', icon: Megaphone },
{ href: '/admin/tutors', label: 'Tutors Management', icon: BookOpen },
{ href: '/admin/developers', label: 'Developers Management', icon: Code2 },
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: Film },
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity },
{ href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils },
{ href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: PenTool },
{ href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: Megaphone },
],
[
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness },
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
{ href: '/admin/applications', label: 'Applications Management', icon: ClipboardList },
{ href: '/admin/responses', label: 'Responses Management', icon: MessageSquare },
{ href: '/admin/review', label: 'Review Management', icon: Star },
],
[
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards },
@ -65,31 +66,23 @@ const GROUPS: NavItem[][] = [
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
{ href: '/admin/order', label: 'Order Management', icon: ShoppingCart },
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck },
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
],
[
{ href: '/admin/kb', label: 'Knowledge Base Management', icon: BookMarked },
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
{ href: '/admin/review', label: 'Review Management', icon: Star },
{ href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon },
{ href: '/admin/report', label: 'Report Management', icon: BarChart3 },
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
],
];
const FIGMA_GROUPS: NavItem[][] = [
GROUPS[0],
GROUPS[1],
];
export default function AdminSidebar(props: {
collapsed: boolean;
onToggle: () => void;
onNavigate?: () => void;
adminName: string;
adminInitials: string;
variant?: 'default' | 'figma';
}) {
const location = useLocation();
const groups = () => (props.variant === 'figma' ? FIGMA_GROUPS : GROUPS);
const isActive = (item: NavItem) => {
if (location.pathname === '/admin') return item.href === '/admin';
@ -100,32 +93,50 @@ export default function AdminSidebar(props: {
return (
<aside
class={`flex h-full flex-col border-r border-[#e5e7eb] bg-white shadow-[0px_20px_25px_0px_rgba(0,0,0,0.1),0px_8px_10px_0px_rgba(0,0,0,0.1)] transition-all duration-300 ${
props.collapsed ? 'w-[92px]' : 'w-[288px]'
class={`flex h-full flex-col bg-white border-r border-[#E5E7EB] transition-all duration-300 ${
props.collapsed ? 'w-[72px]' : 'w-[272px]'
}`}
>
<div class="relative h-[101px] shrink-0 border-b border-[#e5e7eb] px-8 pt-8">
<A href="/admin" class={`inline-flex items-center justify-center ${props.collapsed ? 'h-[36px] w-[54px]' : 'h-[20px] w-[84px]'}`} onClick={props.onNavigate}>
<img src={props.collapsed ? '/nxtgauge-icon.png' : '/nxtgauge-logo.png'} alt="Nxtgauge" class={`${props.collapsed ? 'h-[36px]' : 'h-[20px]'} w-auto object-contain`} />
{/* 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`}
/>
</A>
<button
type="button"
onClick={props.onToggle}
class="absolute right-6 top-8 inline-flex h-5 w-5 items-center justify-center text-[#9195ad] hover:text-[#000032]"
aria-label="Toggle sidebar"
>
<ChevronLeft size={20} class={`${props.collapsed ? 'rotate-180' : ''} transition-transform`} />
</button>
<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>
</div>
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto py-6">
<For each={groups()}>
{/* Navigation */}
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-4">
<For each={GROUPS}>
{(group, gi) => (
<>
<Show when={gi() > 0}>
<div class="my-4 mx-6 h-px bg-[#e5e7eb]" />
<div class="my-3 h-px bg-[#E5E7EB]" />
</Show>
<div class="space-y-[2px] pl-4 pr-4">
<div class="space-y-0.5">
<For each={group}>
{(item) => {
const active = () => isActive(item);
@ -135,19 +146,26 @@ export default function AdminSidebar(props: {
href={item.href}
onClick={props.onNavigate}
title={props.collapsed ? item.label : undefined}
class={`relative flex h-12 items-center rounded-[12px] pl-4 pr-3 text-[15px] font-semibold leading-5 transition-colors ${
class={`relative flex h-[40px] w-full items-center rounded-lg text-[13px] font-medium transition-colors ${
props.collapsed ? 'justify-center px-0' : 'px-3'
} ${
active()
? 'bg-[#FFF1EA] text-[#FA5A1F]'
: 'text-[#232B4D] hover:bg-[#F8FAFC]'
? 'bg-[#FFF1EB] text-[#FF5E13]'
: 'text-[#6B7280] hover:bg-[#F3F4F6] hover:text-[#111827]'
}`}
aria-current={active() ? 'page' : undefined}
>
<Show when={active()}>
<span class="absolute left-[-4px] top-2 h-8 w-1 rounded-full bg-[#FA5A1F]" />
{/* 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={20} class={`${active() ? 'text-[#FA5A1F]' : 'text-[#5f6683]'} shrink-0`} strokeWidth={2.4} />
<Icon
size={18}
class={`shrink-0 ${active() ? 'text-[#FF5E13]' : 'text-[#9CA3AF]'}`}
strokeWidth={2}
/>
<Show when={!props.collapsed}>
<span class="ml-4 truncate">{item.label}</span>
<span class="ml-3 truncate">{item.label}</span>
</Show>
</A>
);
@ -159,15 +177,16 @@ export default function AdminSidebar(props: {
</For>
</nav>
<div class="h-[99px] shrink-0 border-t border-[#e5e7eb] bg-[#f9fafb] px-4 pt-[17px]">
<div class={`flex h-[66px] items-center rounded-[14px] border border-[#e5e7eb] bg-white px-[17px] ${props.collapsed ? 'justify-center' : 'gap-3'}`}>
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-[#fa5014] to-[#ff6b3d] text-[14px] font-bold text-white shadow-[0px_4px_6px_0px_rgba(0,0,0,0.1),0px_2px_4px_0px_rgba(0,0,0,0.1)]">
{/* 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">
<p class="truncate text-[14px] font-semibold leading-5 text-[#000032]">{props.adminName}</p>
<p class="text-[12px] leading-4 text-[rgba(0,0,50,0.5)]">Super Admin</p>
<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>

View file

@ -10,23 +10,60 @@ type DepartmentRecord = CrudRecord & {
createdDate?: string;
departmentHead?: string;
departmentEmail?: string;
transfersEnabled?: boolean;
};
const FALLBACK_DEPARTMENTS: DepartmentRecord[] = [
{ id: 'd1', name: 'Marketing', code: 'MKT-002', description: 'Brand management and digital marketing', totalEmployees: 23, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd2', name: 'Human Resources', code: 'HR-003', description: 'Employee relations and talent acquisition', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd3', name: 'Finance', code: 'FIN-004', description: 'Financial planning and accounting', totalEmployees: 18, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd4', name: 'Operations', code: 'OPS-005', description: 'Business operations and process management', totalEmployees: 31, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd5', name: 'Customer Success', code: 'CS-006', description: 'Client support and relationship management', totalEmployees: 27, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd6', name: 'Product', code: 'PRD-007', description: 'Product strategy and development', totalEmployees: 19, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd7', name: 'Sales', code: 'SAL-008', description: 'Revenue generation and client acquisition', totalEmployees: 34, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd8', name: 'Engineering', code: 'ENG-001', description: 'Software development and technical architecture', totalEmployees: 45, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd1', name: 'Engineering', code: 'ENG-001', description: 'Software development and technical architecture', totalEmployees: 45, departmentHead: 'Arun Kumar', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' },
{ id: 'd2', name: 'Marketing', code: 'MKT-002', description: 'Brand management and digital marketing', totalEmployees: 23, departmentHead: 'Priya Sharma', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' },
{ id: 'd3', name: 'Human Resources', code: 'HR-003', description: 'Employee relations and talent acquisition', totalEmployees: 12, departmentHead: 'Rekha Nair', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' },
{ id: 'd4', name: 'Finance', code: 'FIN-004', description: 'Financial planning and accounting', totalEmployees: 18, departmentHead: 'Suresh Menon', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' },
{ id: 'd5', name: 'Operations', code: 'OPS-005', description: 'Business operations and process management', totalEmployees: 31, departmentHead: 'Deepak Verma', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' },
{ id: 'd6', name: 'Customer Success', code: 'CS-006', description: 'Client support and relationship management', totalEmployees: 27, departmentHead: 'Anita Pillai', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' },
{ id: 'd7', name: 'Product', code: 'PRD-007', description: 'Product strategy and development', totalEmployees: 19, departmentHead: 'Kiran Rao', status: 'INACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
{ id: 'd8', name: 'Sales', code: 'SAL-008', description: 'Revenue generation and client acquisition', totalEmployees: 34, departmentHead: 'Manoj Iyer', status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
];
const permissionGroups = [
{ title: 'Employee Management', items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'] },
{ title: 'Role Management', items: ['View Roles', 'Assign Roles'] },
{ title: 'Department Settings', items: ['Manage Department Settings'] },
];
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]'}`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label class="block">
<span class="text-[13px] font-semibold text-[#374151]">
{props.label}{props.required && <span class="ml-0.5 text-[#FF5E13]">*</span>}
</span>
<input
type={props.type ?? 'text'}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
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>
);
}
export default function DepartmentManagementPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [view, setView] = createSignal<'list' | 'form'>('list');
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('all');
const [rows, setRows] = createSignal<DepartmentRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
@ -46,18 +83,17 @@ export default function DepartmentManagementPage() {
const payload = await res.json().catch(() => null);
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
if (list.length > 0) {
setRows(
list.map((item: any, i: number) => ({
id: String(item.id ?? item.department_id ?? `dep-${i + 1}`),
name: String(item.name ?? item.department_name ?? ''),
code: String(item.code ?? item.department_code ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? item.employee_count ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? new Date().toISOString().slice(0, 10)),
createdDate: String(item.createdDate ?? item.created_at ?? new Date().toISOString().slice(0, 10)),
})),
);
setRows(list.map((item: any, i: number) => ({
id: String(item.id ?? `dep-${i + 1}`),
name: String(item.name ?? ''),
code: String(item.code ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
departmentHead: String(item.departmentHead ?? item.department_head ?? ''),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
createdDate: String(item.createdDate ?? item.created_at ?? ''),
})));
return;
}
}
@ -72,259 +108,413 @@ export default function DepartmentManagementPage() {
onMount(() => void load());
const filteredRows = createMemo(() => {
let r = rows();
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
const q = search().toLowerCase();
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q));
return r;
});
const resetForm = () => {
setEditingId(null);
setName('');
setCode('');
setDescription('');
setDepartmentHead('');
setDepartmentEmail('');
setStatus('ACTIVE');
setTransfersEnabled(false);
setCreateTab('general');
setEditingId(null); setName(''); setCode(''); setDescription('');
setDepartmentHead(''); setDepartmentEmail(''); setStatus('ACTIVE');
setTransfersEnabled(false); setFormTab('general');
};
const openCreate = () => {
resetForm();
setMainTab('create');
};
const openCreate = () => { resetForm(); setView('form'); };
const openEdit = (row: DepartmentRecord) => {
setEditingId(row.id);
setName(row.name || '');
setCode(String(row.code || ''));
setName(row.name || ''); setCode(String(row.code || ''));
setDescription(String(row.description || ''));
setDepartmentHead(String(row.departmentHead || ''));
setDepartmentEmail(String(row.departmentEmail || ''));
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
setMainTab('create');
setCreateTab('general');
setTransfersEnabled(Boolean(row.transfersEnabled));
setFormTab('general'); setView('form'); setOpenMenuId(null);
};
const saveDepartment = async () => {
const save = async () => {
const payload: Partial<DepartmentRecord> = {
name: name().trim() || 'New Department',
code: code().trim() || undefined,
description: description().trim(),
departmentHead: departmentHead().trim(),
departmentEmail: departmentEmail().trim(),
status: status(),
name: name().trim() || 'New Department', code: code().trim() || undefined,
description: description().trim(), departmentHead: departmentHead().trim(),
departmentEmail: departmentEmail().trim(), status: status(),
transfersEnabled: transfersEnabled(),
};
if (editingId()) {
await updateModuleRecord<DepartmentRecord>('department', editingId()!, payload);
} else {
await createModuleRecord<DepartmentRecord>('department', payload);
}
setMainTab('all');
setOpenMenuId(null);
resetForm();
await load();
setView('list'); resetForm(); await load();
};
const filteredRows = createMemo(() => rows() ?? []);
const permissionGroups = [
{
title: 'Employee Management',
items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'],
},
{
title: 'Role Management',
items: ['View Roles', 'Assign Roles'],
},
{
title: 'Department Settings',
items: ['Manage Department Settings'],
},
];
const formatDate = (value?: string) => {
const input = value || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
const fallback = input || new Date().toISOString().slice(0, 10);
return fallback.slice(0, 10);
const formatDate = (v?: string) => {
const s = v || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
return s.slice(0, 10) || '—';
};
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),
}));
return (
<AdminShell>
<div class="space-y-5">
<section>
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Department Management</h1>
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all departments and organizational structure</p>
</section>
<div class="w-full space-y-8 pb-8">
<section class="overflow-hidden rounded-[24px] border border-[#d9dde6] bg-[#f7f7f8]">
<div class="flex items-center gap-2 border-b border-[#e1e5ee] px-5 pt-4">
<button onClick={() => setMainTab('all')} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'all' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
All Departments
<Show when={mainTab() === 'all'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
</button>
<button onClick={openCreate} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'create' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
{editingId() ? 'Edit Department' : 'Create Department'}
<Show when={mainTab() === 'create'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
</button>
{/* Page header */}
<div class="flex items-end justify-between">
<div>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">
{view() === 'form' ? 'Department Management' : 'Organisation'}
</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">
{view() === 'form' ? (editingId() ? 'Edit Department' : 'Create Department') : 'Department Management'}
</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">
{view() === 'form'
? 'Dashboard / Department Management / ' + (editingId() ? 'Edit Department' : 'Create Department')
: 'Manage all departments and organisational structure'}
</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 Department
</button>
</Show>
</div>
<Show when={mainTab() === 'all'}>
<div class="space-y-5 p-5">
<label class="flex h-[48px] items-center rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#8a90a8]">
<svg class="mr-3 h-5 w-5 text-[#8a90a8]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
<input
value={search()}
onInput={(e) => {
setSearch(e.currentTarget.value);
void load();
}}
placeholder="Search departments..."
class="w-full border-0 bg-transparent text-[16px] text-[#1a2147] outline-none placeholder:text-[#8a90a8]"
/>
</label>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<div class="relative rounded-[18px] border border-[#d8dce6] bg-[#f7f7f8]">
<table class="min-w-full table-fixed text-left">
<thead class="bg-[#030047] text-white">
<tr>
<th class="w-[18%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT NAME</th>
<th class="w-[14%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT CODE</th>
<th class="w-[31%] px-6 py-4 text-[14px] font-semibold">DESCRIPTION</th>
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">TOTAL EMPLOYEES</th>
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">STATUS</th>
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">CREATED DATE</th>
<th class="w-[7%] px-6 py-4 text-[14px] font-semibold">ACTIONS</th>
{/* Summary cards */}
<div class="grid grid-cols-4 gap-5">
{[
{ label: 'Total Departments', 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-[18px] 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 class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Filters */}
<div class="flex items-center gap-3 border-b border-[#F3F4F6] p-5">
{/* Search */}
<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
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search departments..."
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>
{/* Status filter */}
<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> departments
</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]">Department 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 Head</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-[#dde1ea] text-[#222948]">
<For each={filteredRows()}>
{(row) => (
<tr class="bg-[#f7f7f8]">
<td class="px-6 py-4 text-[15px] font-semibold">{row.name}</td>
<td class="px-6 py-4 text-[15px] font-medium text-[#505779]">{String(row.code || '')}</td>
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.description || '')}</td>
<td class="px-6 py-4 text-[15px] font-semibold">{Number(row.totalEmployees || 0)}</td>
<td class="px-6 py-4">
<span class={`inline-flex rounded-[10px] border px-3 py-1.5 text-[14px] font-semibold ${row.status === 'ACTIVE' ? 'border-[#ffc2aa] bg-[#ffeee6] text-[#fd6116]' : 'border-[#c7ccda] bg-[#eceff6] text-[#101848]'}`}>
{row.status === 'ACTIVE' ? 'Active' : 'Inactive'}
</span>
</td>
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td class="relative px-6 py-4">
<button onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} class="inline-flex h-10 w-10 items-center justify-center rounded-lg text-[#6c7292] hover:bg-[#eceff5]" aria-label="More actions">
<span class="text-[20px] leading-none"></span>
<tbody class="divide-y divide-[#F3F4F6]">
<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="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>
<Show when={openMenuId() === row.id}>
<div class="absolute right-6 top-14 z-20 w-[220px] rounded-2xl border border-[#d6dbe6] bg-white p-2 shadow-[0_16px_28px_rgba(5,0,38,0.16)]">
<button onClick={() => openEdit(row)} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]">
<svg class="h-5 w-5 text-[#fd6116]" 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 Department
</button>
<button onClick={async () => { await updateModuleRecord<DepartmentRecord>('department', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]">
<svg class="h-5 w-5 text-[#fd6116]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M8 8l8 8" /></svg>
{row.status === 'ACTIVE' ? 'Deactivate Department' : 'Activate Department'}
</button>
<button onClick={async () => { await deleteModuleRecord('department', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]">
<svg class="h-5 w-5 text-[#fd6116]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18" /><path d="M8 6V4h8v2" /><path d="M19 6l-1 14H6L5 6" /><path d="M10 11v6M14 11v6" /></svg>
Delete Department
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
}
>
<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>
<p class="mt-0.5 text-[12px] text-[#9CA3AF] line-clamp-1">{String(row.description || '')}</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.departmentHead || '—')}</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>
</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 Department
</button>
<button
type="button"
onClick={async () => { await updateModuleRecord<DepartmentRecord>('department', 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('department', 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 class="flex items-center justify-between border-t border-[#dde1ea] px-6 py-4">
<p class="text-[14px] text-[#707895]">Showing <span class="font-semibold text-[#283055]">1-{rows().length}</span> of <span class="font-semibold text-[#283055]">{rows().length}</span> departments</p>
<div class="flex items-center gap-2">
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] bg-[#fd6116] font-semibold text-white">1</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] font-semibold text-[#2a3052]">2</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] font-semibold text-[#2a3052]">3</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
</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> departments
</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>
</div>
</Show>
</div>
</Show>
{/* ── FORM VIEW (Create / Edit) ── */}
<Show when={view() === 'form'}>
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Tab nav */}
<div class="flex items-center gap-1 border-b border-[#F3F4F6] px-6">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Department Settings', 'Permissions'];
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]'
}`}
>
{labels[i]}
<Show when={formTab() === tab}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</Show>
</button>
);
})}
</div>
<div class="p-6">
{/* General Information */}
<Show when={formTab() === 'general'}>
<div class="space-y-5">
<div class="grid grid-cols-2 gap-5">
<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>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this department's purpose..."
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>
<div class="grid grid-cols-2 gap-5">
<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>
</div>
</Show>
{/* Department Settings */}
<Show when={formTab() === 'settings'}>
<div class="space-y-6">
<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-3 flex gap-2">
{(['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]'
}`}
>
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
</button>
))}
</div>
</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-3 grid grid-cols-2 gap-3">
{[
{ key: 'internal', label: 'Internal', desc: 'Visible to internal employees only' },
{ key: 'external', label: 'External', desc: 'Visible to external users and partners' },
].map((opt) => (
<div class="flex items-start gap-3 rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div class="mt-0.5 h-4 w-4 shrink-0 rounded-full border-2 border-[#E5E7EB] bg-white" />
<div>
<p class="text-[13px] font-semibold text-[#111827]">{opt.label}</p>
<p class="mt-0.5 text-[12px] text-[#6B7280]">{opt.desc}</p>
</div>
</div>
))}
</div>
</div>
<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]">Allow Employee Transfers</p>
<p class="mt-0.5 text-[12px] text-[#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]'}`}
>
<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'}`} />
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === 'permissions'}>
<div class="space-y-6">
<p class="text-[13px] text-[#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-2">
<For each={group.items}>
{(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>
</div>
)}
</For>
</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"
>
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 Department' : 'Create Department'}
</button>
</div>
</div>
</Show>
<Show when={mainTab() === 'create'}>
<div class="space-y-6 p-5">
<div class="flex items-center gap-2 border-b border-[#e1e5ee] pb-3">
<button onClick={() => setCreateTab('general')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'general' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>General Information<Show when={createTab() === 'general'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
<button onClick={() => setCreateTab('settings')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'settings' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Department Settings<Show when={createTab() === 'settings'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
<button onClick={() => setCreateTab('permissions')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'permissions' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Permissions<Show when={createTab() === 'permissions'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
</div>
<Show when={createTab() === 'general'}>
<div class="space-y-5">
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Department Name <span class="text-[#fd6116]">*</span><input value={name()} onInput={(e) => setName(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department name" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Department Code <span class="text-[#fd6116]">*</span><input value={code()} onInput={(e) => setCode(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="e.g., ENG-001" /></label>
</div>
<label class="block text-[14px] font-semibold text-[#101848]">Department Description<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} class="mt-2 h-[110px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 py-3 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department description" /></label>
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Department Head<input value={departmentHead()} onInput={(e) => setDepartmentHead(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Department Email<input value={departmentEmail()} onInput={(e) => setDepartmentEmail(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="department@example.com" /></label>
</div>
</div>
</Show>
<Show when={createTab() === 'settings'}>
<div class="space-y-6">
<div>
<p class="text-[18px] font-semibold text-[#101848]">Department Status</p>
<div class="mt-3 flex gap-3">
<button onClick={() => setStatus('ACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'ACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Active</button>
<button onClick={() => setStatus('INACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'INACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Inactive</button>
</div>
</div>
<div>
<p class="text-[18px] font-semibold text-[#101848]">Department Visibility</p>
<div class="mt-3 space-y-3">
<div class="rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<p class="text-[16px] font-semibold text-[#101848]">Internal</p>
<p class="text-[14px] text-[#7d849f]">Only visible to internal employees</p>
</div>
<div class="rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<p class="text-[16px] font-semibold text-[#101848]">External</p>
<p class="text-[14px] text-[#7d849f]">Visible to external users and partners</p>
</div>
</div>
</div>
<div class="flex items-center justify-between rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Employee Transfers</p><p class="text-[14px] text-[#7d849f]">Enable employees to request transfer to this department</p></div>
<button onClick={() => setTransfersEnabled((v) => !v)} class={`relative h-9 w-16 rounded-full transition ${transfersEnabled() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-7 w-7 rounded-full bg-white transition ${transfersEnabled() ? 'left-8' : 'left-1'}`} /></button>
</div>
</div>
</Show>
<Show when={createTab() === 'permissions'}>
<div class="space-y-4">
<p class="text-[16px] text-[#707895]">Select permissions for this department</p>
<For each={permissionGroups}>
{(group) => (
<section class="space-y-3">
<h3 class="text-[18px] font-semibold text-[#101848]">{group.title}</h3>
<For each={group.items}>
{(item) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{item}</div>}
</For>
</section>
)}
</For>
</div>
</Show>
<div class="flex justify-end gap-3 border-t border-[#e1e5ee] pt-4">
<button onClick={() => { setMainTab('all'); resetForm(); }} class="h-[44px] rounded-[12px] border border-[#d2d8e4] bg-[#f7f7f8] px-6 text-[16px] font-semibold text-[#232b4d]">Cancel</button>
<button onClick={() => void saveDepartment()} class="h-[44px] rounded-[12px] bg-[#030047] px-8 text-[16px] font-semibold text-white">{editingId() ? 'Save Department' : 'Create Department'}</button>
</div>
</div>
</Show>
</section>
</div>
</AdminShell>
);

View file

@ -15,16 +15,69 @@ type DesignationRecord = CrudRecord & {
};
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' },
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' },
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' },
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-05' },
{ id: 'z5', name: 'Product Manager', code: 'PM-005', department: 'Product', level: 'Manager', totalEmployees: 6, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
{ 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 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]'}`} />
{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 Toggle(props: { value: boolean; onChange: () => void; label: string; desc: string }) {
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]'}`}
>
<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>
);
}
export default function DesignationManagementPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [view, setView] = createSignal<'list' | 'form'>('list');
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [search, setSearch] = createSignal('');
const [deptFilter, setDeptFilter] = createSignal('all');
const [statusFilter, setStatusFilter] = createSignal('all');
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
@ -45,20 +98,18 @@ export default function DesignationManagementPage() {
const payload = await res.json().catch(() => null);
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
if (list.length > 0) {
setRows(
list.map((item: any, i: number) => ({
id: String(item.id ?? item.designation_id ?? `des-${i + 1}`),
name: String(item.name ?? item.designation_name ?? ''),
code: String(item.code ?? item.designation_code ?? ''),
department: String(item.department ?? item.department_name ?? ''),
level: String(item.level ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? item.employee_count ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? new Date().toISOString().slice(0, 10)),
createdDate: String(item.createdDate ?? item.created_at ?? new Date().toISOString().slice(0, 10)),
})),
);
setRows(list.map((item: any, i: number) => ({
id: String(item.id ?? `des-${i + 1}`),
name: String(item.name ?? ''),
code: String(item.code ?? ''),
department: String(item.department ?? item.department_name ?? ''),
level: String(item.level ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
createdDate: String(item.createdDate ?? item.created_at ?? ''),
})));
return;
}
}
@ -70,221 +121,382 @@ export default function DesignationManagementPage() {
setRows(FALLBACK_DESIGNATIONS);
}
};
onMount(() => void load());
const filteredRows = createMemo(() => {
let r = rows();
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
if (deptFilter() !== 'all') r = r.filter((d) => d.department === deptFilter());
const q = search().toLowerCase();
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q));
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);
setCreateTab('general');
setEditingId(null); setName(''); setCode(''); setDepartment('');
setLevel(''); setDescription(''); setStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false); setFormTab('general');
};
const openCreate = () => {
resetForm();
setMainTab('create');
};
const openCreate = () => { resetForm(); setView('form'); };
const openEdit = (row: DesignationRecord) => {
setEditingId(row.id);
setName(row.name || '');
setCode(String(row.code || ''));
setDepartment(String(row.department || ''));
setLevel(String(row.level || ''));
setName(row.name || ''); setCode(String(row.code || ''));
setDepartment(String(row.department || '')); setLevel(String(row.level || ''));
setDescription(String(row.description || ''));
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
setCanManageTeam(Boolean(row.canManageTeam));
setCanApprove(Boolean(row.canApprove));
setMainTab('create');
setCreateTab('general');
setCanManageTeam(Boolean(row.canManageTeam)); setCanApprove(Boolean(row.canApprove));
setFormTab('general'); setView('form'); setOpenMenuId(null);
};
const saveDesignation = async () => {
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(),
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);
}
setMainTab('all');
setOpenMenuId(null);
resetForm();
await load();
setView('list'); resetForm(); await load();
};
const filteredRows = createMemo(() => rows() ?? []);
const formatDate = (value?: string) => {
const input = value || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
return (input || new Date().toISOString().slice(0, 10)).slice(0, 10);
const formatDate = (v?: string) => {
const s = v || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
return s.slice(0, 10) || '—';
};
return (
<AdminShell>
<div class="space-y-5">
<section>
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Designation Management</h1>
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all designations and job positions</p>
</section>
<div class="w-full space-y-8 pb-8">
<section class="overflow-hidden rounded-[24px] border border-[#d9dde6] bg-[#f7f7f8]">
<div class="flex items-center gap-2 border-b border-[#e1e5ee] px-5 pt-4">
<button onClick={() => setMainTab('all')} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'all' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
All Designations
<Show when={mainTab() === 'all'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
</button>
<button onClick={openCreate} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'create' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
{editingId() ? 'Edit Designation' : 'Create Designation'}
<Show when={mainTab() === 'create'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
</button>
{/* 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>
<Show when={mainTab() === 'all'}>
<div class="space-y-5 p-5">
<div class="grid gap-3 md:grid-cols-[1fr_190px_130px]">
<label class="flex h-[48px] items-center rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#8a90a8]">
<svg class="mr-3 h-5 w-5 text-[#8a90a8]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
<input value={search()} onInput={(e) => { setSearch(e.currentTarget.value); void load(); }} placeholder="Search designations..." class="w-full border-0 bg-transparent text-[16px] text-[#1a2147] outline-none placeholder:text-[#8a90a8]" />
</label>
<div class="h-[48px] rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8]" />
<div class="h-[48px] rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8]" />
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
{/* 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 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>
<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"
/>
</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>
<div class="relative rounded-[18px] border border-[#d8dce6] bg-[#f7f7f8]">
<table class="min-w-full table-fixed text-left">
<thead class="bg-[#030047] text-white">
<tr>
<th class="w-[17%] px-6 py-4 text-[14px] font-semibold">DESIGNATION NAME</th>
<th class="w-[16%] px-6 py-4 text-[14px] font-semibold">DESIGNATION CODE</th>
<th class="w-[18%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT</th>
<th class="w-[12%] px-6 py-4 text-[14px] font-semibold">LEVEL</th>
<th class="w-[11%] px-6 py-4 text-[14px] font-semibold">TOTAL EMPLOYEES</th>
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">STATUS</th>
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">CREATED DATE</th>
<th class="w-[6%] px-6 py-4 text-[14px] font-semibold">ACTIONS</th>
{/* 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-[#dde1ea] text-[#222948]">
<For each={filteredRows()}>
{(row) => (
<tr class="bg-[#f7f7f8]">
<td class="px-6 py-4 text-[15px] font-semibold">{row.name}</td>
<td class="px-6 py-4 text-[15px] font-medium text-[#505779]">{String(row.code || '')}</td>
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.department || '')}</td>
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.level || '')}</td>
<td class="px-6 py-4 text-[15px] font-semibold">{Number(row.totalEmployees || 0)}</td>
<td class="px-6 py-4"><span class={`inline-flex rounded-[10px] border px-3 py-1.5 text-[14px] font-semibold ${row.status === 'ACTIVE' ? 'border-[#ffc2aa] bg-[#ffeee6] text-[#fd6116]' : 'border-[#c7ccda] bg-[#eceff6] text-[#101848]'}`}>{row.status === 'ACTIVE' ? 'Active' : 'Inactive'}</span></td>
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
<td class="relative px-6 py-4">
<button onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} class="inline-flex h-10 w-10 items-center justify-center rounded-lg text-[#6c7292] hover:bg-[#eceff5]" aria-label="More actions"><span class="text-[20px] leading-none"></span></button>
<Show when={openMenuId() === row.id}>
<div class="absolute right-6 top-14 z-20 w-[220px] rounded-2xl border border-[#d6dbe6] bg-white p-2 shadow-[0_16px_28px_rgba(5,0,38,0.16)]">
<button onClick={() => openEdit(row)} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]"></span>Edit Designation</button>
<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-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]"></span>{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}</button>
<button onClick={async () => { await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">🗑</span>Delete</button>
</div>
</Show>
<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>
)}
</For>
}
>
<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>
</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>
<div class="flex items-center justify-between border-t border-[#dde1ea] px-6 py-4">
<p class="text-[14px] text-[#707895]">Showing <span class="font-semibold text-[#283055]">1-{rows().length}</span> of <span class="font-semibold text-[#283055]">{rows().length}</span> designations</p>
<div class="flex items-center gap-2">
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] bg-[#fd6116] font-semibold text-white">1</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
{/* 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>
</div>
</Show>
</div>
</Show>
{/* ── FORM VIEW ── */}
<Show when={view() === 'form'}>
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
{/* Tab nav */}
<div class="flex items-center gap-1 border-b border-[#F3F4F6] px-6">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Designation Settings', 'Permissions'];
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]'
}`}
>
{labels[i]}
<Show when={formTab() === tab}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</Show>
</button>
);
})}
</div>
<div class="p-6">
{/* 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>
<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>
<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>
</div>
</Show>
{/* Designation Settings */}
<Show when={formTab() === 'settings'}>
<div class="space-y-5">
<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">
{(['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]'
}`}
>
{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>
</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>
</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">
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>
</div>
</div>
</Show>
<Show when={mainTab() === 'create'}>
<div class="space-y-6 p-5">
<div class="flex items-center gap-2 border-b border-[#e1e5ee] pb-3">
<button onClick={() => setCreateTab('general')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'general' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>General Information<Show when={createTab() === 'general'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
<button onClick={() => setCreateTab('settings')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'settings' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Designation Settings<Show when={createTab() === 'settings'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
<button onClick={() => setCreateTab('permissions')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'permissions' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Permissions<Show when={createTab() === 'permissions'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
</div>
<Show when={createTab() === 'general'}>
<div class="space-y-5">
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Designation Name <span class="text-[#fd6116]">*</span><input value={name()} onInput={(e) => setName(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="Enter designation name" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Designation Code <span class="text-[#fd6116]">*</span><input value={code()} onInput={(e) => setCode(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="e.g., SSE-001" /></label>
</div>
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Department <span class="text-[#fd6116]">*</span><input value={department()} onInput={(e) => setDepartment(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Designation Level <span class="text-[#fd6116]">*</span><input value={level()} onInput={(e) => setLevel(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
</div>
<label class="block text-[14px] font-semibold text-[#101848]">Description<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} class="mt-2 h-[110px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 py-3 text-[16px] text-[#1a2147] outline-none" placeholder="Enter designation description" /></label>
</div>
</Show>
<Show when={createTab() === 'settings'}>
<div class="space-y-6">
<div>
<p class="text-[18px] font-semibold text-[#101848]">Designation Status</p>
<div class="mt-3 flex gap-3">
<button onClick={() => setStatus('ACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'ACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Active</button>
<button onClick={() => setStatus('INACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'INACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Inactive</button>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-5 py-4">
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Designation to Manage Team Members</p><p class="text-[14px] text-[#7d849f]">Enable this designation to manage team members</p></div>
<button onClick={() => setCanManageTeam((v) => !v)} class={`relative h-8 w-14 rounded-full transition ${canManageTeam() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-6 w-6 rounded-full bg-white transition ${canManageTeam() ? 'left-7' : 'left-1'}`} /></button>
</div>
<div class="flex items-center justify-between rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-5 py-4">
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Approval Permissions</p><p class="text-[14px] text-[#7d849f]">Enable this designation to approve requests</p></div>
<button onClick={() => setCanApprove((v) => !v)} class={`relative h-8 w-14 rounded-full transition ${canApprove() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-6 w-6 rounded-full bg-white transition ${canApprove() ? 'left-7' : 'left-1'}`} /></button>
</div>
</div>
</div>
</Show>
<Show when={createTab() === 'permissions'}>
<div class="space-y-5">
<p class="text-[16px] text-[#707895]">Select permissions for this designation</p>
<div>
<h3 class="text-[18px] font-semibold text-[#11194a]">Employee Management</h3>
<div class="mt-3 space-y-3">
<For each={['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees']}>{(label) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{label}</div>}</For>
</div>
</div>
<div>
<h3 class="text-[18px] font-semibold text-[#11194a]">Additional Permissions</h3>
<div class="mt-3 rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">Assign Roles</div>
</div>
</div>
</Show>
<div class="flex justify-end gap-3 border-t border-[#e1e5ee] pt-4">
<button onClick={() => { setMainTab('all'); resetForm(); }} class="h-[44px] rounded-[12px] border border-[#d2d8e4] bg-[#f7f7f8] px-6 text-[16px] font-semibold text-[#232b4d]">Cancel</button>
<button onClick={() => void saveDesignation()} class="h-[44px] rounded-[12px] bg-[#030047] px-8 text-[16px] font-semibold text-white">{editingId() ? 'Save Designation' : 'Create Designation'}</button>
</div>
</div>
</Show>
</section>
</div>
</AdminShell>
);

View file

@ -1,30 +1,29 @@
import { For } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { ActionButton } from '~/components/admin/AdminUi';
import { Eye, Pencil, Trash2 } from 'lucide-solid';
import { Eye, GripVertical, LayoutDashboard } from 'lucide-solid';
const kpis = [
{ title: 'Total Users', value: '12,458', delta: '+12.5%', note: '+1,245 from last month', tone: 'up' as const, icon: 'users' as const },
{ title: 'Active Companies', value: '1,234', delta: '+8.2%', note: '+94 from last month', tone: 'up' as const, icon: 'building' as const },
{ title: 'Open Leads', value: '847', delta: '-3.1%', note: '-27 from last month', tone: 'down' as const, icon: 'trend' as const },
{ title: 'Credits Purchased', value: '$45,890', delta: '+18.7%', note: '+$7,234 from last month', tone: 'up' as const, icon: 'card' as const },
{ title: 'Total Users', value: '12,458', delta: '+12.5%', note: '+1,245 this month', tone: 'up' as const, icon: 'users' as const },
{ title: 'Active Companies', value: '1,234', delta: '+8.2%', note: '+94 this month', tone: 'up' as const, icon: 'building' as const },
{ title: 'Open Leads', value: '847', delta: '-3.1%', note: '27 fewer than last month', tone: 'down' as const, icon: 'trend' as const },
{ title: 'Credits Purchased', value: '45,890', delta: '+18.7%', note: '₹7,234 more this month', tone: 'up' as const, icon: 'card' as const },
];
const trendSeries = [62, 70, 81, 75, 88, 102];
const revSeries = [42000, 48000, 55000, 51000, 62000, 69000];
const maxAmount = 80000;
const recentLeads = [
{ title: 'Website Redesign Project', customer: 'TechCorp Inc.', category: 'Developers', budget: '$15,000', status: 'Active' },
{ title: 'Corporate Event Photography', customer: 'EventMasters LLC', category: 'Photographer', budget: '$3,500', status: 'Pending' },
{ title: 'Marketing Campaign Design', customer: 'BrandHub Co.', category: 'Graphics Designer', budget: '$8,200', status: 'Active' },
{ title: 'Social Media Management', customer: 'GrowthStart', category: 'Social Media Manager', budget: '$5,000', status: 'Negotiating' },
{ title: 'Website Redesign Project', customer: 'TechCorp Inc.', category: 'Developers', budget: '15,000', status: 'Active' },
{ title: 'Corporate Event Photography', customer: 'EventMasters LLC', category: 'Photographer', budget: '3,500', status: 'Pending' },
{ title: 'Marketing Campaign Design', customer: 'BrandHub Co.', category: 'Graphic Designer', budget: '8,200', status: 'Active' },
{ title: 'Social Media Management', customer: 'GrowthStart', category: 'Social Media Manager', budget: '5,000', status: 'Negotiating' },
];
function KpiIcon(props: { kind: 'users' | 'building' | 'trend' | 'card' }) {
const common = 'h-10 w-10 text-[#fd6116]';
if (props.kind === 'users') {
return (
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<path d="M8 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
<path d="M16 11a3 3 0 1 0 0-6" />
<path d="M3.5 20a5 5 0 0 1 9 0" />
@ -34,7 +33,7 @@ function KpiIcon(props: { kind: 'users' | 'building' | 'trend' | 'card' }) {
}
if (props.kind === 'building') {
return (
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<rect x="4" y="3" width="16" height="18" rx="2.5" />
<path d="M8 7h2M12 7h2M8 11h2M12 11h2M8 15h2M12 15h2M11 21v-3h2v3" />
</svg>
@ -42,217 +41,253 @@ function KpiIcon(props: { kind: 'users' | 'building' | 'trend' | 'card' }) {
}
if (props.kind === 'trend') {
return (
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<path d="m3 16 6-6 4 4 8-8" />
<path d="M16 6h5v5" />
</svg>
);
}
return (
<svg class={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2.5" />
<path d="M3 10h18" />
</svg>
);
}
function DragHandle() {
return (
<button
type="button"
class="cursor-grab touch-none text-[#D1D5DB] hover:text-[#9CA3AF] transition-colors"
aria-label="Drag to reorder"
>
<GripVertical size={18} />
</button>
);
}
export default function AdminHomePage() {
return (
<AdminShell>
<div class="w-full space-y-6">
<section class="flex items-center justify-between">
<div>
<h1 class="text-[24px] font-bold leading-[32px] text-[#000032]">Dashboard Overview</h1>
<p class="mt-1 text-[12px] leading-4 text-[rgba(0,0,50,0.6)]">Welcome back! Here&apos;s what&apos;s happening with your platform today.</p>
</div>
<ActionButton tone="primary" class="h-10 rounded-2xl px-5 text-[14px] font-medium">
<span class="inline-flex items-center gap-2">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 3v11" />
<path d="m8 10 4 4 4-4" />
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
</svg>
Export Report
</span>
</ActionButton>
</section>
<div class="w-full space-y-8 pb-8">
<section class="grid grid-cols-4 gap-4">
<For each={kpis}>
{(item) => (
<article class="min-h-[182px] rounded-2xl border-2 border-[#e5e7eb] bg-white px-[22px] pb-[2px] pt-[22px]">
<div class="flex items-center justify-between">
<div class="h-10 w-10">
<KpiIcon kind={item.icon} />
</div>
<span
class={`inline-flex h-6 items-center gap-1 rounded-[10px] px-[10px] text-[12px] font-bold ${
item.tone === 'up' ? 'bg-[rgba(250,80,20,0.1)] text-[#fa5014]' : 'bg-[rgba(0,0,50,0.1)] text-[#000032]'
}`}
>
<span class="leading-none">{item.tone === 'up' ? '↗' : '↘'}</span>
{item.delta}
</span>
</div>
<p class="mt-3 text-[12px] font-medium leading-4 text-[rgba(0,0,50,0.6)]">{item.title}</p>
<p class="mt-1 text-[24px] font-bold leading-[32px] tracking-[-0.01em] text-[#000032]">{item.value}</p>
<p class="mt-2 text-[12px] leading-4 text-[rgba(0,0,50,0.5)]">{item.note}</p>
</article>
)}
</For>
</section>
<section class="grid grid-cols-2 gap-4">
<article class="rounded-2xl border-2 border-[#e5e7eb] bg-white px-[22px] pb-[2px] pt-[22px] shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_0px_rgba(0,0,0,0.1)]">
<h2 class="text-[18px] font-bold leading-7 text-[#000032]">Leads Trend</h2>
<p class="mt-0.5 text-[12px] leading-4 text-[rgba(0,0,50,0.6)]">Monthly leads performance overview</p>
<div class="mt-4">
<div class="grid grid-cols-[52px_1fr] gap-3">
<div class="flex h-64 flex-col justify-between pb-8 text-right text-xs font-semibold text-[#283055]">
<span>120</span>
<span>90</span>
<span>60</span>
<span>30</span>
<span>0</span>
</div>
<div>
<div class="relative h-64">
<div class="absolute inset-0">
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-dashed border-[#d9dde6]" />}</For>
</div>
<svg viewBox="0 0 100 40" class="relative h-full w-full overflow-visible" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#fd6116" stop-opacity="0.28" />
<stop offset="100%" stop-color="#fd6116" stop-opacity="0.02" />
</linearGradient>
</defs>
<polyline
fill="none"
stroke="#050026"
stroke-width="1"
points={trendSeries.map((v, i) => `${i * 20},${40 - v / 3}`).join(' ')}
/>
<polygon
fill="url(#trendFill)"
points={`0,40 ${trendSeries.map((v, i) => `${i * 20},${40 - v / 3}`).join(' ')} 100,40`}
/>
</svg>
</div>
<div class="mt-2 grid grid-cols-6 text-center text-xs font-semibold text-[#3f4562]">
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>{(month) => <span>{month}</span>}</For>
</div>
<div class="mt-4 flex items-center justify-center gap-6 text-[14px] font-medium">
<span class="inline-flex items-center gap-2 text-[#fd6116]"><span class="h-2.5 w-2.5 rounded-full bg-[#fd6116]" />Total Leads</span>
<span class="inline-flex items-center gap-2 text-[#050026]"><span class="h-2.5 w-2.5 rounded-full bg-[#050026]" />Converted</span>
</div>
</div>
</div>
</div>
</article>
<article class="rounded-2xl border-2 border-[#e5e7eb] bg-white px-[22px] pb-[2px] pt-[22px] shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_0px_rgba(0,0,0,0.1)]">
<h2 class="text-[18px] font-bold leading-7 text-[#000032]">Revenue Overview</h2>
<p class="mt-0.5 text-[12px] leading-4 text-[rgba(0,0,50,0.6)]">Monthly revenue vs expenses comparison</p>
<div class="mt-4">
<div class="grid grid-cols-[88px_1fr] gap-3">
<div class="flex h-64 flex-col justify-between pb-8 text-right text-xs font-semibold text-[#283055]">
<span>80000</span>
<span>60000</span>
<span>40000</span>
<span>20000</span>
<span>0</span>
</div>
<div>
<div class="relative h-64">
<div class="absolute inset-0">
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-dashed border-[#d9dde6]" />}</For>
</div>
<div class="relative flex h-full items-end gap-4 px-2">
<For each={revSeries}>
{(value) => (
<div class="flex h-full flex-1 items-end justify-center">
<div class="w-3 rounded-t bg-[#050026]" style={{ height: `${(value / maxAmount) * 100}%` }} />
</div>
)}
</For>
</div>
</div>
<div class="mt-2 grid grid-cols-6 text-center text-xs font-semibold text-[#3f4562]">
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>{(month) => <span>{month}</span>}</For>
</div>
<div class="mt-4 flex items-center justify-center gap-6 text-[14px] font-medium">
<span class="inline-flex items-center gap-2 text-[#fd6116]"><span class="h-2.5 w-2.5 rounded-full bg-[#fd6116]" />Revenue</span>
<span class="inline-flex items-center gap-2 text-[#050026]"><span class="h-2.5 w-2.5 rounded-full bg-[#050026]" />Expenses</span>
</div>
</div>
</div>
</div>
</article>
</section>
<section class="overflow-hidden rounded-2xl border-2 border-[#e5e7eb] bg-white shadow-[0px_1px_3px_0px_rgba(0,0,0,0.1),0px_1px_2px_-1px_rgba(0,0,0,0.1)]">
<div class="flex h-20 items-center justify-between border-b-2 border-[#e5e7eb] bg-[linear-gradient(90deg,#ffffff_0%,#fdfdfe_33%,#fbfcfc_66%,#f9fafb_100%)] px-6">
{/* Page header */}
<div class="flex items-end justify-between">
<div>
<h2 class="text-[18px] font-bold leading-7 text-[#000032]">Recent Leads</h2>
<p class="text-[12px] leading-4 text-[rgba(0,0,50,0.6)]">Latest customer inquiries and opportunities</p>
<p class="text-[12px] font-semibold uppercase tracking-widest text-[#FF5E13]">Overview</p>
<h1 class="mt-1 text-[28px] font-bold leading-tight text-[#111827]">Dashboard</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Welcome back here's what's happening on your platform today.</p>
</div>
<ActionButton tone="primary" class="h-9 rounded-2xl px-5 text-[12px] font-semibold">View All Leads</ActionButton>
<button
type="button"
class="inline-flex items-center gap-2 rounded-xl border border-[#E5E7EB] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#374151] shadow-sm hover:border-[#FF5E13] hover:text-[#FF5E13] transition-colors"
>
<LayoutDashboard size={15} />
Customise Dashboard
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead class="border-b-2 border-[#e5e7eb] bg-[#f9fafb] text-left">
<tr class="h-[41px] text-[12px] font-bold tracking-[0.6px] text-[rgba(0,0,50,0.6)]">
<th class="px-6 py-0 uppercase">LEAD TITLE</th>
<th class="px-6 py-0 uppercase">CUSTOMER</th>
<th class="px-6 py-0 uppercase">CATEGORY</th>
<th class="px-6 py-0 uppercase">BUDGET</th>
<th class="px-6 py-0 uppercase">STATUS</th>
<th class="px-6 py-0 uppercase">ACTIONS</th>
</tr>
</thead>
<tbody class="divide-y divide-[#e5e7eb]">
<For each={recentLeads}>
{(lead, index) => (
<tr class={index() < 3 ? 'h-[89px]' : 'h-[68px]'}>
<td class="px-6 py-0 text-[14px] font-semibold leading-5 text-[#000032]">{lead.title}</td>
<td class="px-6 py-0 text-[14px] font-normal leading-5 text-[rgba(0,0,50,0.8)]">{lead.customer}</td>
<td class="px-6 py-0 text-[14px] font-normal leading-5 text-[rgba(0,0,50,0.6)]">{lead.category}</td>
<td class="px-6 py-0 text-[14px] font-bold leading-5 text-[#000032]">{lead.budget}</td>
<td class="px-6 py-4">
<span
class={`inline-flex h-[30px] items-center rounded-[10px] border px-3 text-[12px] font-bold ${
lead.status === 'Active'
? 'border-[rgba(250,80,20,0.2)] bg-[rgba(250,80,20,0.1)] text-[#fa5014]'
: lead.status === 'Pending'
? 'border-[rgba(0,0,50,0.2)] bg-[rgba(0,0,50,0.1)] text-[#000032]'
: 'border-[#e5e7eb] bg-[#f9fafb] text-[rgba(0,0,50,0.6)]'
}`}
>
{lead.status}
</span>
</td>
<td class="px-6 py-0">
<div class="inline-flex items-center gap-[6px] text-[#707795]">
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-[10px] text-[#6b728a] hover:bg-[#f3f4f6]">
<Eye size={16} />
</button>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-[10px] text-[#6b728a] hover:bg-[#f3f4f6]">
<Pencil size={16} />
</button>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-[10px] text-[#6b728a] hover:bg-[#f3f4f6]">
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
{/* KPI cards */}
<div class="grid grid-cols-4 gap-6">
<For each={kpis}>
{(item) => (
<div class="group relative overflow-hidden rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-shadow hover:shadow-md">
{/* top accent */}
<div class="absolute inset-x-0 top-0 h-[3px] rounded-t-2xl bg-gradient-to-r from-[#FF5E13] to-[#ff9a6c] opacity-0 transition-opacity group-hover:opacity-100" />
<div class="flex items-start justify-between gap-3">
{/* icon box */}
<div class="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-[#FFF1EB] text-[#FF5E13]">
<KpiIcon kind={item.icon} />
</div>
{/* delta badge */}
<span
class={`inline-flex shrink-0 items-center gap-0.5 rounded-full px-2.5 py-1 text-[11px] font-bold leading-none ${
item.tone === 'up'
? 'bg-[#ECFDF5] text-[#059669]'
: 'bg-[#FEF2F2] text-[#DC2626]'
}`}
>
{item.tone === 'up' ? '↑' : '↓'} {item.delta.replace(/^[+-]/, '')}
</span>
</div>
<p class="mt-5 text-[13px] font-medium text-[#6B7280]">{item.title}</p>
<p class="mt-1 text-[26px] font-bold tracking-tight text-[#111827]">{item.value}</p>
<p class="mt-2 text-[12px] text-[#9CA3AF]">{item.note}</p>
</div>
)}
</For>
</div>
</section>
{/* Chart widgets */}
<div class="grid grid-cols-2 gap-6">
{/* Leads Trend */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
<div class="flex items-center justify-between border-b border-[#F3F4F6] px-6 py-5">
<div>
<h2 class="text-[15px] font-bold text-[#111827]">Leads Trend</h2>
<p class="mt-0.5 text-[12px] text-[#6B7280]">Monthly leads performance</p>
</div>
<DragHandle />
</div>
<div class="p-6">
<div class="grid grid-cols-[44px_1fr] gap-4">
<div class="flex h-56 flex-col justify-between text-right text-[11px] font-medium text-[#9CA3AF]">
<span>120</span>
<span>90</span>
<span>60</span>
<span>30</span>
<span>0</span>
</div>
<div>
<div class="relative h-56">
<div class="absolute inset-0">
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-[#F3F4F6]" />}</For>
</div>
<svg viewBox="0 0 100 40" class="relative h-full w-full overflow-visible" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FF5E13" stop-opacity="0.2" />
<stop offset="100%" stop-color="#FF5E13" stop-opacity="0" />
</linearGradient>
</defs>
<polygon
fill="url(#trendFill)"
points={`0,40 ${trendSeries.map((v, i) => `${i * 20},${40 - v / 3}`).join(' ')} 100,40`}
/>
<polyline
fill="none"
stroke="#FF5E13"
stroke-width="1.5"
stroke-linejoin="round"
stroke-linecap="round"
points={trendSeries.map((v, i) => `${i * 20},${40 - v / 3}`).join(' ')}
/>
</svg>
</div>
<div class="mt-3 grid grid-cols-6 text-center text-[11px] font-medium text-[#9CA3AF]">
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>{(m) => <span>{m}</span>}</For>
</div>
<div class="mt-4 flex items-center justify-center gap-5 text-[12px] font-medium text-[#6B7280]">
<span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-[#FF5E13]" />Total Leads</span>
<span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-[#E5E7EB]" />Converted</span>
</div>
</div>
</div>
</div>
</div>
{/* Revenue Overview */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
<div class="flex items-center justify-between border-b border-[#F3F4F6] px-6 py-5">
<div>
<h2 class="text-[15px] font-bold text-[#111827]">Revenue Overview</h2>
<p class="mt-0.5 text-[12px] text-[#6B7280]">Monthly revenue vs expenses</p>
</div>
<DragHandle />
</div>
<div class="p-6">
<div class="grid grid-cols-[56px_1fr] gap-4">
<div class="flex h-56 flex-col justify-between text-right text-[11px] font-medium text-[#9CA3AF]">
<span>80k</span>
<span>60k</span>
<span>40k</span>
<span>20k</span>
<span>0</span>
</div>
<div>
<div class="relative h-56">
<div class="absolute inset-0">
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-[#F3F4F6]" />}</For>
</div>
<div class="relative flex h-full items-end gap-2 px-1">
<For each={revSeries}>
{(value) => (
<div class="flex h-full flex-1 items-end">
<div
class="w-full rounded-t-lg bg-gradient-to-t from-[#111827] to-[#374151] transition-all"
style={{ height: `${(value / maxAmount) * 100}%` }}
/>
</div>
)}
</For>
</div>
</div>
<div class="mt-3 grid grid-cols-6 text-center text-[11px] font-medium text-[#9CA3AF]">
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>{(m) => <span>{m}</span>}</For>
</div>
<div class="mt-4 flex items-center justify-center gap-5 text-[12px] font-medium text-[#6B7280]">
<span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-[#FF5E13]" />Revenue</span>
<span class="inline-flex items-center gap-1.5"><span class="h-2 w-2 rounded-full bg-[#111827]" />Expenses</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Recent Leads widget */}
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm">
<div class="flex items-center justify-between border-b border-[#F3F4F6] px-6 py-5">
<div>
<h2 class="text-[15px] font-bold text-[#111827]">Recent Leads</h2>
<p class="mt-0.5 text-[12px] text-[#6B7280]">Latest customer inquiries and opportunities</p>
</div>
<DragHandle />
</div>
<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]">Lead Title</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Customer</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Category</th>
<th class="px-6 py-3.5 text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">Budget</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]">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
<For each={recentLeads}>
{(lead) => (
<tr class="group hover:bg-[#FAFAFA] transition-colors">
<td class="px-6 py-4 text-[13px] font-semibold text-[#111827]">{lead.title}</td>
<td class="px-6 py-4 text-[13px] text-[#374151]">{lead.customer}</td>
<td class="px-6 py-4 text-[13px] text-[#6B7280]">{lead.category}</td>
<td class="px-6 py-4 text-[13px] font-semibold text-[#111827]">{lead.budget}</td>
<td class="px-6 py-4">
<span
class={`inline-flex items-center rounded-full px-3 py-1 text-[11px] font-semibold ${
lead.status === 'Active'
? 'bg-[#ECFDF5] text-[#059669]'
: lead.status === 'Pending'
? 'bg-[#FEF9C3] text-[#854D0E]'
: 'bg-[#F3F4F6] text-[#6B7280]'
}`}
>
{lead.status}
</span>
</td>
<td class="px-6 py-4">
<button
type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#9CA3AF] hover:bg-[#F3F4F6] hover:text-[#374151] transition-colors"
>
<Eye size={15} />
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</div>
</AdminShell>
);

View file

@ -160,33 +160,24 @@ export default function CreateInternalRolePage() {
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="w-full space-y-8 pb-8">
{/* Page title */}
<div class="bg-white border-b border-[#e5e7eb] px-6 py-5">
<h1 class="text-[20px] font-semibold text-[#000032]">Internal Role Management</h1>
<p class="text-[13px] text-[rgba(0,0,50,0.5)] mt-0.5">Manage internal roles and permissions</p>
{/* Page 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]">Create Internal Role</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Dashboard / Internal Role Management / Create Role</p>
</div>
<A href="/admin/roles" class="inline-flex items-center gap-2 rounded-xl border border-[#E5E7EB] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors">
Back to Roles
</A>
</div>
<div class="p-6">
<div class="rounded-xl border border-[#e5e7eb] bg-white shadow-sm overflow-hidden">
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm overflow-hidden">
{/* Outer tabs: All Roles | Create Role */}
<div class="flex border-b border-[#e5e7eb] px-6">
<A
href="/admin/roles"
class="py-4 text-[14px] font-medium text-[rgba(0,0,50,0.5)] hover:text-[#000032] mr-6"
>
All Roles
</A>
<button class="relative py-4 text-[14px] font-semibold text-[#fa5014]">
Create Role
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#fa5014] rounded-t" />
</button>
</div>
{/* Inner sub-tabs */}
<div class="flex border-b border-[#e5e7eb] px-6 gap-6">
{/* Sub-tabs */}
<div class="flex items-center gap-1 border-b border-[#F3F4F6] px-6">
{(
[
{ key: 'general', label: 'General Information' },
@ -195,16 +186,15 @@ export default function CreateInternalRolePage() {
] as const
).map((t) => (
<button
type="button"
onClick={() => setSubTab(t.key)}
class={`relative py-3.5 text-[13px] font-medium transition-colors ${
subTab() === t.key
? 'text-[#000032] font-semibold'
: 'text-[rgba(0,0,50,0.5)] hover:text-[#000032]'
class={`relative px-4 py-4 text-[13px] font-semibold transition-colors ${
subTab() === t.key ? 'text-[#111827]' : 'text-[#9CA3AF] hover:text-[#6B7280]'
}`}
>
{t.label}
<Show when={subTab() === t.key}>
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#000032] rounded-t" />
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</Show>
</button>
))}
@ -222,7 +212,7 @@ export default function CreateInternalRolePage() {
<div class="p-6 space-y-5">
<div class="grid grid-cols-2 gap-5">
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">
Role Name <span class="text-red-500">*</span>
</label>
<input
@ -230,11 +220,11 @@ export default function CreateInternalRolePage() {
placeholder="Enter role name"
value={roleName()}
onInput={(e) => setRoleName(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#000032] placeholder-[rgba(0,0,50,0.3)]"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#0D0D2A] placeholder-[rgba(13,13,42,0.3)]"
/>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">
Role Code <span class="text-red-500">*</span>
</label>
<input
@ -242,18 +232,18 @@ export default function CreateInternalRolePage() {
placeholder="e.g., ENG-LEAD-001"
value={roleCode()}
onInput={(e) => setRoleCode(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#000032] placeholder-[rgba(0,0,50,0.3)]"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#0D0D2A] placeholder-[rgba(13,13,42,0.3)]"
/>
</div>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">
Department <span class="text-red-500">*</span>
</label>
<select
value={departmentId()}
onChange={(e) => setDepartmentId(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] bg-white text-[#000032]"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#0D0D2A]"
>
<option value="">Select department</option>
<For each={departments() ?? []}>
@ -262,13 +252,13 @@ export default function CreateInternalRolePage() {
</select>
</div>
<div>
<label class="block text-[13px] font-medium text-[#000032] mb-1.5">Description</label>
<label class="block text-[13px] font-medium text-[#0D0D2A] mb-1.5">Description</label>
<textarea
placeholder="Enter role description"
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
rows={4}
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#fa5014] focus:ring-1 focus:ring-[#fa5014] text-[#000032] placeholder-[rgba(0,0,50,0.3)] resize-none"
class="w-full px-3 py-2.5 text-[13px] border border-[#e5e7eb] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#0D0D2A] placeholder-[rgba(13,13,42,0.3)] resize-none"
/>
</div>
</div>
@ -277,23 +267,23 @@ export default function CreateInternalRolePage() {
{/* ── Tab: Module Access ── */}
<Show when={subTab() === 'module'}>
<div class="p-6">
<p class="text-[13px] text-[rgba(0,0,50,0.5)] mb-4">
<p class="text-[13px] text-[rgba(13,13,42,0.5)] mb-4">
Configure module access permissions for this role.
</p>
<div class="overflow-x-auto rounded-lg border border-[#e5e7eb]">
<table class="w-full">
<thead>
<tr class="bg-[#000032] text-white text-[12px] font-semibold uppercase tracking-wide">
<th class="px-5 py-3 text-left w-[40%]"> </th>
<th class="px-4 py-3 text-center">View</th>
<th class="px-4 py-3 text-center">Create</th>
<th class="px-4 py-3 text-center">Update</th>
<th class="px-4 py-3 text-center">Delete</th>
<th class="px-4 py-3 text-center">
<tr class="border-b border-[#F3F4F6] bg-[#FAFAFA] text-[11px] font-semibold uppercase tracking-wider text-[#9CA3AF]">
<th class="px-5 py-3.5 text-left w-[40%]">Module</th>
<th class="px-4 py-3.5 text-center">View</th>
<th class="px-4 py-3.5 text-center">Create</th>
<th class="px-4 py-3.5 text-center">Update</th>
<th class="px-4 py-3.5 text-center">Delete</th>
<th class="px-4 py-3.5 text-center">
<button
type="button"
onClick={() => (allSelected() ? deselectAll() : selectAll())}
class="text-[11px] font-semibold text-white hover:text-[#fca87c] transition-colors whitespace-nowrap"
class="text-[11px] font-semibold text-[#FF5E13] hover:text-[#e04d0a] transition-colors whitespace-nowrap"
>
{allSelected() ? 'Deselect All' : 'Select All'}
</button>
@ -303,7 +293,7 @@ export default function CreateInternalRolePage() {
<tbody class="divide-y divide-[#e5e7eb]">
<Show when={permissions.loading}>
<tr>
<td colspan="6" class="px-5 py-6 text-center text-[13px] text-[rgba(0,0,50,0.4)]">
<td colspan="6" class="px-5 py-6 text-center text-[13px] text-[rgba(13,13,42,0.4)]">
Loading modules
</td>
</tr>
@ -319,7 +309,7 @@ export default function CreateInternalRolePage() {
const rowAllSelected = () => perms().every((p) => selectedKeys().has(p.key));
return (
<tr class="hover:bg-[#fafafa]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#000032]">
<td class="px-5 py-3.5 text-[13px] font-medium text-[#0D0D2A]">
{module}
</td>
{ACTIONS.map((action) => {
@ -334,7 +324,7 @@ export default function CreateInternalRolePage() {
type="checkbox"
checked={selectedKeys().has(p()!.key)}
onChange={() => toggleKey(p()!.key)}
class="h-4 w-4 accent-[#fa5014] cursor-pointer"
class="h-4 w-4 accent-[#FF5E13] cursor-pointer"
/>
</Show>
</td>
@ -345,7 +335,7 @@ export default function CreateInternalRolePage() {
type="checkbox"
checked={rowAllSelected()}
onChange={() => toggleRow(module)}
class="h-4 w-4 accent-[#fa5014] cursor-pointer"
class="h-4 w-4 accent-[#FF5E13] cursor-pointer"
/>
</td>
</tr>
@ -363,15 +353,15 @@ export default function CreateInternalRolePage() {
<div class="p-6 space-y-6">
{/* Status toggle */}
<div>
<p class="text-[13px] font-semibold text-[#000032] mb-3">Role Status</p>
<p class="text-[13px] font-semibold text-[#0D0D2A] mb-3">Role Status</p>
<div class="flex items-center gap-2">
<button
type="button"
onClick={() => setIsActive(true)}
class={`px-5 py-2 rounded-lg text-[13px] font-medium transition-colors ${
class={`h-[38px] rounded-xl border px-5 text-[13px] font-semibold transition-colors ${
isActive()
? 'bg-[#fa5014] text-white'
: 'border border-[#e5e7eb] text-[rgba(0,0,50,0.6)] hover:border-[#000032]'
? 'border-[#059669] bg-[#ECFDF5] text-[#059669]'
: 'border-[#E5E7EB] bg-white text-[#6B7280] hover:bg-[#F9FAFB]'
}`}
>
Active
@ -379,10 +369,10 @@ export default function CreateInternalRolePage() {
<button
type="button"
onClick={() => setIsActive(false)}
class={`px-5 py-2 rounded-lg text-[13px] font-medium transition-colors ${
class={`h-[38px] rounded-xl border px-5 text-[13px] font-semibold transition-colors ${
!isActive()
? 'bg-[#fa5014] text-white'
: 'border border-[#e5e7eb] text-[rgba(0,0,50,0.6)] hover:border-[#000032]'
? 'border-[#6B7280] bg-[#F3F4F6] text-[#374151]'
: 'border-[#E5E7EB] bg-white text-[#6B7280] hover:bg-[#F9FAFB]'
}`}
>
Inactive
@ -409,23 +399,24 @@ export default function CreateInternalRolePage() {
</Show>
{/* Footer actions */}
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#e5e7eb]">
<div class="flex items-center justify-end gap-3 border-t border-[#F3F4F6] px-6 py-4">
<A
href="/admin/roles"
class="px-5 py-2.5 text-[13px] font-medium border border-[#e5e7eb] rounded-lg text-[rgba(0,0,50,0.7)] hover:border-[#000032] transition-colors"
class="h-[40px] inline-flex items-center rounded-xl border border-[#E5E7EB] bg-white px-5 text-[13px] font-semibold text-[#374151] hover:bg-[#F9FAFB] transition-colors"
>
Cancel
</A>
<button
type="button"
onClick={handleSave}
disabled={saving()}
class="px-5 py-2.5 text-[13px] font-semibold bg-[#000032] text-white rounded-lg hover:bg-[#000050] transition-colors disabled:opacity-60"
class="h-[40px] rounded-xl bg-[#0D0D2A] px-6 text-[13px] font-semibold text-white hover:bg-[#1a1a3e] transition-colors disabled:opacity-60"
>
{saving() ? 'Creating…' : 'Create Role'}
</button>
</div>
</div>
</div>
</div>
</AdminShell>
);
@ -441,8 +432,8 @@ function SettingToggle(props: {
return (
<div class="flex items-center justify-between rounded-xl border border-[#e5e7eb] px-5 py-4">
<div>
<p class="text-[13px] font-semibold text-[#000032]">{props.label}</p>
<p class="text-[12px] text-[rgba(0,0,50,0.5)] mt-0.5">{props.description}</p>
<p class="text-[13px] font-semibold text-[#0D0D2A]">{props.label}</p>
<p class="text-[12px] text-[rgba(13,13,42,0.5)] mt-0.5">{props.description}</p>
</div>
<button
type="button"
@ -450,7 +441,7 @@ function SettingToggle(props: {
aria-checked={props.value}
onClick={() => props.onChange(!props.value)}
class={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
props.value ? 'bg-[#fa5014]' : 'bg-[#d1d5db]'
props.value ? 'bg-[#FF5E13]' : 'bg-[#d1d5db]'
}`}
>
<span

View file

@ -1,6 +1,5 @@
import { A, useNavigate } from '@solidjs/router';
import { A } from '@solidjs/router';
import { createResource, createSignal, For, Show } from 'solid-js';
import { Search } from 'lucide-solid';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
@ -18,56 +17,59 @@ type Role = {
created_at: string;
};
type ListResponse = {
roles: Role[];
total: number;
page: number;
per_page: number;
};
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' },
];
async function loadRoles(params: { q: string; page: number }): Promise<ListResponse> {
try {
const qs = new URLSearchParams({
audience: 'INTERNAL',
q: params.q,
page: String(params.page),
per_page: '8',
});
const qs = new URLSearchParams({ audience: 'INTERNAL', q: params.q, page: String(params.page), per_page: '10' });
const res = await fetch(`${API}/api/admin/roles?${qs}`);
if (!res.ok) throw new Error('Failed');
const data = await res.json();
if (data.roles) return data;
// flat array fallback
return { roles: Array.isArray(data) ? data : [], total: 0, page: 1, per_page: 8 };
if (data.roles?.length > 0) return data;
throw new Error('empty');
} catch {
return { roles: [], total: 0, page: 1, per_page: 8 };
const q = params.q.toLowerCase();
const filtered = q ? FALLBACK_ROLES.filter((r) => r.name.toLowerCase().includes(q) || r.key.toLowerCase().includes(q)) : FALLBACK_ROLES;
return { roles: filtered, total: filtered.length, page: 1, per_page: 10 };
}
}
function formatDate(iso: string) {
try {
const d = new Date(iso);
return d.toISOString().slice(0, 10);
} catch {
return '—';
}
try { return new Date(iso).toISOString().slice(0, 10); } 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={`mr-1.5 h-1.5 w-1.5 rounded-full ${props.active ? 'bg-[#059669]' : 'bg-[#9CA3AF]'}`} />
{props.active ? 'Active' : 'Inactive'}
</span>
);
}
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) => {
setSearch(val);
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
setDebouncedSearch(val);
setPage(1);
}, 300);
debounceTimer = setTimeout(() => { setDebouncedSearch(val); setPage(1); }, 300);
};
const [data, { refetch }] = createResource(
@ -81,6 +83,14 @@ export default function InternalRolesListPage() {
return Math.max(1, Math.ceil(d.total / d.per_page));
};
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);
@ -89,185 +99,228 @@ export default function InternalRolesListPage() {
if (!res.ok) throw new Error('Failed');
refetch();
} finally {
setDeleting('');
setDeleting(''); setOpenMenuId(null);
}
};
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header & Title */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Internal Role Management</h1>
</div>
<div class="flex items-center gap-3">
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
Export Data
</button>
<A
href="/admin/roles/create"
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
>
<span class="mr-2 text-lg leading-none">+</span> Add Role
</A>
<div class="w-full space-y-8 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>
</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>
{/* 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>
<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>
</div>
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Tabs */}
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
<For each={['Active Roles', 'Archived Roles']}>
{(t) => (
<button
class={`pb-3 text-[14px] font-bold transition-colors border-b-2 ${
t === 'Active Roles'
? 'border-[#050026] text-[#050026]'
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
}`}
>
{t}
</button>
)}
</For>
</div>
{/* Filters Row */}
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
<div class="relative w-full md:w-[320px]">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Search roles..."
value={search()}
onInput={(e) => handleSearch(e.currentTarget.value)}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
/>
</div>
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
<div class="flex-1"></div>
<Show when={!data.loading}>
<span class="text-[13px] text-[#8087a0] font-medium">
Showing {((page() - 1) * (data()?.per_page ?? 8)) + 1}{Math.min(page() * (data()?.per_page ?? 8), data()?.total ?? 0)} of {data()?.total ?? 0} roles
</span>
{/* 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>
</thead>
<tbody class="divide-y divide-[#F3F4F6]">
<Show when={data.loading}>
<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>
</tr>
)}
</For>
</Show>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">ROLE ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE NAME</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CATEGORY</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ASSOCIATED USERS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ACTIVE PERMISSIONS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
</tr>
</thead>
<tbody>
<Show when={data.loading}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">Loading roles...</td></tr>
</Show>
<Show when={!data.loading && (data()?.roles?.length ?? 0) === 0}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">No internal roles found.</td></tr>
</Show>
<For each={data()?.roles ?? []}>
{(role) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">{role.key.toUpperCase()}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{role.name}</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{role.department_name || '—'}</td>
<td class="px-6 py-4 text-[14px] font-semibold text-[#050026]">
<div class="flex -space-x-2 mr-2 inline-flex items-center">
{/* Placeholder for avatar group if users > 0 */}
<Show when={role.users_assigned > 0} fallback={<span class="text-[#c1c7d0] font-normal">0 users</span>}>
<div class="h-6 w-6 rounded-full bg-[#e2e8f0] border-2 border-white flex items-center justify-center text-[10px] font-bold text-[#475569]">U</div>
<div class="h-6 w-6 rounded-full bg-[#f1f5f9] border-2 border-white flex items-center justify-center text-[10px] font-bold text-[#64748b]">+{role.users_assigned - 1}</div>
</Show>
</div>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#f8f9fc] text-[#050026] text-[12px] font-bold border border-[#e2e6ee]">
{role.permissions_count} Permissions
</span>
</td>
<td class="px-6 py-4 text-[14px] text-[#475569]">{formatDate(role.created_at)}</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<A
title="View Details"
href={`/admin/roles/${role.id}`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<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="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>
</tr>
</Show>
<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>
</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>
<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>
<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
title="Edit"
href={`/admin/roles/${role.id}/edit`}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
<A 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
title="Archive"
type="button"
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
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" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<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>
</tr>
)}
</For>
</tbody>
</table>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<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>
</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"
></button>
<For each={Array.from({ length: Math.min(totalPages(), 5) }, (_, i) => i + 1)}>
{(p) => (
<button
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]'
}`}
>{p}</button>
)}
</For>
<button
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"
></button>
</div>
{/* Pagination */}
<Show when={totalPages() > 1}>
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
<span class="text-[13px] font-medium text-[#8087a0]">Page {page()} of {totalPages()}</span>
<div class="flex items-center gap-2">
<button
disabled={page() === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<button
disabled={page() >= totalPages()}
onClick={() => setPage((p) => Math.min(totalPages(), p + 1))}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
</Show>
</div>
</section>
</Show>
</div>
</div>
</AdminShell>
);