- Replace all /api/gateway/* with /api/* to match gateway routing - Fix AdminShell.tsx: update UGC route to singular and fix logout URL - Remove Applications and Responses from sidebar (unused) - Move conflicting route files into folders (company, approval, verification, users, jobs, kb, leads, photographer) as index.tsx to avoid catch-all interference - Upgrade ProfessionAdminListPage to match Department Management UI: • Dark headers with white text • Icons on Sort/Filters/Export buttons • Pagination UI • Improved empty state with Create button • Hover effects and consistent spacing - Update all pages using ProfessionAdminListPage to benefit from new UI - Fix jobs admin endpoint to use /api/admin/companies/jobs with auth - Add authentication headers to jobs and leads fetch calls These changes unify the API architecture and bring a consistent, professional look to all management tables.
261 lines
9.8 KiB
TypeScript
261 lines
9.8 KiB
TypeScript
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
|
|
|
const API = '';
|
|
|
|
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 (
|
|
<>
|
|
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
|
|
|
{/* ── Page header ── */}
|
|
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-xl font-semibold text-gray-900">Module Registry</h1>
|
|
<p class="text-sm text-gray-500 mt-0.5">Manage internal module definitions and activation state.</p>
|
|
</div>
|
|
<button
|
|
class="btn-primary"
|
|
onClick={() => openModal()}
|
|
>
|
|
Add Module
|
|
</button>
|
|
</div>
|
|
|
|
{/* ── Content ── */}
|
|
<div class="p-6">
|
|
<Show when={error() && !isModalOpen()}>
|
|
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
|
|
</Show>
|
|
|
|
<div class="table-card">
|
|
<div class="overflow-x-auto">
|
|
<table class="data-table w-full text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Key</th>
|
|
<th>Description</th>
|
|
<th>Status</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<Show when={modules.loading}>
|
|
<tr><td colspan="5" class="py-10 text-center text-sm text-slate-400">Loading modules…</td></tr>
|
|
</Show>
|
|
<Show when={!modules.loading && (modules() || []).length === 0}>
|
|
<tr><td colspan="5" class="py-10 text-center text-sm text-slate-400">No modules found.</td></tr>
|
|
</Show>
|
|
<For each={modules() || []}>
|
|
{(item) => (
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="font-medium text-gray-900">{item.name}</td>
|
|
<td><code class="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{item.key}</code></td>
|
|
<td class="text-slate-500">{item.description || '—'}</td>
|
|
<td>
|
|
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${item.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-700'}`}>
|
|
{item.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button
|
|
class="rounded-lg border border-gray-200 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
|
onClick={() => openModal(item)}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
class="rounded-lg border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-100 transition-colors"
|
|
onClick={() => removeModule(item.id)}
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Modal ── */}
|
|
<Show when={isModalOpen()}>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
|
<div class="w-full max-w-lg rounded-xl bg-white shadow-xl">
|
|
<div class="border-b border-gray-200 px-6 py-4">
|
|
<h2 class="text-lg font-semibold text-gray-900">{modalTitle()}</h2>
|
|
</div>
|
|
<form onSubmit={submitForm} class="px-6 py-5 space-y-4">
|
|
<div>
|
|
<label class="mb-1.5 block text-sm font-medium text-gray-700">Name *</label>
|
|
<input
|
|
value={form().name}
|
|
onInput={(event) => setForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
|
|
placeholder="e.g. Job Board"
|
|
required
|
|
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="mb-1.5 block text-sm font-medium text-gray-700">Key *</label>
|
|
<input
|
|
value={form().key}
|
|
onInput={(event) => setForm((prev) => ({ ...prev, key: event.currentTarget.value }))}
|
|
placeholder="e.g. manage_jobs"
|
|
required
|
|
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="mb-1.5 block text-sm font-medium text-gray-700">Description</label>
|
|
<textarea
|
|
rows="3"
|
|
value={form().description}
|
|
onInput={(event) => setForm((prev) => ({ ...prev, description: event.currentTarget.value }))}
|
|
placeholder="Short description..."
|
|
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#0a1d37] focus:ring-1 focus:ring-[#0a1d37]"
|
|
/>
|
|
</div>
|
|
<label class="flex items-center gap-2 text-sm text-gray-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={form().isActive}
|
|
onChange={(event) => setForm((prev) => ({ ...prev, isActive: event.currentTarget.checked }))}
|
|
class="rounded"
|
|
/>
|
|
Active
|
|
</label>
|
|
|
|
<Show when={error()}>
|
|
<p class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</p>
|
|
</Show>
|
|
|
|
<div class="flex justify-end gap-3 border-t border-gray-100 pt-4">
|
|
<button
|
|
type="button"
|
|
class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
|
onClick={closeModal}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="btn-primary"
|
|
disabled={submitting()}
|
|
>
|
|
{editing() ? 'Save Changes' : 'Create'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
</>
|
|
);
|
|
}
|