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

425 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;
description?: string;
is_archived?: boolean;
status?: string | number;
created_at?: string;
createdAt?: string;
};
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 isArchived(item: Department): 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;
}
function deptLabel(item: Department): string {
return item.departmentName || item.name || '—';
}
function fmtDate(val?: string): string {
if (!val) return '—';
try {
return new Date(val).toLocaleDateString();
} catch {
return val;
}
}
export default function DepartmentPage() {
const [departments, { refetch }] = createResource(loadDepartments);
// tabs
const [tab, setTab] = createSignal<'active' | 'archived'>('active');
// create form
const [showCreate, setShowCreate] = createSignal(false);
const [createName, setCreateName] = 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 [editDesc, setEditDesc] = createSignal('');
const [saving, setSaving] = createSignal(false);
const [editError, setEditError] = createSignal('');
// row-level busy
const [busy, setBusy] = createSignal('');
const [actionError, setActionError] = createSignal('');
const filtered = createMemo(() => {
const all = departments() ?? [];
return tab() === 'archived'
? all.filter((d) => isArchived(d))
: all.filter((d) => !isArchived(d));
});
// ---------- CREATE ----------
const handleCreate = async (e: Event) => {
e.preventDefault();
if (!createName().trim()) return;
setCreating(true);
setCreateError('');
try {
const res = await fetch(`${API}/api/admin/departments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: createName().trim(),
description: createDesc().trim(),
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).message || 'Failed to create');
}
setCreateName('');
setCreateDesc('');
setShowCreate(false);
setTab('active');
refetch();
} catch (err: any) {
setCreateError(err.message || 'Failed to create department');
} finally {
setCreating(false);
}
};
// ---------- EDIT ----------
const startEdit = (item: Department) => {
setEditingId(item.id);
setEditName(deptLabel(item));
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/departments/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: editName().trim(),
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 department');
} finally {
setSaving(false);
}
};
// ---------- ARCHIVE / RESTORE ----------
const handleArchive = async (id: string) => {
setBusy(id);
setActionError('');
try {
const res = await fetch(`${API}/api/admin/departments/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: true }),
});
if (!res.ok) throw new Error('Failed to archive');
refetch();
} catch (err: any) {
setActionError(err.message || 'Failed to archive department');
} finally {
setBusy('');
}
};
const handleRestore = async (id: string) => {
setBusy(id);
setActionError('');
try {
const res = await fetch(`${API}/api/admin/departments/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_archived: false }),
});
if (!res.ok) throw new Error('Failed to restore');
refetch();
} catch (err: any) {
setActionError(err.message || 'Failed to restore department');
} finally {
setBusy('');
}
};
// ---------- DELETE ----------
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Delete department "${name}"?`)) return;
setBusy(id);
setActionError('');
try {
const res = await fetch(`${API}/api/admin/departments/${id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error('Failed to delete');
refetch();
} catch (err: any) {
setActionError(err.message || 'Failed to delete department');
} finally {
setBusy('');
}
};
return (
<AdminShell>
{/* Header */}
<div class="page-actions">
<div>
<h1 class="page-title">Departments</h1>
<p class="page-subtitle">Manage organization departments</p>
</div>
<button
class="btn navy"
onClick={() => {
setShowCreate((v) => !v);
setCreateError('');
}}
>
{showCreate() ? 'Cancel' : 'Add Department'}
</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. Engineering"
value={createName()}
onInput={(e) => setCreateName(e.currentTarget.value)}
/>
</div>
<div class="field">
<label>Description</label>
<input
type="text"
placeholder="Optional description"
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()}>
{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>Description</th>
<th>Created At</th>
<th class="align-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={departments.loading}>
<tr>
<td colspan="4" style="text-align:center;padding:32px;color:#64748b">
Loading...
</td>
</tr>
</Show>
<Show when={!departments.loading && departments.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={!departments.loading && !departments.error && filtered().length === 0}>
<tr>
<td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">
No departments found.
</td>
</tr>
</Show>
<Show when={!departments.loading && !departments.error && filtered().length > 0}>
<For each={filtered()}>
{(item) => (
<>
<tr>
<td style="font-weight:600;color:#0f172a">{deptLabel(item)}</td>
<td style="color:#475569">{item.description || '—'}</td>
<td style="color:#475569">{fmtDate(item.createdAt || item.created_at)}</td>
<td>
<div class="table-actions">
<button
class="btn"
onClick={() => startEdit(item)}
>
Edit
</button>
<Show when={tab() === 'active'}>
<button
class="btn"
disabled={busy() === item.id}
onClick={() => handleArchive(item.id)}
>
{busy() === item.id ? '...' : 'Archive'}
</button>
</Show>
<Show when={tab() === 'archived'}>
<button
class="btn"
disabled={busy() === item.id}
onClick={() => handleRestore(item.id)}
>
{busy() === item.id ? '...' : 'Restore'}
</button>
</Show>
<button
class="btn danger"
disabled={busy() === item.id}
onClick={() => handleDelete(item.id, deptLabel(item))}
>
{busy() === 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>Description</label>
<input
type="text"
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>
);
}