From d6405b55f611ddf4553dc1520a77bf87f01ae2ae Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Tue, 24 Mar 2026 04:47:05 +0100 Subject: [PATCH] ui(step-1): match reference layout for dept/designation/employees/roles pages - All pages: white sticky page header + tab bar with orange underline, -mx-6 -mt-6 negative margin to stretch headers edge-to-edge - department: full columns (ID, Name, Description, Created By, etc.), icon-only action buttons, navy Add Department button - designation: Designations List / Add Designation tabs, status filter dropdown, inline create/edit form, full columns with status badge - employees: View/Add tabs, icon-only action buttons, status badges - roles/index: clean table with Name+code subtext, Description, actions Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin/approval.tsx | 45 ++- src/routes/admin/department.tsx | 585 ++++++++++++++------------- src/routes/admin/designation.tsx | 652 +++++++++++++++---------------- src/routes/admin/employees.tsx | 432 +++++++++----------- src/routes/admin/roles/index.tsx | 181 ++++----- 5 files changed, 908 insertions(+), 987 deletions(-) diff --git a/src/routes/admin/approval.tsx b/src/routes/admin/approval.tsx index dd25da8..408566e 100644 --- a/src/routes/admin/approval.tsx +++ b/src/routes/admin/approval.tsx @@ -675,34 +675,39 @@ export default function ApprovalPage() { return ( - {/* Page header */} -
-
-

Approval Management

-

Review, approve, reject and configure approval workflows.

-
+
+ + {/* ── Page header ── */} +
+

Approval Management

+

Review, approve, reject and configure approval workflows.

- {/* Status tabs */} -
+ {/* ── Status tabs ── */} +
{(t) => { const count = countFor(t.key); return (
+
+
{actionError()}
@@ -1058,6 +1065,8 @@ export default function ApprovalPage() {
+
+
); } diff --git a/src/routes/admin/department.tsx b/src/routes/admin/department.tsx index f8b0b86..04c6d90 100644 --- a/src/routes/admin/department.tsx +++ b/src/routes/admin/department.tsx @@ -1,28 +1,36 @@ -import { A } from '@solidjs/router'; import { createResource, createSignal, createMemo, Show, For } from 'solid-js'; +import { Plus, Pencil, Archive, RotateCcw, Trash2, ChevronLeft, ChevronRight } from 'lucide-solid'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; type Department = { id: string; + departmentId?: string; name?: string; departmentName?: string; description?: string; + createdBy?: string; + updatedBy?: string; is_archived?: boolean; status?: string | number; - created_at?: string; createdAt?: string; + created_at?: string; + updatedAt?: string; }; -async function loadDepartments(): Promise { +type ViewMode = 'list' | 'create' | 'update'; + +async function loadDepartments(params: { page: number; limit: number; status: string }): Promise<{ items: Department[]; total: number }> { try { - const res = await fetch(`${API}/api/admin/departments`); + const res = await fetch(`${API}/api/admin/departments?page=${params.page}&limit=${params.limit}&status=${params.status}`); if (!res.ok) throw new Error('Failed to load'); const data = await res.json(); - return Array.isArray(data) ? data : (data.departments ?? []); + const items = Array.isArray(data) ? data : (data.departments ?? []); + const total = data.total ?? items.length; + return { items, total }; } catch { - return []; + return { items: [], total: 0 }; } } @@ -41,21 +49,24 @@ function deptLabel(item: Department): string { function fmtDate(val?: string): string { if (!val) return '—'; - try { - return new Date(val).toLocaleDateString(); - } catch { - return val; - } + try { return new Date(val).toLocaleDateString(); } catch { return val; } } export default function DepartmentPage() { - const [departments, { refetch }] = createResource(loadDepartments); + const [view, setView] = createSignal('list'); + const [statusFilter, setStatusFilter] = createSignal<'active' | 'archived'>('active'); + const [page, setPage] = createSignal(1); + const limit = 10; - // tabs - const [tab, setTab] = createSignal<'active' | 'archived'>('active'); + const fetchParams = createMemo(() => ({ + page: page(), + limit, + status: statusFilter() === 'archived' ? '2' : '1', + })); - // create form - const [showCreate, setShowCreate] = createSignal(false); + const [data, { refetch }] = createResource(fetchParams, loadDepartments); + + // form state const [createName, setCreateName] = createSignal(''); const [createDesc, setCreateDesc] = createSignal(''); const [creating, setCreating] = createSignal(false); @@ -68,18 +79,20 @@ export default function DepartmentPage() { const [saving, setSaving] = createSignal(false); const [editError, setEditError] = createSignal(''); - // row-level busy const [busy, setBusy] = createSignal(''); const [actionError, setActionError] = createSignal(''); + const items = () => data()?.items ?? []; + const total = () => data()?.total ?? 0; + const totalPages = () => Math.ceil(total() / limit); + const filtered = createMemo(() => { - const all = departments() ?? []; - return tab() === 'archived' + const all = items(); + return statusFilter() === 'archived' ? all.filter((d) => isArchived(d)) : all.filter((d) => !isArchived(d)); }); - // ---------- CREATE ---------- const handleCreate = async (e: Event) => { e.preventDefault(); if (!createName().trim()) return; @@ -89,19 +102,14 @@ export default function DepartmentPage() { const res = await fetch(`${API}/api/admin/departments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: createName().trim(), - description: createDesc().trim(), - }), + body: JSON.stringify({ name: createName().trim(), description: createDesc().trim() }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any).message || 'Failed to create'); } - setCreateName(''); - setCreateDesc(''); - setShowCreate(false); - setTab('active'); + setCreateName(''); setCreateDesc(''); + setView('list'); setStatusFilter('active'); setPage(1); refetch(); } catch (err: any) { setCreateError(err.message || 'Failed to create department'); @@ -110,315 +118,304 @@ export default function DepartmentPage() { } }; - // ---------- EDIT ---------- const startEdit = (item: Department) => { - setEditingId(item.id); - setEditName(deptLabel(item)); - setEditDesc(item.description ?? ''); - setEditError(''); - }; - - const cancelEdit = () => { - setEditingId(''); - setEditError(''); + setEditingId(item.id); setEditName(deptLabel(item)); setEditDesc(item.description ?? ''); setEditError(''); }; + const cancelEdit = () => { setEditingId(''); setEditError(''); }; const handleUpdate = async (id: string) => { if (!editName().trim()) return; - setSaving(true); - setEditError(''); + setSaving(true); setEditError(''); try { const res = await fetch(`${API}/api/admin/departments/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: editName().trim(), - description: editDesc().trim(), - }), + body: JSON.stringify({ name: editName().trim(), description: editDesc().trim() }), }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error((err as any).message || 'Failed to update'); - } - setEditingId(''); - refetch(); - } catch (err: any) { - setEditError(err.message || 'Failed to update department'); - } finally { - setSaving(false); - } + if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any).message || 'Failed to update'); } + setEditingId(''); refetch(); + } catch (err: any) { setEditError(err.message || 'Failed to update department'); } + finally { setSaving(false); } }; - // ---------- ARCHIVE / RESTORE ---------- const handleArchive = async (id: string) => { - setBusy(id); - setActionError(''); + setBusy(id); setActionError(''); try { const res = await fetch(`${API}/api/admin/departments/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_archived: true }), }); if (!res.ok) throw new Error('Failed to archive'); refetch(); - } catch (err: any) { - setActionError(err.message || 'Failed to archive department'); - } finally { - setBusy(''); - } + } catch (err: any) { setActionError(err.message || 'Failed to archive department'); } + finally { setBusy(''); } }; const handleRestore = async (id: string) => { - setBusy(id); - setActionError(''); + setBusy(id); setActionError(''); try { const res = await fetch(`${API}/api/admin/departments/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_archived: false }), }); if (!res.ok) throw new Error('Failed to restore'); refetch(); - } catch (err: any) { - setActionError(err.message || 'Failed to restore department'); - } finally { - setBusy(''); - } + } catch (err: any) { setActionError(err.message || 'Failed to restore department'); } + finally { setBusy(''); } }; - // ---------- DELETE ---------- const handleDelete = async (id: string, name: string) => { if (!confirm(`Delete department "${name}"?`)) return; - setBusy(id); - setActionError(''); + setBusy(id); setActionError(''); try { - const res = await fetch(`${API}/api/admin/departments/${id}`, { - method: 'DELETE', - }); + const res = await fetch(`${API}/api/admin/departments/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); refetch(); - } catch (err: any) { - setActionError(err.message || 'Failed to delete department'); - } finally { - setBusy(''); - } + } catch (err: any) { setActionError(err.message || 'Failed to delete department'); } + finally { setBusy(''); } + }; + + const switchTab = (t: 'active' | 'archived') => { + setView('list'); setStatusFilter(t); setPage(1); setEditingId(''); }; return ( - {/* Header */} -
-
-

Departments

-

Manage organization departments

-
- -
+
- {/* Create form */} - -
-
-
-
- - setCreateName(e.currentTarget.value)} - /> -
-
- - setCreateDesc(e.currentTarget.value)} - /> -
-
- -

{createError()}

+ {/* ── Page header ── */} +
+

Departments

+

Manage organization structure and units.

+
+ + {/* ── Tab + action bar ── */} +
+
+ + {(t) => ( + + )} + + + -
- - -
- -
-
- - {/* Tabs */} -
- - -
- - {/* Action error */} - -
{actionError()}
-
- - {/* Table */} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - 0}> - - {(item) => ( - <> - - - - - - - {/* Inline edit row */} - - - - - - - )} - - - -
NameDescriptionCreated AtActions
- Loading... -
- Failed to load. Is the backend running? -
- No departments found. -
{deptLabel(item)}{item.description || '—'}{fmtDate(item.createdAt || item.created_at)} -
- - - - - - - - -
-
-
-
- - setEditName(e.currentTarget.value)} - /> -
-
- - setEditDesc(e.currentTarget.value)} - /> -
-
- -

{editError()}

-
-
- - -
-
+ +
+ + +
- + + {/* ── Content ── */} +
+ + {/* Create form */} + +
+
+
+ + setCreateName(e.currentTarget.value)} + class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]" + /> +
+
+ + setCreateDesc(e.currentTarget.value)} + class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]" + /> +
+ +

{createError()}

+
+
+ + +
+
+
+
+ + {/* List view */} + +
+ +
{actionError()}
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + {(item) => ( + <> + + + + + + + + + + + {/* Inline edit row */} + + + + + + + )} + + +
IDNameDescriptionCreated ByCreatedLast Updated ByLast Updated AtActions
Loading…
Failed to load. Is the backend running?
No departments found.
{item.departmentId || item.id.slice(0, 8)}{deptLabel(item)}{item.description || '—'}{item.createdBy || '—'}{fmtDate(item.createdAt || item.created_at)}{item.updatedBy || item.createdBy || '—'}{fmtDate(item.updatedAt)} +
+ + + + + + + + +
+
+
+
+ + setEditName(e.currentTarget.value)} + class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]" /> +
+
+ + setEditDesc(e.currentTarget.value)} + class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]" /> +
+
+ +

{editError()}

+
+
+ + +
+
+
+
+ + {/* Pagination */} + 1}> +
+ Page {page()} of {totalPages()} +
+ + +
+
+
+
+
+
+
); } diff --git a/src/routes/admin/designation.tsx b/src/routes/admin/designation.tsx index 000a0db..4742f19 100644 --- a/src/routes/admin/designation.tsx +++ b/src/routes/admin/designation.tsx @@ -1,5 +1,5 @@ -import { A } from '@solidjs/router'; import { createResource, createSignal, createMemo, Show, For } from 'solid-js'; +import { Pencil, Archive, RotateCcw, ChevronLeft, ChevronRight } from 'lucide-solid'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; @@ -12,30 +12,40 @@ type Department = { type Designation = { id: string; + designationId?: string; name: string; departmentId?: string; departmentName?: string; department?: string; description?: string; + activeUsersCount?: number; + activeJobsCount?: number; + createdBy?: string; + updatedBy?: string; + createdAt?: string; + updatedAt?: string; is_archived?: boolean; status?: string; }; -async function loadDesignations(): Promise { +type ViewMode = 'list' | 'create' | 'edit'; + +async function loadDesignations(params: { page: number; limit: number; status: string }): Promise<{ items: Designation[]; total: number }> { try { - const res = await fetch(`${API}/api/admin/designations`); + const res = await fetch(`${API}/api/admin/designations?page=${params.page}&limit=${params.limit}&status=${params.status}`); if (!res.ok) throw new Error('Failed to load'); const data = await res.json(); - return Array.isArray(data) ? data : (data.designations ?? []); + const items = Array.isArray(data) ? data : (data.designations ?? []); + return { items, total: data.total ?? items.length }; } catch { - return []; + return { items: [], total: 0 }; } } async function loadDepartments(): Promise { try { - const res = await fetch(`${API}/api/admin/departments`); - if (!res.ok) throw new Error('Failed to load'); + const res = await fetch(`${API}/api/admin/departments?status=1&limit=200`); + if (!res.ok) throw new Error('Failed'); const data = await res.json(); return Array.isArray(data) ? data : (data.departments ?? []); } catch { @@ -43,14 +53,14 @@ async function loadDepartments(): Promise { } } -function deptDisplay(item: Designation): string { - return item.departmentName || item.department || '—'; -} - function deptName(d: Department): string { return d.departmentName || d.name || d.id; } +function deptDisplay(item: Designation): string { + return item.departmentName || item.department || '—'; +} + function isArchived(item: Designation): boolean { if (item.is_archived !== undefined) return item.is_archived; if (item.status !== undefined) { @@ -60,366 +70,344 @@ function isArchived(item: Designation): boolean { return false; } +function fmtDate(val?: string): string { + if (!val) return '—'; + try { return new Date(val).toLocaleDateString(); } catch { return val; } +} + export default function DesignationPage() { - const [designations, { refetch }] = createResource(loadDesignations); + const [view, setView] = createSignal('list'); + const [statusFilter, setStatusFilter] = createSignal<'active' | 'archived'>('active'); + const [page, setPage] = createSignal(1); + const limit = 10; + + const fetchParams = createMemo(() => ({ + page: page(), + limit, + status: statusFilter() === 'archived' ? 'ARCHIVED' : 'ACTIVE', + })); + + const [data, { refetch }] = createResource(fetchParams, loadDesignations); const [departments] = createResource(loadDepartments); - // tabs - const [tab, setTab] = createSignal<'active' | 'archived'>('active'); + // editing + const [editingDesignation, setEditingDesignation] = createSignal(null); - // create form - const [showCreate, setShowCreate] = createSignal(false); - const [createName, setCreateName] = createSignal(''); - const [createDeptId, setCreateDeptId] = createSignal(''); - const [createDesc, setCreateDesc] = createSignal(''); - const [creating, setCreating] = createSignal(false); - const [createError, setCreateError] = createSignal(''); + // form state (shared create/edit) + const [formName, setFormName] = createSignal(''); + const [formDeptId, setFormDeptId] = createSignal(''); + const [formDesc, setFormDesc] = createSignal(''); + const [formLoading, setFormLoading] = createSignal(false); + const [formError, setFormError] = createSignal(''); - // inline edit - const [editingId, setEditingId] = createSignal(''); - const [editName, setEditName] = createSignal(''); - const [editDeptId, setEditDeptId] = createSignal(''); - const [editDesc, setEditDesc] = createSignal(''); - const [saving, setSaving] = createSignal(false); - const [editError, setEditError] = createSignal(''); - - // row busy / errors - const [deleting, setDeleting] = createSignal(''); + const [busy, setBusy] = createSignal(''); const [actionError, setActionError] = createSignal(''); + const items = () => data()?.items ?? []; + const total = () => data()?.total ?? 0; + const totalPages = () => Math.ceil(total() / limit); + const filtered = createMemo(() => { - const all = designations() ?? []; - return tab() === 'archived' + const all = items(); + return statusFilter() === 'archived' ? all.filter((d) => isArchived(d)) : all.filter((d) => !isArchived(d)); }); - // ---------- CREATE ---------- + const resetForm = () => { + const depts = departments() ?? []; + setFormName(''); setFormDeptId(depts[0]?.id ?? ''); setFormDesc(''); setFormError(''); + }; + + const openCreate = () => { + resetForm(); setEditingDesignation(null); setView('create'); + }; + + const openEdit = (item: Designation) => { + setEditingDesignation(item); + setFormName(item.name); setFormDeptId(item.departmentId ?? ''); setFormDesc(item.description ?? ''); setFormError(''); + setView('edit'); + }; + const handleCreate = async (e: Event) => { e.preventDefault(); - if (!createName().trim() || !createDeptId()) return; - setCreating(true); - setCreateError(''); + if (!formName().trim() || !formDeptId()) return; + setFormLoading(true); setFormError(''); try { const res = await fetch(`${API}/api/admin/designations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: createName().trim(), - department_id: createDeptId(), - description: createDesc().trim(), - }), + body: JSON.stringify({ name: formName().trim(), department_id: formDeptId(), description: formDesc().trim() }), }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error((err as any).message || 'Failed to create'); - } - setCreateName(''); - setCreateDeptId(''); - setCreateDesc(''); - setShowCreate(false); - setTab('active'); - refetch(); - } catch (err: any) { - setCreateError(err.message || 'Failed to create designation'); - } finally { - setCreating(false); - } + if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any).message || 'Failed to create'); } + resetForm(); setView('list'); setStatusFilter('active'); setPage(1); refetch(); + } catch (err: any) { setFormError(err.message || 'Failed to create designation'); } + finally { setFormLoading(false); } }; - // ---------- INLINE EDIT ---------- - const startEdit = (item: Designation) => { - setEditingId(item.id); - setEditName(item.name); - setEditDeptId(item.departmentId ?? ''); - setEditDesc(item.description ?? ''); - setEditError(''); - }; - - const cancelEdit = () => { - setEditingId(''); - setEditError(''); - }; - - const handleUpdate = async (id: string) => { - if (!editName().trim()) return; - setSaving(true); - setEditError(''); + const handleUpdate = async (e: Event) => { + e.preventDefault(); + const editing = editingDesignation(); + if (!editing || !formName().trim() || !formDeptId()) return; + setFormLoading(true); setFormError(''); try { - const res = await fetch(`${API}/api/admin/designations/${id}`, { + const res = await fetch(`${API}/api/admin/designations/${editing.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: editName().trim(), - department_id: editDeptId(), - description: editDesc().trim(), - }), + body: JSON.stringify({ name: formName().trim(), department_id: formDeptId(), description: formDesc().trim() }), }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error((err as any).message || 'Failed to update'); - } - setEditingId(''); - refetch(); - } catch (err: any) { - setEditError(err.message || 'Failed to update designation'); - } finally { - setSaving(false); - } + if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as any).message || 'Failed to update'); } + setEditingDesignation(null); setView('list'); refetch(); + } catch (err: any) { setFormError(err.message || 'Failed to update designation'); } + finally { setFormLoading(false); } }; - // ---------- DELETE ---------- - const handleDelete = async (id: string, name: string) => { - if (!confirm(`Delete designation "${name}"?`)) return; - setDeleting(id); - setActionError(''); + const handleArchive = async (id: string) => { + setBusy(id); setActionError(''); try { const res = await fetch(`${API}/api/admin/designations/${id}`, { - method: 'DELETE', + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_archived: true }), }); - if (!res.ok) throw new Error('Failed to delete'); + if (!res.ok) throw new Error('Failed to archive'); refetch(); - } catch (err: any) { - setActionError(err.message || 'Failed to delete designation'); - } finally { - setDeleting(''); - } + } catch (err: any) { setActionError(err.message || 'Failed to archive designation'); } + finally { setBusy(''); } + }; + + const handleRestore = async (id: string) => { + setBusy(id); setActionError(''); + try { + const res = await fetch(`${API}/api/admin/designations/${id}`, { + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_archived: false }), + }); + if (!res.ok) throw new Error('Failed to restore'); + refetch(); + } catch (err: any) { setActionError(err.message || 'Failed to restore designation'); } + finally { setBusy(''); } + }; + + const FormContent = () => { + const depts = () => departments() ?? []; + return ( +
+
+
+ + setFormName(e.currentTarget.value)} + class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]" + /> +
+
+ + +
+
+ +