nxtgauge-admin-solid/src/routes/admin/designation.tsx

869 lines
49 KiB
TypeScript
Raw Normal View History

import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { useSearchParams } from '@solidjs/router';
import type { CrudRecord } from '~/lib/admin/types';
const API = '';
type DesignationRecord = CrudRecord & {
code?: string;
department?: string;
departmentId?: string;
level?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
canManageTeam?: boolean;
canApprove?: boolean;
};
type DepartmentOption = { id: string; name: string };
const permissionGroups = [
{ title: 'Employee Management', items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'] },
{ title: 'Role Management', items: ['View Roles', 'Assign Roles'] },
{ title: 'Workflow Actions', items: ['Approve Requests', 'Manage Team Members'] },
];
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst'];
type DesignationListResponse = {
designations?: any[];
data?: any[];
items?: any[];
};
function normalizeDesignation(item: any, idx: number): DesignationRecord {
const status = String(item.status ?? '').toUpperCase();
const isActive = typeof item.is_active === 'boolean'
? item.is_active
: status
? status === 'ACTIVE'
: true;
return {
id: String(item.id ?? `des-${idx + 1}`),
name: String(item.name ?? ''),
code: item.code ? String(item.code) : undefined,
department: item.department_name ? String(item.department_name) : undefined,
departmentId: item.department_id ? String(item.department_id) : undefined,
level: item.level ? String(item.level) : undefined,
description: item.description ? String(item.description) : undefined,
totalEmployees: Number(item.total_employees ?? 0),
canManageTeam: Boolean(item.can_manage_team ?? false),
canApprove: Boolean(item.can_approve ?? false),
status: isActive ? 'ACTIVE' : 'INACTIVE',
updatedAt: String(item.updated_at ?? ''),
createdDate: String(item.created_at ?? ''),
};
}
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<input
type={props.type ?? 'text'}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
);
}
function FormSelect(props: { label: string; required?: boolean; value: string; onChange: (v: string) => void; children: any }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<select
value={props.value}
onChange={(e) => props.onChange(e.currentTarget.value)}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;appearance:none"
>
{props.children}
</select>
</label>
);
}
export default function DesignationManagementPage() {
const [searchParams] = useSearchParams();
const isPreview = () => searchParams._preview === '1';
const [view, setView] = createSignal<'list' | 'form'>('list');
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
const [search, setSearch] = createSignal('');
const [deptFilter, setDeptFilter] = createSignal('all');
const [statusFilter, setStatusFilter] = createSignal('all');
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'employees_desc' | 'employees_asc'>('name_asc');
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [openMenuPos, setOpenMenuPos] = createSignal({ x: 0, y: 0 });
const [editingId, setEditingId] = createSignal<string | null>(null);
const [viewingRecord, setViewingRecord] = createSignal<DesignationRecord | null>(null);
const [deleteTarget, setDeleteTarget] = createSignal<DesignationRecord | null>(null);
const [isDeleting, setIsDeleting] = createSignal(false);
const [name, setName] = createSignal('');
const [code, setCode] = createSignal('');
const [departmentId, setDepartmentId] = createSignal('');
const [level, setLevel] = createSignal('');
const [description, setDescription] = createSignal('');
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [canManageTeam, setCanManageTeam] = createSignal(false);
const [canApprove, setCanApprove] = createSignal(false);
const [isLoading, setIsLoading] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false);
const [error, setError] = createSignal('');
const [departments, setDepartments] = createSignal<DepartmentOption[]>([]);
const load = async () => {
setIsLoading(true);
setError('');
try {
const accessToken = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
const params = new URLSearchParams({
page: '1',
per_page: '100',
q: search().trim(),
});
if (statusFilter() !== 'all') {
params.set('status', statusFilter());
}
if (deptFilter() !== 'all') {
params.set('department_id', deptFilter());
}
const res = await fetch(`${API}/api/admin/designations?${params.toString()}`, {
headers: {
Accept: 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: 'include',
});
if (!res.ok) throw new Error(`Request failed (${res.status})`);
const payload = (await res.json().catch(() => null)) as DesignationListResponse | null;
const list = Array.isArray(payload)
? payload
: Array.isArray(payload?.designations)
? payload.designations
: Array.isArray(payload?.data)
? payload.data
: Array.isArray(payload?.items)
? payload.items
: [];
setRows(list.map(normalizeDesignation));
} catch (err: any) {
setError(err?.message || 'Could not reach designations API.');
setRows([]);
} finally {
setIsLoading(false);
}
};
const loadDepartments = async () => {
try {
const accessToken = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
const res = await fetch(`${API}/api/admin/departments?per_page=100`, {
headers: {
Accept: 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: 'include',
});
if (!res.ok) return;
const payload = await res.json().catch(() => null);
const list: any[] = Array.isArray(payload)
? payload
: Array.isArray(payload?.departments)
? payload.departments
: Array.isArray(payload?.data)
? payload.data
: Array.isArray(payload?.items)
? payload.items
: [];
setDepartments(list.map((d: any) => ({ id: String(d.id), name: String(d.name) })));
} catch {
// departments dropdown will just be empty
}
};
onMount(() => { void load(); void loadDepartments(); });
const filteredRows = createMemo(() => {
let r = rows();
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
if (deptFilter() !== 'all') {
const selected = String(deptFilter()).trim().toLowerCase();
r = r.filter((d) => {
const byId = String(d.departmentId || '').trim().toLowerCase();
const byName = String(d.department || '').trim().toLowerCase();
return byId === selected || byName === selected;
});
}
const q = search().toLowerCase();
if (q) {
r = r.filter((d) =>
d.name.toLowerCase().includes(q)
|| String(d.code ?? '').toLowerCase().includes(q)
|| String(d.description ?? '').toLowerCase().includes(q)
);
}
const sorted = [...r];
const mode = sortBy();
sorted.sort((a, b) => {
if (mode === 'name_desc') return b.name.localeCompare(a.name);
if (mode === 'employees_desc') return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0);
if (mode === 'employees_asc') return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0);
return a.name.localeCompare(b.name);
});
r = sorted;
return r;
});
const exportCsv = () => {
const headers = ['Designation Name', 'Code', 'Department', 'Level', 'Employees', 'Status'];
const rowsData = filteredRows().map((row) => [
row.name || '',
row.code || '',
row.department || '',
row.level || '',
String(row.totalEmployees ?? 0),
row.status || '',
]);
const csv = [
headers,
...rowsData,
]
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `designation-management-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const resetForm = () => {
setEditingId(null); setName(''); setCode(''); setDepartmentId('');
setLevel(''); setDescription(''); setStatus('ACTIVE');
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError('');
};
const openCreate = () => { resetForm(); setView('form'); };
const openEdit = (row: DesignationRecord) => {
setEditingId(row.id);
setName(row.name || ''); setCode(row.code || '');
setDepartmentId(row.departmentId || ''); setLevel(row.level || '');
setDescription(row.description || '');
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
setCanManageTeam(Boolean(row.canManageTeam)); setCanApprove(Boolean(row.canApprove));
setFormTab('general'); setView('form'); setOpenMenuId(null);
};
const save = async () => {
if (isSaving()) return;
if (!name().trim() || !code().trim()) {
setError('Designation name and code are required.');
setFormTab('general');
return;
}
setIsSaving(true);
setError('');
const payload: Record<string, unknown> = {
name: name().trim(),
code: code().trim(),
level: level().trim() || null,
description: description().trim() || null,
status: status(),
can_manage_team: canManageTeam(),
can_approve: canApprove(),
};
if (departmentId().trim()) {
payload.department_id = departmentId().trim();
}
try {
const accessToken = typeof sessionStorage !== 'undefined'
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim()
: '';
const endpoint = editingId()
? `${API}/api/admin/designations/${editingId()}`
: `${API}/api/admin/designations`;
const method = editingId() ? 'PATCH' : 'POST';
const res = await fetch(endpoint, {
method,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: 'include',
body: JSON.stringify(payload),
});
const raw = await res.text();
let message = '';
if (raw) {
try {
const parsed = JSON.parse(raw) as { message?: string; error?: string };
message = parsed?.message || parsed?.error || '';
} catch {
message = raw;
}
}
if (!res.ok) throw new Error(message || `Request failed (${res.status})`);
setView('list');
resetForm();
await load();
} catch (err: any) {
const msg = String(err?.message || '').trim();
setError(msg || 'Failed to save designation.');
} finally {
setIsSaving(false);
}
};
const formatDate = (v?: string) => {
const s = v || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
return s.slice(0, 10) || '—';
};
return (
<div class="w-full space-y-6 pb-8">
{/* Page header */}
<div style="margin-bottom: 1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Designation Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Manage all job designations and position levels</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ key: 'all', label: 'All Designations', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } },
{ key: 'create', label: 'Create Designation', action: () => { setListTab('create'); openCreate(); } },
{ key: 'view', label: 'View Designation', action: () => setListTab('view') },
] as const).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
{/* View Record panel */}
<Show when={listTab() === 'view'}>
<Show
when={!viewingRecord()}
>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No designation selected</p>
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong></strong> menu on any designation row and choose <strong>View Details</strong>.</p>
</div>
</Show>
<Show when={viewingRecord()}>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Header */}
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<div>
<h2 style="font-size:18px;font-weight:700;color:#111827">{viewingRecord()!.name}</h2>
<p style="margin-top:2px;font-size:13px;color:#6B7280">{viewingRecord()!.description || 'No description'}</p>
</div>
<StatusBadge status={viewingRecord()!.status} />
</div>
{/* Details grid */}
<div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Designation Code</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{viewingRecord()!.code || '—'}</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Department</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{viewingRecord()!.department || '—'}</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Level</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{viewingRecord()!.level || '—'}</p>
</div>
</div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Total Employees</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{String(viewingRecord()!.totalEmployees ?? 0)}</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Can Manage Team</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{viewingRecord()!.canManageTeam ? 'Yes' : 'No'}</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Created Date</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{viewingRecord()!.createdDate?.slice(0, 10) || '—'}</p>
</div>
</div>
</div>
{/* Actions */}
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
<button type="button" onClick={() => openEdit(viewingRecord()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Designation</button>
<button type="button" onClick={() => { setViewingRecord(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
</div>
</div>
</Show>
</Show>
{/* Table card */}
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
<div style="position:relative;margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
{/* Filter bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
placeholder="Search designations..."
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
<div style="position:relative">
<button
type="button"
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
Sort
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['name_asc', 'name_desc', 'employees_desc', 'employees_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Employees (High-Low)', 'Employees (Low-High)'][i]}
</button>
))}
</div>
</Show>
</div>
<div style="position:relative">
<button
type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
Filters
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:240px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
<p style="padding:6px 12px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Status</p>
{(['all', 'active', 'inactive'] as const).map((s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); void load(); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : 'Inactive'}
</button>
))}
<div style="height:1px;background:#F3F4F6;margin:6px 0" />
<p style="padding:6px 12px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Department</p>
<button
type="button"
onClick={() => { setDeptFilter('all'); setFilterMenuOpen(false); void load(); }}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${deptFilter() === 'all' ? '#FF5E13' : '#374151'};background:${deptFilter() === 'all' ? '#FFF1EB' : 'transparent'}`}
>
All Departments
</button>
<For each={departments()}>
{(dept) => (
<button
type="button"
onClick={() => { setDeptFilter(dept.id); setFilterMenuOpen(false); void load(); }}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${deptFilter() === dept.id ? '#FF5E13' : '#374151'};background:${deptFilter() === dept.id ? '#FFF1EB' : 'transparent'}`}
>
{dept.name}
</button>
)}
</For>
</div>
</Show>
</div>
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export
</button>
</div>
{/* Table */}
<div class="overflow-x-auto overflow-y-visible">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Designation Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Code</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Level</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Employees</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
</tr>
</thead>
<tbody>
<Show
when={filteredRows().length > 0}
fallback={
<tr>
<td colspan="7" class="px-6 py-16 text-center">
<p class="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">
Create Designation
</button>
</td>
</tr>
}
>
<For each={filteredRows()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td>
<td style="padding:12px 20px">
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">{String(row.level || '—')}</span>
</td>
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
<td style="padding:12px 20px;position:relative">
<button
type="button"
onClick={(e) => {
if (openMenuId() === row.id) {
setOpenMenuId(null);
return;
}
const rect = e.currentTarget.getBoundingClientRect();
setOpenMenuPos({ x: rect.right, y: rect.top - 8 });
setOpenMenuId(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 style={`position:fixed;left:${openMenuPos().x}px;top:${openMenuPos().y}px;transform:translate(-100%, -100%);z-index:9999;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)`}>
<button type="button" onClick={() => { setViewingRecord(row); setOpenMenuId(null); setListTab('view'); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
View Details
</button>
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
Edit Designation
</button>
<button
type="button"
onClick={async () => {
try {
const res = await fetch(`${API}/api/admin/designations/${row.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }) });
if (!res.ok) throw new Error(`Request failed (${res.status})`);
} catch (err: any) { setError(err?.message || 'Failed to update status.'); }
finally { setOpenMenuId(null); await load(); }
}}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer;text-align:left"
>
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
{row.status === 'ACTIVE' ? 'Deactivate Designation' : 'Activate Designation'}
</button>
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
<button
type="button"
onClick={async () => {
setOpenMenuId(null);
setDeleteTarget(row);
}}
style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left"
>
<svg style="width:16px;height:16px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
Delete Designation
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<Show when={filteredRows().length > 0}>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing <strong style="font-weight:600;color:#111827">1{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations
</p>
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
</div>
</div>
</Show>
</div>
</div>
</div>
</Show>
<Show when={deleteTarget()}>
<div style="position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(17,24,39,0.45);padding:16px">
<div style="width:min(92vw,480px);border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 24px 48px rgba(0,0,0,0.2)">
<div style="padding:20px 22px;border-bottom:1px solid #F3F4F6">
<h3 style="font-size:18px;font-weight:700;color:#111827">Delete Designation?</h3>
<p style="margin-top:8px;font-size:13px;line-height:1.5;color:#4B5563">
You are about to permanently delete
<strong style="color:#111827"> {deleteTarget()?.name}</strong>.
This action cannot be undone.
</p>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px;padding:14px 22px">
<button
type="button"
onClick={() => setDeleteTarget(null)}
disabled={isDeleting()}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
disabled={isDeleting()}
onClick={async () => {
const target = deleteTarget();
if (!target) return;
setIsDeleting(true);
try {
const res = await fetch(`${API}/api/admin/designations/${target.id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Request failed (${res.status})`);
setDeleteTarget(null);
await load();
} catch (err: any) {
setError(err?.message || 'Failed to delete designation.');
} finally {
setIsDeleting(false);
}
}}
style="height:38px;border-radius:10px;border:none;background:#0D0D2A;padding:0 14px;font-size:13px;font-weight:700;color:white;cursor:pointer"
>
{isDeleting() ? 'Deleting...' : 'Delete Designation'}
</button>
</div>
</div>
</div>
</Show>
{/* ── FORM VIEW (Create / Edit) ── */}
<Show when={view() === 'form'}>
{/* Top tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button type="button" onClick={() => setView('list')} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
All Designations
</button>
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
{editingId() ? 'Edit Designation' : 'Create Designation'}
</button>
</div>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
const labels = ['General Information', 'Designation Settings', 'Permissions'];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
</div>
<div style="padding:24px">
<Show when={error()}>
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
{error()}
</div>
</Show>
{/* General Information */}
<Show when={formTab() === 'general'}>
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Designation Name" required value={name()} onInput={setName} placeholder="e.g. Senior Software Engineer" />
<FormInput label="Designation Code" required value={code()} onInput={setCode} placeholder="e.g. SSE-001" />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormSelect label="Department" value={departmentId()} onChange={setDepartmentId}>
<option value="">Select department</option>
<For each={departments()}>{(d) => <option value={d.id}>{d.name}</option>}</For>
</FormSelect>
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}>
<option value="">Select level</option>
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
</FormSelect>
</div>
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Brief description of this designation's responsibilities..."
rows="3"
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
/>
</label>
</div>
</Show>
{/* Designation Settings */}
<Show when={formTab() === 'settings'}>
<div style="display:flex;flex-direction:column;gap:32px">
<div>
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
<button
type="button"
onClick={() => setStatus(s)}
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
>
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
</button>
))}
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</p>
</div>
<button
type="button"
onClick={() => setCanManageTeam((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? '22px' : '2px'}`} />
</button>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p>
</div>
<button
type="button"
onClick={() => setCanApprove((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
>
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? '22px' : '2px'}`} />
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === 'permissions'}>
<div style="display:flex;flex-direction:column;gap:24px">
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p>
<For each={permissionGroups}>
{(group) => (
<div>
<p style="margin-bottom:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#9CA3AF">{group.title}</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<For each={group.items}>
{(item) => (
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" />
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
</label>
)}
</For>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
{/* Form actions */}
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
<button
type="button"
onClick={() => { setView('list'); resetForm(); }}
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
onClick={() => void save()}
disabled={isSaving()}
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:1"
>
{isSaving() ? 'Saving...' : editingId() ? 'Update Designation' : 'Create Designation'}
</button>
</div>
</div>
</Show>
</div>
);
}