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

292 lines
20 KiB
TypeScript
Raw Normal View History

import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
import type { CrudRecord } from '~/lib/admin/types';
type DesignationRecord = CrudRecord & {
code?: string;
department?: string;
level?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
canManageTeam?: boolean;
canApprove?: boolean;
};
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
];
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 () => {
try {
const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100&q=${encodeURIComponent(search().trim())}`);
if (res.ok) {
const payload = await res.json().catch(() => null);
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
if (list.length > 0) {
setRows(
list.map((item: any, i: number) => ({
id: String(item.id ?? item.designation_id ?? `des-${i + 1}`),
name: String(item.name ?? item.designation_name ?? ''),
code: String(item.code ?? item.designation_code ?? ''),
department: String(item.department ?? item.department_name ?? ''),
level: String(item.level ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? item.employee_count ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? new Date().toISOString().slice(0, 10)),
createdDate: String(item.createdDate ?? item.created_at ?? new Date().toISOString().slice(0, 10)),
})),
);
return;
}
}
} catch {}
try {
const data = await listModuleRecords<DesignationRecord>('designation', { q: search().trim() || undefined });
setRows(Array.isArray(data) && data.length > 0 ? data : FALLBACK_DESIGNATIONS);
} catch {
setRows(FALLBACK_DESIGNATIONS);
}
};
onMount(() => void load());
const resetForm = () => {
setEditingId(null);
setName('');
setCode('');
setDepartment('');
setLevel('');
setDescription('');
setStatus('ACTIVE');
setCanManageTeam(false);
setCanApprove(false);
setCreateTab('general');
};
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 (
<AdminShell>
<div class="space-y-5">
<section>
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Designation Management</h1>
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all designations and job positions</p>
</section>
<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>
<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>
<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>
</AdminShell>
);
}