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

226 lines
7.3 KiB
TypeScript
Raw Normal View History

import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import AdminShell from '~/components/AdminShell';
const API = '/api/gateway';
type ModuleRecord = {
id: string;
name: string;
key: string;
description?: string;
isActive: boolean;
};
type ModuleForm = {
name: string;
key: string;
description: string;
isActive: boolean;
};
const EMPTY_FORM: ModuleForm = { name: '', key: '', description: '', isActive: true };
async function fetchModules(): Promise<ModuleRecord[]> {
const res = await fetch(`${API}/api/modules`);
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : data?.modules || [];
}
export default function ModulesPage() {
const [refreshToken, setRefreshToken] = createSignal(0);
const [isModalOpen, setIsModalOpen] = createSignal(false);
const [editing, setEditing] = createSignal<ModuleRecord | null>(null);
const [form, setForm] = createSignal<ModuleForm>({ ...EMPTY_FORM });
const [error, setError] = createSignal('');
const [submitting, setSubmitting] = createSignal(false);
const [modules] = createResource(refreshToken, fetchModules);
const modalTitle = createMemo(() => editing() ? 'Edit Module' : 'Create Module');
function openModal(item?: ModuleRecord) {
if (item) {
setEditing(item);
setForm({
name: item.name || '',
key: item.key || '',
description: item.description || '',
isActive: Boolean(item.isActive),
});
} else {
setEditing(null);
setForm({ ...EMPTY_FORM });
}
setError('');
setIsModalOpen(true);
}
function closeModal() {
setIsModalOpen(false);
setEditing(null);
setForm({ ...EMPTY_FORM });
setError('');
}
async function submitForm(event: Event) {
event.preventDefault();
const current = form();
if (!current.name.trim() || !current.key.trim()) {
setError('Name and key are required.');
return;
}
setSubmitting(true);
setError('');
try {
const target = editing();
const method = target ? 'PATCH' : 'POST';
const url = target ? `${API}/api/modules/${target.id}` : `${API}/api/modules`;
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(current),
});
if (!res.ok) {
const message = await res.text();
throw new Error(message || 'Failed to save module');
}
closeModal();
setRefreshToken((value) => value + 1);
} catch (nextError: any) {
setError(nextError?.message || 'Failed to save module');
} finally {
setSubmitting(false);
}
}
async function removeModule(id: string) {
if (!confirm('Are you sure you want to delete this module?')) return;
try {
const res = await fetch(`${API}/api/modules/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Delete failed');
setRefreshToken((value) => value + 1);
} catch {
setError('Failed to delete module.');
}
}
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div>
<h1 class="page-title">Module Registry</h1>
<p class="page-subtitle">Manage internal module definitions and activation state.</p>
</div>
<button class="btn primary" onClick={() => openModal()}>Add Module</button>
</div>
<Show when={error() && !isModalOpen()}>
<div class="card"><p class="error-note">{error()}</p></div>
</Show>
<Show when={modules.loading}>
<div class="card"><p class="notice">Loading modules...</p></div>
</Show>
<div class="card">
<div class="table-wrap">
<table class="list-table">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Description</th>
<th>Status</th>
<th class="align-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={(modules() || []).length > 0} fallback={
<tr><td colspan="5" class="notice" style="padding:16px">No modules found.</td></tr>
}>
<For each={modules() || []}>
{(item) => (
<tr>
<td>{item.name}</td>
<td><code>{item.key}</code></td>
<td>{item.description || '—'}</td>
<td>
<span class={`status-pill ${item.isActive ? 'status-approved' : 'status-rejected'}`}>
{item.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td class="align-right">
<div class="table-actions">
<button class="btn" onClick={() => openModal(item)}>Edit</button>
<button class="btn danger" onClick={() => removeModule(item.id)}>Delete</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
<Show when={isModalOpen()}>
<div class="modal-backdrop">
<div class="modal">
<h2 style="margin-top:0">{modalTitle()}</h2>
<form onSubmit={submitForm}>
<div class="field">
<label>Name</label>
<input
value={form().name}
onInput={(event) => setForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
placeholder="e.g. Job Board"
required
/>
</div>
<div class="field">
<label>Key</label>
<input
value={form().key}
onInput={(event) => setForm((prev) => ({ ...prev, key: event.currentTarget.value }))}
placeholder="e.g. manage_jobs"
required
/>
</div>
<div class="field">
<label>Description</label>
<textarea
rows="3"
value={form().description}
onInput={(event) => setForm((prev) => ({ ...prev, description: event.currentTarget.value }))}
placeholder="Short description..."
/>
</div>
<label style="display:flex;gap:8px;align-items:center;font-size:13px">
<input
type="checkbox"
checked={form().isActive}
onChange={(event) => setForm((prev) => ({ ...prev, isActive: event.currentTarget.checked }))}
/>
Active
</label>
<Show when={error()}>
<p class="error-note">{error()}</p>
</Show>
<div class="actions" style="justify-content:flex-end">
<button type="button" class="btn" onClick={closeModal}>Cancel</button>
<button type="submit" class="btn primary" disabled={submitting()}>
{editing() ? 'Save Changes' : 'Create'}
</button>
</div>
</form>
</div>
</div>
</Show>
</AdminShell>
);
}