nxtgauge-admin-solid/src/routes/admin/designation.tsx

426 lines
14 KiB
TypeScript
Raw Normal View History

import { A } from '@solidjs/router';
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type Department = {
id: string;
name?: string;
departmentName?: string;
};
type Designation = {
id: string;
name: string;
departmentId?: string;
departmentName?: string;
department?: string;
description?: string;
is_archived?: boolean;
status?: string;
};
async function loadDesignations(): Promise<Designation[]> {
try {
const res = await fetch(`${API}/api/admin/designations`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.designations ?? []);
} catch {
return [];
}
}
async function loadDepartments(): Promise<Department[]> {
try {
const res = await fetch(`${API}/api/admin/departments`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
return Array.isArray(data) ? data : (data.departments ?? []);
} catch {
return [];
}
}
function deptDisplay(item: Designation): string {
return item.departmentName || item.department || '—';
}
function deptName(d: Department): string {
return d.departmentName || d.name || d.id;
}
function isArchived(item: Designation): boolean {
if (item.is_archived !== undefined) return item.is_archived;
if (item.status !== undefined) {
const s = String(item.status).toUpperCase();
return s === 'ARCHIVED' || s === '2';
}
return false;
}
export default function DesignationPage() {
const [designations, { refetch }] = createResource(loadDesignations);
const [departments] = createResource(loadDepartments);
// tabs
const [tab, setTab] = createSignal<'active' | 'archived'>('active');
// create form
const [showCreate, setShowCreate] = createSignal(false);
const [createName, setCreateName] = createSignal('');
const [createDeptId, setCreateDeptId] = createSignal('');
const [createDesc, setCreateDesc] = createSignal('');
const [creating, setCreating] = createSignal(false);
const [createError, setCreateError] = createSignal('');
// inline edit
const [editingId, setEditingId] = createSignal('');
const [editName, setEditName] = createSignal('');
const [editDeptId, setEditDeptId] = createSignal('');
const [editDesc, setEditDesc] = createSignal('');
const [saving, setSaving] = createSignal(false);
const [editError, setEditError] = createSignal('');
// row busy / errors
const [deleting, setDeleting] = createSignal('');
const [actionError, setActionError] = createSignal('');
const filtered = createMemo(() => {
const all = designations() ?? [];
return tab() === 'archived'
? all.filter((d) => isArchived(d))
: all.filter((d) => !isArchived(d));
});
// ---------- CREATE ----------
const handleCreate = async (e: Event) => {
e.preventDefault();
if (!createName().trim() || !createDeptId()) return;
setCreating(true);
setCreateError('');
try {
const res = await fetch(`${API}/api/admin/designations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: createName().trim(),
department_id: createDeptId(),
description: createDesc().trim(),
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).message || 'Failed to create');
}
setCreateName('');
setCreateDeptId('');
setCreateDesc('');
setShowCreate(false);
setTab('active');
refetch();
} catch (err: any) {
setCreateError(err.message || 'Failed to create designation');
} finally {
setCreating(false);
}
};
// ---------- INLINE EDIT ----------
const startEdit = (item: Designation) => {
setEditingId(item.id);
setEditName(item.name);
setEditDeptId(item.departmentId ?? '');
setEditDesc(item.description ?? '');
setEditError('');
};
const cancelEdit = () => {
setEditingId('');
setEditError('');
};
const handleUpdate = async (id: string) => {
if (!editName().trim()) return;
setSaving(true);
setEditError('');
try {
const res = await fetch(`${API}/api/admin/designations/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: editName().trim(),
department_id: editDeptId(),
description: editDesc().trim(),
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).message || 'Failed to update');
}
setEditingId('');
refetch();
} catch (err: any) {
setEditError(err.message || 'Failed to update designation');
} finally {
setSaving(false);
}
};
// ---------- DELETE ----------
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete designation "${name}"?`)) return;
setDeleting(id);
setActionError('');
try {
const res = await fetch(`${API}/api/admin/designations/${id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error('Failed to delete');
refetch();
} catch (err: any) {
setActionError(err.message || 'Failed to delete designation');
} finally {
setDeleting('');
}
};
return (
<AdminShell>
{/* Header */}
<div class="page-actions">
<div>
<h1 class="page-title">Designations</h1>
<p class="page-subtitle">Manage job designations</p>
</div>
<button
class="btn navy"
onClick={() => {
setShowCreate((v) => !v);
setCreateError('');
// Pre-select first department when opening
const depts = departments() ?? [];
if (!createDeptId() && depts.length > 0) setCreateDeptId(depts[0].id);
}}
>
{showCreate() ? 'Cancel' : 'Add Designation'}
</button>
</div>
{/* Create form */}
<Show when={showCreate()}>
<section class="card role-form-section">
<form onSubmit={handleCreate}>
<div class="field-grid-2">
<div class="field">
<label>Name *</label>
<input
type="text"
required
placeholder="e.g. Senior Engineer"
value={createName()}
onInput={(e) => setCreateName(e.currentTarget.value)}
/>
</div>
<div class="field">
<label>Department *</label>
<select
required
value={createDeptId()}
onChange={(e) => setCreateDeptId(e.currentTarget.value)}
>
<option value="">Select a department...</option>
<Show when={departments.loading}>
<option disabled>Loading departments...</option>
</Show>
<For each={departments() ?? []}>
{(d) => <option value={d.id}>{deptName(d)}</option>}
</For>
</select>
</div>
<div class="field" style="grid-column:1/-1">
<label>Description</label>
<textarea
placeholder="Optional description"
rows="3"
value={createDesc()}
onInput={(e) => setCreateDesc(e.currentTarget.value)}
/>
</div>
</div>
<Show when={createError()}>
<p class="error-box" style="margin-top:10px">{createError()}</p>
</Show>
<div class="actions" style="margin-top:16px">
<button
type="button"
class="btn"
onClick={() => {
setShowCreate(false);
setCreateError('');
}}
>
Cancel
</button>
<button
type="submit"
class="btn navy"
disabled={creating() || (departments() ?? []).length === 0}
>
{creating() ? 'Saving...' : 'Save'}
</button>
</div>
</form>
</section>
</Show>
{/* Tabs */}
<div style="display:flex;gap:24px;border-bottom:1px solid #e2e8f0;margin-bottom:16px">
<button
class={`admin-tab${tab() === 'active' ? ' active' : ''}`}
onClick={() => setTab('active')}
>
Active
</button>
<button
class={`admin-tab${tab() === 'archived' ? ' active' : ''}`}
onClick={() => setTab('archived')}
>
Archived
</button>
</div>
{/* Action error */}
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
</Show>
{/* Table */}
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<thead>
<tr>
<th>Name</th>
<th>Department</th>
<th>Description</th>
<th class="align-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={designations.loading}>
<tr>
<td colspan="4" style="text-align:center;padding:32px;color:#64748b">
Loading...
</td>
</tr>
</Show>
<Show when={!designations.loading && designations.error}>
<tr>
<td colspan="4" style="text-align:center;padding:32px;color:#b91c1c">
Failed to load. Is the backend running?
</td>
</tr>
</Show>
<Show when={!designations.loading && !designations.error && filtered().length === 0}>
<tr>
<td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">
No designations found.
</td>
</tr>
</Show>
<Show when={!designations.loading && !designations.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<>
<tr>
<td style="font-weight:600;color:#0f172a">{item.name}</td>
<td style="color:#475569">{deptDisplay(item)}</td>
<td style="color:#475569">{item.description || '—'}</td>
<td>
<div class="table-actions">
<button
class="btn"
onClick={() => startEdit(item)}
>
Edit
</button>
<button
class="btn danger"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
{deleting() === item.id ? '...' : 'Delete'}
</button>
</div>
</td>
</tr>
{/* Inline edit row */}
<Show when={editingId() === item.id}>
<tr>
<td colspan="4" style="background:#f8fafc;padding:16px">
<div class="field-grid-2" style="margin-bottom:10px">
<div class="field">
<label>Name *</label>
<input
type="text"
required
value={editName()}
onInput={(e) => setEditName(e.currentTarget.value)}
/>
</div>
<div class="field">
<label>Department</label>
<select
value={editDeptId()}
onChange={(e) => setEditDeptId(e.currentTarget.value)}
>
<option value="">Select a department...</option>
<For each={departments() ?? []}>
{(d) => (
<option value={d.id}>{deptName(d)}</option>
)}
</For>
</select>
</div>
<div class="field" style="grid-column:1/-1">
<label>Description</label>
<textarea
rows="3"
value={editDesc()}
onInput={(e) => setEditDesc(e.currentTarget.value)}
/>
</div>
</div>
<Show when={editError()}>
<p class="error-box" style="margin-bottom:8px">{editError()}</p>
</Show>
<div class="actions">
<button class="btn" type="button" onClick={cancelEdit}>
Cancel
</button>
<button
class="btn navy"
type="button"
disabled={saving()}
onClick={() => handleUpdate(item.id)}
>
{saving() ? 'Saving...' : 'Save'}
</button>
</div>
</td>
</tr>
</Show>
</>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</AdminShell>
);
}