fix: admin panel bugs found in QA audit
All checks were successful
build-and-release / build (push) Successful in 59s
All checks were successful
build-and-release / build (push) Successful in 59s
- users: wire suspend/block buttons to PATCH /api/admin/users/:id/status (previously only mutated local state, reverted on refresh); add load/action error banners instead of silently emptying the table - [...module]: stop rendering a dead localhost:9201 iframe for legacy modules when VITE_LEGACY_ADMIN_ORIGIN isn't configured (it never is in production); show a clear "not available" message instead - dashboard: surface a banner when /api/admin/dashboard/metrics fails instead of silently showing "No Data" on every widget - credit: fix stray extra closing </Show> tag that broke the whole file's JSX parse; restore missing API/authHeaders module helpers dropped in a previous refactor (AI Credits handlers referenced them but they were undefined); replace a dead, broken exportLedgerCsv/ filteredLedger implementation with one matching actual call sites; fix activeTab type/tab keys so the Balance & Ledger and Platform Ledger tabs were actually reachable (they compared against 'balance' /'platform' but the tab buttons only ever set 'ledger') - roles: surface errors when fetching a role's permissions for edit fails, instead of silently swallowing them - runtime-roles: surface fetch/delete errors instead of silently swallowing them or presenting fallback sample data as if real Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
98a41d833a
commit
7ae59fee20
6 changed files with 186 additions and 96 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { A, useParams } from "@solidjs/router";
|
||||
import { createMemo, lazy } from "solid-js";
|
||||
import { Show, createMemo, lazy } from "solid-js";
|
||||
|
||||
const ApprovalManagementPage = lazy(() => import("./approval"));
|
||||
const VerificationManagementPage = lazy(() => import("./verification"));
|
||||
|
|
@ -77,6 +77,9 @@ export default function LegacyModuleShellPage() {
|
|||
const moduleName = createMemo(() => toTitle(modulePath || "Management"));
|
||||
const legacyPath = createMemo(() => resolveLegacyPath(modulePath));
|
||||
const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`);
|
||||
const legacyOriginConfigured = createMemo(
|
||||
() => Boolean(import.meta.env.VITE_LEGACY_ADMIN_ORIGIN) || import.meta.env.DEV
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -85,26 +88,39 @@ export default function LegacyModuleShellPage() {
|
|||
Live legacy module embedded for exact design and functionality parity during migration.
|
||||
</p>
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<div class="actions">
|
||||
<A
|
||||
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
href={legacyUrl()}
|
||||
target="_blank"
|
||||
>
|
||||
Open Module In New Tab
|
||||
</A>
|
||||
</div>
|
||||
<iframe
|
||||
src={legacyUrl()}
|
||||
title={`${moduleName()} (Legacy)`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "72vh",
|
||||
border: "1px solid #e2e8f0",
|
||||
"border-radius": "10px",
|
||||
"margin-top": "10px",
|
||||
}}
|
||||
/>
|
||||
<Show
|
||||
when={legacyOriginConfigured()}
|
||||
fallback={
|
||||
<div class="p-6 text-sm text-gray-600">
|
||||
<p class="font-medium text-gray-900">This module isn’t available yet.</p>
|
||||
<p class="mt-1">
|
||||
No legacy admin service is configured for this environment
|
||||
(VITE_LEGACY_ADMIN_ORIGIN is unset), so “{moduleName()}” can’t be embedded here.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="actions">
|
||||
<A
|
||||
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
href={legacyUrl()}
|
||||
target="_blank"
|
||||
>
|
||||
Open Module In New Tab
|
||||
</A>
|
||||
</div>
|
||||
<iframe
|
||||
src={legacyUrl()}
|
||||
title={`${moduleName()} (Legacy)`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "72vh",
|
||||
border: "1px solid #e2e8f0",
|
||||
"border-radius": "10px",
|
||||
"margin-top": "10px",
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createMemo, createResource, createSignal, Show, For } from 'solid-js';
|
||||
import { createResource, createSignal, Show, For } from 'solid-js';
|
||||
import {
|
||||
api,
|
||||
AdminLedgerEntry,
|
||||
|
|
@ -14,8 +14,22 @@ import {
|
|||
|
||||
type ActiveTab = 'balance' | 'adjust' | 'reconcile' | 'platform';
|
||||
|
||||
const API = '';
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
const token =
|
||||
typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export default function CreditPage() {
|
||||
const [activeTab, setActiveTab] = createSignal<'ledger' | 'adjust' | 'reconcile' | 'ai_credits'>('ledger');
|
||||
const [activeTab, setActiveTab] = createSignal<ActiveTab | 'ai_credits'>('balance');
|
||||
|
||||
// AI Credits sub-tab state
|
||||
const [aiCreditsSubTab, setAiCreditsSubTab] = createSignal<'balance' | 'adjust' | 'reconcile'>('balance');
|
||||
|
|
@ -158,10 +172,11 @@ export default function CreditPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const tabs: { key: 'ledger' | 'adjust' | 'reconcile' | 'ai_credits'; label: string }[] = [
|
||||
{ key: 'ledger', label: 'Balance & Ledger' },
|
||||
const tabs: { key: ActiveTab | 'ai_credits'; label: string }[] = [
|
||||
{ key: 'balance', label: 'Balance & Ledger' },
|
||||
{ key: 'adjust', label: 'Reward / Deduct' },
|
||||
{ key: 'reconcile', label: 'Reconcile' },
|
||||
{ key: 'platform', label: 'Platform Ledger' },
|
||||
{ key: 'ai_credits', label: 'AI Credits' },
|
||||
];
|
||||
|
||||
|
|
@ -253,40 +268,15 @@ export default function CreditPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const filteredLedger = createMemo(() => {
|
||||
let data = ledger();
|
||||
const q = ledgerSearch().toLowerCase().trim();
|
||||
if (q) {
|
||||
data = data.filter((entry) =>
|
||||
String(entry.referenceId || '').toLowerCase().includes(q)
|
||||
|| String(entry.transactionType || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
if (ledgerTypeFilter() !== 'all') {
|
||||
data = data.filter((entry) => entry.transactionType === ledgerTypeFilter());
|
||||
}
|
||||
const sorted = [...data];
|
||||
sorted.sort((a, b) => {
|
||||
const aCreated = new Date(a.createdAt || 0).getTime();
|
||||
const bCreated = new Date(b.createdAt || 0).getTime();
|
||||
const aAmt = Number(a.amount ?? 0);
|
||||
const bAmt = Number(b.amount ?? 0);
|
||||
if (ledgerSortBy() === 'oldest') return aCreated - bCreated;
|
||||
if (ledgerSortBy() === 'amount_desc') return bAmt - aAmt;
|
||||
if (ledgerSortBy() === 'amount_asc') return aAmt - bAmt;
|
||||
return bCreated - aCreated;
|
||||
});
|
||||
return sorted;
|
||||
});
|
||||
|
||||
const exportLedgerCsv = () => {
|
||||
const headers = ['Type', 'Amount', 'Ref ID', 'Expires At', 'Date'];
|
||||
const rows = filteredLedger().map((entry) => [
|
||||
entry.transactionType,
|
||||
`${entry.transactionType === 'ADD' ? '+' : '-'}${entry.amount ?? 0}`,
|
||||
entry.referenceId || '—',
|
||||
entry.expiresAt ? new Date(entry.expiresAt).toLocaleDateString() : '—',
|
||||
entry.createdAt ? new Date(entry.createdAt).toLocaleString() : '—',
|
||||
const exportLedgerCsv = (entries: AdminLedgerEntry[], filename: string) => {
|
||||
const headers = ['Type', 'Amount', 'Balance After', 'Reason', 'Reference', 'Date'];
|
||||
const rows = entries.map((entry) => [
|
||||
entry.entry_type,
|
||||
`${entry.amount > 0 ? '+' : ''}${entry.amount ?? 0}`,
|
||||
entry.balance_after ?? '—',
|
||||
entry.reason || '—',
|
||||
entry.reference_id || '—',
|
||||
entry.created_at ? new Date(entry.created_at).toLocaleString('en-IN') : '—',
|
||||
]);
|
||||
const csv = [headers, ...rows]
|
||||
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
|
|
@ -926,7 +916,6 @@ export default function CreditPage() {
|
|||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -413,6 +413,15 @@ export default function AdminHomePage() {
|
|||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={Boolean(metrics.error)}>
|
||||
<div
|
||||
class="rounded-2xl border border-[#FECACA] bg-[#FEF2F2] px-6 py-4 text-[13px] font-medium text-[#B91C1C] shadow-sm"
|
||||
style="margin-bottom: 18px"
|
||||
>
|
||||
Failed to load dashboard metrics. Widgets below may show stale or missing data — try
|
||||
refreshing the page.
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="rounded-2xl border border-[#E5E7EB] bg-white px-6 py-5 shadow-sm md:px-8"
|
||||
style="margin-bottom: 28px"
|
||||
|
|
|
|||
|
|
@ -420,16 +420,22 @@ export default function RoleManagementPage() {
|
|||
setFormTab("general");
|
||||
setView("form");
|
||||
setOpenMenuId(null);
|
||||
// Fetch permission_keys for this role
|
||||
setFormError("");
|
||||
fetch(`${API}/api/admin/roles/${row.id}`)
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error("Failed to fetch role permissions");
|
||||
return r.json();
|
||||
})
|
||||
.then((detail) => {
|
||||
if (Array.isArray(detail?.permission_keys)) {
|
||||
const keys = (detail.permission_keys as any[]).map((k) => String(k));
|
||||
setSelectedPermissions(new Set<string>(keys));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setFormError("Failed to load existing permissions for this role. They may be incomplete.");
|
||||
});
|
||||
};
|
||||
|
||||
const openDetail = async (row: RoleRecord) => {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ const FALLBACK: ExternalRole[] = [
|
|||
{ id: 'r5', roleKey: 'MAKEUP_ARTIST', displayName: 'Makeup Artist', vertical: 'Professional', enabledModules: ['Portfolio', 'Leads', 'Reviews'], onboardingSchemaId: 'schema-professional', isActive: false },
|
||||
];
|
||||
|
||||
async function fetchExternalRoles(): Promise<ExternalRole[]> {
|
||||
async function fetchExternalRoles(): Promise<{ roles: ExternalRole[]; error: string }> {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/roles?audience=EXTERNAL`);
|
||||
if (!res.ok) throw new Error();
|
||||
if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
|
||||
const data = await res.json();
|
||||
const rows = (Array.isArray(data) ? data : (data.roles ?? []))
|
||||
.filter((r: any) => String(r?.audience || '').toUpperCase() === 'EXTERNAL');
|
||||
|
|
@ -41,9 +41,13 @@ async function fetchExternalRoles(): Promise<ExternalRole[]> {
|
|||
isActive: r.is_active !== false,
|
||||
};
|
||||
});
|
||||
return list.length > 0 ? list : FALLBACK;
|
||||
} catch {
|
||||
return FALLBACK;
|
||||
return { roles: list, error: '' };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
roles: FALLBACK,
|
||||
error: 'Failed to load external roles from the server. Showing sample data — changes here will not be saved.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,30 +61,38 @@ function StatusBadge(props: { active: boolean }) {
|
|||
}
|
||||
|
||||
export default function ExternalRolesPage() {
|
||||
const [roles, { refetch }] = createResource(fetchExternalRoles);
|
||||
const [result, { refetch }] = createResource(fetchExternalRoles);
|
||||
const roles = () => result()?.roles ?? [];
|
||||
const roleFetchLoading = () => result.loading;
|
||||
const loadError = () => result()?.error ?? '';
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [openMenu, setOpenMenu] = createSignal('');
|
||||
const [deleting, setDeleting] = createSignal('');
|
||||
const [deleteError, setDeleteError] = createSignal('');
|
||||
|
||||
const filtered = () => {
|
||||
const q = search().toLowerCase();
|
||||
return (roles() ?? []).filter(r =>
|
||||
return roles().filter(r =>
|
||||
!q || r.displayName.toLowerCase().includes(q) || r.roleKey.toLowerCase().includes(q) || r.vertical.toLowerCase().includes(q)
|
||||
);
|
||||
};
|
||||
|
||||
const stats = () => {
|
||||
const list = roles() ?? [];
|
||||
const list = roles();
|
||||
return { total: list.length, active: list.filter(r => r.isActive).length, inactive: list.filter(r => !r.isActive).length };
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string, name: string) => {
|
||||
if (!confirm(`Delete external role "${name}"? This cannot be undone.`)) { setOpenMenu(''); return; }
|
||||
setDeleting(id); setOpenMenu('');
|
||||
setDeleting(id); setOpenMenu(''); setDeleteError('');
|
||||
try {
|
||||
await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' });
|
||||
const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
|
||||
refetch();
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setDeleteError(`Failed to delete "${name}". Please try again.`);
|
||||
}
|
||||
finally { setDeleting(''); }
|
||||
};
|
||||
|
||||
|
|
@ -103,13 +115,24 @@ export default function ExternalRolesPage() {
|
|||
</A>
|
||||
</div>
|
||||
|
||||
<Show when={loadError()}>
|
||||
<div class="rounded-xl border border-[#FECACA] bg-[#FEF2F2] px-4 py-3 text-[13px] font-medium text-[#B91C1C]">
|
||||
{loadError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={deleteError()}>
|
||||
<div class="rounded-xl border border-[#FECACA] bg-[#FEF2F2] px-4 py-3 text-[13px] font-medium text-[#B91C1C]">
|
||||
{deleteError()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Stat cards */}
|
||||
<div class="grid grid-cols-2 gap-5 lg:grid-cols-4">
|
||||
{([
|
||||
{ label: 'Total Roles', value: () => stats().total, Icon: Globe, bg: 'bg-[#FFF1EB]', color: 'text-[#FF5E13]' },
|
||||
{ label: 'Active Roles', value: () => stats().active, Icon: ShieldCheck, bg: 'bg-[#ECFDF5]', color: 'text-[#059669]' },
|
||||
{ label: 'Inactive Roles', value: () => stats().inactive, Icon: ShieldOff, bg: 'bg-[#FEF2F2]', color: 'text-[#DC2626]' },
|
||||
{ label: 'Total Modules', value: () => (roles() ?? []).reduce((a, r) => a + r.enabledModules.length, 0), Icon: Layers, bg: 'bg-[#EFF6FF]', color: 'text-[#2563EB]' },
|
||||
{ label: 'Total Modules', value: () => roles().reduce((a, r) => a + r.enabledModules.length, 0), Icon: Layers, bg: 'bg-[#EFF6FF]', color: 'text-[#2563EB]' },
|
||||
] as const).map(s => (
|
||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
|
||||
<div class="flex items-start justify-between">
|
||||
|
|
@ -158,7 +181,7 @@ export default function ExternalRolesPage() {
|
|||
</thead>
|
||||
<tbody class="divide-y divide-[#F3F4F6]">
|
||||
|
||||
<Show when={roles.loading}>
|
||||
<Show when={roleFetchLoading()}>
|
||||
<For each={[1, 2, 3]}>
|
||||
{() => (
|
||||
<tr class="animate-pulse">
|
||||
|
|
@ -173,7 +196,7 @@ export default function ExternalRolesPage() {
|
|||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={!roles.loading && filtered().length === 0}>
|
||||
<Show when={!roleFetchLoading() && filtered().length === 0}>
|
||||
<tr>
|
||||
<td colspan="6" class="px-5 py-16 text-center">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -91,19 +91,26 @@ export default function UsersManagementPage() {
|
|||
const [filterOpen, setFilterOpen] = createSignal(false);
|
||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||
const [selectedUser, setSelectedUser] = createSignal<ExternalUserRecord | null>(null);
|
||||
const [loadError, setLoadError] = createSignal("");
|
||||
const [actionError, setActionError] = createSignal("");
|
||||
|
||||
const currentUser = createMemo<ExternalUserRecord | null>(() => selectedUser());
|
||||
|
||||
const authHeaders = (): Record<string, string> => {
|
||||
const accessToken =
|
||||
typeof sessionStorage !== "undefined"
|
||||
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||
: "";
|
||||
return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const accessToken =
|
||||
typeof sessionStorage !== "undefined"
|
||||
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||
: "";
|
||||
setLoadError("");
|
||||
const r = await fetch("/api/admin/users", {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
...authHeaders(),
|
||||
},
|
||||
credentials: "include",
|
||||
});
|
||||
|
|
@ -149,6 +156,7 @@ export default function UsersManagementPage() {
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
setRows([]);
|
||||
setLoadError("Failed to load users. Please refresh or try again.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -273,22 +281,46 @@ export default function UsersManagementPage() {
|
|||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const toggleSuspend = (user: ExternalUserRecord) => {
|
||||
const nextStatus = user.accountStatus === "SUSPENDED" ? "ACTIVE" : "SUSPENDED";
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: nextStatus } : r))
|
||||
);
|
||||
if (selectedUser()?.id === user.id) setSelectedUser({ ...user, accountStatus: nextStatus });
|
||||
const applyAccountStatus = async (user: ExternalUserRecord, nextStatus: string) => {
|
||||
const previousStatus = user.accountStatus;
|
||||
setActionError("");
|
||||
setOpenMenuId(null);
|
||||
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: nextStatus as any } : r))
|
||||
);
|
||||
if (selectedUser()?.id === user.id)
|
||||
setSelectedUser({ ...user, accountStatus: nextStatus as any });
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/admin/users/${user.id}/status`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...authHeaders(),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ status: nextStatus }),
|
||||
});
|
||||
if (!r.ok) throw new Error("Failed to update user status");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: previousStatus } : r))
|
||||
);
|
||||
if (selectedUser()?.id === user.id)
|
||||
setSelectedUser({ ...user, accountStatus: previousStatus });
|
||||
setActionError(`Failed to update status for ${user.name}. Please try again.`);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSuspend = (user: ExternalUserRecord) => {
|
||||
void applyAccountStatus(user, user.accountStatus === "SUSPENDED" ? "ACTIVE" : "SUSPENDED");
|
||||
};
|
||||
|
||||
const toggleBlock = (user: ExternalUserRecord) => {
|
||||
const nextStatus = user.accountStatus === "BLOCKED" ? "ACTIVE" : "BLOCKED";
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: nextStatus } : r))
|
||||
);
|
||||
if (selectedUser()?.id === user.id) setSelectedUser({ ...user, accountStatus: nextStatus });
|
||||
setOpenMenuId(null);
|
||||
void applyAccountStatus(user, user.accountStatus === "BLOCKED" ? "ACTIVE" : "BLOCKED");
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -301,6 +333,17 @@ export default function UsersManagementPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={loadError()}>
|
||||
<div style="margin-bottom:16px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;color:#B91C1C;padding:12px 16px;font-size:13px;font-weight:500">
|
||||
{loadError()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={actionError()}>
|
||||
<div style="margin-bottom:16px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;color:#B91C1C;padding:12px 16px;font-size:13px;font-weight:500">
|
||||
{actionError()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={listTab() !== "view"}>
|
||||
<div style="display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px">
|
||||
{[
|
||||
|
|
@ -839,9 +882,13 @@ export default function UsersManagementPage() {
|
|||
fallback={
|
||||
<tr>
|
||||
<td colSpan={10} style="padding:32px;text-align:center">
|
||||
<p style="font-size:15px;font-weight:600;color:#111827">No users found</p>
|
||||
<p style="font-size:15px;font-weight:600;color:#111827">
|
||||
{loadError() ? "Unable to load users" : "No users found"}
|
||||
</p>
|
||||
<p style="margin-top:6px;font-size:13px;color:#6B7280">
|
||||
Try changing filters or search.
|
||||
{loadError()
|
||||
? "There was a problem contacting the server. Try refreshing the page."
|
||||
: "Try changing filters or search."}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue