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:
parent
244895b241
commit
5b97af4e0f
6 changed files with 1428 additions and 928 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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's what'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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue