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

387 lines
26 KiB
TypeScript

import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { useSearchParams } from '@solidjs/router';
import AdminShell from '~/components/AdminShell';
import type { CrudRecord } from '~/lib/admin/types';
const API = '/api/gateway';
type ExternalUserRecord = {
id: string;
name: string;
username?: string;
email: string;
phone?: string;
userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER';
primaryActiveRole?: string;
onboardingStatus: 'NOT_STARTED' | 'IN_PROGRESS' | 'SUBMITTED' | 'COMPLETED';
verificationStatus: 'UNVERIFIED' | 'PENDING' | 'IN_REVIEW' | 'VERIFIED' | 'REJECTED' | 'RE_UPLOAD_REQUESTED';
accountStatus: 'ACTIVE' | 'INACTIVE' | 'BLOCKED' | 'SUSPENDED';
createdDate?: string;
lastLogin?: string;
status: 'ACTIVE' | 'INACTIVE';
updatedAt: string;
};
const FALLBACK_USERS: ExternalUserRecord[] = [
{ id: 'u1', name: 'Arun Kumar', username: 'arun_pro', email: 'arun.k@example.com', phone: '+91 98765 43210', userType: 'PROFESSIONAL', primaryActiveRole: 'Photographer', onboardingStatus: 'COMPLETED', verificationStatus: 'VERIFIED', accountStatus: 'ACTIVE', createdDate: '2026-01-15', lastLogin: '2026-03-27 10:45 AM', status: 'ACTIVE', updatedAt: '2026-03-27' },
{ id: 'u2', name: 'Tech Solutions', username: 'tech_sol', email: 'contact@techsol.com', phone: '+91 98765 43211', userType: 'COMPANY', primaryActiveRole: 'No Active Role', onboardingStatus: 'SUBMITTED', verificationStatus: 'IN_REVIEW', accountStatus: 'ACTIVE', createdDate: '2026-02-10', lastLogin: '2026-03-26 04:20 PM', status: 'ACTIVE', updatedAt: '2026-03-27' },
{ id: 'u3', name: 'Priya Sharma', username: 'priya_s', email: 'priya.s@example.com', phone: '+91 98765 43212', userType: 'CUSTOMER', primaryActiveRole: 'Customer', onboardingStatus: 'COMPLETED', verificationStatus: 'VERIFIED', accountStatus: 'ACTIVE', createdDate: '2026-03-01', lastLogin: '2026-03-27 09:15 AM', status: 'ACTIVE', updatedAt: '2026-03-27' },
{ id: 'u4', name: 'Deepak Verma', username: 'deepak_v', email: 'deepak.v@example.com', phone: '+91 98765 43213', userType: 'JOBSEEKER', primaryActiveRole: 'No Active Role', onboardingStatus: 'IN_PROGRESS', verificationStatus: 'UNVERIFIED', accountStatus: 'INACTIVE', createdDate: '2026-03-15', lastLogin: '—', status: 'INACTIVE', updatedAt: '2026-03-27' },
];
function StatusBadge(props: { status: string; type?: 'account' | 'verification' | 'onboarding' }) {
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 rejected = () => props.status === 'REJECTED' || props.status === 'BLOCKED' || props.status === 'SUSPENDED';
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : pending() ? '#F6D78F' : rejected() ? '#FECACA' : '#D1D5DB'};background:${active() ? '#FFF1EB' : pending() ? '#FFF3D6' : rejected() ? '#FEF2F2' : '#F3F4F6'};color:${active() ? '#FF5E13' : pending() ? '#B7791F' : rejected() ? '#DC2626' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : pending() ? '#B7791F' : rejected() ? '#DC2626' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{props.status.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ')}
</span>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
</span>
<input
type={props.type ?? 'text'}
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
);
}
export default function UsersManagementPage() {
const [view, setView] = createSignal<'list' | 'form'>('list');
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
const [formTab, setFormTab] = createSignal<'basic' | 'status' | 'security'>('basic');
const [detailTab, setDetailTab] = createSignal<'overview' | 'roles' | 'onboarding' | 'verification' | 'logs'>('overview');
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal('all');
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'created_desc' | 'created_asc'>('name_asc');
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
const [rows, setRows] = createSignal<ExternalUserRecord[]>([]);
const [viewingUser, setViewingUser] = createSignal<ExternalUserRecord | null>(null);
const [editingId, setEditingId] = createSignal<string | null>(null);
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
// Form Signals
const [name, setName] = createSignal('');
const [username, setUsername] = createSignal('');
const [email, setEmail] = createSignal('');
const [phone, setPhone] = createSignal('');
const [userType, setUserType] = createSignal<ExternalUserRecord['userType']>('CUSTOMER');
const load = async () => {
setRows(FALLBACK_USERS);
};
onMount(() => void load());
const filteredRows = createMemo(() => {
let r = rows();
if (statusFilter() !== 'all') r = r.filter((d) => d.accountStatus === statusFilter().toUpperCase());
const q = search().toLowerCase();
if (q) {
r = r.filter(r => r.name.toLowerCase().includes(q) || r.email.toLowerCase().includes(q) || r.id.toLowerCase().includes(q));
}
const sorted = [...r];
const mode = sortBy();
sorted.sort((a, b) => {
if (mode === 'name_desc') return b.name.localeCompare(a.name);
if (mode === 'created_desc') return (b.createdDate || '').localeCompare(a.createdDate || '');
if (mode === 'created_asc') return (a.createdDate || '').localeCompare(b.createdDate || '');
return a.name.localeCompare(b.name);
});
return sorted;
});
const resetForm = () => {
setEditingId(null); setViewingUser(null); setName(''); setUsername(''); setEmail(''); setPhone(''); setUserType('CUSTOMER'); setFormTab('basic');
};
const openCreate = () => { resetForm(); setView('form'); };
const openEdit = (row: ExternalUserRecord) => {
setEditingId(row.id); setViewingUser(row); setName(row.name); setUsername(row.username || '');
setEmail(row.email); setPhone(row.phone || ''); setUserType(row.userType);
setView('form'); setOpenMenuId(null);
};
const openDetail = (row: ExternalUserRecord) => {
setViewingUser(row); setListTab('view'); setOpenMenuId(null);
};
return (
<AdminShell>
<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 external accounts, onboarding status, and role registrations</p>
</div>
{/* ── LIST VIEW ── */}
<Show when={view() === 'list'}>
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
{ key: 'all', label: 'All Users', action: () => { setListTab('all'); setStatusFilter('all'); } },
{ key: 'create', label: 'Create User', action: () => { setListTab('create'); openCreate(); } },
{ key: 'view', label: 'View Profile', action: () => setListTab('view') },
] as const).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
{/* View Profile panel */}
<Show when={listTab() === 'view'}>
<Show
when={!viewingUser()}
>
<div style="margin-top:24px;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">Click the <strong></strong> menu on any user row and choose <strong>View Profile</strong>.</p>
</div>
</Show>
<Show when={viewingUser()}>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:24px">
<div style="width:64px;height:64px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:#9CA3AF;border:1px solid #E5E7EB">
{viewingUser()!.name.charAt(0)}
</div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:12px">
<h2 style="font-size:20px;font-weight:700;color:#111827">{viewingUser()!.name}</h2>
<StatusBadge status={viewingUser()!.accountStatus} />
</div>
<p style="font-size:14px;color:#6B7280;margin-top:2px">{viewingUser()!.email} ID: {viewingUser()!.id}</p>
</div>
</div>
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(['overview', 'roles', 'onboarding', 'verification', 'logs'] as const).map((tab, i) => {
const labels = ['Overview', 'Registered Roles', 'Onboarding', 'Verification', 'Activity Logs'];
const active = () => detailTab() === tab;
return (
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
{labels[i]}
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
</div>
<div style="padding:24px">
<Show when={detailTab() === 'overview'}>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Account Summary</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
{ l: 'User Type', v: viewingUser()!.userType },
{ l: 'Primary Role', v: viewingUser()!.primaryActiveRole || 'None' },
{ l: 'Email Verified', v: 'Yes' },
{ l: 'Joined Date', v: viewingUser()!.createdDate || '—' },
].map(item => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v}</span>
</div>
))}
</div>
</div>
</div>
</Show>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
<button type="button" onClick={() => openEdit(viewingUser()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Profile</button>
<button type="button" onClick={() => { setViewingUser(null); 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>
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;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 name, email, or ID..."
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={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(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={sortMenuOpen()}>
<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)">
{(['name_asc', 'name_desc', 'created_desc', 'created_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(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() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Joined (Newest)', 'Joined (Oldest)'][i]}
</button>
))}
</div>
</Show>
</div>
<div style="position:relative">
<button
type="button"
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(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={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'active', 'inactive', 'blocked'] as const).map((s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(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:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : s === 'inactive' ? 'Inactive' : 'Blocked'}
</button>
))}
</div>
</Show>
</div>
<button type="button" 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">Export</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['User Details', 'Type', 'Active Role', 'Verification', 'Account Status', 'Actions'].map(h => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
<For each={filteredRows()}>
{(row) => (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px">
<div style="display:flex;align-items:center;gap:12px">
<div style="width:36px;height:36px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#9CA3AF;border:1px solid #E5E7EB">
{row.name.charAt(0)}
</div>
<div>
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
<p style="font-size:11px;color:#6B7280">{row.email}</p>
</div>
</div>
</td>
<td style="padding:12px 20px;font-size:12px;font-weight:600;color:#374151">{row.userType}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.primaryActiveRole || '—'}</td>
<td style="padding:12px 20px"><StatusBadge status={row.verificationStatus} /></td>
<td style="padding:12px 20px"><StatusBadge status={row.accountStatus} /></td>
<td style="padding:12px 20px;position:relative">
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
</button>
<Show when={openMenuId() === row.id}>
<div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
<button type="button" onClick={() => openDetail(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Profile</button>
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Edit User</button>
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Block User</button>
</div>
</Show>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</div>
</div>
</Show>
{/* ── FORM VIEW ── */}
<Show when={view() === 'form'}>
<div>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All Users</button>
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">{editingId() ? 'Edit User' : 'Create User'}</button>
</div>
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(['basic', 'status', 'security'] as const).map((tab, i) => {
const labels = ['Basic Information', 'Account Status', 'Security Settings'];
const active = () => formTab() === tab;
return (
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
{labels[i]}
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
</div>
<div style="padding:24px">
<Show when={formTab() === 'basic'}>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Full Name" required value={name()} onInput={setName} />
<FormInput label="Username" required value={username()} onInput={setUsername} />
<FormInput label="Email Address" required value={email()} onInput={setEmail} type="email" />
<FormInput label="Phone Number" value={phone()} onInput={setPhone} />
</div>
</Show>
<Show when={formTab() === 'status'}>
<div style="display:flex;flex-direction:column;gap:20px">
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:8px;background:#F9FAFB;border:1px solid #E5E7EB">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Email Verified</p>
<p style="font-size:12px;color:#6B7280">The user has confirmed their email address.</p>
</div>
<div style="width:40px;height:20px;background:#FF5E13;border-radius:10px;position:relative;cursor:pointer"><div style="width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px" /></div>
</div>
</div>
</Show>
<Show when={formTab() === 'security'}>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput label="Password" value="" onInput={() => {}} type="password" />
<FormInput label="Confirm Password" value="" onInput={() => {}} type="password" />
</div>
</Show>
</div>
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel</button>
<button type="button" style="height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">
{editingId() ? 'Update User' : 'Create User'}
</button>
</div>
</div>
</div>
</Show>
</div>
</AdminShell>
);
}