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

332 lines
22 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 DepartmentRecord = CrudRecord & {
code?: string;
description?: string;
totalEmployees?: number;
createdDate?: string;
departmentHead?: string;
departmentEmail?: string;
};
const FALLBACK_DEPARTMENTS: DepartmentRecord[] = [
{ id: 'd1', name: 'Marketing', code: 'MKT-002', description: 'Brand management and digital marketing', totalEmployees: 23, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd2', name: 'Human Resources', code: 'HR-003', description: 'Employee relations and talent acquisition', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd3', name: 'Finance', code: 'FIN-004', description: 'Financial planning and accounting', totalEmployees: 18, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd4', name: 'Operations', code: 'OPS-005', description: 'Business operations and process management', totalEmployees: 31, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd5', name: 'Customer Success', code: 'CS-006', description: 'Client support and relationship management', totalEmployees: 27, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd6', name: 'Product', code: 'PRD-007', description: 'Product strategy and development', totalEmployees: 19, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd7', name: 'Sales', code: 'SAL-008', description: 'Revenue generation and client acquisition', totalEmployees: 34, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
{ id: 'd8', name: 'Engineering', code: 'ENG-001', description: 'Software development and technical architecture', totalEmployees: 45, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-03-01' },
];
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 () => {
try {
const res = await fetch(`/api/gateway/api/admin/departments?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.department_id ?? `dep-${i + 1}`),
name: String(item.name ?? item.department_name ?? ''),
code: String(item.code ?? item.department_code ?? ''),
description: String(item.description ?? ''),
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? item.employee_count ?? 0),
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
updatedAt: String(item.updatedAt ?? item.updated_at ?? new Date().toISOString().slice(0, 10)),
createdDate: String(item.createdDate ?? item.created_at ?? new Date().toISOString().slice(0, 10)),
})),
);
return;
}
}
} catch {}
try {
const data = await listModuleRecords<DepartmentRecord>('department', { q: search().trim() || undefined });
setRows(Array.isArray(data) && data.length > 0 ? data : FALLBACK_DEPARTMENTS);
} catch {
setRows(FALLBACK_DEPARTMENTS);
}
};
onMount(() => void load());
const resetForm = () => {
setEditingId(null);
setName('');
setCode('');
setDescription('');
setDepartmentHead('');
setDepartmentEmail('');
setStatus('ACTIVE');
setTransfersEnabled(false);
setCreateTab('general');
};
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 permissionGroups = [
{
title: 'Employee Management',
items: ['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees'],
},
{
title: 'Role Management',
items: ['View Roles', 'Assign Roles'],
},
{
title: 'Department Settings',
items: ['Manage Department Settings'],
},
];
const formatDate = (value?: string) => {
const input = value || '';
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
const fallback = input || new Date().toISOString().slice(0, 10);
return fallback.slice(0, 10);
};
return (
<AdminShell>
<div class="space-y-5">
<section>
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Department Management</h1>
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all departments and organizational structure</p>
</section>
<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>
<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>
<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]">
<svg class="h-5 w-5 text-[#fd6116]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 20h9" /><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z" /></svg>
Edit Department
</button>
<button onClick={async () => { await updateModuleRecord<DepartmentRecord>('department', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]">
<svg class="h-5 w-5 text-[#fd6116]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M8 8l8 8" /></svg>
{row.status === 'ACTIVE' ? 'Deactivate Department' : 'Activate Department'}
</button>
<button onClick={async () => { await deleteModuleRecord('department', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]">
<svg class="h-5 w-5 text-[#fd6116]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 6h18" /><path d="M8 6V4h8v2" /><path d="M19 6l-1 14H6L5 6" /><path d="M10 11v6M14 11v6" /></svg>
Delete Department
</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</tbody>
</table>
<div class="flex items-center justify-between border-t border-[#dde1ea] px-6 py-4">
<p class="text-[14px] text-[#707895]">Showing <span class="font-semibold text-[#283055]">1-{rows().length}</span> of <span class="font-semibold text-[#283055]">{rows().length}</span> departments</p>
<div class="flex items-center gap-2">
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] bg-[#fd6116] font-semibold text-white">1</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] font-semibold text-[#2a3052]">2</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] font-semibold text-[#2a3052]">3</button>
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]"></button>
</div>
</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]'}`}>Department Settings<Show when={createTab() === 'settings'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
<button onClick={() => setCreateTab('permissions')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'permissions' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Permissions<Show when={createTab() === 'permissions'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
</div>
<Show when={createTab() === 'general'}>
<div class="space-y-5">
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Department Name <span class="text-[#fd6116]">*</span><input value={name()} onInput={(e) => setName(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department name" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Department Code <span class="text-[#fd6116]">*</span><input value={code()} onInput={(e) => setCode(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="e.g., ENG-001" /></label>
</div>
<label class="block text-[14px] font-semibold text-[#101848]">Department Description<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} class="mt-2 h-[110px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 py-3 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department description" /></label>
<div class="grid gap-5 md:grid-cols-2">
<label class="block text-[14px] font-semibold text-[#101848]">Department Head<input value={departmentHead()} onInput={(e) => setDepartmentHead(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
<label class="block text-[14px] font-semibold text-[#101848]">Department Email<input value={departmentEmail()} onInput={(e) => setDepartmentEmail(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="department@example.com" /></label>
</div>
</div>
</Show>
<Show when={createTab() === 'settings'}>
<div class="space-y-6">
<div>
<p class="text-[18px] font-semibold text-[#101848]">Department Status</p>
<div class="mt-3 flex gap-3">
<button onClick={() => setStatus('ACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'ACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Active</button>
<button onClick={() => setStatus('INACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'INACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Inactive</button>
</div>
</div>
<div>
<p class="text-[18px] font-semibold text-[#101848]">Department Visibility</p>
<div class="mt-3 space-y-3">
<div class="rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<p class="text-[16px] font-semibold text-[#101848]">Internal</p>
<p class="text-[14px] text-[#7d849f]">Only visible to internal employees</p>
</div>
<div class="rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<p class="text-[16px] font-semibold text-[#101848]">External</p>
<p class="text-[14px] text-[#7d849f]">Visible to external users and partners</p>
</div>
</div>
</div>
<div class="flex items-center justify-between rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Employee Transfers</p><p class="text-[14px] text-[#7d849f]">Enable employees to request transfer to this department</p></div>
<button onClick={() => setTransfersEnabled((v) => !v)} class={`relative h-9 w-16 rounded-full transition ${transfersEnabled() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-7 w-7 rounded-full bg-white transition ${transfersEnabled() ? 'left-8' : 'left-1'}`} /></button>
</div>
</div>
</Show>
<Show when={createTab() === 'permissions'}>
<div class="space-y-4">
<p class="text-[16px] text-[#707895]">Select permissions for this department</p>
<For each={permissionGroups}>
{(group) => (
<section class="space-y-3">
<h3 class="text-[18px] font-semibold text-[#101848]">{group.title}</h3>
<For each={group.items}>
{(item) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{item}</div>}
</For>
</section>
)}
</For>
</div>
</Show>
<div class="flex justify-end gap-3 border-t border-[#e1e5ee] pt-4">
<button onClick={() => { setMainTab('all'); resetForm(); }} class="h-[44px] rounded-[12px] border border-[#d2d8e4] bg-[#f7f7f8] px-6 text-[16px] font-semibold text-[#232b4d]">Cancel</button>
<button onClick={() => void saveDepartment()} class="h-[44px] rounded-[12px] bg-[#030047] px-8 text-[16px] font-semibold text-white">{editingId() ? 'Save Department' : 'Create Department'}</button>
</div>
</div>
</Show>
</section>
</div>
</AdminShell>
);
}