Wire department and designation management to module CRUD backend
This commit is contained in:
parent
94d4623248
commit
0996f12227
5 changed files with 556 additions and 229 deletions
|
|
@ -67,30 +67,30 @@ export async function updateApproval(id: string, patch: Partial<ApprovalCase>) {
|
|||
return parse<ApprovalCase>(res);
|
||||
}
|
||||
|
||||
export async function listModuleRecords(moduleKey: string, query?: { q?: string; status?: string }) {
|
||||
export async function listModuleRecords<T extends CrudRecord = CrudRecord>(moduleKey: string, query?: { q?: string; status?: string }) {
|
||||
const qp = new URLSearchParams();
|
||||
if (query?.q) qp.set('q', query.q);
|
||||
if (query?.status) qp.set('status', query.status);
|
||||
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records${qp.toString() ? `?${qp.toString()}` : ''}`);
|
||||
return parse<CrudRecord[]>(res);
|
||||
return parse<T[]>(res);
|
||||
}
|
||||
|
||||
export async function createModuleRecord(moduleKey: string, payload: Partial<CrudRecord>) {
|
||||
export async function createModuleRecord<T extends CrudRecord = CrudRecord>(moduleKey: string, payload: Partial<T>) {
|
||||
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return parse<CrudRecord>(res);
|
||||
return parse<T>(res);
|
||||
}
|
||||
|
||||
export async function updateModuleRecord(moduleKey: string, id: string, patch: Partial<CrudRecord>) {
|
||||
export async function updateModuleRecord<T extends CrudRecord = CrudRecord>(moduleKey: string, id: string, patch: Partial<T>) {
|
||||
const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
return parse<CrudRecord>(res);
|
||||
return parse<T>(res);
|
||||
}
|
||||
|
||||
export async function deleteModuleRecord(moduleKey: string, id: string) {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,12 @@ const filterByQuery = <T extends { id: string; status?: string; applicantName?:
|
|||
});
|
||||
};
|
||||
|
||||
const createCrudService = (seed: CrudRecord[]): CrudService<CrudRecord> => {
|
||||
const createCrudService = (
|
||||
seed: CrudRecord[],
|
||||
options?: {
|
||||
createFromPayload?: (payload: Partial<CrudRecord>, currentRows: CrudRecord[]) => CrudRecord;
|
||||
},
|
||||
): CrudService<CrudRecord> => {
|
||||
let rows = [...seed];
|
||||
return {
|
||||
async list(query) {
|
||||
|
|
@ -72,12 +77,15 @@ const createCrudService = (seed: CrudRecord[]): CrudService<CrudRecord> => {
|
|||
return rows.find((r) => r.id === id) ?? null;
|
||||
},
|
||||
async create(payload) {
|
||||
const item: CrudRecord = {
|
||||
id: `ADM-${Date.now()}`,
|
||||
name: payload.name || 'New Item',
|
||||
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
const item: CrudRecord =
|
||||
options?.createFromPayload?.(payload, rows) ??
|
||||
({
|
||||
...(payload as CrudRecord),
|
||||
id: String(payload.id || `ADM-${Date.now()}`),
|
||||
name: payload.name || 'New Item',
|
||||
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||
updatedAt: nowIso(),
|
||||
} as CrudRecord);
|
||||
rows = [item, ...rows];
|
||||
return item;
|
||||
},
|
||||
|
|
@ -217,6 +225,32 @@ export const genericCrudService = createCrudService(mockCrudRows);
|
|||
const moduleCrudServices = new Map<string, CrudService<CrudRecord>>();
|
||||
|
||||
function seedRowsForModule(moduleKey: string): CrudRecord[] {
|
||||
if (moduleKey === 'department') {
|
||||
return [
|
||||
{ id: 'eng-001', name: 'Engineering', code: 'ENG-001', description: 'Software development and technical operations', totalEmployees: 45, status: 'ACTIVE', createdDate: '2024-01-15', updatedAt: nowIso() },
|
||||
{ id: 'mkt-002', name: 'Marketing', code: 'MKT-002', description: 'Brand management and customer acquisition', totalEmployees: 28, status: 'ACTIVE', createdDate: '2024-01-20', updatedAt: nowIso() },
|
||||
{ id: 'sal-003', name: 'Sales', code: 'SAL-003', description: 'Revenue generation and client relations', totalEmployees: 35, status: 'ACTIVE', createdDate: '2024-02-01', updatedAt: nowIso() },
|
||||
{ id: 'hr-004', name: 'Human Resources', code: 'HR-004', description: 'Employee management and recruitment', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-02-10', updatedAt: nowIso() },
|
||||
{ id: 'fin-005', name: 'Finance', code: 'FIN-005', description: 'Financial planning and accounting', totalEmployees: 18, status: 'ACTIVE', createdDate: '2024-02-15', updatedAt: nowIso() },
|
||||
{ id: 'ops-006', name: 'Operations', code: 'OPS-006', description: 'Operations and logistics', totalEmployees: 22, status: 'INACTIVE', createdDate: '2024-03-01', updatedAt: nowIso() },
|
||||
{ id: 'cs-007', name: 'Customer Success', code: 'CS-007', description: 'Customer experience and technical support', totalEmployees: 31, status: 'ACTIVE', createdDate: '2024-03-05', updatedAt: nowIso() },
|
||||
{ id: 'prd-008', name: 'Product', code: 'PRD-008', description: 'Product strategy and roadmap execution', totalEmployees: 19, status: 'ACTIVE', createdDate: '2024-03-10', updatedAt: nowIso() },
|
||||
];
|
||||
}
|
||||
|
||||
if (moduleKey === 'designation') {
|
||||
return [
|
||||
{ id: 'sse-001', name: 'Senior Software Engineer', code: 'SSE-001', department: 'Engineering', level: 'Senior', totalEmployees: 12, status: 'ACTIVE', createdDate: '2024-01-15', updatedAt: nowIso() },
|
||||
{ id: 'mm-002', name: 'Marketing Manager', code: 'MM-002', department: 'Marketing', level: 'Manager', totalEmployees: 8, status: 'ACTIVE', createdDate: '2024-01-20', updatedAt: nowIso() },
|
||||
{ id: 'se-003', name: 'Sales Executive', code: 'SE-003', department: 'Sales', level: 'Executive', totalEmployees: 15, status: 'ACTIVE', createdDate: '2024-02-01', updatedAt: nowIso() },
|
||||
{ id: 'hrs-004', name: 'HR Specialist', code: 'HRS-004', department: 'Human Resources', level: 'Specialist', totalEmployees: 5, status: 'ACTIVE', createdDate: '2024-02-10', updatedAt: nowIso() },
|
||||
{ id: 'fa-005', name: 'Financial Analyst', code: 'FA-005', department: 'Finance', level: 'Analyst', totalEmployees: 6, status: 'ACTIVE', createdDate: '2024-02-15', updatedAt: nowIso() },
|
||||
{ id: 'om-006', name: 'Operations Manager', code: 'OM-006', department: 'Operations', level: 'Manager', totalEmployees: 4, status: 'INACTIVE', createdDate: '2024-03-01', updatedAt: nowIso() },
|
||||
{ id: 'csl-007', name: 'Customer Support Lead', code: 'CSL-007', department: 'Customer Support', level: 'Lead', totalEmployees: 9, status: 'ACTIVE', createdDate: '2024-03-05', updatedAt: nowIso() },
|
||||
{ id: 'pd-008', name: 'Product Designer', code: 'PD-008', department: 'Product', level: 'Designer', totalEmployees: 7, status: 'ACTIVE', createdDate: '2024-03-10', updatedAt: nowIso() },
|
||||
];
|
||||
}
|
||||
|
||||
const base = moduleKey
|
||||
.replace(/[^a-z0-9]+/gi, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
|
|
@ -236,7 +270,48 @@ export function getModuleCrudService(moduleKey: string): CrudService<CrudRecord>
|
|||
const found = moduleCrudServices.get(key);
|
||||
if (found) return found;
|
||||
|
||||
const created = createCrudService(seedRowsForModule(key));
|
||||
const created = createCrudService(seedRowsForModule(key), {
|
||||
createFromPayload(payload) {
|
||||
const baseName = String(payload.name || 'New Item');
|
||||
if (key === 'department') {
|
||||
return {
|
||||
...(payload as CrudRecord),
|
||||
id: String(payload.id || `dep-${Date.now()}`),
|
||||
name: baseName,
|
||||
code: String(payload.code || baseName.slice(0, 3).toUpperCase()),
|
||||
description: String(payload.description || ''),
|
||||
totalEmployees: Number(payload.totalEmployees || 0),
|
||||
createdDate: String(payload.createdDate || new Date().toISOString().slice(0, 10)),
|
||||
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'designation') {
|
||||
return {
|
||||
...(payload as CrudRecord),
|
||||
id: String(payload.id || `des-${Date.now()}`),
|
||||
name: baseName,
|
||||
code: String(payload.code || baseName.slice(0, 3).toUpperCase()),
|
||||
department: String(payload.department || ''),
|
||||
level: String(payload.level || ''),
|
||||
description: String(payload.description || ''),
|
||||
totalEmployees: Number(payload.totalEmployees || 0),
|
||||
createdDate: String(payload.createdDate || new Date().toISOString().slice(0, 10)),
|
||||
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(payload as CrudRecord),
|
||||
id: String(payload.id || `ADM-${Date.now()}`),
|
||||
name: baseName,
|
||||
status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE',
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
},
|
||||
});
|
||||
moduleCrudServices.set(key, created);
|
||||
return created;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export type CrudRecord = {
|
|||
name: string;
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
updatedAt: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type AdminModuleConfig = {
|
||||
|
|
|
|||
|
|
@ -1,123 +1,244 @@
|
|||
import { createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi';
|
||||
import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
|
||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
|
||||
import type { CrudRecord } from '~/lib/admin/types';
|
||||
|
||||
export default function DepartmentManagementPage() {
|
||||
const [tab, setTab] = createSignal<'view' | 'create'>('view');
|
||||
const [query, setQuery] = createSignal('');
|
||||
const [rows, setRows] = createSignal<CrudRecord[]>([]);
|
||||
const [nameInput, setNameInput] = createSignal('');
|
||||
const [codeInput, setCodeInput] = createSignal('');
|
||||
type DepartmentRecord = CrudRecord & {
|
||||
code?: string;
|
||||
description?: string;
|
||||
totalEmployees?: number;
|
||||
createdDate?: string;
|
||||
departmentHead?: string;
|
||||
departmentEmail?: string;
|
||||
};
|
||||
|
||||
export default function DepartmentManagementPage() {
|
||||
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
|
||||
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [rows, setRows] = createSignal<DepartmentRecord[]>([]);
|
||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
|
||||
const [name, setName] = createSignal('');
|
||||
const [code, setCode] = createSignal('');
|
||||
const [description, setDescription] = createSignal('');
|
||||
const [departmentHead, setDepartmentHead] = createSignal('');
|
||||
const [departmentEmail, setDepartmentEmail] = createSignal('');
|
||||
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
||||
const [transfersEnabled, setTransfersEnabled] = createSignal(false);
|
||||
|
||||
const load = async () => {
|
||||
const data = await listModuleRecords<DepartmentRecord>('department', { q: search().trim() || undefined });
|
||||
setRows(data);
|
||||
};
|
||||
|
||||
const load = async () => setRows(await listModuleRecords('department', { q: query() }));
|
||||
onMount(() => void load());
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const q = query().trim().toLowerCase();
|
||||
if (!q) return rows();
|
||||
return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q));
|
||||
});
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setName('');
|
||||
setCode('');
|
||||
setDescription('');
|
||||
setDepartmentHead('');
|
||||
setDepartmentEmail('');
|
||||
setStatus('ACTIVE');
|
||||
setTransfersEnabled(false);
|
||||
setCreateTab('general');
|
||||
};
|
||||
|
||||
const metrics = createMemo(() => {
|
||||
const all = rows();
|
||||
const active = all.filter((item) => item.status === 'ACTIVE').length;
|
||||
const inactive = all.filter((item) => item.status === 'INACTIVE').length;
|
||||
return [
|
||||
{ label: 'Total Departments', value: String(all.length || 0) },
|
||||
{ label: 'Active Departments', value: String(active), tone: 'positive' as const },
|
||||
{ label: 'Inactive Departments', value: String(inactive), tone: 'warning' as const },
|
||||
{ label: 'Updated Today', value: String(Math.min(active, 6)), tone: 'info' as const },
|
||||
];
|
||||
});
|
||||
const openCreate = () => {
|
||||
resetForm();
|
||||
setMainTab('create');
|
||||
};
|
||||
|
||||
const openEdit = (row: DepartmentRecord) => {
|
||||
setEditingId(row.id);
|
||||
setName(row.name || '');
|
||||
setCode(String(row.code || ''));
|
||||
setDescription(String(row.description || ''));
|
||||
setDepartmentHead(String(row.departmentHead || ''));
|
||||
setDepartmentEmail(String(row.departmentEmail || ''));
|
||||
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
|
||||
setMainTab('create');
|
||||
setCreateTab('general');
|
||||
};
|
||||
|
||||
const saveDepartment = async () => {
|
||||
const payload: Partial<DepartmentRecord> = {
|
||||
name: name().trim() || 'New Department',
|
||||
code: code().trim() || undefined,
|
||||
description: description().trim(),
|
||||
departmentHead: departmentHead().trim(),
|
||||
departmentEmail: departmentEmail().trim(),
|
||||
status: status(),
|
||||
transfersEnabled: transfersEnabled(),
|
||||
};
|
||||
|
||||
if (editingId()) {
|
||||
await updateModuleRecord<DepartmentRecord>('department', editingId()!, payload);
|
||||
} else {
|
||||
await createModuleRecord<DepartmentRecord>('department', payload);
|
||||
}
|
||||
|
||||
setMainTab('all');
|
||||
setOpenMenuId(null);
|
||||
resetForm();
|
||||
await load();
|
||||
};
|
||||
|
||||
const filteredRows = createMemo(() => rows());
|
||||
|
||||
const formatDate = (value?: string) => {
|
||||
const input = value || '';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
|
||||
const fallback = input || new Date().toISOString().slice(0, 10);
|
||||
return fallback.slice(0, 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-5">
|
||||
<PageHeader
|
||||
title="Department Management"
|
||||
subtitle="Manage operational department structure and ownership mappings."
|
||||
actions={<Tabs value={tab()} onChange={setTab} items={[{ key: 'view', label: 'View Departments' }, { key: 'create', label: 'Create Department' }]} />}
|
||||
/>
|
||||
<section>
|
||||
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Department Management</h1>
|
||||
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all departments and organizational structure</p>
|
||||
</section>
|
||||
|
||||
<MetricCards items={metrics()} />
|
||||
<section class="overflow-hidden rounded-[24px] border border-[#d9dde6] bg-[#f7f7f8]">
|
||||
<div class="flex items-center gap-2 border-b border-[#e1e5ee] px-5 pt-4">
|
||||
<button onClick={() => setMainTab('all')} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'all' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
|
||||
All Departments
|
||||
<Show when={mainTab() === 'all'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
|
||||
</button>
|
||||
<button onClick={openCreate} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'create' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
|
||||
{editingId() ? 'Edit Department' : 'Create Department'}
|
||||
<Show when={mainTab() === 'create'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tab() === 'view' ? (
|
||||
<SectionCard
|
||||
title="Departments"
|
||||
subtitle="Search, activate, and maintain department records."
|
||||
actions={
|
||||
<>
|
||||
<ActionButton>Export</ActionButton>
|
||||
<ActionButton tone="primary" onClick={() => setTab('create')}>Add Department</ActionButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<SearchFilters
|
||||
query={query()}
|
||||
onQuery={(value) => {
|
||||
setQuery(value);
|
||||
void load();
|
||||
}}
|
||||
right={<ActionButton>Filter</ActionButton>}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
headers={['Department ID', 'Department Name', 'Status', 'Updated', 'Actions']}
|
||||
rows={filtered().map((row) => [
|
||||
<span class="font-medium text-[#050026]">{row.id}</span>,
|
||||
<span>{row.name}</span>,
|
||||
<StatusBadge label={row.status} tone={row.status === 'ACTIVE' ? 'positive' : 'warning'} />,
|
||||
<span class="text-xs text-slate-500">{new Date(row.updatedAt).toLocaleString()}</span>,
|
||||
<div class="flex gap-2">
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
|
||||
void updateModuleRecord('department', row.id, { status: next }).then(() => void load());
|
||||
}}
|
||||
>
|
||||
Toggle
|
||||
</ActionButton>
|
||||
<ActionButton tone="ghost">View</ActionButton>
|
||||
</div>,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
) : (
|
||||
<SectionCard title="Create Department" subtitle="Create a new department with active runtime status.">
|
||||
<form
|
||||
class="grid gap-4 rounded-xl border border-[#e5eaf3] bg-[#f9fbff] p-4 md:grid-cols-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const name = nameInput().trim() || codeInput().trim() || 'New Department';
|
||||
void createModuleRecord('department', {
|
||||
id: codeInput().trim() || undefined,
|
||||
name,
|
||||
status: 'ACTIVE',
|
||||
}).then(() => {
|
||||
setNameInput('');
|
||||
setCodeInput('');
|
||||
setTab('view');
|
||||
void load();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<label class="text-sm font-medium text-slate-700">
|
||||
Department Name
|
||||
<input value={nameInput()} onInput={(e) => setNameInput(e.currentTarget.value)} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
|
||||
<Show when={mainTab() === 'all'}>
|
||||
<div class="space-y-5 p-5">
|
||||
<label class="flex h-[48px] items-center rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#8a90a8]">
|
||||
<svg class="mr-3 h-5 w-5 text-[#8a90a8]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
|
||||
<input
|
||||
value={search()}
|
||||
onInput={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
void load();
|
||||
}}
|
||||
placeholder="Search departments..."
|
||||
class="w-full border-0 bg-transparent text-[16px] text-[#1a2147] outline-none placeholder:text-[#8a90a8]"
|
||||
/>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">
|
||||
Department Code
|
||||
<input value={codeInput()} onInput={(e) => setCodeInput(e.currentTarget.value.toUpperCase())} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
|
||||
</label>
|
||||
<div class="md:col-span-2 flex justify-end gap-2">
|
||||
<ActionButton onClick={() => setTab('view')}>Cancel</ActionButton>
|
||||
<ActionButton type="submit" tone="primary">Save Department</ActionButton>
|
||||
|
||||
<div class="relative rounded-[18px] border border-[#d8dce6] bg-[#f7f7f8]">
|
||||
<table class="min-w-full table-fixed text-left">
|
||||
<thead class="bg-[#030047] text-white">
|
||||
<tr>
|
||||
<th class="w-[18%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT NAME</th>
|
||||
<th class="w-[14%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT CODE</th>
|
||||
<th class="w-[31%] px-6 py-4 text-[14px] font-semibold">DESCRIPTION</th>
|
||||
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">TOTAL EMPLOYEES</th>
|
||||
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">STATUS</th>
|
||||
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">CREATED DATE</th>
|
||||
<th class="w-[7%] px-6 py-4 text-[14px] font-semibold">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-[#dde1ea] text-[#222948]">
|
||||
<For each={filteredRows()}>
|
||||
{(row) => (
|
||||
<tr class="bg-[#f7f7f8]">
|
||||
<td class="px-6 py-4 text-[15px] font-semibold">{row.name}</td>
|
||||
<td class="px-6 py-4 text-[15px] font-medium text-[#505779]">{String(row.code || '')}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.description || '')}</td>
|
||||
<td class="px-6 py-4 text-[15px] font-semibold">{Number(row.totalEmployees || 0)}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class={`inline-flex rounded-[10px] border px-3 py-1.5 text-[14px] font-semibold ${row.status === 'ACTIVE' ? 'border-[#ffc2aa] bg-[#ffeee6] text-[#fd6116]' : 'border-[#c7ccda] bg-[#eceff6] text-[#101848]'}`}>
|
||||
{row.status === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
|
||||
<td class="relative px-6 py-4">
|
||||
<button onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} class="inline-flex h-10 w-10 items-center justify-center rounded-lg text-[#6c7292] hover:bg-[#eceff5]" aria-label="More actions">
|
||||
<span class="text-[20px] leading-none">⋮</span>
|
||||
</button>
|
||||
<Show when={openMenuId() === row.id}>
|
||||
<div class="absolute right-6 top-14 z-20 w-[220px] rounded-2xl border border-[#d6dbe6] bg-white p-2 shadow-[0_16px_28px_rgba(5,0,38,0.16)]">
|
||||
<button onClick={() => openEdit(row)} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">✎</span>Edit Department</button>
|
||||
<button onClick={async () => { await updateModuleRecord<DepartmentRecord>('department', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">⊗</span>{row.status === 'ACTIVE' ? 'Deactivate Department' : 'Activate Department'}</button>
|
||||
<button onClick={async () => { await deleteModuleRecord('department', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">🗑</span>Delete Department</button>
|
||||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex items-center justify-between border-t border-[#dde1ea] px-6 py-4">
|
||||
<p class="text-[14px] text-[#707895]">Showing <span class="font-semibold text-[#283055]">1-{rows().length}</span> of <span class="font-semibold text-[#283055]">{rows().length}</span> departments</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]">‹</button>
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] bg-[#fd6116] font-semibold text-white">1</button>
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SectionCard>
|
||||
)}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={mainTab() === 'create'}>
|
||||
<div class="space-y-6 p-5">
|
||||
<div class="flex items-center gap-2 border-b border-[#e1e5ee] pb-3">
|
||||
<button onClick={() => setCreateTab('general')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'general' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>General Information<Show when={createTab() === 'general'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
<button onClick={() => setCreateTab('settings')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'settings' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Department Settings<Show when={createTab() === 'settings'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
<button onClick={() => setCreateTab('permissions')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'permissions' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Permissions<Show when={createTab() === 'permissions'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
</div>
|
||||
|
||||
<Show when={createTab() === 'general'}>
|
||||
<div class="space-y-5">
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department Name <span class="text-[#fd6116]">*</span><input value={name()} onInput={(e) => setName(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department name" /></label>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department Code <span class="text-[#fd6116]">*</span><input value={code()} onInput={(e) => setCode(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="e.g., ENG-001" /></label>
|
||||
</div>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department Description<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} class="mt-2 h-[110px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 py-3 text-[16px] text-[#1a2147] outline-none" placeholder="Enter department description" /></label>
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department Head<input value={departmentHead()} onInput={(e) => setDepartmentHead(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department Email<input value={departmentEmail()} onInput={(e) => setDepartmentEmail(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="department@example.com" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={createTab() === 'settings'}>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<p class="text-[18px] font-semibold text-[#101848]">Department Status</p>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<button onClick={() => setStatus('ACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'ACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Active</button>
|
||||
<button onClick={() => setStatus('INACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'INACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Inactive</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-[18px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-4">
|
||||
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Employee Transfers</p><p class="text-[14px] text-[#7d849f]">Enable employees to request transfer to this department</p></div>
|
||||
<button onClick={() => setTransfersEnabled((v) => !v)} class={`relative h-9 w-16 rounded-full transition ${transfersEnabled() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-7 w-7 rounded-full bg-white transition ${transfersEnabled() ? 'left-8' : 'left-1'}`} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={createTab() === 'permissions'}>
|
||||
<div class="space-y-4">
|
||||
<p class="text-[16px] text-[#707895]">Select permissions for this department</p>
|
||||
<For each={['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees', 'Assign Roles']}>
|
||||
{(label) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{label}</div>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex justify-end gap-3 border-t border-[#e1e5ee] pt-4">
|
||||
<button onClick={() => { setMainTab('all'); resetForm(); }} class="h-[44px] rounded-[12px] border border-[#d2d8e4] bg-[#f7f7f8] px-6 text-[16px] font-semibold text-[#232b4d]">Cancel</button>
|
||||
<button onClick={() => void saveDepartment()} class="h-[44px] rounded-[12px] bg-[#030047] px-8 text-[16px] font-semibold text-white">{editingId() ? 'Save Department' : 'Create Department'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,123 +1,253 @@
|
|||
import { createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi';
|
||||
import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
|
||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client';
|
||||
import type { CrudRecord } from '~/lib/admin/types';
|
||||
|
||||
export default function DesignationManagementPage() {
|
||||
const [tab, setTab] = createSignal<'view' | 'create'>('view');
|
||||
const [query, setQuery] = createSignal('');
|
||||
const [rows, setRows] = createSignal<CrudRecord[]>([]);
|
||||
const [nameInput, setNameInput] = createSignal('');
|
||||
const [codeInput, setCodeInput] = createSignal('');
|
||||
type DesignationRecord = CrudRecord & {
|
||||
code?: string;
|
||||
department?: string;
|
||||
level?: string;
|
||||
description?: string;
|
||||
totalEmployees?: number;
|
||||
createdDate?: string;
|
||||
canManageTeam?: boolean;
|
||||
canApprove?: boolean;
|
||||
};
|
||||
|
||||
const load = async () => setRows(await listModuleRecords('designation', { q: query() }));
|
||||
export default function DesignationManagementPage() {
|
||||
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
|
||||
const [createTab, setCreateTab] = createSignal<'general' | 'settings' | 'permissions'>('general');
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [rows, setRows] = createSignal<DesignationRecord[]>([]);
|
||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
|
||||
const [name, setName] = createSignal('');
|
||||
const [code, setCode] = createSignal('');
|
||||
const [department, setDepartment] = createSignal('');
|
||||
const [level, setLevel] = createSignal('');
|
||||
const [description, setDescription] = createSignal('');
|
||||
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
||||
const [canManageTeam, setCanManageTeam] = createSignal(false);
|
||||
const [canApprove, setCanApprove] = createSignal(false);
|
||||
|
||||
const load = async () => {
|
||||
const data = await listModuleRecords<DesignationRecord>('designation', { q: search().trim() || undefined });
|
||||
setRows(data);
|
||||
};
|
||||
onMount(() => void load());
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const q = query().trim().toLowerCase();
|
||||
if (!q) return rows();
|
||||
return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q));
|
||||
});
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setName('');
|
||||
setCode('');
|
||||
setDepartment('');
|
||||
setLevel('');
|
||||
setDescription('');
|
||||
setStatus('ACTIVE');
|
||||
setCanManageTeam(false);
|
||||
setCanApprove(false);
|
||||
setCreateTab('general');
|
||||
};
|
||||
|
||||
const metrics = createMemo(() => {
|
||||
const all = rows();
|
||||
const active = all.filter((item) => item.status === 'ACTIVE').length;
|
||||
const inactive = all.filter((item) => item.status === 'INACTIVE').length;
|
||||
return [
|
||||
{ label: 'Total Designations', value: String(all.length || 0) },
|
||||
{ label: 'Active Designations', value: String(active), tone: 'positive' as const },
|
||||
{ label: 'Inactive Designations', value: String(inactive), tone: 'warning' as const },
|
||||
{ label: 'Updated Today', value: String(Math.min(active, 8)), tone: 'info' as const },
|
||||
];
|
||||
});
|
||||
const openCreate = () => {
|
||||
resetForm();
|
||||
setMainTab('create');
|
||||
};
|
||||
|
||||
const openEdit = (row: DesignationRecord) => {
|
||||
setEditingId(row.id);
|
||||
setName(row.name || '');
|
||||
setCode(String(row.code || ''));
|
||||
setDepartment(String(row.department || ''));
|
||||
setLevel(String(row.level || ''));
|
||||
setDescription(String(row.description || ''));
|
||||
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
|
||||
setCanManageTeam(Boolean(row.canManageTeam));
|
||||
setCanApprove(Boolean(row.canApprove));
|
||||
setMainTab('create');
|
||||
setCreateTab('general');
|
||||
};
|
||||
|
||||
const saveDesignation = async () => {
|
||||
const payload: Partial<DesignationRecord> = {
|
||||
name: name().trim() || 'New Designation',
|
||||
code: code().trim() || undefined,
|
||||
department: department().trim(),
|
||||
level: level().trim(),
|
||||
description: description().trim(),
|
||||
status: status(),
|
||||
canManageTeam: canManageTeam(),
|
||||
canApprove: canApprove(),
|
||||
};
|
||||
if (editingId()) {
|
||||
await updateModuleRecord<DesignationRecord>('designation', editingId()!, payload);
|
||||
} else {
|
||||
await createModuleRecord<DesignationRecord>('designation', payload);
|
||||
}
|
||||
setMainTab('all');
|
||||
setOpenMenuId(null);
|
||||
resetForm();
|
||||
await load();
|
||||
};
|
||||
|
||||
const filteredRows = createMemo(() => rows());
|
||||
|
||||
const formatDate = (value?: string) => {
|
||||
const input = value || '';
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) return input;
|
||||
return (input || new Date().toISOString().slice(0, 10)).slice(0, 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-5">
|
||||
<PageHeader
|
||||
title="Designation Management"
|
||||
subtitle="Manage designation taxonomy used by internal and external role systems."
|
||||
actions={<Tabs value={tab()} onChange={setTab} items={[{ key: 'view', label: 'View Designations' }, { key: 'create', label: 'Create Designation' }]} />}
|
||||
/>
|
||||
<section>
|
||||
<h1 class="text-[24px] font-semibold leading-[1.1] tracking-[-0.01em] text-[#050026]">Designation Management</h1>
|
||||
<p class="mt-2 text-[16px] leading-[1.35] text-[#7a8099]">Manage all designations and job positions</p>
|
||||
</section>
|
||||
|
||||
<MetricCards items={metrics()} />
|
||||
<section class="overflow-hidden rounded-[24px] border border-[#d9dde6] bg-[#f7f7f8]">
|
||||
<div class="flex items-center gap-2 border-b border-[#e1e5ee] px-5 pt-4">
|
||||
<button onClick={() => setMainTab('all')} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'all' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
|
||||
All Designations
|
||||
<Show when={mainTab() === 'all'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
|
||||
</button>
|
||||
<button onClick={openCreate} class={`relative px-8 pb-4 pt-2 text-[16px] font-semibold ${mainTab() === 'create' ? 'text-[#0c123f]' : 'text-[#737a96]'}`}>
|
||||
{editingId() ? 'Edit Designation' : 'Create Designation'}
|
||||
<Show when={mainTab() === 'create'}><span class="absolute inset-x-0 -bottom-[1px] h-[4px] rounded-full bg-[#0a0a50]" /></Show>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{tab() === 'view' ? (
|
||||
<SectionCard
|
||||
title="Designations"
|
||||
subtitle="Search and update designation availability."
|
||||
actions={
|
||||
<>
|
||||
<ActionButton>Export</ActionButton>
|
||||
<ActionButton tone="primary" onClick={() => setTab('create')}>Add Designation</ActionButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<SearchFilters
|
||||
query={query()}
|
||||
onQuery={(value) => {
|
||||
setQuery(value);
|
||||
void load();
|
||||
}}
|
||||
right={<ActionButton>Filter</ActionButton>}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
headers={['Designation ID', 'Designation Name', 'Status', 'Updated', 'Actions']}
|
||||
rows={filtered().map((row) => [
|
||||
<span class="font-medium text-[#050026]">{row.id}</span>,
|
||||
<span>{row.name}</span>,
|
||||
<StatusBadge label={row.status} tone={row.status === 'ACTIVE' ? 'positive' : 'warning'} />,
|
||||
<span class="text-xs text-slate-500">{new Date(row.updatedAt).toLocaleString()}</span>,
|
||||
<div class="flex gap-2">
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
|
||||
void updateModuleRecord('designation', row.id, { status: next }).then(() => void load());
|
||||
}}
|
||||
>
|
||||
Toggle
|
||||
</ActionButton>
|
||||
<ActionButton tone="ghost">View</ActionButton>
|
||||
</div>,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
) : (
|
||||
<SectionCard title="Create Designation" subtitle="Add a new designation used in role and employee mapping.">
|
||||
<form
|
||||
class="grid gap-4 rounded-xl border border-[#e5eaf3] bg-[#f9fbff] p-4 md:grid-cols-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const name = nameInput().trim() || codeInput().trim() || 'New Designation';
|
||||
void createModuleRecord('designation', {
|
||||
id: codeInput().trim() || undefined,
|
||||
name,
|
||||
status: 'ACTIVE',
|
||||
}).then(() => {
|
||||
setNameInput('');
|
||||
setCodeInput('');
|
||||
setTab('view');
|
||||
void load();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<label class="text-sm font-medium text-slate-700">
|
||||
Designation Name
|
||||
<input value={nameInput()} onInput={(e) => setNameInput(e.currentTarget.value)} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">
|
||||
Designation Code
|
||||
<input value={codeInput()} onInput={(e) => setCodeInput(e.currentTarget.value.toUpperCase())} class="mt-1 block w-full rounded-lg border border-[#d8dfec] bg-white px-3 py-2 text-sm outline-none focus:border-[#fd6116]" />
|
||||
</label>
|
||||
<div class="md:col-span-2 flex justify-end gap-2">
|
||||
<ActionButton onClick={() => setTab('view')}>Cancel</ActionButton>
|
||||
<ActionButton type="submit" tone="primary">Save Designation</ActionButton>
|
||||
<Show when={mainTab() === 'all'}>
|
||||
<div class="space-y-5 p-5">
|
||||
<div class="grid gap-3 md:grid-cols-[1fr_190px_130px]">
|
||||
<label class="flex h-[48px] items-center rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#8a90a8]">
|
||||
<svg class="mr-3 h-5 w-5 text-[#8a90a8]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></svg>
|
||||
<input value={search()} onInput={(e) => { setSearch(e.currentTarget.value); void load(); }} placeholder="Search designations..." class="w-full border-0 bg-transparent text-[16px] text-[#1a2147] outline-none placeholder:text-[#8a90a8]" />
|
||||
</label>
|
||||
<div class="h-[48px] rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8]" />
|
||||
<div class="h-[48px] rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8]" />
|
||||
</div>
|
||||
</form>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
<div class="relative rounded-[18px] border border-[#d8dce6] bg-[#f7f7f8]">
|
||||
<table class="min-w-full table-fixed text-left">
|
||||
<thead class="bg-[#030047] text-white">
|
||||
<tr>
|
||||
<th class="w-[17%] px-6 py-4 text-[14px] font-semibold">DESIGNATION NAME</th>
|
||||
<th class="w-[16%] px-6 py-4 text-[14px] font-semibold">DESIGNATION CODE</th>
|
||||
<th class="w-[18%] px-6 py-4 text-[14px] font-semibold">DEPARTMENT</th>
|
||||
<th class="w-[12%] px-6 py-4 text-[14px] font-semibold">LEVEL</th>
|
||||
<th class="w-[11%] px-6 py-4 text-[14px] font-semibold">TOTAL EMPLOYEES</th>
|
||||
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">STATUS</th>
|
||||
<th class="w-[10%] px-6 py-4 text-[14px] font-semibold">CREATED DATE</th>
|
||||
<th class="w-[6%] px-6 py-4 text-[14px] font-semibold">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-[#dde1ea] text-[#222948]">
|
||||
<For each={filteredRows()}>
|
||||
{(row) => (
|
||||
<tr class="bg-[#f7f7f8]">
|
||||
<td class="px-6 py-4 text-[15px] font-semibold">{row.name}</td>
|
||||
<td class="px-6 py-4 text-[15px] font-medium text-[#505779]">{String(row.code || '')}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.department || '')}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{String(row.level || '')}</td>
|
||||
<td class="px-6 py-4 text-[15px] font-semibold">{Number(row.totalEmployees || 0)}</td>
|
||||
<td class="px-6 py-4"><span class={`inline-flex rounded-[10px] border px-3 py-1.5 text-[14px] font-semibold ${row.status === 'ACTIVE' ? 'border-[#ffc2aa] bg-[#ffeee6] text-[#fd6116]' : 'border-[#c7ccda] bg-[#eceff6] text-[#101848]'}`}>{row.status === 'ACTIVE' ? 'Active' : 'Inactive'}</span></td>
|
||||
<td class="px-6 py-4 text-[14px] font-medium text-[#6b7393]">{formatDate(String(row.createdDate || row.updatedAt || ''))}</td>
|
||||
<td class="relative px-6 py-4">
|
||||
<button onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} class="inline-flex h-10 w-10 items-center justify-center rounded-lg text-[#6c7292] hover:bg-[#eceff5]" aria-label="More actions"><span class="text-[20px] leading-none">⋮</span></button>
|
||||
<Show when={openMenuId() === row.id}>
|
||||
<div class="absolute right-6 top-14 z-20 w-[220px] rounded-2xl border border-[#d6dbe6] bg-white p-2 shadow-[0_16px_28px_rgba(5,0,38,0.16)]">
|
||||
<button onClick={() => openEdit(row)} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">✎</span>Edit Designation</button>
|
||||
<button onClick={async () => { await updateModuleRecord<DesignationRecord>('designation', row.id, { status: row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE' }); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">⊗</span>{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}</button>
|
||||
<button onClick={async () => { await deleteModuleRecord('designation', row.id); setOpenMenuId(null); await load(); }} class="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-[16px] font-medium text-[#20284d] hover:bg-[#f5f7fb]"><span class="text-[#fd6116]">🗑</span>Delete</button>
|
||||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-[#dde1ea] px-6 py-4">
|
||||
<p class="text-[14px] text-[#707895]">Showing <span class="font-semibold text-[#283055]">1-{rows().length}</span> of <span class="font-semibold text-[#283055]">{rows().length}</span> designations</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]">‹</button>
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] bg-[#fd6116] font-semibold text-white">1</button>
|
||||
<button class="inline-flex h-10 w-10 items-center justify-center rounded-[12px] border border-[#d5dae6] text-[#8a90a8]">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={mainTab() === 'create'}>
|
||||
<div class="space-y-6 p-5">
|
||||
<div class="flex items-center gap-2 border-b border-[#e1e5ee] pb-3">
|
||||
<button onClick={() => setCreateTab('general')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'general' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>General Information<Show when={createTab() === 'general'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
<button onClick={() => setCreateTab('settings')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'settings' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Designation Settings<Show when={createTab() === 'settings'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
<button onClick={() => setCreateTab('permissions')} class={`relative px-6 pb-3 text-[16px] font-semibold ${createTab() === 'permissions' ? 'text-[#0b123f]' : 'text-[#767d98]'}`}>Permissions<Show when={createTab() === 'permissions'}><span class="absolute inset-x-0 -bottom-[2px] h-[4px] rounded-full bg-[#0a0a50]" /></Show></button>
|
||||
</div>
|
||||
|
||||
<Show when={createTab() === 'general'}>
|
||||
<div class="space-y-5">
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Designation Name <span class="text-[#fd6116]">*</span><input value={name()} onInput={(e) => setName(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="Enter designation name" /></label>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Designation Code <span class="text-[#fd6116]">*</span><input value={code()} onInput={(e) => setCode(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" placeholder="e.g., SSE-001" /></label>
|
||||
</div>
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Department <span class="text-[#fd6116]">*</span><input value={department()} onInput={(e) => setDepartment(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Designation Level <span class="text-[#fd6116]">*</span><input value={level()} onInput={(e) => setLevel(e.currentTarget.value)} class="mt-2 h-[48px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 text-[16px] text-[#1a2147] outline-none" /></label>
|
||||
</div>
|
||||
<label class="block text-[14px] font-semibold text-[#101848]">Description<textarea value={description()} onInput={(e) => setDescription(e.currentTarget.value)} class="mt-2 h-[110px] w-full rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-4 py-3 text-[16px] text-[#1a2147] outline-none" placeholder="Enter designation description" /></label>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={createTab() === 'settings'}>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<p class="text-[18px] font-semibold text-[#101848]">Designation Status</p>
|
||||
<div class="mt-3 flex gap-3">
|
||||
<button onClick={() => setStatus('ACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'ACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Active</button>
|
||||
<button onClick={() => setStatus('INACTIVE')} class={`h-[44px] rounded-[12px] border px-6 text-[16px] font-semibold ${status() === 'INACTIVE' ? 'border-[#fd6116] bg-[#fd6116] text-white' : 'border-[#d3d8e4] bg-[#f7f7f8] text-[#1a2147]'}`}>Inactive</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-5 py-4">
|
||||
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Designation to Manage Team Members</p><p class="text-[14px] text-[#7d849f]">Enable this designation to manage team members</p></div>
|
||||
<button onClick={() => setCanManageTeam((v) => !v)} class={`relative h-8 w-14 rounded-full transition ${canManageTeam() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-6 w-6 rounded-full bg-white transition ${canManageTeam() ? 'left-7' : 'left-1'}`} /></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-5 py-4">
|
||||
<div><p class="text-[16px] font-semibold text-[#101848]">Allow Approval Permissions</p><p class="text-[14px] text-[#7d849f]">Enable this designation to approve requests</p></div>
|
||||
<button onClick={() => setCanApprove((v) => !v)} class={`relative h-8 w-14 rounded-full transition ${canApprove() ? 'bg-[#fd6116]' : 'bg-[#e0e4ec]'}`}><span class={`absolute top-1 h-6 w-6 rounded-full bg-white transition ${canApprove() ? 'left-7' : 'left-1'}`} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={createTab() === 'permissions'}>
|
||||
<div class="space-y-5">
|
||||
<p class="text-[16px] text-[#707895]">Select permissions for this designation</p>
|
||||
<div>
|
||||
<h3 class="text-[18px] font-semibold text-[#11194a]">Employee Management</h3>
|
||||
<div class="mt-3 space-y-3">
|
||||
<For each={['View Employees', 'Create Employees', 'Edit Employees', 'Delete Employees']}>{(label) => <div class="rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">{label}</div>}</For>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-[18px] font-semibold text-[#11194a]">Additional Permissions</h3>
|
||||
<div class="mt-3 rounded-[14px] border border-[#d9dde6] bg-[#f7f7f8] px-6 py-3 text-[16px] font-semibold text-[#1c244a]">Assign Roles</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex justify-end gap-3 border-t border-[#e1e5ee] pt-4">
|
||||
<button onClick={() => { setMainTab('all'); resetForm(); }} class="h-[44px] rounded-[12px] border border-[#d2d8e4] bg-[#f7f7f8] px-6 text-[16px] font-semibold text-[#232b4d]">Cancel</button>
|
||||
<button onClick={() => void saveDesignation()} class="h-[44px] rounded-[12px] bg-[#030047] px-8 text-[16px] font-semibold text-white">{editingId() ? 'Save Designation' : 'Create Designation'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue