Some checks failed
build-and-release / build (push) Failing after 30s
- Add RoleWizard inline to SwitchServicesPage after successful role registration — same wizard flow as ProfilePage, no separate step - Pre-fill identity documents (Aadhaar, PAN, selfie, etc.) from the user's active role profile so they don't re-upload on second role - Add identity_shared flag to RuntimeOnboardingField for admin- configurable per-field opt-in; CONVENTION_IDENTITY_FIELD_IDS covers common IDs without schema changes - Extract roleKeyToPrefix() into src/lib/role-utils.ts (shared util) - Fix solid/reactivity: snapshot portfolioForm()/form()/docUrls() synchronously before the first await in handleSubmit - Reuse badge (♻ purple) distinguishes pre-filled docs from new uploads; Change link clears the pre-fill and lets user re-upload - Pending roles now show 'Under Review' status badge instead of Switch - ArrowLeft back-button in header exits wizard without full page reload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1073 lines
50 KiB
TypeScript
1073 lines
50 KiB
TypeScript
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
||
|
||
type RegisteredRole = {
|
||
key: string;
|
||
name: string;
|
||
status: "ACTIVE" | "PENDING" | "INACTIVE";
|
||
registeredOn?: string;
|
||
};
|
||
|
||
type ExternalUserRecord = {
|
||
id: string;
|
||
userCode: string;
|
||
name: string;
|
||
email: string;
|
||
phone?: string;
|
||
location: string;
|
||
joinedOn: string;
|
||
lastActive?: string;
|
||
userType: "CUSTOMER" | "PROFESSIONAL" | "COMPANY" | "JOBSEEKER" | "NONE";
|
||
accountStatus: "ACTIVE" | "INACTIVE" | "SUSPENDED" | "BLOCKED";
|
||
verificationStatus:
|
||
| "UNVERIFIED"
|
||
| "PENDING"
|
||
| "IN_REVIEW"
|
||
| "VERIFIED"
|
||
| "REJECTED"
|
||
| "RE_UPLOAD_REQUESTED";
|
||
onboardingStatus: "NOT_STARTED" | "IN_PROGRESS" | "SUBMITTED" | "COMPLETED";
|
||
registeredRoles: RegisteredRole[];
|
||
portfolioCount: number;
|
||
notes?: string;
|
||
updatedAt: string;
|
||
};
|
||
|
||
function StatusBadge(props: { status: string }) {
|
||
const active = () =>
|
||
props.status === "ACTIVE" || props.status === "VERIFIED" || props.status === "COMPLETED";
|
||
const pending = () =>
|
||
props.status === "PENDING" ||
|
||
props.status === "IN_REVIEW" ||
|
||
props.status === "SUBMITTED" ||
|
||
props.status === "IN_PROGRESS";
|
||
const suspended = () =>
|
||
props.status === "SUSPENDED" || props.status === "REJECTED" || props.status === "BLOCKED";
|
||
|
||
return (
|
||
<span
|
||
style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? "#B7E4C7" : pending() ? "#FDE68A" : suspended() ? "#FECACA" : "#D1D5DB"};background:${active() ? "#DEF7E8" : pending() ? "#FFFBEB" : suspended() ? "#FEF2F2" : "#F3F4F6"};color:${active() ? "#166534" : pending() ? "#92400E" : suspended() ? "#B91C1C" : "#4B5563"};padding:2px 10px;font-size:12px;font-weight:600`}
|
||
>
|
||
<span
|
||
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? "#16A34A" : pending() ? "#D97706" : suspended() ? "#DC2626" : "#9CA3AF"};margin-right:5px;flex-shrink:0`}
|
||
/>
|
||
{props.status
|
||
.split("_")
|
||
.map((w) => w.charAt(0) + w.slice(1).toLowerCase())
|
||
.join(" ")}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function RoleChip(props: { role: RegisteredRole }) {
|
||
return (
|
||
<span
|
||
style={`display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:8px;font-size:11px;font-weight:600;border:1px solid #E5E7EB;background:${props.role.status === "ACTIVE" ? "#EEF2FF" : props.role.status === "PENDING" ? "#FFF7ED" : "#F3F4F6"};color:${props.role.status === "ACTIVE" ? "#3730A3" : props.role.status === "PENDING" ? "#C2410C" : "#6B7280"}`}
|
||
>
|
||
{props.role.name}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export default function UsersManagementPage() {
|
||
const [rows, setRows] = createSignal<ExternalUserRecord[]>([]);
|
||
const [listTab, setListTab] = createSignal<
|
||
"all" | "no_role" | "registered" | "pending" | "approved" | "suspended" | "view"
|
||
>("all");
|
||
const [detailTab, setDetailTab] = createSignal<
|
||
"overview" | "personal" | "roles" | "portfolio" | "verification" | "activity" | "notes"
|
||
>("overview");
|
||
|
||
const [search, setSearch] = createSignal("");
|
||
const [sortBy, setSortBy] = createSignal<"newest" | "oldest" | "name_asc" | "name_desc">(
|
||
"newest"
|
||
);
|
||
const [statusFilter, setStatusFilter] = createSignal<
|
||
"all" | "active" | "pending" | "suspended" | "blocked"
|
||
>("all");
|
||
const [roleFilter, setRoleFilter] = createSignal<
|
||
"all" | "no_role" | "professional" | "company" | "jobseeker" | "customer"
|
||
>("all");
|
||
const [sortOpen, setSortOpen] = createSignal(false);
|
||
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 {
|
||
setLoadError("");
|
||
const r = await fetch("/api/admin/users", {
|
||
headers: {
|
||
Accept: "application/json",
|
||
...authHeaders(),
|
||
},
|
||
credentials: "include",
|
||
});
|
||
if (!r.ok) throw new Error("Failed to fetch users");
|
||
const data = await r.json();
|
||
|
||
const mapped: ExternalUserRecord[] = (Array.isArray(data) ? data : []).map((u: any) => ({
|
||
id: u.id,
|
||
userCode: u.reference_number || String(u.id).slice(0, 8).toUpperCase(),
|
||
name:
|
||
u.first_name || u.last_name
|
||
? `${u.first_name || ""} ${u.last_name || ""}`.trim()
|
||
: u.email.split("@")[0],
|
||
email: u.email,
|
||
location: "Not Specified",
|
||
joinedOn: u.created_at ? u.created_at.slice(0, 10) : "—",
|
||
userType:
|
||
(u.roles || []).length === 0
|
||
? "NONE"
|
||
: u.roles.includes("COMPANY")
|
||
? "COMPANY"
|
||
: u.roles.includes("JOBSEEKER")
|
||
? "JOBSEEKER"
|
||
: u.roles.includes("CUSTOMER")
|
||
? "CUSTOMER"
|
||
: "PROFESSIONAL",
|
||
accountStatus: (u.status || "ACTIVE").toUpperCase(),
|
||
verificationStatus: "VERIFIED",
|
||
onboardingStatus: "COMPLETED",
|
||
registeredRoles: (u.roles || []).map((r: string) => ({
|
||
key: r.toLowerCase(),
|
||
name: r
|
||
.split("_")
|
||
.map((w) => w.charAt(0) + w.slice(1).toLowerCase())
|
||
.join(" "),
|
||
status: "ACTIVE",
|
||
})),
|
||
portfolioCount: 0,
|
||
updatedAt: u.updated_at || u.created_at || "—",
|
||
}));
|
||
|
||
setRows(mapped);
|
||
} catch (e) {
|
||
console.error(e);
|
||
setRows([]);
|
||
setLoadError("Failed to load users. Please refresh or try again.");
|
||
}
|
||
};
|
||
|
||
onMount(() => void load());
|
||
|
||
const metrics = createMemo(() => {
|
||
const all = rows();
|
||
const noRoleUsers = all.filter((u) => u.registeredRoles.length === 0);
|
||
const activeRoleUsers = all.filter((u) => u.registeredRoles.some((r) => r.status === "ACTIVE"));
|
||
const pendingRoles = all.filter((u) => u.registeredRoles.some((r) => r.status === "PENDING"));
|
||
const suspended = all.filter((u) => u.accountStatus === "SUSPENDED");
|
||
const newThisMonth = all.filter((u) => String(u.joinedOn || "").startsWith("2026-04")).length;
|
||
return {
|
||
totalUsers: all.length,
|
||
noRoleUsers: noRoleUsers.length,
|
||
activeRoleUsers: activeRoleUsers.length,
|
||
pendingRoles: pendingRoles.length,
|
||
suspended: suspended.length,
|
||
newThisMonth,
|
||
};
|
||
});
|
||
|
||
const INTERNAL_ROLES = ["SUPER_ADMIN", "ADMIN", "SUPPORT"];
|
||
|
||
const scopedRows = createMemo(() => {
|
||
let list = rows().filter((u) => {
|
||
const hasExternalRole = u.registeredRoles.some(
|
||
(r) => !INTERNAL_ROLES.includes(r.key.toUpperCase())
|
||
);
|
||
const hasNoRole = u.registeredRoles.length === 0;
|
||
return hasExternalRole || hasNoRole;
|
||
});
|
||
|
||
if (listTab() === "no_role") list = list.filter((u) => u.registeredRoles.length === 0);
|
||
if (listTab() === "registered") list = list.filter((u) => u.registeredRoles.length > 0);
|
||
if (listTab() === "pending")
|
||
list = list.filter(
|
||
(u) =>
|
||
u.registeredRoles.some((r) => r.status === "PENDING") ||
|
||
u.verificationStatus === "PENDING" ||
|
||
u.verificationStatus === "IN_REVIEW"
|
||
);
|
||
if (listTab() === "approved") list = list.filter((u) => u.verificationStatus === "VERIFIED");
|
||
if (listTab() === "suspended") list = list.filter((u) => u.accountStatus === "SUSPENDED");
|
||
|
||
if (statusFilter() === "active") list = list.filter((u) => u.accountStatus === "ACTIVE");
|
||
if (statusFilter() === "pending")
|
||
list = list.filter(
|
||
(u) => u.verificationStatus === "PENDING" || u.verificationStatus === "IN_REVIEW"
|
||
);
|
||
if (statusFilter() === "suspended") list = list.filter((u) => u.accountStatus === "SUSPENDED");
|
||
if (statusFilter() === "blocked") list = list.filter((u) => u.accountStatus === "BLOCKED");
|
||
|
||
if (roleFilter() === "no_role") list = list.filter((u) => u.registeredRoles.length === 0);
|
||
if (roleFilter() === "professional") list = list.filter((u) => u.userType === "PROFESSIONAL");
|
||
if (roleFilter() === "company") list = list.filter((u) => u.userType === "COMPANY");
|
||
if (roleFilter() === "jobseeker") list = list.filter((u) => u.userType === "JOBSEEKER");
|
||
if (roleFilter() === "customer") list = list.filter((u) => u.userType === "CUSTOMER");
|
||
|
||
const q = search().trim().toLowerCase();
|
||
if (q) {
|
||
list = list.filter((u) =>
|
||
[u.userCode, u.name, u.email, u.location].some((v) =>
|
||
String(v || "")
|
||
.toLowerCase()
|
||
.includes(q)
|
||
)
|
||
);
|
||
}
|
||
|
||
const sorted = [...list];
|
||
sorted.sort((a, b) => {
|
||
if (sortBy() === "name_asc") return a.name.localeCompare(b.name);
|
||
if (sortBy() === "name_desc") return b.name.localeCompare(a.name);
|
||
if (sortBy() === "oldest") return String(a.joinedOn).localeCompare(String(b.joinedOn));
|
||
return String(b.joinedOn).localeCompare(String(a.joinedOn));
|
||
});
|
||
|
||
return sorted;
|
||
});
|
||
|
||
const exportCsv = () => {
|
||
const headers = [
|
||
"User ID",
|
||
"Name",
|
||
"Email",
|
||
"Type",
|
||
"Registered Roles",
|
||
"Role Count",
|
||
"Status",
|
||
"Joined On",
|
||
];
|
||
const lines = scopedRows().map((u) => [
|
||
u.userCode,
|
||
u.name,
|
||
u.email,
|
||
u.userType,
|
||
u.registeredRoles.map((r) => r.name).join(" | ") || "No Role Assigned",
|
||
String(u.registeredRoles.length),
|
||
u.accountStatus,
|
||
u.joinedOn,
|
||
]);
|
||
|
||
const csv = [headers, ...lines]
|
||
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","))
|
||
.join("\n");
|
||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement("a");
|
||
link.href = url;
|
||
link.download = `users-management-${new Date().toISOString().slice(0, 10)}.csv`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const openView = (user: ExternalUserRecord) => {
|
||
setSelectedUser(user);
|
||
setDetailTab("overview");
|
||
setListTab("view");
|
||
setOpenMenuId(null);
|
||
};
|
||
|
||
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) => {
|
||
void applyAccountStatus(user, user.accountStatus === "BLOCKED" ? "ACTIVE" : "BLOCKED");
|
||
};
|
||
|
||
return (
|
||
<div class="w-full space-y-6 pb-8">
|
||
<div style="margin-bottom:1.5rem">
|
||
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Users Management</h1>
|
||
<p class="mt-1 text-[14px] text-[#6B7280]">
|
||
Manage all external user accounts, monitor registered roles, and review complete user
|
||
activity.
|
||
</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">
|
||
<For each={[
|
||
{ label: "Total Users", value: metrics().totalUsers, accent: "#111827" },
|
||
{ label: "No Role Users", value: metrics().noRoleUsers, accent: "#111827" },
|
||
{ label: "Active Role Users", value: metrics().activeRoleUsers, accent: "#111827" },
|
||
{ label: "Pending Roles", value: metrics().pendingRoles, accent: "#111827" },
|
||
{ label: "Suspended", value: metrics().suspended, accent: "#111827" },
|
||
{ label: "New This Month", value: metrics().newThisMonth, accent: "#111827" },
|
||
]}>{(card) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:14px;background:white;padding:16px 18px;min-height:100px">
|
||
<p style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#667085">
|
||
{card.label}
|
||
</p>
|
||
<p
|
||
style={`margin-top:8px;font-size:40px;line-height:1;font-weight:700;color:${card.accent}`}
|
||
>
|
||
{card.value}
|
||
</p>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</Show>
|
||
|
||
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
|
||
<For each={[
|
||
{ key: "all", label: `All Users (${rows().length})` },
|
||
{ key: "no_role", label: `No Role Users (${metrics().noRoleUsers})` },
|
||
{ key: "registered", label: `Registered Role Users (${metrics().activeRoleUsers})` },
|
||
{ key: "pending", label: `Pending Users (${metrics().pendingRoles})` },
|
||
{ key: "approved", label: "Approved Users" },
|
||
{ key: "suspended", label: `Suspended (${metrics().suspended})` },
|
||
{ key: "view", label: "View User" },
|
||
] as const}>{(tab) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setListTab(tab.key)}
|
||
style={`height:44px;padding:0 2px;white-space:nowrap;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13" : "color:#6B7280"}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
)}</For>
|
||
</div>
|
||
|
||
<Show when={listTab() === "view"}>
|
||
<Show when={!selectedUser()}>
|
||
<div style="margin-top:18px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
||
<p style="font-size:15px;font-weight:600;color:#111827">No user selected</p>
|
||
<p style="margin-top:6px;font-size:13px;color:#6B7280">
|
||
Open actions menu in table and click <strong>View User</strong>.
|
||
</p>
|
||
</div>
|
||
</Show>
|
||
<Show when={selectedUser()}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:16px;background:white;overflow:hidden">
|
||
<div style="padding:18px 22px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;gap:16px">
|
||
<div>
|
||
<h2 style="font-size:22px;font-weight:700;color:#111827">
|
||
User Profile: {currentUser()?.name}
|
||
</h2>
|
||
<p style="font-size:14px;color:#6B7280;margin-top:4px">
|
||
View and manage account information, registered roles, and activity history.
|
||
</p>
|
||
</div>
|
||
<div style="display:flex;gap:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleSuspend(currentUser()!)}
|
||
style="height:40px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
|
||
>
|
||
{currentUser()?.accountStatus === "SUSPENDED" ? "Activate" : "Suspend"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleBlock(currentUser()!)}
|
||
style="height:40px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
|
||
>
|
||
{currentUser()?.accountStatus === "BLOCKED" ? "Unblock" : "Block"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
style="height:40px;border-radius:10px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer"
|
||
>
|
||
Edit Profile
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="padding:18px 22px;display:grid;grid-template-columns:2fr 1fr;gap:14px;border-bottom:1px solid #E5E7EB">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:14px;display:grid;grid-template-columns:120px 1fr 1fr 1fr;gap:14px;align-items:center">
|
||
<div style="width:110px;height:110px;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#94A3B8">
|
||
{currentUser()?.name.charAt(0)}
|
||
</div>
|
||
<div>
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
User ID
|
||
</p>
|
||
<p style="font-size:20px;font-weight:700;color:#111827">
|
||
{currentUser()?.userCode}
|
||
</p>
|
||
<p style="margin-top:8px;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
Account Status
|
||
</p>
|
||
<StatusBadge status={currentUser()?.accountStatus || "ACTIVE"} />
|
||
</div>
|
||
<div>
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
Email Address
|
||
</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">{currentUser()?.email}</p>
|
||
<p style="margin-top:8px;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
Joined Date
|
||
</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.joinedOn}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
Phone
|
||
</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.phone || "—"}
|
||
</p>
|
||
<p style="margin-top:8px;font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.06em">
|
||
Location
|
||
</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.location}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:12px">
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase">Total Roles</p>
|
||
<p style="font-size:30px;font-weight:700;color:#111827">
|
||
{currentUser()?.registeredRoles.length || 0}
|
||
</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:12px">
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase">Active Roles</p>
|
||
<p style="font-size:30px;font-weight:700;color:#FF5E13">
|
||
{currentUser()?.registeredRoles.filter((r) => r.status === "ACTIVE").length ||
|
||
0}
|
||
</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:12px">
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase">Pending</p>
|
||
<p style="font-size:30px;font-weight:700;color:#111827">
|
||
{currentUser()?.registeredRoles.filter((r) => r.status === "PENDING").length ||
|
||
0}
|
||
</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:12px">
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase">Portfolio</p>
|
||
<p style="font-size:30px;font-weight:700;color:#111827">
|
||
{currentUser()?.portfolioCount || 0}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;gap:22px;min-height:44px;border-bottom:1px solid #E5E7EB;padding:0 22px;overflow:auto">
|
||
<For each={[
|
||
{ key: "overview", label: "Overview" },
|
||
{ key: "personal", label: "Personal Details" },
|
||
{ key: "roles", label: "Registered Roles" },
|
||
{ key: "portfolio", label: "Portfolio" },
|
||
{ key: "verification", label: "Verification & Approval" },
|
||
{ key: "activity", label: "Activity" },
|
||
{ key: "notes", label: "Notes" },
|
||
] as const}>{(tab) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setDetailTab(tab.key)}
|
||
style={`height:44px;padding:0 2px;white-space:nowrap;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${detailTab() === tab.key ? "color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13" : "color:#6B7280"}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
)}</For>
|
||
</div>
|
||
|
||
<div style="padding:22px">
|
||
<Show when={detailTab() === "overview"}>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<p style="font-size:18px;font-weight:700;color:#111827">Personal Summary</p>
|
||
<div style="margin-top:14px;display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;justify-content:space-between">
|
||
<span style="font-size:13px;color:#6B7280">Full Name</span>
|
||
<span style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.name}
|
||
</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between">
|
||
<span style="font-size:13px;color:#6B7280">User Type</span>
|
||
<span style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.userType}
|
||
</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between">
|
||
<span style="font-size:13px;color:#6B7280">Last Active</span>
|
||
<span style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.lastActive || "—"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<p style="font-size:18px;font-weight:700;color:#111827">Registered Roles</p>
|
||
<Show
|
||
when={(currentUser()?.registeredRoles.length || 0) > 0}
|
||
fallback={
|
||
<p style="margin-top:14px;font-size:14px;color:#6B7280">
|
||
No role registered yet.
|
||
</p>
|
||
}
|
||
>
|
||
<div style="margin-top:14px;display:flex;flex-wrap:wrap;gap:8px">
|
||
<For each={currentUser()?.registeredRoles}>
|
||
{(role) => <RoleChip role={role} />}
|
||
</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "personal"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px;display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||
<div>
|
||
<p style="font-size:12px;color:#9CA3AF">Name</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.name}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p style="font-size:12px;color:#9CA3AF">Email</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.email}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p style="font-size:12px;color:#9CA3AF">Phone</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.phone || "—"}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p style="font-size:12px;color:#9CA3AF">Location</p>
|
||
<p style="font-size:14px;font-weight:600;color:#111827">
|
||
{currentUser()?.location}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "roles"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<Show
|
||
when={(currentUser()?.registeredRoles.length || 0) > 0}
|
||
fallback={
|
||
<p style="font-size:14px;color:#6B7280">No role registrations yet.</p>
|
||
}
|
||
>
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px">
|
||
<For each={currentUser()?.registeredRoles}>
|
||
{(role) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;padding:12px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="font-size:15px;font-weight:700;color:#111827">
|
||
{role.name}
|
||
</p>
|
||
<StatusBadge status={role.status} />
|
||
</div>
|
||
<p style="margin-top:6px;font-size:12px;color:#6B7280">
|
||
Registered on {role.registeredOn || "—"}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "portfolio"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<p style="font-size:14px;color:#111827;font-weight:600">
|
||
Portfolio assets submitted: {currentUser()?.portfolioCount || 0}
|
||
</p>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "verification"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px;display:flex;gap:12px;align-items:center">
|
||
<StatusBadge status={currentUser()?.verificationStatus || "VERIFIED"} />
|
||
<span style="font-size:14px;color:#374151">
|
||
Onboarding:{" "}
|
||
{currentUser()?.onboardingStatus.split("_").join(" ") || "COMPLETED"}
|
||
</span>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "activity"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<p style="font-size:14px;color:#111827;font-weight:600">
|
||
Last Active: {currentUser()?.lastActive || "—"}
|
||
</p>
|
||
<p style="font-size:13px;color:#6B7280;margin-top:8px">
|
||
Joined on {currentUser()?.joinedOn}
|
||
</p>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={detailTab() === "notes"}>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:16px">
|
||
<p style="font-size:14px;color:#111827">
|
||
{currentUser()?.notes || "No notes yet."}
|
||
</p>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:10px;padding:14px 22px;border-top:1px solid #E5E7EB">
|
||
<button
|
||
type="button"
|
||
onClick={() => setListTab("all")}
|
||
style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer"
|
||
>
|
||
Back to List
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</Show>
|
||
|
||
<Show when={listTab() !== "view"}>
|
||
<div style="position:relative;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||
<input
|
||
value={search()}
|
||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||
placeholder="Search by ID, name or email..."
|
||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
||
/>
|
||
|
||
<div style="position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setSortOpen((v) => !v);
|
||
setFilterOpen(false);
|
||
}}
|
||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||
>
|
||
<svg
|
||
width="13"
|
||
height="13"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path d="M7 4v13" />
|
||
<path d="m3 13 4 4 4-4" />
|
||
<path d="M17 20V7" />
|
||
<path d="m21 11-4-4-4 4" />
|
||
</svg>
|
||
Sort
|
||
</button>
|
||
<Show when={sortOpen()}>
|
||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||
<For each={[
|
||
{ key: "newest", label: "Newest First" },
|
||
{ key: "oldest", label: "Oldest First" },
|
||
{ key: "name_asc", label: "Name (A-Z)" },
|
||
{ key: "name_desc", label: "Name (Z-A)" },
|
||
] as const}>{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setSortBy(item.key);
|
||
setSortOpen(false);
|
||
}}
|
||
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? "#FF5E13" : "#374151"};background:${sortBy() === item.key ? "#FFF1EB" : "transparent"}`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
)}</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
<div style="position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setFilterOpen((v) => !v);
|
||
setSortOpen(false);
|
||
}}
|
||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||
>
|
||
<svg
|
||
width="13"
|
||
height="13"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path d="M3 5h18M6 12h12M10 19h4" />
|
||
</svg>
|
||
Filters
|
||
</button>
|
||
<Show when={filterOpen()}>
|
||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">
|
||
Status
|
||
</p>
|
||
<For each={[
|
||
{ key: "all", label: "All Status" },
|
||
{ key: "active", label: "Active" },
|
||
{ key: "pending", label: "Pending" },
|
||
{ key: "suspended", label: "Suspended" },
|
||
{ key: "blocked", label: "Blocked" },
|
||
] as const}>{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setStatusFilter(item.key);
|
||
setFilterOpen(false);
|
||
}}
|
||
style={`display:block;width:100%;border:none;background:${statusFilter() === item.key ? "#FFF1EB" : "transparent"};color:${statusFilter() === item.key ? "#FF5E13" : "#374151"};padding:8px 10px;border-radius:8px;text-align:left;font-size:13px;cursor:pointer`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
)}</For>
|
||
<div style="height:1px;background:#F3F4F6;margin:6px 0" />
|
||
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">
|
||
Role Group
|
||
</p>
|
||
<For each={[
|
||
{ key: "all", label: "All Roles" },
|
||
{ key: "no_role", label: "No Role Users" },
|
||
{ key: "professional", label: "Professionals" },
|
||
{ key: "company", label: "Companies" },
|
||
{ key: "jobseeker", label: "Job Seekers" },
|
||
{ key: "customer", label: "Service Seekers" },
|
||
] as const}>{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setRoleFilter(item.key);
|
||
setFilterOpen(false);
|
||
}}
|
||
style={`display:block;width:100%;border:none;background:${roleFilter() === item.key ? "#FFF1EB" : "transparent"};color:${roleFilter() === item.key ? "#FF5E13" : "#374151"};padding:8px 10px;border-radius:8px;text-align:left;font-size:13px;cursor:pointer`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
)}</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={exportCsv}
|
||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer"
|
||
>
|
||
<svg
|
||
width="13"
|
||
height="13"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||
<polyline points="7 10 12 15 17 10" />
|
||
<line x1="12" y1="15" x2="12" y2="3" />
|
||
</svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
|
||
<div class="overflow-x-auto overflow-y-visible">
|
||
<table class="min-w-full">
|
||
<thead>
|
||
<tr style="background:#0D0D2A;text-align:left">
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
User ID
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
First Name
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Last Name
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Email
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
User Type
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Roles
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Status
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Verification
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap">
|
||
Joined
|
||
</th>
|
||
<th style="padding:10px 16px;font-size:11px;font-weight:600;letter-spacing:0.05em;color:#FFFFFF;text-transform:uppercase;white-space:nowrap;text-align:center">
|
||
Actions
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<Show
|
||
when={scopedRows().length > 0}
|
||
fallback={
|
||
<tr>
|
||
<td colSpan={10} style="padding:32px;text-align:center">
|
||
<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">
|
||
{loadError()
|
||
? "There was a problem contacting the server. Try refreshing the page."
|
||
: "Try changing filters or search."}
|
||
</p>
|
||
</td>
|
||
</tr>
|
||
}
|
||
>
|
||
<For each={scopedRows()}>
|
||
{(user) => {
|
||
const nameParts = user.name.split(" ");
|
||
const firstName = nameParts[0] || "";
|
||
const lastName = nameParts.slice(1).join(" ") || "";
|
||
return (
|
||
<tr
|
||
style="border-bottom:1px solid #F3F4F6"
|
||
class="hover:bg-[#FAFAFA] transition-colors"
|
||
>
|
||
<td style="padding:12px 16px;font-size:12px;font-weight:700;color:#334155;white-space:nowrap">
|
||
{user.userCode}
|
||
</td>
|
||
<td style="padding:12px 16px;font-size:13px;color:#111827">
|
||
{firstName}
|
||
</td>
|
||
<td style="padding:12px 16px;font-size:13px;color:#111827">{lastName}</td>
|
||
<td style="padding:12px 16px;font-size:13px;color:#374151;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||
{user.email}
|
||
</td>
|
||
<td style="padding:12px 16px">
|
||
<span
|
||
style={`font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;${user.userType === "NONE" ? "background:#FEF3C7;color:#92400E" : user.userType === "COMPANY" ? "background:#DBEAFE;color:#1E40AF" : user.userType === "JOBSEEKER" ? "background:#D1FAE5;color:#065F46" : user.userType === "CUSTOMER" ? "background:#EDE9FE;color:#5B21B6" : "background:#F3F4F6;color:#374151"}`}
|
||
>
|
||
{user.userType}
|
||
</span>
|
||
</td>
|
||
<td style="padding:12px 16px;min-width:160px">
|
||
<Show
|
||
when={user.registeredRoles.length > 0}
|
||
fallback={
|
||
<span style="font-size:11px;color:#94A3B8;font-style:italic">
|
||
No Role
|
||
</span>
|
||
}
|
||
>
|
||
<div style="display:flex;flex-wrap:wrap;gap:4px">
|
||
<For each={user.registeredRoles.slice(0, 2)}>
|
||
{(role) => <RoleChip role={role} />}
|
||
</For>
|
||
{user.registeredRoles.length > 2 && (
|
||
<span style="font-size:10px;color:#6B7280">
|
||
+{user.registeredRoles.length - 2}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</Show>
|
||
</td>
|
||
<td style="padding:12px 16px">
|
||
<StatusBadge status={user.accountStatus} />
|
||
</td>
|
||
<td style="padding:12px 16px">
|
||
<StatusBadge status={user.verificationStatus} />
|
||
</td>
|
||
<td style="padding:12px 16px;font-size:12px;color:#64748B;white-space:nowrap">
|
||
{user.joinedOn}
|
||
</td>
|
||
<td style="padding:12px 16px;white-space:nowrap">
|
||
<div style="display:flex;gap:6px;justify-content:center">
|
||
<button
|
||
type="button"
|
||
onClick={() => openView(user)}
|
||
title="View User"
|
||
style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#FFFFFF;background:#0D0D2A;border:none;cursor:pointer"
|
||
>
|
||
<svg
|
||
width="14"
|
||
height="14"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||
<circle cx="12" cy="12" r="3" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
title={
|
||
user.accountStatus === "SUSPENDED"
|
||
? "Activate User"
|
||
: "Suspend User"
|
||
}
|
||
onClick={() => toggleSuspend(user)}
|
||
style={`display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;${user.accountStatus === "SUSPENDED" ? "background:#D1FAE5;color:#065F46" : "background:#FEF3C7;color:#92400E"}`}
|
||
>
|
||
<svg
|
||
width="14"
|
||
height="14"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<circle cx="12" cy="12" r="10" />
|
||
{user.accountStatus === "SUSPENDED" ? (
|
||
<path d="M8 12l3 3 5-5" />
|
||
) : (
|
||
<path d="M4.9 4.9 19.1 19.1" />
|
||
)}
|
||
</svg>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
title={
|
||
user.accountStatus === "BLOCKED" ? "Unblock User" : "Block User"
|
||
}
|
||
onClick={() => toggleBlock(user)}
|
||
style={`display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;${user.accountStatus === "BLOCKED" ? "background:#D1FAE5;color:#065F46" : "background:#FEE2E2;color:#B91C1C"}`}
|
||
>
|
||
<svg
|
||
width="14"
|
||
height="14"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
>
|
||
<circle cx="12" cy="12" r="10" />
|
||
{user.accountStatus === "BLOCKED" ? (
|
||
<path d="M8 12l3 3 5-5" />
|
||
) : (
|
||
<>
|
||
<path d="M8 8l8 8" />
|
||
<path d="M16 8l-8 8" />
|
||
</>
|
||
)}
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}}
|
||
</For>
|
||
</Show>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<Show when={scopedRows().length > 0}>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||
<p style="font-size:13px;color:#6B7280">
|
||
Showing{" "}
|
||
<strong style="font-weight:600;color:#111827">1–{scopedRows().length}</strong> of{" "}
|
||
<strong style="font-weight:600;color:#111827">{rows().length}</strong> users
|
||
</p>
|
||
<div style="display:flex;align-items:center;gap:4px">
|
||
<button
|
||
type="button"
|
||
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"
|
||
>
|
||
‹
|
||
</button>
|
||
<button
|
||
type="button"
|
||
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer"
|
||
>
|
||
1
|
||
</button>
|
||
<button
|
||
type="button"
|
||
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer"
|
||
>
|
||
2
|
||
</button>
|
||
<button
|
||
type="button"
|
||
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer"
|
||
>
|
||
3
|
||
</button>
|
||
<button
|
||
type="button"
|
||
style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
}
|