feat: phase 3 identity & configuration ui matching figma designs
This commit is contained in:
parent
83a1f45e4f
commit
648b6be849
4 changed files with 648 additions and 481 deletions
|
|
@ -182,87 +182,60 @@ export default function DepartmentPage() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
|
||||
{/* ── Page header ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Departments</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage organization structure and units.</p>
|
||||
</div>
|
||||
|
||||
{/* ── Tab + action bar ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-10">
|
||||
<div class="flex gap-8">
|
||||
<For each={(['active', 'archived'] as const)}>
|
||||
{(t) => (
|
||||
<button
|
||||
onClick={() => switchTab(t)}
|
||||
class={`py-3 border-b-2 text-sm font-medium capitalize transition-colors ${
|
||||
view() === 'list' && statusFilter() === t
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t} Departments
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<Show when={view() === 'create'}>
|
||||
<button class="py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium">
|
||||
Create Department
|
||||
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
|
||||
{/* Header & Title */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Department Management</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
|
||||
Export Data
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={view() === 'update'}>
|
||||
<button class="py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium">
|
||||
Update Department
|
||||
<button
|
||||
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
|
||||
onClick={() => { setView('create'); setCreateError(''); setCreateName(''); setCreateDesc(''); }}
|
||||
>
|
||||
<span class="mr-2 text-lg leading-none">+</span> Create Department
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={view() === 'list'}>
|
||||
<button
|
||||
onClick={() => { setView('create'); setCreateError(''); setCreateName(''); setCreateDesc(''); }}
|
||||
class="btn-primary"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Add Department
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* ── Content ── */}
|
||||
<div class="flex-1">
|
||||
|
||||
{/* Create form */}
|
||||
<Show when={view() === 'create'}>
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-6">
|
||||
<form onSubmit={handleCreate} class="grid grid-cols-1 gap-6 md:grid-cols-2 max-w-4xl">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Department Name *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g. Engineering"
|
||||
value={createName()}
|
||||
onInput={(e) => 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]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional description"
|
||||
value={createDesc()}
|
||||
onInput={(e) => 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]"
|
||||
/>
|
||||
{/* Create form */}
|
||||
<div class="bg-white border focus-within:border-[#0a1d37] border-[#e2e6ee] rounded-3xl p-6 mb-8 max-w-4xl shadow-sm">
|
||||
<h2 class="text-[22px] font-bold text-[#050026] mb-6">Create New Department</h2>
|
||||
<form onSubmit={handleCreate} class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Department Name *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g. Engineering"
|
||||
value={createName()}
|
||||
onInput={(e) => setCreateName(e.currentTarget.value)}
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional description"
|
||||
value={createDesc()}
|
||||
onInput={(e) => setCreateDesc(e.currentTarget.value)}
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={createError()}>
|
||||
<p class="md:col-span-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{createError()}</p>
|
||||
<p class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{createError()}</p>
|
||||
</Show>
|
||||
<div class="md:col-span-2 flex justify-end gap-3 pt-2 border-t border-gray-100">
|
||||
<button type="button" onClick={() => setView('list')} class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-[#e2e6ee]">
|
||||
<button type="button" onClick={() => setView('list')} class="h-11 rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" disabled={creating()} class="btn-primary">
|
||||
<button type="submit" disabled={creating()} class="h-11 rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors disabled:opacity-70">
|
||||
{creating() ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -270,66 +243,105 @@ export default function DepartmentPage() {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
{/* List view */}
|
||||
<Show when={view() === 'list'}>
|
||||
<div class="p-6">
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
{/* Main Table Section */}
|
||||
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
|
||||
<div class="rounded-[20px] bg-white p-5">
|
||||
|
||||
{/* Error Message */}
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
|
||||
<div class="table-card">
|
||||
{/* Tabs */}
|
||||
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
|
||||
<For each={(['active', 'archived'] as const)}>
|
||||
{(t) => (
|
||||
<button
|
||||
onClick={() => switchTab(t)}
|
||||
class={`pb-3 text-[14px] font-bold capitalize transition-colors border-b-2 ${
|
||||
statusFilter() === t
|
||||
? 'border-[#050026] text-[#050026]'
|
||||
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
|
||||
}`}
|
||||
>
|
||||
{t} Department
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
|
||||
<div class="relative w-full md:w-[320px]">
|
||||
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search departments..."
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
||||
<div class="flex-1"></div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table w-full text-sm">
|
||||
<table class="w-full min-w-[1000px] border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Created By</th>
|
||||
<th>Created</th>
|
||||
<th>Last Updated By</th>
|
||||
<th>Last Updated At</th>
|
||||
<th class="text-right">Actions</th>
|
||||
<tr class="bg-[#050026] text-left text-white">
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">DEPARTMENT ID</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DEPARTMENT NAME</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DESCRIPTION</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CREATED BY</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CREATED</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED BY</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED AT</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={data.loading}>
|
||||
<tr><td colspan="8" class="py-10 text-center text-sm text-slate-400">Loading…</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-[14px]">Loading departments...</td></tr>
|
||||
</Show>
|
||||
<Show when={!data.loading && data.error}>
|
||||
<tr><td colspan="8" class="py-10 text-center text-sm text-red-500">Failed to load. Is the backend running?</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-red-500 text-[14px]">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!data.loading && !data.error && filtered().length === 0}>
|
||||
<tr><td colspan="8" class="py-10 text-center text-sm text-slate-400">No departments found.</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-[14px]">No departments found.</td></tr>
|
||||
</Show>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<>
|
||||
<tr class="group hover:bg-slate-50">
|
||||
<td class="font-mono text-xs text-slate-500">{item.departmentId || item.id.slice(0, 8)}</td>
|
||||
<td class="font-semibold text-slate-900">{deptLabel(item)}</td>
|
||||
<td class="text-slate-500">{item.description || '—'}</td>
|
||||
<td class="text-blue-600 hover:underline"><a href={`mailto:${item.createdBy}`}>{item.createdBy || '—'}</a></td>
|
||||
<td class="text-slate-500">{fmtDate(item.createdAt || item.created_at)}</td>
|
||||
<td class="text-blue-600 hover:underline"><a href={`mailto:${item.updatedBy || item.createdBy}`}>{item.updatedBy || item.createdBy || '—'}</a></td>
|
||||
<td class="text-slate-500">{fmtDate(item.updatedAt)}</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
|
||||
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">{item.departmentId || item.id.slice(0, 8).toUpperCase()}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{deptLabel(item)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{item.description || '—'}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#0ea5e9] hover:underline cursor-pointer">{item.createdBy || 'System'}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{fmtDate(item.createdAt || item.created_at)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#0ea5e9] hover:underline cursor-pointer">{item.updatedBy || item.createdBy || 'System'}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{fmtDate(item.updatedAt)}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Show when={!isArchived(item)}>
|
||||
<button
|
||||
title="Edit"
|
||||
onClick={() => startEdit(item)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<Pencil size={14} class="text-gray-600" />
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
title="Archive"
|
||||
disabled={busy() === item.id}
|
||||
onClick={() => handleArchive(item.id)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<Archive size={14} class="text-gray-600" />
|
||||
<Archive size={14} />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={isArchived(item)}>
|
||||
|
|
@ -337,18 +349,18 @@ export default function DepartmentPage() {
|
|||
title="Restore"
|
||||
disabled={busy() === item.id}
|
||||
onClick={() => handleRestore(item.id)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-green-200 bg-green-50 text-[#00c853] hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<RotateCcw size={14} class="text-green-600" />
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
title="Delete"
|
||||
disabled={busy() === item.id}
|
||||
onClick={() => handleDelete(item.id, deptLabel(item))}
|
||||
class="action-btn flex items-center justify-center border-red-100 bg-red-50 hover:bg-red-100 transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-red-200 bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} class="text-red-600" />
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -356,28 +368,28 @@ export default function DepartmentPage() {
|
|||
{/* Inline edit row */}
|
||||
<Show when={editingId() === item.id}>
|
||||
<tr>
|
||||
<td colspan="8" class="bg-slate-50 px-6 py-4">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 max-w-2xl">
|
||||
<td colspan="8" class="bg-[#f8f9fc] px-6 py-4 border-b border-[#e2e6ee]">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 max-w-2xl bg-white p-4 rounded-xl shadow-sm border border-[#e2e6ee]">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">Name *</label>
|
||||
<label class="mb-1 block text-xs font-semibold text-[#383e5c]">Name *</label>
|
||||
<input type="text" required value={editName()} onInput={(e) => 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]" />
|
||||
class="h-10 w-full rounded-lg border border-[#d9dde6] bg-[#f9fafb] px-3 text-sm outline-none focus:border-[#050026] focus:bg-white transition-colors" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-700">Description</label>
|
||||
<label class="mb-1 block text-xs font-semibold text-[#383e5c]">Description</label>
|
||||
<input type="text" value={editDesc()} onInput={(e) => 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]" />
|
||||
class="h-10 w-full rounded-lg border border-[#d9dde6] bg-[#f9fafb] px-3 text-sm outline-none focus:border-[#050026] focus:bg-white transition-colors" />
|
||||
</div>
|
||||
<Show when={editError()}>
|
||||
<div class="col-span-2 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">{editError()}</div>
|
||||
</Show>
|
||||
<div class="col-span-2 mt-2 flex justify-end gap-2 pt-3 border-t border-[#e2e6ee]">
|
||||
<button type="button" onClick={cancelEdit} class="h-9 rounded-lg border border-[#d9dde6] bg-white px-4 text-[13px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors">Cancel</button>
|
||||
<button type="button" disabled={saving()} onClick={() => handleUpdate(item.id)}
|
||||
class="h-9 rounded-lg bg-[#050026] px-4 text-[13px] font-semibold text-white hover:bg-[#0a0044] transition-colors disabled:opacity-70">
|
||||
{saving() ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={editError()}>
|
||||
<p class="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">{editError()}</p>
|
||||
</Show>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button type="button" onClick={cancelEdit} class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">Cancel</button>
|
||||
<button type="button" disabled={saving()} onClick={() => handleUpdate(item.id)}
|
||||
class="btn-primary disabled:opacity-60">
|
||||
{saving() ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -388,31 +400,32 @@ export default function DepartmentPage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Show when={totalPages() > 1}>
|
||||
<div class="mt-4 flex items-center justify-between border-t border-gray-200 pt-4">
|
||||
<span class="text-sm text-gray-500">Page {page()} of {totalPages()}</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
disabled={page() === 1}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
class="rounded-lg border border-gray-200 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 transition-colors"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<button
|
||||
disabled={page() >= totalPages()}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
class="rounded-lg border border-gray-200 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 transition-colors"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
{/* Pagination */}
|
||||
<Show when={totalPages() > 1}>
|
||||
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
|
||||
<span class="text-[13px] font-medium text-[#8087a0]">Page {page()} of {totalPages()}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
disabled={page() === 1}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<button
|
||||
disabled={page() >= totalPages()}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -248,134 +248,145 @@ export default function DesignationPage() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
|
||||
{/* ── Page header ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Designations</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage job titles and roles within departments.</p>
|
||||
</div>
|
||||
|
||||
{/* ── Tab + action bar ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-10">
|
||||
<div class="flex gap-8">
|
||||
<button
|
||||
onClick={() => { setView('list'); setEditingDesignation(null); }}
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${
|
||||
view() === 'list' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Designations List
|
||||
</button>
|
||||
<button
|
||||
onClick={openCreate}
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${
|
||||
view() === 'create' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Add Designation
|
||||
</button>
|
||||
<Show when={view() === 'edit' && editingDesignation()}>
|
||||
<button class="py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium">
|
||||
Edit: {editingDesignation()?.name}
|
||||
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
|
||||
{/* Header & Title */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Designation Management</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
|
||||
Export Data
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
|
||||
onClick={openCreate}
|
||||
>
|
||||
<span class="mr-2 text-lg leading-none">+</span> Create Designation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status filter — only in list view */}
|
||||
<Show when={view() === 'list'}>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => { setStatusFilter(e.currentTarget.value as 'active' | 'archived'); setPage(1); }}
|
||||
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37]"
|
||||
>
|
||||
<option value="active">Active</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* ── Content ── */}
|
||||
<div class="flex-1">
|
||||
|
||||
{/* Create / Edit form */}
|
||||
<Show when={view() === 'create' || view() === 'edit'}>
|
||||
<div class="bg-white px-6 py-6 border-b border-gray-100">
|
||||
<div class="bg-white border focus-within:border-[#0a1d37] border-[#e2e6ee] rounded-3xl p-6 mb-8 max-w-4xl shadow-sm">
|
||||
<h2 class="text-[22px] font-bold text-[#050026] mb-6">
|
||||
{view() === 'create' ? 'Create New Designation' : `Edit: ${editingDesignation()?.name}`}
|
||||
</h2>
|
||||
<FormContent />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* List */}
|
||||
<Show when={view() === 'list'}>
|
||||
<div class="p-6">
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
|
||||
<div class="rounded-[20px] bg-white p-5">
|
||||
|
||||
{/* Error Message */}
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
|
||||
<div class="table-card">
|
||||
{/* Tabs */}
|
||||
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
|
||||
<For each={(['active', 'archived'] as const)}>
|
||||
{(t) => (
|
||||
<button
|
||||
onClick={() => { setStatusFilter(t); setPage(1); }}
|
||||
class={`pb-3 text-[14px] font-bold capitalize transition-colors border-b-2 ${
|
||||
statusFilter() === t
|
||||
? 'border-[#050026] text-[#050026]'
|
||||
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
|
||||
}`}
|
||||
>
|
||||
{t} Designation
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
|
||||
<div class="relative w-full md:w-[320px]">
|
||||
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search designations..."
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
||||
<div class="flex-1"></div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table w-full text-sm">
|
||||
<table class="w-full min-w-[1000px] border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Department</th>
|
||||
<th>Description</th>
|
||||
<th>Active Users</th>
|
||||
<th>Active Jobs</th>
|
||||
<th>Status</th>
|
||||
<th>Created By</th>
|
||||
<th>Created At</th>
|
||||
<th>Last Updated By</th>
|
||||
<th>Last Updated At</th>
|
||||
<th class="text-right">Actions</th>
|
||||
<tr class="bg-[#050026] text-left text-white">
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">DESIGNATION ID</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DESIGNATION TITLE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DEPARTMENT</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CREATED BY</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CREATED</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED BY</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST UPDATED AT</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={data.loading}>
|
||||
<tr><td colspan="12" class="py-10 text-center text-sm text-slate-400">Loading…</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-[14px]">Loading designations...</td></tr>
|
||||
</Show>
|
||||
<Show when={!data.loading && data.error}>
|
||||
<tr><td colspan="12" class="py-10 text-center text-sm text-red-500">Failed to load. Is the backend running?</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-red-500 text-[14px]">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!data.loading && !data.error && filtered().length === 0}>
|
||||
<tr><td colspan="12" class="py-10 text-center text-sm text-slate-400">No designations found.</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-[14px]">No designations found.</td></tr>
|
||||
</Show>
|
||||
<For each={filtered()}>
|
||||
{(item) => {
|
||||
const archived = () => isArchived(item);
|
||||
return (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-mono text-xs text-slate-500">{(item.designationId || item.id).slice(0, 16)}</td>
|
||||
<td class="font-semibold text-slate-900">{item.name}</td>
|
||||
<td class="text-slate-500">{deptDisplay(item)}</td>
|
||||
<td class="text-slate-500 max-w-xs truncate" title={item.description}>{item.description || '—'}</td>
|
||||
<td class="text-slate-500">{item.activeUsersCount ?? 0}</td>
|
||||
<td class="text-slate-500">{item.activeJobsCount ?? 0}</td>
|
||||
<td>
|
||||
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${archived() ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}`}>
|
||||
{archived() ? 'ARCHIVED' : 'ACTIVE'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-blue-600 hover:underline"><a href={`mailto:${item.createdBy}`}>{item.createdBy || '—'}</a></td>
|
||||
<td class="text-slate-500">{fmtDate(item.createdAt)}</td>
|
||||
<td class="text-blue-600 hover:underline"><a href={`mailto:${item.updatedBy}`}>{item.updatedBy || '—'}</a></td>
|
||||
<td class="text-slate-500">{fmtDate(item.updatedAt)}</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<button title="Edit" onClick={() => openEdit(item)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors">
|
||||
<Pencil size={14} class="text-gray-600" />
|
||||
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
|
||||
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">{(item.designationId || item.id).slice(0, 16).toUpperCase()}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{item.name}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{deptDisplay(item)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#0ea5e9] hover:underline cursor-pointer">{item.createdBy || 'System'}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{fmtDate(item.createdAt)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#0ea5e9] hover:underline cursor-pointer">{item.updatedBy || item.createdBy || 'System'}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{fmtDate(item.updatedAt)}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
title="Edit"
|
||||
onClick={() => openEdit(item)}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<Show when={!archived()}>
|
||||
<button title="Archive" disabled={busy() === item.id} onClick={() => handleArchive(item.id)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors">
|
||||
<Archive size={14} class="text-gray-600" />
|
||||
<button
|
||||
title="Archive"
|
||||
disabled={busy() === item.id}
|
||||
onClick={() => handleArchive(item.id)}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<Archive size={14} />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={archived()}>
|
||||
<button title="Restore" disabled={busy() === item.id} onClick={() => handleRestore(item.id)}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors">
|
||||
<RotateCcw size={14} class="text-green-600" />
|
||||
<button
|
||||
title="Restore"
|
||||
disabled={busy() === item.id}
|
||||
onClick={() => handleRestore(item.id)}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-green-200 bg-green-50 text-[#00c853] hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -387,24 +398,32 @@ export default function DesignationPage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={totalPages() > 1}>
|
||||
<div class="mt-4 flex items-center justify-between border-t border-gray-200 pt-4">
|
||||
<span class="text-sm text-gray-500">Page {page()} of {totalPages()}</span>
|
||||
<div class="flex gap-2">
|
||||
<button disabled={page() === 1} onClick={() => setPage((p) => p - 1)}
|
||||
class="rounded-lg border border-gray-200 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 transition-colors">
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<button disabled={page() >= totalPages()} onClick={() => setPage((p) => p + 1)}
|
||||
class="rounded-lg border border-gray-200 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 transition-colors">
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
{/* Pagination */}
|
||||
<Show when={totalPages() > 1}>
|
||||
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
|
||||
<span class="text-[13px] font-medium text-[#8087a0]">Page {page()} of {totalPages()}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
disabled={page() === 1}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<button
|
||||
disabled={page() >= totalPages()}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -124,58 +124,50 @@ export default function EmployeesPage() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
|
||||
{/* ── Page header ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Internal User Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage internal team members and their access.</p>
|
||||
</div>
|
||||
|
||||
{/* ── Tab bar ── */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-10">
|
||||
<div class="flex gap-8">
|
||||
<button
|
||||
onClick={() => setView('list')}
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${view() === 'list' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||||
>
|
||||
View Internal Users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { resetForm(); setView('create'); }}
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${view() === 'create' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||||
>
|
||||
Add Internal User
|
||||
</button>
|
||||
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
|
||||
{/* Header & Title */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Employee Management</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
|
||||
Export Data
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
|
||||
onClick={() => { resetForm(); setView('create'); }}
|
||||
>
|
||||
<span class="mr-2 text-lg leading-none">+</span> Add Employee
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Content ── */}
|
||||
<div class="flex-1">
|
||||
|
||||
{/* Create form */}
|
||||
<Show when={view() === 'create'}>
|
||||
<div class="bg-white border-b border-gray-100 px-6 py-6">
|
||||
<form onSubmit={handleCreate} class="max-w-4xl space-y-6">
|
||||
{/* Create form */}
|
||||
<div class="bg-white border focus-within:border-[#0a1d37] border-[#e2e6ee] rounded-3xl p-6 mb-8 max-w-4xl shadow-sm">
|
||||
<h2 class="text-[22px] font-bold text-[#050026] mb-6">Create New Employee</h2>
|
||||
<form onSubmit={handleCreate} class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Full Name *</label>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Full Name *</label>
|
||||
<input type="text" required placeholder="e.g. John Doe" value={formName()} onInput={(e) => 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]" />
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Email *</label>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Email *</label>
|
||||
<input type="email" required placeholder="e.g. john@company.com" value={formEmail()} onInput={(e) => setFormEmail(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]" />
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Password *</label>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Password *</label>
|
||||
<input type="password" required placeholder="Set a password" value={formPassword()} onInput={(e) => setFormPassword(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]" />
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700">Role</label>
|
||||
<label class="mb-1.5 block text-sm font-semibold text-[#383e5c]">Internal Role</label>
|
||||
<select value={formRoleId()} onChange={(e) => setFormRoleId(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]">
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white appearance-none">
|
||||
<option value="">Select a role…</option>
|
||||
<Show when={roles.loading}><option disabled>Loading roles…</option></Show>
|
||||
<For each={roles() ?? []}>{(r) => <option value={r.id}>{r.name}</option>}</For>
|
||||
|
|
@ -183,87 +175,144 @@ export default function EmployeesPage() {
|
|||
</div>
|
||||
</div>
|
||||
<Show when={createError()}>
|
||||
<p class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{createError()}</p>
|
||||
<p class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{createError()}</p>
|
||||
</Show>
|
||||
<div class="flex justify-end gap-3 border-t border-gray-100 pt-4">
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-[#e2e6ee]">
|
||||
<button type="button" onClick={() => { resetForm(); setView('list'); }}
|
||||
class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
class="h-11 rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" disabled={creating()}
|
||||
class="btn-primary">
|
||||
{creating() ? 'Creating…' : 'Create Internal User'}
|
||||
class="h-11 rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors disabled:opacity-70">
|
||||
{creating() ? 'Creating…' : 'Create Employee'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* List */}
|
||||
<Show when={view() === 'list'}>
|
||||
<div class="p-6">
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
{/* 4 KPI Cards Row */}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Total Employees</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">{employees()?.length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Active Employees</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">{employees()?.filter((e) => isActive(e)).length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Inactive Employees</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">{employees()?.filter((e) => !isActive(e)).length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Leave Today</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
{/* Main Table Section */}
|
||||
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
|
||||
<div class="rounded-[20px] bg-white p-5">
|
||||
|
||||
{/* Error Message */}
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
|
||||
</Show>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="relative w-[320px]">
|
||||
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search employees..."
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
||||
<div class="h-11 w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb]"></div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table w-full text-sm">
|
||||
<table class="w-full min-w-[1000px] border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th class="text-right">Actions</th>
|
||||
<tr class="bg-[#050026] text-left text-white">
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">EMPLOYEE ID</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">NAME & TITLE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DEPARTMENT</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DESIGNATION</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">INTERNAL ROLE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">JOINING DATE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center whitespace-nowrap">STATUS</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={employees.loading}>
|
||||
<tr><td colspan="5" class="py-10 text-center text-sm text-slate-400">Loading…</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-sm">Loading employees...</td></tr>
|
||||
</Show>
|
||||
<Show when={!employees.loading && employees.error}>
|
||||
<tr><td colspan="5" class="py-10 text-center text-sm text-red-500">Failed to load. Is the backend running?</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-red-500 text-sm">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!employees.loading && !employees.error && (employees()?.length ?? 0) === 0}>
|
||||
<tr><td colspan="5" class="py-10 text-center text-sm text-slate-400">No internal users found.</td></tr>
|
||||
<tr><td colspan="8" class="text-center py-12 text-[#8087a0] text-sm">No employees found. Add the first one.</td></tr>
|
||||
</Show>
|
||||
<For each={employees()}>
|
||||
<For each={employees() ?? []}>
|
||||
{(item) => {
|
||||
const active = () => isActive(item);
|
||||
return (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{employeeName(item)}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td class="text-slate-500">{roleName(item)}</td>
|
||||
<td>
|
||||
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${active() ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-700'}`}>
|
||||
{active() ? 'ACTIVE' : 'INACTIVE'}
|
||||
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
|
||||
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">EMP-{item.id.slice(0, 6).toUpperCase()}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-[#f1f5f9] text-[#050026] font-bold uppercase shrink-0">
|
||||
{employeeName(item).charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[14px] font-bold text-[#050026]">{employeeName(item)}</div>
|
||||
<div class="text-[13px] text-[#64748b]">{item.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Operations</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">—</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{roleName(item)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Oct 24, 2023</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
|
||||
active() ? 'bg-[#e6f9ed] text-[#00c853]' : 'bg-[#fff0eb] text-[#ff6e30]'
|
||||
}`}>
|
||||
{active() ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<A href={`/admin/employees/${item.id}/edit`}
|
||||
class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors" title="Edit">
|
||||
<Pencil size={14} class="text-gray-600" />
|
||||
</A>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
title={active() ? 'Deactivate' : 'Activate'}
|
||||
disabled={toggling() === item.id}
|
||||
onClick={() => handleToggleStatus(item.id, active())}
|
||||
class={`action-btn flex items-center justify-center transition-colors ${active() ? 'border-red-100 bg-red-50 hover:bg-red-100' : 'hover:bg-green-50 border-green-100 bg-green-50'}`}
|
||||
class={`flex h-8 w-8 items-center justify-center rounded-lg border transition-colors ${
|
||||
active() ? 'border-red-100 bg-red-50 text-red-600 hover:bg-red-100' : 'border-green-100 bg-green-50 text-green-600 hover:bg-green-100'
|
||||
}`}
|
||||
>
|
||||
<Show when={active()} fallback={<UserCheck size={14} class="text-green-600" />}>
|
||||
<UserX size={14} class="text-red-600" />
|
||||
<Show when={active()} fallback={<UserCheck size={14} />}>
|
||||
<UserX size={14} />
|
||||
</Show>
|
||||
</button>
|
||||
<button
|
||||
title="Delete"
|
||||
disabled={deleting() === item.id}
|
||||
onClick={() => handleDelete(item.id, employeeName(item))}
|
||||
class="action-btn flex items-center justify-center border-red-100 bg-red-50 hover:bg-red-100 transition-colors"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-red-100 bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} class="text-red-600" />
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -275,7 +324,7 @@ export default function EmployeesPage() {
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -107,169 +107,255 @@ export default function UsersPage() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
{/* White page header */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">External User Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all external platform users.</p>
|
||||
</div>
|
||||
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
|
||||
{/* Header & Title */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">User Management</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]">
|
||||
Export Data
|
||||
</button>
|
||||
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]">
|
||||
<span class="mr-2 text-lg leading-none">+</span> Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
|
||||
<button
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${activeTab() === 'list' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||||
type="button"
|
||||
onClick={() => setActiveTab('list')}
|
||||
>
|
||||
User List
|
||||
</button>
|
||||
<button
|
||||
class={`py-3 border-b-2 text-sm font-medium transition-colors ${activeTab() === 'view' ? 'border-orange-500 text-orange-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||||
type="button"
|
||||
disabled={!selectedUser()}
|
||||
onClick={() => setActiveTab('view')}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div class="flex-1 p-6">
|
||||
<Show when={activeTab() === 'list'}>
|
||||
{/* Filters */}
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] w-64"
|
||||
/>
|
||||
<select
|
||||
value={filterRole()}
|
||||
onChange={(e) => {
|
||||
setFilterRole(e.currentTarget.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37]"
|
||||
>
|
||||
<option value="">All Roles</option>
|
||||
<For each={ROLE_OPTIONS}>
|
||||
{(r) => <option value={r}>{r}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<select
|
||||
value={filterStatus()}
|
||||
onChange={(e) => {
|
||||
setFilterStatus(e.currentTarget.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37]"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
<Show when={!users.loading}>
|
||||
<span class="text-xs text-slate-500 ml-auto">
|
||||
Showing {paginated().length} of {filtered().length} users
|
||||
</span>
|
||||
</Show>
|
||||
{/* KPI Cards Row */}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Total Users</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#050026] leading-none">{users()?.length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Active Users</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#00c853] leading-none">{users()?.filter((u) => u.status === 'ACTIVE').length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">Inactive Users</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#ff6e30] leading-none">{users()?.filter((u) => u.status === 'INACTIVE').length || 0}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[#e2e6ee] bg-white p-5 shadow-sm">
|
||||
<p class="text-[13px] text-[#8087a0] font-medium leading-snug">New Users Today</p>
|
||||
<p class="mt-3 text-[32px] font-bold text-[#64748b] leading-none">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Registration Role</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="7" class="text-center py-8 text-slate-500">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="7" class="text-center py-8 text-red-700">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && paginated().length === 0}>
|
||||
<tr><td colspan="7" class="text-center py-8 text-slate-400">No users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && paginated().length > 0}>
|
||||
{/* Main Table Section */}
|
||||
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
|
||||
<div class="rounded-[20px] bg-white p-5">
|
||||
|
||||
{/* Tabs */}
|
||||
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee]">
|
||||
<For each={['All Users', 'Active', 'Inactive']}>
|
||||
{(t) => {
|
||||
const isActiveTab = (t === 'All Users' && !filterStatus()) ||
|
||||
(t === 'Active' && filterStatus() === 'ACTIVE') ||
|
||||
(t === 'Inactive' && filterStatus() === 'INACTIVE');
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (t === 'All Users') setFilterStatus('');
|
||||
else if (t === 'Active') setFilterStatus('ACTIVE');
|
||||
else if (t === 'Inactive') setFilterStatus('INACTIVE');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
class={`pb-3 text-[14px] font-bold transition-colors border-b-2 ${
|
||||
isActiveTab
|
||||
? 'border-[#050026] text-[#050026]'
|
||||
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
|
||||
}`}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Filters Row */}
|
||||
<div class="flex flex-col gap-4 md:flex-row items-center mb-6">
|
||||
<div class="relative w-full md:w-[320px]">
|
||||
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users..."
|
||||
value={search()}
|
||||
onInput={(e) => { setSearch(e.currentTarget.value); setCurrentPage(1); }}
|
||||
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={filterRole()}
|
||||
onChange={(e) => { setFilterRole(e.currentTarget.value); setCurrentPage(1); }}
|
||||
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] outline-none transition-colors focus:border-[#050026] focus:bg-white appearance-none"
|
||||
>
|
||||
<option value="">All Roles</option>
|
||||
<For each={ROLE_OPTIONS}>{(r) => <option value={r}>{r}</option>}</For>
|
||||
</select>
|
||||
<div class="flex-1"></div>
|
||||
<Show when={!users.loading}>
|
||||
<span class="text-[13px] text-[#8087a0] font-medium">
|
||||
Showing {paginated().length} of {filtered().length} users
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full min-w-[1000px] border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-[#050026] text-left text-white">
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">USER ID</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">USER DETAILS</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">JOINING DATE</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">LAST LOGIN</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-center whitespace-nowrap">STATUS</th>
|
||||
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">Loading users...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="7" class="text-center py-12 text-red-500 text-[14px]">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && paginated().length === 0}>
|
||||
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">No users found.</td></tr>
|
||||
</Show>
|
||||
<For each={paginated()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="text-slate-500" style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace">{shortId(item.id)}</td>
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td class="text-slate-500">{registrationRole(item)}</td>
|
||||
<td>
|
||||
<StatusBadge status={item.status} />
|
||||
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
|
||||
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">USR-{item.id.slice(0, 6).toUpperCase()}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-[#f1f5f9] text-[#050026] font-bold uppercase shrink-0">
|
||||
{(item.name || item.full_name || 'U').charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[14px] font-bold text-[#050026]">{item.name || item.full_name || '—'}</div>
|
||||
<div class="text-[13px] text-[#64748b]">{item.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{(item.created_at || item.createdAt)
|
||||
? new Date((item.created_at || item.createdAt)!).toLocaleDateString()
|
||||
: '—'}
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{registrationRole(item)}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">
|
||||
{(item.created_at || item.createdAt) ? new Date((item.created_at || item.createdAt)!).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/users/details/${item.id}`} title="Open Detail Page">↗</A>
|
||||
<button class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" type="button" onClick={() => onView(item)} title="Quick View">👁</button>
|
||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/users/${item.id}/edit`} title="Edit User">✎</A>
|
||||
<button class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm" type="button" title="Delete User">🗑</button>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">—</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
|
||||
item.status === 'ACTIVE' ? 'bg-[#e6f9ed] text-[#00c853]' : item.status === 'PENDING' ? 'bg-amber-100 text-amber-700' : 'bg-[#fff0eb] text-[#ff6e30]'
|
||||
}`}>
|
||||
{item.status.charAt(0) + item.status.slice(1).toLowerCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
title="View Details"
|
||||
onClick={() => onView(item)}
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
title="Edit"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Show when={totalPages() > 1}>
|
||||
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
|
||||
<button
|
||||
disabled={currentPage() === 1}
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span class="text-[13px] font-medium text-[#8087a0]">Page {currentPage()} of {totalPages()}</span>
|
||||
<button
|
||||
disabled={currentPage() >= totalPages()}
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}
|
||||
class="flex h-9 px-4 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[13px] font-medium text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="admin-pagination">
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {currentPage()} of {totalPages()}</span>
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
{/* Details View */}
|
||||
<Show when={activeTab() === 'view'}>
|
||||
<div class="table-card">
|
||||
<Show when={selectedUser()} fallback={<p class="notice">Select a user from list to view details.</p>}>
|
||||
<div class="list-header">
|
||||
<h2>User Details</h2>
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={() => setActiveTab('list')}>Back To List</button>
|
||||
<div class="bg-white rounded-3xl border border-[#e2e6ee] p-6 shadow-sm">
|
||||
<Show when={selectedUser()}>
|
||||
<div class="flex items-center justify-between mb-8 border-b border-[#e2e6ee] pb-6">
|
||||
<h2 class="text-[22px] font-bold text-[#050026]">User Details</h2>
|
||||
<button
|
||||
class="h-10 rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] hover:bg-[#f8f9fc] transition-colors"
|
||||
type="button"
|
||||
onClick={() => setActiveTab('list')}
|
||||
>
|
||||
Back To List
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid" style="margin-top:0">
|
||||
<div class="list-item">
|
||||
<h3>Profile</h3>
|
||||
<p><strong>User ID:</strong> {selectedUser()!.id}</p>
|
||||
<p><strong>Name:</strong> {selectedUser()!.name || selectedUser()!.full_name || '—'}</p>
|
||||
<p><strong>Email:</strong> {selectedUser()!.email}</p>
|
||||
<p><strong>Role:</strong> {registrationRole(selectedUser()!)}</p>
|
||||
<p><strong>Status:</strong> {selectedUser()!.status}</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="bg-[#f9fafb] p-6 rounded-2xl border border-[#e2e6ee]">
|
||||
<h3 class="text-[16px] font-bold text-[#050026] mb-4">Profile Information</h3>
|
||||
<div class="space-y-4">
|
||||
<div><p class="text-[13px] text-[#64748b] font-medium">User ID</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.id}</p></div>
|
||||
<div><p class="text-[13px] text-[#64748b] font-medium">Name</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.name || selectedUser()!.full_name || '—'}</p></div>
|
||||
<div><p class="text-[13px] text-[#64748b] font-medium">Email</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.email}</p></div>
|
||||
<div><p class="text-[13px] text-[#64748b] font-medium">Role</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{registrationRole(selectedUser()!)}</p></div>
|
||||
<div>
|
||||
<p class="text-[13px] text-[#64748b] font-medium mb-1">Status</p>
|
||||
<span class={`inline-flex items-center justify-center rounded-lg px-3 py-1 text-[12px] font-bold ${
|
||||
selectedUser()!.status === 'ACTIVE' ? 'bg-[#e6f9ed] text-[#00c853]' : selectedUser()!.status === 'PENDING' ? 'bg-amber-100 text-amber-700' : 'bg-[#fff0eb] text-[#ff6e30]'
|
||||
}`}>
|
||||
{selectedUser()!.status.charAt(0) + selectedUser()!.status.slice(1).toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<h3>Account</h3>
|
||||
<p><strong>Created:</strong> {(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}</p>
|
||||
<p><strong>Role ID:</strong> {selectedUser()!.roleId || '—'}</p>
|
||||
<div class="actions">
|
||||
<A class="btn-primary" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
|
||||
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" type="button">Deactivate</button>
|
||||
<div class="bg-[#f9fafb] p-6 rounded-2xl border border-[#e2e6ee]">
|
||||
<h3 class="text-[16px] font-bold text-[#050026] mb-4">Account Metadata</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-[13px] text-[#64748b] font-medium">Created</p>
|
||||
<p class="text-[15px] font-semibold text-[#050026] mt-1">{(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}</p>
|
||||
</div>
|
||||
<div><p class="text-[13px] text-[#64748b] font-medium">Role ID</p><p class="text-[15px] font-semibold text-[#050026] mt-1">{selectedUser()!.roleId || '—'}</p></div>
|
||||
<div class="pt-6 border-t border-[#e2e6ee] mt-6 gap-3 flex">
|
||||
<A class="h-10 rounded-xl bg-[#050026] px-5 flex items-center justify-center text-[14px] font-semibold text-white hover:bg-[#0a0044] transition-colors" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
|
||||
<button class="h-10 rounded-xl border border-red-200 bg-red-50 flex items-center justify-center px-5 text-[14px] font-semibold text-red-600 hover:bg-red-100 transition-colors" type="button">Deactivate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue