Wire department and designation management to module CRUD backend

This commit is contained in:
Ashwin Kumar 2026-03-25 21:32:03 +01:00
parent 94d4623248
commit 0996f12227
5 changed files with 556 additions and 229 deletions

View file

@ -67,30 +67,30 @@ export async function updateApproval(id: string, patch: Partial<ApprovalCase>) {
return parse<ApprovalCase>(res);
}
export async function listModuleRecords(moduleKey: string, query?: { q?: string; status?: string }) {
export async function listModuleRecords<T extends CrudRecord = CrudRecord>(moduleKey: string, query?: { q?: string; status?: string }) {
const qp = new URLSearchParams();
if (query?.q) qp.set('q', query.q);
if (query?.status) qp.set('status', query.status);
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records${qp.toString() ? `?${qp.toString()}` : ''}`);
return parse<CrudRecord[]>(res);
return parse<T[]>(res);
}
export async function createModuleRecord(moduleKey: string, payload: Partial<CrudRecord>) {
export async function createModuleRecord<T extends CrudRecord = CrudRecord>(moduleKey: string, payload: Partial<T>) {
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return parse<CrudRecord>(res);
return parse<T>(res);
}
export async function updateModuleRecord(moduleKey: string, id: string, patch: Partial<CrudRecord>) {
export async function updateModuleRecord<T extends CrudRecord = CrudRecord>(moduleKey: string, id: string, patch: Partial<T>) {
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
return parse<CrudRecord>(res);
return parse<T>(res);
}
export async function deleteModuleRecord(moduleKey: string, id: string) {

View file

@ -62,7 +62,12 @@ const filterByQuery = <T extends { id: string; status?: string; applicantName?:
});
};
const createCrudService = (seed: CrudRecord[]): CrudService<CrudRecord> => {
const createCrudService = (
seed: CrudRecord[],
options?: {
createFromPayload?: (payload: Partial<CrudRecord>, currentRows: CrudRecord[]) => CrudRecord;
},
): CrudService<CrudRecord> => {
let rows = [...seed];
return {
async list(query) {
@ -72,12 +77,15 @@ const createCrudService = (seed: CrudRecord[]): CrudService<CrudRecord> => {
return rows.find((r) => r.id === id) ?? null;
},
async create(payload) {
const item: CrudRecord = {
id: `ADM-${Date.now()}`,
name: payload.name || 'New Item',
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: nowIso(),
};
const item: CrudRecord =
options?.createFromPayload?.(payload, rows) ??
({
...(payload as CrudRecord),
id: String(payload.id || `ADM-${Date.now()}`),
name: payload.name || 'New Item',
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: nowIso(),
} as CrudRecord);
rows = [item, ...rows];
return item;
},
@ -217,6 +225,32 @@ export const genericCrudService = createCrudService(mockCrudRows);
const moduleCrudServices = new Map<string, CrudService<CrudRecord>>();
function seedRowsForModule(moduleKey: string): CrudRecord[] {
if (moduleKey === 'department') {
return [
{ id: 'eng-001', name: 'Engineering', code: 'ENG-001', description: 'Software development and technical operations', totalEmployees: 45, status: 'ACTIVE', createdDate: '2024-01-15', updatedAt: nowIso() },
{ id: 'mkt-002', name: 'Marketing', code: 'MKT-002', description: 'Brand management and customer acquisition', totalEmployees: 28, status: 'ACTIVE', createdDate: '2024-01-20', updatedAt: nowIso() },
{ id: 'sal-003', name: 'Sales', code: 'SAL-003', description: 'Revenue generation and client relations', totalEmployees: 35, status: 'ACTIVE', createdDate: '2024-02-01', updatedAt: nowIso() },
{ id: 'hr-004', name: 'Human Resources', code: 'HR-004', description: 'Employee management and recruitment', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-02-10', updatedAt: nowIso() },
{ id: 'fin-005', name: 'Finance', code: 'FIN-005', description: 'Financial planning and accounting', totalEmployees: 18, status: 'ACTIVE', createdDate: '2024-02-15', updatedAt: nowIso() },
{ id: 'ops-006', name: 'Operations', code: 'OPS-006', description: 'Operations and logistics', totalEmployees: 22, status: 'INACTIVE', createdDate: '2024-03-01', updatedAt: nowIso() },
{ id: 'cs-007', name: 'Customer Success', code: 'CS-007', description: 'Customer experience and technical support', totalEmployees: 31, status: 'ACTIVE', createdDate: '2024-03-05', updatedAt: nowIso() },
{ id: 'prd-008', name: 'Product', code: 'PRD-008', description: 'Product strategy and roadmap execution', totalEmployees: 19, status: 'ACTIVE', createdDate: '2024-03-10', updatedAt: nowIso() },
];
}
if (moduleKey === 'designation') {
return [
{ id: 'sse-001', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-01-15', updatedAt: nowIso() },
{ id: 'mm-002', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', createdDate: '2024-01-20', updatedAt: nowIso() },
{ id: 'se-003', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', createdDate: '2024-02-01', updatedAt: nowIso() },
{ id: 'hrs-004', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', createdDate: '2024-02-10', updatedAt: nowIso() },
{ id: 'fa-005', name: 'Financial Analyst', code: 'FA-005', department: 'Finance', level: 'Analyst', totalEmployees: 6, status: 'ACTIVE', createdDate: '2024-02-15', updatedAt: nowIso() },
{ id: 'om-006', name: 'Operations Manager', code: 'OM-006', department: 'Operations', level: 'Manager', totalEmployees: 4, status: 'INACTIVE', createdDate: '2024-03-01', updatedAt: nowIso() },
{ id: 'csl-007', name: 'Customer Support Lead', code: 'CSL-007', department: 'Customer Support', level: 'Lead', totalEmployees: 9, status: 'ACTIVE', createdDate: '2024-03-05', updatedAt: nowIso() },
{ id: 'pd-008', name: 'Product Designer', code: 'PD-008', department: 'Product', level: 'Designer', totalEmployees: 7, status: 'ACTIVE', createdDate: '2024-03-10', updatedAt: nowIso() },
];
}
const base = moduleKey
.replace(/[^a-z0-9]+/gi, '_')
.replace(/^_+|_+$/g, '')
@ -236,7 +270,48 @@ export function getModuleCrudService(moduleKey: string): CrudService<CrudRecord>
const found = moduleCrudServices.get(key);
if (found) return found;
const created = createCrudService(seedRowsForModule(key));
const created = createCrudService(seedRowsForModule(key), {
createFromPayload(payload) {
const baseName = String(payload.name || 'New Item');
if (key === 'department') {
return {
...(payload as CrudRecord),
id: String(payload.id || `dep-${Date.now()}`),
name: baseName,
code: String(payload.code || baseName.slice(0, 3).toUpperCase()),
description: String(payload.description || ''),
totalEmployees: Number(payload.totalEmployees || 0),
createdDate: String(payload.createdDate || new Date().toISOString().slice(0, 10)),
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: nowIso(),
};
}
if (key === 'designation') {
return {
...(payload as CrudRecord),
id: String(payload.id || `des-${Date.now()}`),
name: baseName,
code: String(payload.code || baseName.slice(0, 3).toUpperCase()),
department: String(payload.department || ''),
level: String(payload.level || ''),
description: String(payload.description || ''),
totalEmployees: Number(payload.totalEmployees || 0),
createdDate: String(payload.createdDate || new Date().toISOString().slice(0, 10)),
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: nowIso(),
};
}
return {
...(payload as CrudRecord),
id: String(payload.id || `ADM-${Date.now()}`),
name: baseName,
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: nowIso(),
};
},
});
moduleCrudServices.set(key, created);
return created;
}

View file

@ -32,6 +32,7 @@ export type CrudRecord = {
name: string;
status: 'ACTIVE' | 'INACTIVE';
updatedAt: string;
[key: string]: unknown;
};
export type AdminModuleConfig = {

View file

@ -1,123 +1,244 @@
import { createMemo, createSignal, onMount } from 'solid-js';
import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi';
import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import type { CrudRecord } from '~/lib/admin/types';
export default function DepartmentManagementPage() {
const [tab, setTab] = createSignal<'view' | 'create'>('view');
const [query, setQuery] = createSignal('');
const [rows, setRows] = createSignal<CrudRecord[]>([]);
const [nameInput, setNameInput] = createSignal('');
const [codeInput, setCodeInput] = createSignal('');
type DepartmentRecord = CrudRecord & {
code?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
departmentHead?: string;
departmentEmail?: string;
};
export default function DepartmentManagementPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [search, setSearch] = createSignal('');
const [rows, setRows] = createSignal<DepartmentRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
const [name, setName] = createSignal('');
const [code, setCode] = createSignal('');
const [description, setDescription] = createSignal('');
const [departmentHead, setDepartmentHead] = createSignal('');
const [departmentEmail, setDepartmentEmail] = createSignal('');
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [transfersEnabled, setTransfersEnabled] = createSignal(false);
const load = async () => {
const data = await listModuleRecords<DepartmentRecord>('department', { q: search().trim() || undefined });
setRows(data);
};
const load = async () => setRows(await listModuleRecords('department', { q: query() }));
onMount(() => void load());
const filtered = createMemo(() => {
const q = query().trim().toLowerCase();
if (!q) return rows();
return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q));
});
const resetForm = () => {
setEditingId(null);
setName('');
setCode('');
setDescription('');
setDepartmentHead('');
setDepartmentEmail('');
setStatus('ACTIVE');
setTransfersEnabled(false);
setCreateTab('general');
};
const metrics = createMemo(() => {
const all = rows();
const active = all.filter((item) => item.status === 'ACTIVE').length;
const inactive = all.filter((item) => item.status === 'INACTIVE').length;
return [
{ label: 'Total Departments', value: String(all.length || 0) },
{ label: 'Active Departments', value: String(active), tone: 'positive' as const },
{ label: 'Inactive Departments', value: String(inactive), tone: 'warning' as const },
{ label: 'Updated Today', value: String(Math.min(active, 6)), tone: 'info' as const },
];
});
const openCreate = () => {
resetForm();
setMainTab('create');
};
const openEdit = (row: DepartmentRecord) => {
setEditingId(row.id);
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');
};
const saveDepartment = 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(),
transfersEnabled: transfersEnabled(),
};
if (editingId()) {
await updateModuleRecord<DepartmentRecord>('department', editingId()!, payload);
} else {
await createModuleRecord<DepartmentRecord>('department', payload);
}
setMainTab('all');
setOpenMenuId(null);
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;
const fallback = input || new Date().toISOString().slice(0, 10);
return fallback.slice(0, 10);
};
return (
<div class="space-y-5">
<PageHeader
title="Department Management"
subtitle="Manage operational department structure and ownership mappings."
actions={<Tabs value={tab()} onChange={setTab} items={[{ key: 'view', label: 'View Departments' }, { key: 'create', label: 'Create Department' }]} />}
/>
<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>
<MetricCards items={metrics()} />
<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>
</div>
{tab() === 'view' ? (
<SectionCard
title="Departments"
subtitle="Search, activate, and maintain department records."
actions={
<>
<ActionButton>Export</ActionButton>
<ActionButton tone="primary" onClick={() => setTab('create')}>Add Department</ActionButton>
</>
}
>
<div class="space-y-3">
<SearchFilters
query={query()}
onQuery={(value) => {
setQuery(value);
void load();
}}
right={<ActionButton>Filter</ActionButton>}
/>
<DataTable
headers={['Department ID', 'Department Name', 'Status', 'Updated', 'Actions']}
rows={filtered().map((row) => [
<span class="font-medium text-[#050026]">{row.id}</span>,
<span>{row.name}</span>,
<StatusBadge label={row.status} tone={row.status === 'ACTIVE' ? 'positive' : 'warning'} />,
<span class="text-xs text-slate-500">{new Date(row.updatedAt).toLocaleString()}</span>,
<div class="flex gap-2">
<ActionButton
onClick={() => {
const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
void updateModuleRecord('department', row.id, { status: next }).then(() => void load());
}}
>
Toggle
</ActionButton>
<ActionButton tone="ghost">View</ActionButton>
</div>,
])}
/>
</div>
</SectionCard>
) : (
<SectionCard title="Create Department" subtitle="Create a new department with active runtime status.">
<form
class="grid gap-4 rounded-xl border border-[#e5eaf3] bg-[#f9fbff] p-4 md:grid-cols-2"
onSubmit={(e) => {
e.preventDefault();
const name = nameInput().trim() || codeInput().trim() || 'New Department';
void createModuleRecord('department', {
id: codeInput().trim() || undefined,
name,
status: 'ACTIVE',
}).then(() => {
setNameInput('');
setCodeInput('');
setTab('view');
void load();
});
}}
>
<label class="text-sm font-medium text-slate-700">
Department Name
<input value={nameInput()} onInput={(e) => setNameInput(e.currentTarget.value)} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
<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>
<label class="text-sm font-medium text-slate-700">
Department Code
<input value={codeInput()} onInput={(e) => setCodeInput(e.currentTarget.value.toUpperCase())} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
</label>
<div class="md:col-span-2 flex justify-end gap-2">
<ActionButton onClick={() => setTab('view')}>Cancel</ActionButton>
<ActionButton type="submit" tone="primary">Save Department</ActionButton>
<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>
</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>
</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 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]"><span class="text-[#fd6116]"></span>{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]"><span class="text-[#fd6116]">🗑</span>Delete Department</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</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] text-[#8a90a8]"></button>
</div>
</div>
</div>
</form>
</SectionCard>
)}
</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 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={['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles']}>
{(label) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{label}</div>}
</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>
);
}

View file

@ -1,123 +1,253 @@
import { createMemo, createSignal, onMount } from 'solid-js';
import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi';
import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import type { CrudRecord } from '~/lib/admin/types';
export default function DesignationManagementPage() {
const [tab, setTab] = createSignal<'view' | 'create'>('view');
const [query, setQuery] = createSignal('');
const [rows, setRows] = createSignal<CrudRecord[]>([]);
const [nameInput, setNameInput] = createSignal('');
const [codeInput, setCodeInput] = createSignal('');
type DesignationRecord = CrudRecord & {
code?: string;
department?: string;
level?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
canManageTeam?: boolean;
canApprove?: boolean;
};
const load = async () => setRows(await listModuleRecords('designation', { q: query() }));
export default function DesignationManagementPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
const [search, setSearch] = createSignal('');
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
const [name, setName] = createSignal('');
const [code, setCode] = createSignal('');
const [department, setDepartment] = 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 load = async () => {
const data = await listModuleRecords<DesignationRecord>('designation', { q: search().trim() || undefined });
setRows(data);
};
onMount(() => void load());
const filtered = createMemo(() => {
const q = query().trim().toLowerCase();
if (!q) return rows();
return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q));
});
const resetForm = () => {
setEditingId(null);
setName('');
setCode('');
setDepartment('');
setLevel('');
setDescription('');
setStatus('ACTIVE');
setCanManageTeam(false);
setCanApprove(false);
setCreateTab('general');
};
const metrics = createMemo(() => {
const all = rows();
const active = all.filter((item) => item.status === 'ACTIVE').length;
const inactive = all.filter((item) => item.status === 'INACTIVE').length;
return [
{ label: 'Total Designations', value: String(all.length || 0) },
{ label: 'Active Designations', value: String(active), tone: 'positive' as const },
{ label: 'Inactive Designations', value: String(inactive), tone: 'warning' as const },
{ label: 'Updated Today', value: String(Math.min(active, 8)), tone: 'info' as const },
];
});
const openCreate = () => {
resetForm();
setMainTab('create');
};
const openEdit = (row: DesignationRecord) => {
setEditingId(row.id);
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');
};
const saveDesignation = async () => {
const payload: Partial<DesignationRecord> = {
name: name().trim() || 'New Designation',
code: code().trim() || undefined,
department: department().trim(),
level: level().trim(),
description: description().trim(),
status: status(),
canManageTeam: canManageTeam(),
canApprove: canApprove(),
};
if (editingId()) {
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
} else {
await createModuleRecord<DesignationRecord>('designation', payload);
}
setMainTab('all');
setOpenMenuId(null);
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);
};
return (
<div class="space-y-5">
<PageHeader
title="Designation Management"
subtitle="Manage designation taxonomy used by internal and external role systems."
actions={<Tabs value={tab()} onChange={setTab} items={[{ key: 'view', label: 'View Designations' }, { key: 'create', label: 'Create Designation' }]} />}
/>
<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>
<MetricCards items={metrics()} />
<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>
</div>
{tab() === 'view' ? (
<SectionCard
title="Designations"
subtitle="Search and update designation availability."
actions={
<>
<ActionButton>Export</ActionButton>
<ActionButton tone="primary" onClick={() => setTab('create')}>Add Designation</ActionButton>
</>
}
>
<div class="space-y-3">
<SearchFilters
query={query()}
onQuery={(value) => {
setQuery(value);
void load();
}}
right={<ActionButton>Filter</ActionButton>}
/>
<DataTable
headers={['Designation ID', 'Designation Name', 'Status', 'Updated', 'Actions']}
rows={filtered().map((row) => [
<span class="font-medium text-[#050026]">{row.id}</span>,
<span>{row.name}</span>,
<StatusBadge label={row.status} tone={row.status === 'ACTIVE' ? 'positive' : 'warning'} />,
<span class="text-xs text-slate-500">{new Date(row.updatedAt).toLocaleString()}</span>,
<div class="flex gap-2">
<ActionButton
onClick={() => {
const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
void updateModuleRecord('designation', row.id, { status: next }).then(() => void load());
}}
>
Toggle
</ActionButton>
<ActionButton tone="ghost">View</ActionButton>
</div>,
])}
/>
</div>
</SectionCard>
) : (
<SectionCard title="Create Designation" subtitle="Add a new designation used in role and employee mapping.">
<form
class="grid gap-4 rounded-xl border border-[#e5eaf3] bg-[#f9fbff] p-4 md:grid-cols-2"
onSubmit={(e) => {
e.preventDefault();
const name = nameInput().trim() || codeInput().trim() || 'New Designation';
void createModuleRecord('designation', {
id: codeInput().trim() || undefined,
name,
status: 'ACTIVE',
}).then(() => {
setNameInput('');
setCodeInput('');
setTab('view');
void load();
});
}}
>
<label class="text-sm font-medium text-slate-700">
Designation Name
<input value={nameInput()} onInput={(e) => setNameInput(e.currentTarget.value)} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
</label>
<label class="text-sm font-medium text-slate-700">
Designation Code
<input value={codeInput()} onInput={(e) => setCodeInput(e.currentTarget.value.toUpperCase())} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
</label>
<div class="md:col-span-2 flex justify-end gap-2">
<ActionButton onClick={() => setTab('view')}>Cancel</ActionButton>
<ActionButton type="submit" tone="primary">Save Designation</ActionButton>
<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]" />
</div>
</form>
</SectionCard>
)}
<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>
</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>
</td>
</tr>
)}
</For>
</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> 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>
</div>
</div>
</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>
);
}