From 0996f12227bce7c6f5cf640e990fb7109861584f Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 25 Mar 2026 21:32:03 +0100 Subject: [PATCH] Wire department and designation management to module CRUD backend --- src/lib/admin/client.ts | 12 +- src/lib/admin/data.ts | 91 +++++++- src/lib/admin/types.ts | 1 + src/routes/admin/department.tsx | 335 ++++++++++++++++++++---------- src/routes/admin/designation.tsx | 346 +++++++++++++++++++++---------- 5 files changed, 556 insertions(+), 229 deletions(-) diff --git a/src/lib/admin/client.ts b/src/lib/admin/client.ts index 2ce1670..1dd710e 100644 --- a/src/lib/admin/client.ts +++ b/src/lib/admin/client.ts @@ -67,30 +67,30 @@ export async function updateApproval(id: string, patch: Partial) { return parse(res); } -export async function listModuleRecords(moduleKey: string, query?: { q?: string; status?: string }) { +export async function listModuleRecords(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(res); + return parse(res); } -export async function createModuleRecord(moduleKey: string, payload: Partial) { +export async function createModuleRecord(moduleKey: string, payload: Partial) { const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); - return parse(res); + return parse(res); } -export async function updateModuleRecord(moduleKey: string, id: string, patch: Partial) { +export async function updateModuleRecord(moduleKey: string, id: string, patch: Partial) { 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(res); + return parse(res); } export async function deleteModuleRecord(moduleKey: string, id: string) { diff --git a/src/lib/admin/data.ts b/src/lib/admin/data.ts index 9afd7ab..b429b41 100644 --- a/src/lib/admin/data.ts +++ b/src/lib/admin/data.ts @@ -62,7 +62,12 @@ const filterByQuery = => { +const createCrudService = ( + seed: CrudRecord[], + options?: { + createFromPayload?: (payload: Partial, currentRows: CrudRecord[]) => CrudRecord; + }, +): CrudService => { let rows = [...seed]; return { async list(query) { @@ -72,12 +77,15 @@ const createCrudService = (seed: CrudRecord[]): CrudService => { 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>(); 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 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; } diff --git a/src/lib/admin/types.ts b/src/lib/admin/types.ts index 1edab38..882688d 100644 --- a/src/lib/admin/types.ts +++ b/src/lib/admin/types.ts @@ -32,6 +32,7 @@ export type CrudRecord = { name: string; status: 'ACTIVE' | 'INACTIVE'; updatedAt: string; + [key: string]: unknown; }; export type AdminModuleConfig = { diff --git a/src/routes/admin/department.tsx b/src/routes/admin/department.tsx index 561433e..3f4b2bf 100644 --- a/src/routes/admin/department.tsx +++ b/src/routes/admin/department.tsx @@ -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([]); - 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([]); + const [openMenuId, setOpenMenuId] = createSignal(null); + const [editingId, setEditingId] = createSignal(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('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 = { + 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('department', editingId()!, payload); + } else { + await createModuleRecord('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 (
- } - /> +
+

Department Management

+

Manage all departments and organizational structure

+
- +
+
+ + +
- {tab() === 'view' ? ( - - Export - setTab('create')}>Add Department - - } - > -
- { - setQuery(value); - void load(); - }} - right={Filter} - /> - - [ - {row.id}, - {row.name}, - , - {new Date(row.updatedAt).toLocaleString()}, -
- { - const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; - void updateModuleRecord('department', row.id, { status: next }).then(() => void load()); - }} - > - Toggle - - View -
, - ])} - /> -
-
- ) : ( - -
{ - 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(); - }); - }} - > -