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

868 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}