- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes) - AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section - AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar - Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles - Designation: full rewrite matching department pattern — same tabs, filter bar, table, form - Roles/Employees: compact filter bar and table cell sizing fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
516 lines
33 KiB
TypeScript
516 lines
33 KiB
TypeScript
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||
import AdminShell from '~/components/AdminShell';
|
||
import { updateModuleRecord, deleteModuleRecord } from '~/lib/admin/client';
|
||
import type { CrudRecord } from '~/lib/admin/types';
|
||
|
||
type DesignationRecord = CrudRecord & {
|
||
code?: string;
|
||
department?: string;
|
||
level?: string;
|
||
description?: string;
|
||
totalEmployees?: number;
|
||
createdDate?: string;
|
||
canManageTeam?: boolean;
|
||
canApprove?: boolean;
|
||
};
|
||
|
||
const FALLBACK_DESIGNATIONS: DesignationRecord[] = [
|
||
{ id: 'z1', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-15' },
|
||
{ id: 'z2', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-01-20' },
|
||
{ id: 'z3', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-01' },
|
||
{ id: 'z4', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-05' },
|
||
{ id: 'z5', name: 'Product Manager', code: 'PM-005', department: 'Product', level: 'Manager', totalEmployees: 6, status: 'ACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-10' },
|
||
{ id: 'z6', name: 'Finance Analyst', code: 'FA-006', department: 'Finance', level: 'Analyst', totalEmployees: 9, status: 'INACTIVE', updatedAt: '2026-03-01', createdDate: '2026-02-15' },
|
||
];
|
||
|
||
const LEVELS = ['Intern', 'Junior', 'Mid-Level', 'Senior', 'Lead', 'Manager', 'Director', 'VP', 'C-Level', 'Executive', 'Specialist', 'Analyst'];
|
||
const DEPARTMENTS = ['Engineering', 'Marketing', 'Sales', 'Human Resources', 'Finance', 'Operations', 'Product', 'Customer Success'];
|
||
|
||
function StatusBadge(props: { status: string }) {
|
||
const active = () => props.status === 'ACTIVE';
|
||
return (
|
||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
|
||
{active() ? 'Active' : 'Inactive'}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
|
||
return (
|
||
<label style="display:block">
|
||
<span style="font-size:13px;font-weight:600;color:#374151">
|
||
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
|
||
</span>
|
||
<input
|
||
type={props.type ?? 'text'}
|
||
value={props.value}
|
||
onInput={(e) => props.onInput(e.currentTarget.value)}
|
||
placeholder={props.placeholder}
|
||
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
|
||
/>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
function FormSelect(props: { label: string; required?: boolean; value: string; onChange: (v: string) => void; children: any }) {
|
||
return (
|
||
<label style="display:block">
|
||
<span style="font-size:13px;font-weight:600;color:#374151">
|
||
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
|
||
</span>
|
||
<select
|
||
value={props.value}
|
||
onChange={(e) => props.onChange(e.currentTarget.value)}
|
||
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;appearance:none"
|
||
>
|
||
{props.children}
|
||
</select>
|
||
</label>
|
||
);
|
||
}
|
||
|
||
export default function DesignationManagementPage() {
|
||
const [view, setView] = createSignal<'list' | 'form'>('list');
|
||
const [formTab, setFormTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
|
||
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
||
const [search, setSearch] = createSignal('');
|
||
const [deptFilter, setDeptFilter] = createSignal('all');
|
||
const [statusFilter, setStatusFilter] = createSignal('all');
|
||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
||
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
|
||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||
|
||
const [name, setName] = createSignal('');
|
||
const [code, setCode] = createSignal('');
|
||
const [department, setDepartment] = createSignal('');
|
||
const [level, setLevel] = createSignal('');
|
||
const [description, setDescription] = createSignal('');
|
||
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
||
const [canManageTeam, setCanManageTeam] = createSignal(false);
|
||
const [canApprove, setCanApprove] = createSignal(false);
|
||
const [isSaving, setIsSaving] = createSignal(false);
|
||
const [error, setError] = createSignal('');
|
||
|
||
const load = async () => {
|
||
try {
|
||
const res = await fetch(`/api/gateway/api/admin/designations?page=1&limit=100&q=${encodeURIComponent(search().trim())}`);
|
||
if (res.ok) {
|
||
const payload = await res.json().catch(() => null);
|
||
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.items) ? payload.items : [];
|
||
if (list.length > 0) {
|
||
setRows(list.map((item: any, i: number) => ({
|
||
id: String(item.id ?? `des-${i + 1}`),
|
||
name: String(item.name ?? ''),
|
||
code: String(item.code ?? ''),
|
||
department: String(item.department ?? item.department_name ?? ''),
|
||
level: String(item.level ?? ''),
|
||
description: String(item.description ?? ''),
|
||
totalEmployees: Number(item.totalEmployees ?? item.total_employees ?? 0),
|
||
status: String(item.status ?? 'ACTIVE').toUpperCase() === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||
updatedAt: String(item.updatedAt ?? item.updated_at ?? ''),
|
||
createdDate: String(item.createdDate ?? item.created_at ?? ''),
|
||
})));
|
||
return;
|
||
}
|
||
}
|
||
} catch {}
|
||
setRows(FALLBACK_DESIGNATIONS);
|
||
};
|
||
|
||
onMount(() => void load());
|
||
|
||
const filteredRows = createMemo(() => {
|
||
let r = rows();
|
||
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
|
||
if (deptFilter() !== 'all') r = r.filter((d) => d.department === deptFilter());
|
||
const q = search().toLowerCase();
|
||
if (q) r = r.filter((d) => d.name.toLowerCase().includes(q) || String(d.code ?? '').toLowerCase().includes(q));
|
||
return r;
|
||
});
|
||
|
||
const resetForm = () => {
|
||
setEditingId(null); setName(''); setCode(''); setDepartment('');
|
||
setLevel(''); setDescription(''); setStatus('ACTIVE');
|
||
setCanManageTeam(false); setCanApprove(false); setFormTab('general'); setError('');
|
||
};
|
||
|
||
const openCreate = () => { resetForm(); setView('form'); };
|
||
|
||
const openEdit = (row: DesignationRecord) => {
|
||
setEditingId(row.id);
|
||
setName(row.name || ''); setCode(String(row.code || ''));
|
||
setDepartment(String(row.department || '')); setLevel(String(row.level || ''));
|
||
setDescription(String(row.description || ''));
|
||
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
|
||
setCanManageTeam(Boolean(row.canManageTeam)); setCanApprove(Boolean(row.canApprove));
|
||
setFormTab('general'); setView('form'); setOpenMenuId(null);
|
||
};
|
||
|
||
const save = async () => {
|
||
if (!name().trim()) { setError('Designation name is required.'); setFormTab('general'); return; }
|
||
setIsSaving(true); setError('');
|
||
try {
|
||
const payload: Partial<DesignationRecord> = {
|
||
name: name().trim(), code: code().trim() || undefined,
|
||
department: department().trim(), level: level().trim(),
|
||
description: description().trim(), status: status(),
|
||
canManageTeam: canManageTeam(), canApprove: canApprove(),
|
||
};
|
||
if (editingId()) {
|
||
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
|
||
} else {
|
||
const res = await fetch('/api/gateway/api/admin/designations', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||
}
|
||
setView('list'); resetForm(); await load();
|
||
} catch (err: any) {
|
||
setError(err?.message || 'Failed to save designation.');
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
const formatDate = (v?: string) => {
|
||
const s = v || '';
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
||
return s.slice(0, 10) || '—';
|
||
};
|
||
|
||
return (
|
||
<AdminShell>
|
||
<div style="width:100%;padding-bottom:32px">
|
||
|
||
{/* Page header */}
|
||
<div style="margin-bottom:24px">
|
||
<h1 style="font-size:28px;font-weight:700;color:#111827;line-height:1.2">Designation Management</h1>
|
||
<p style="margin-top:4px;font-size:14px;color:#6B7280">Manage all job designations and position levels</p>
|
||
</div>
|
||
|
||
{/* ── LIST VIEW ── */}
|
||
<Show when={view() === 'list'}>
|
||
<div>
|
||
{/* Tabs */}
|
||
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||
{([
|
||
{ key: 'all', label: 'All Designations', action: () => { setListTab('all'); setStatusFilter('all'); void load(); } },
|
||
{ key: 'create', label: 'Create Designation', action: () => { setListTab('create'); openCreate(); } },
|
||
{ key: 'view', label: 'View Designation', action: () => setListTab('view') },
|
||
] as const).map((tab) => (
|
||
<button
|
||
type="button"
|
||
onClick={tab.action}
|
||
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Table card */}
|
||
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||
|
||
{/* Filter bar */}
|
||
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||
<input
|
||
value={search()}
|
||
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
|
||
placeholder="Search designations..."
|
||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
||
/>
|
||
<select
|
||
value={deptFilter()}
|
||
onChange={(e) => setDeptFilter(e.currentTarget.value)}
|
||
style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
|
||
>
|
||
<option value="all">All Departments</option>
|
||
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
||
</select>
|
||
<div style="position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||
>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
<Show when={filterMenuOpen()}>
|
||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||
{(['all', 'ACTIVE', 'INACTIVE'] as const).map((s) => (
|
||
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
||
{s === 'all' ? 'All Status' : s === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div style="overflow-x:auto">
|
||
<table style="min-width:100%">
|
||
<thead>
|
||
<tr style="background:#0D0D2A;text-align:left">
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Designation Name</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Code</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Department</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Level</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Employees</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Created Date</th>
|
||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<Show
|
||
when={filteredRows().length > 0}
|
||
fallback={
|
||
<tr>
|
||
<td colspan="8" style="padding:64px 24px;text-align:center">
|
||
<p style="font-size:15px;font-weight:600;color:#111827">No designations found</p>
|
||
<p style="margin-top:4px;font-size:13px;color:#6B7280">Create your first designation to get started.</p>
|
||
<button type="button" onClick={openCreate} style="margin-top:16px;display:inline-flex;align-items:center;gap:8px;border-radius:12px;background:#0D0D2A;padding:8px 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">
|
||
Create Designation
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
}
|
||
>
|
||
<For each={filteredRows()}>
|
||
{(row) => (
|
||
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
||
<td style="padding:12px 20px">
|
||
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
||
</td>
|
||
<td style="padding:12px 20px">
|
||
<span style="font-size:12px;font-family:monospace;color:#6B7280">{String(row.code || '—')}</span>
|
||
</td>
|
||
<td style="padding:12px 20px;font-size:13px;color:#374151">{String(row.department || '—')}</td>
|
||
<td style="padding:12px 20px">
|
||
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">{String(row.level || '—')}</span>
|
||
</td>
|
||
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">{Number(row.totalEmployees || 0)}</td>
|
||
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
|
||
<td style="padding:12px 20px;position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)}
|
||
style="display:inline-flex;width:32px;height:32px;align-items:center;justify-content:center;border-radius:8px;border:none;background:none;color:#9CA3AF;cursor:pointer"
|
||
aria-label="More actions"
|
||
>
|
||
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
||
</button>
|
||
<Show when={openMenuId() === row.id}>
|
||
<div style="position:absolute;right:20px;top:44px;z-index:20;width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
|
||
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
|
||
Edit Designation
|
||
</button>
|
||
<button type="button" onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#374151;background:none;border:none;cursor:pointer">
|
||
<svg style="width:16px;height:16px;color:#FF5E13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
|
||
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
|
||
</button>
|
||
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
|
||
<button type="button" onClick={async () => { if (!window.confirm(`Delete designation "${row.name}"?`)) return; await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;font-weight:500;color:#DC2626;background:none;border:none;cursor:pointer">
|
||
<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</Show>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</For>
|
||
</Show>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
<Show when={filteredRows().length > 0}>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||
<p style="font-size:13px;color:#6B7280">
|
||
Showing <strong style="font-weight:600;color:#111827">1–{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> designations
|
||
</p>
|
||
<div style="display:flex;align-items:center;gap:4px">
|
||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* ── FORM VIEW ── */}
|
||
<Show when={view() === 'form'}>
|
||
{/* Top tabs */}
|
||
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
|
||
All Designations
|
||
</button>
|
||
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
|
||
{editingId() ? 'Edit Designation' : 'Create Designation'}
|
||
</button>
|
||
</div>
|
||
|
||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
{/* Sub-tabs */}
|
||
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
||
{(['general', 'settings', 'permissions'] as const).map((tab, i) => {
|
||
const labels = ['General Information', 'Designation Settings', 'Permissions'];
|
||
const active = () => formTab() === tab;
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormTab(tab)}
|
||
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}
|
||
>
|
||
{labels[i]}
|
||
<Show when={active()}>
|
||
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
|
||
</Show>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div style="padding:24px">
|
||
<Show when={error()}>
|
||
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
||
{error()}
|
||
</div>
|
||
</Show>
|
||
|
||
{/* General Information */}
|
||
<Show when={formTab() === 'general'}>
|
||
<div style="display:flex;flex-direction:column;gap:20px">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||
<FormInput label="Designation Name" required value={name()} onInput={setName} placeholder="e.g. Senior Software Engineer" />
|
||
<FormInput label="Designation Code" required value={code()} onInput={setCode} placeholder="e.g. SSE-001" />
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||
<FormSelect label="Department" required value={department()} onChange={setDepartment}>
|
||
<option value="">Select department</option>
|
||
<For each={DEPARTMENTS}>{(d) => <option value={d}>{d}</option>}</For>
|
||
</FormSelect>
|
||
<FormSelect label="Designation Level" required value={level()} onChange={setLevel}>
|
||
<option value="">Select level</option>
|
||
<For each={LEVELS}>{(l) => <option value={l}>{l}</option>}</For>
|
||
</FormSelect>
|
||
</div>
|
||
<label style="display:block">
|
||
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
||
<textarea
|
||
value={description()}
|
||
onInput={(e) => setDescription(e.currentTarget.value)}
|
||
placeholder="Brief description of this designation's responsibilities..."
|
||
rows="3"
|
||
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
|
||
/>
|
||
</label>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* Designation Settings */}
|
||
<Show when={formTab() === 'settings'}>
|
||
<div style="display:flex;flex-direction:column;gap:32px">
|
||
<div>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">Designation Status</p>
|
||
<p style="margin-top:2px;font-size:13px;color:#6B7280">Set whether this designation is currently active</p>
|
||
<div style="margin-top:12px;display:flex;gap:10px">
|
||
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setStatus(s)}
|
||
style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
|
||
>
|
||
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
|
||
<div>
|
||
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
|
||
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can manage team members</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setCanManageTeam((v) => !v)}
|
||
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
||
>
|
||
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? '22px' : '2px'}`} />
|
||
</button>
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
|
||
<div>
|
||
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
|
||
<p style="margin-top:2px;font-size:12px;color:#6B7280">This designation can approve employee requests</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setCanApprove((v) => !v)}
|
||
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
||
>
|
||
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? '22px' : '2px'}`} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* Permissions */}
|
||
<Show when={formTab() === 'permissions'}>
|
||
<div style="display:flex;flex-direction:column;gap:16px">
|
||
<p style="font-size:13px;color:#6B7280">Select the permissions available to employees with this designation.</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
{['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles', 'Approve Requests', 'Manage Team Members'].map((item) => (
|
||
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
|
||
<input type="checkbox" style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer" />
|
||
<span style="font-size:13px;font-weight:500;color:#374151">{item}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
{/* Form actions */}
|
||
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setView('list'); resetForm(); }}
|
||
style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => void save()}
|
||
disabled={isSaving()}
|
||
style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
|
||
>
|
||
{isSaving() ? 'Saving...' : editingId() ? 'Update Designation' : 'Create Designation'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
</div>
|
||
</AdminShell>
|
||
);
|
||
}
|