nxtgauge-admin-solid/src/routes/admin/users/[id]/edit.tsx
Ashwin Kumar 0ec64be905 feat: unify API paths and upgrade table UIs
- 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.
2026-04-07 22:12:52 +02:00

183 lines
7 KiB
TypeScript

import { A, useNavigate, useParams } from '@solidjs/router';
import { createMemo, createResource, createSignal, Show } from 'solid-js';
const API = '';
type Role = {
id: string;
name: string;
};
type User = {
id: string;
name?: string;
full_name?: string;
email: string;
roleId?: string;
role_id?: string;
role?: Role;
status?: 'ACTIVE' | 'INACTIVE' | 'PENDING';
createdAt?: string;
created_at?: string;
};
async function fetchRoles(): Promise<Role[]> {
try {
const res = await fetch(`${API}/api/admin/roles?audience=INTERNAL`);
if (!res.ok) return [];
const data = await res.json();
const rows = Array.isArray(data) ? data : (data.roles || []);
return rows.map((r: any) => ({ id: r.id, name: r.name }));
} catch {
return [];
}
}
async function fetchUser(id: string): Promise<User | null> {
try {
const adminRes = await fetch(`${API}/api/admin/users/${id}`);
if (adminRes.ok) return adminRes.json();
const fallback = await fetch(`${API}/api/users/${id}`);
if (!fallback.ok) return null;
return fallback.json();
} catch {
return null;
}
}
export default function EditUserPage() {
const navigate = useNavigate();
const params = useParams();
const [user] = createResource(() => params.id, fetchUser);
const [roles] = createResource(fetchRoles);
const [name, setName] = createSignal('');
const [email, setEmail] = createSignal('');
const [roleId, setRoleId] = createSignal('');
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE' | 'PENDING'>('ACTIVE');
const [submitting, setSubmitting] = createSignal(false);
const [error, setError] = createSignal('');
createMemo(() => {
const u = user();
if (!u) return null;
setName(u.name || u.full_name || '');
setEmail(u.email || '');
setRoleId(u.roleId || u.role_id || u.role?.id || '');
setStatus((u.status || 'ACTIVE').toUpperCase() as 'ACTIVE' | 'INACTIVE' | 'PENDING');
return null;
});
const save = async () => {
if (!name().trim() || !email().trim() || !roleId()) {
setError('Please fill in name, email, and role.');
return;
}
try {
setSubmitting(true);
setError('');
const body = {
name: name().trim(),
email: email().trim(),
roleId: roleId(),
status: status().toLowerCase(),
};
let res = await fetch(`${API}/api/admin/users/${params.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
res = await fetch(`${API}/api/users/${params.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
if (!res.ok) {
const payload = await res.json().catch(() => ({}));
throw new Error(payload.message || 'Failed to update user');
}
navigate('/admin/users');
} catch (err: any) {
setError(err.message || 'Failed to update user');
} finally {
setSubmitting(false);
}
};
return (
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<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">Edit User</h1>
<p class="text-sm text-gray-500 mt-0.5">Update user profile, role assignment, and account status.</p>
</div>
<div class="flex items-center gap-2">
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/details/${params.id}`}>View Details</A>
<A class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/users">Back to Users</A>
</div>
</div>
<div class="p-6">
<Show when={error()}>
<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>
<Show when={user.loading}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading user...</p></div>
</Show>
<Show when={!user.loading && !user()}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">User not found.</p></div>
</Show>
<Show when={user()}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6 max-w-3xl">
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Full Name</label>
<input 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]" value={name()} onInput={(e) => setName(e.currentTarget.value)} />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Email</label>
<input type="email" 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]" value={email()} onInput={(e) => setEmail(e.currentTarget.value)} />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Role</label>
<select 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]" value={roleId()} onChange={(e) => setRoleId(e.currentTarget.value)}>
<option value="">Select role</option>
<Show when={!roles.loading}>
{roles()?.map((r) => (
<option value={r.id}>{r.name}</option>
))}
</Show>
</select>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Status</label>
<select 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]" value={status()} onChange={(e) => setStatus(e.currentTarget.value as 'ACTIVE' | 'INACTIVE' | 'PENDING')}>
<option value="ACTIVE">Active</option>
<option value="PENDING">Pending</option>
<option value="INACTIVE">Inactive</option>
</select>
</div>
</div>
<div class="mt-6 flex justify-end gap-3 border-t border-gray-100 pt-5">
<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" type="button" onClick={() => navigate('/admin/users')}>Cancel</button>
<button class="btn-primary" type="button" onClick={save} disabled={submitting()}>
{submitting() ? 'Saving…' : 'Save Changes'}
</button>
</div>
</section>
</Show>
</div>
</div>
);
}