feat: enhance user verification filters and update UI elements in AdminUserVerifyPage

This commit is contained in:
seaznCode 2026-01-17 22:03:42 +01:00
parent 8307654458
commit c5327d54d5
2 changed files with 54 additions and 31 deletions

View File

@ -13,6 +13,8 @@ import { PendingUser } from '../../utils/api'
type UserType = 'personal' | 'company'
type UserRole = 'user' | 'admin'
type VerificationReadyFilter = 'all' | 'ready' | 'not_ready'
type StatusFilter = 'all' | 'pending' | 'verifying' | 'active'
export default function AdminUserVerifyPage() {
const {
@ -31,6 +33,8 @@ export default function AdminUserVerifyPage() {
const [search, setSearch] = useState('')
const [fType, setFType] = useState<'all' | UserType>('all')
const [fRole, setFRole] = useState<'all' | UserRole>('all')
const [fReady, setFReady] = useState<VerificationReadyFilter>('all')
const [fStatus, setFStatus] = useState<StatusFilter>('all')
const [perPage, setPerPage] = useState(10)
const [page, setPage] = useState(1)
@ -41,10 +45,18 @@ export default function AdminUserVerifyPage() {
const lastName = u.last_name || ''
const companyName = u.company_name || ''
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
const isReadyToVerify = u.email_verified === 1 && u.profile_completed === 1 &&
u.documents_uploaded === 1 && u.contract_signed === 1
return (
(fType === 'all' || u.user_type === fType) &&
(fRole === 'all' || u.role === fRole) &&
(fStatus === 'all' || u.status === fStatus) &&
(
fReady === 'all' ||
(fReady === 'ready' && isReadyToVerify) ||
(fReady === 'not_ready' && !isReadyToVerify)
) &&
(
!search.trim() ||
u.email.toLowerCase().includes(search.toLowerCase()) ||
@ -52,7 +64,7 @@ export default function AdminUserVerifyPage() {
)
)
})
}, [pendingUsers, search, fType, fRole])
}, [pendingUsers, search, fType, fRole, fReady, fStatus])
const totalPages = Math.max(1, Math.ceil(filtered.length / perPage))
const current = filtered.slice((page - 1) * perPage, page * perPage)
@ -175,9 +187,9 @@ export default function AdminUserVerifyPage() {
<h2 className="text-lg font-semibold text-blue-900">
Search & Filter Pending Users
</h2>
<div className="grid grid-cols-1 md:grid-cols-6 gap-6">
<div className="md:col-span-2">
<label className="sr-only">Search</label>
<div className="grid grid-cols-1 lg:grid-cols-7 gap-4">
<div className="lg:col-span-2">
<label className="block text-xs font-semibold text-blue-900 mb-1">Search</label>
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-blue-300" />
<input
@ -189,6 +201,7 @@ export default function AdminUserVerifyPage() {
</div>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">User Type</label>
<select
value={fType}
onChange={e => { setFType(e.target.value as any); setPage(1) }}
@ -200,6 +213,7 @@ export default function AdminUserVerifyPage() {
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Role</label>
<select
value={fRole}
onChange={e => { setFRole(e.target.value as any); setPage(1) }}
@ -211,6 +225,32 @@ export default function AdminUserVerifyPage() {
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Verification Readiness</label>
<select
value={fReady}
onChange={e => { setFReady(e.target.value as VerificationReadyFilter); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Readiness</option>
<option value="ready">Ready to Verify</option>
<option value="not_ready">Not Ready</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Status</label>
<select
value={fStatus}
onChange={e => { setFStatus(e.target.value as StatusFilter); setPage(1) }}
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="verifying">Verifying</option>
<option value="active">Active</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-blue-900 mb-1">Rows per page</label>
<select
value={perPage}
onChange={e => { setPerPage(parseInt(e.target.value, 10)); setPage(1) }}
@ -219,14 +259,6 @@ export default function AdminUserVerifyPage() {
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
</select>
</div>
<div className="flex items-stretch">
<button
type="submit"
className="w-full inline-flex items-center justify-center rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 text-sm font-semibold px-5 py-3 shadow transition"
>
Filter
</button>
</div>
</div>
</form>
@ -355,6 +387,9 @@ export default function AdminUserVerifyPage() {
setSelectedUserId(null)
}}
userId={selectedUserId}
onUserUpdated={() => {
fetchPendingUsers()
}}
/>
</PageLayout>
)

View File

@ -47,7 +47,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
const token = useAuthStore(state => state.accessToken)
// Contract preview state (lazy-loaded, per contract type)
const [previewLoading, setPreviewLoading] = useState(false)
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
const [previewState, setPreviewState] = useState({
contract: { loading: false, html: null as string | null, error: null as string | null },
@ -88,17 +87,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
}
}
// Load both contract and GDPR previews when modal opens after user is known
useEffect(() => {
if (!isOpen || !userId || !token || !userDetails) return
setPreviewLoading(true)
Promise.all([
loadContractPreview('contract'),
loadContractPreview('gdpr')
]).finally(() => setPreviewLoading(false))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen, userId, token, userDetails])
const handleStatusChange = async (newStatus: UserStatus) => {
if (!userId || !token || newStatus === selectedStatus) return
@ -439,11 +427,11 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => Promise.all([loadContractPreview('contract'), loadContractPreview('gdpr')])}
disabled={previewLoading || previewState.contract.loading || previewState.gdpr.loading}
onClick={() => loadContractPreview(activePreviewTab)}
disabled={previewState[activePreviewTab].loading}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60"
>
{previewLoading || previewState.contract.loading || previewState.gdpr.loading ? 'Loading…' : 'Refresh'}
{previewState[activePreviewTab].loading ? 'Loading…' : 'Preview'}
</button>
<button
type="button"
@ -467,12 +455,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
{previewState[activePreviewTab].error}
</div>
)}
{(previewLoading || previewState[activePreviewTab].loading) && (
{previewState[activePreviewTab].loading && (
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
Loading preview
</div>
)}
{!previewLoading && !previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
{!previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
<div className="rounded-md border border-gray-200 overflow-hidden">
<iframe
title={`Contract Preview ${activePreviewTab}`}
@ -481,8 +469,8 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
/>
</div>
)}
{!previewLoading && !previewState[activePreviewTab].loading && !previewState[activePreviewTab].html && !previewState[activePreviewTab].error && (
<p className="text-sm text-gray-500">Click "Refresh" to render the latest active contract or GDPR template for this user.</p>
{!previewState[activePreviewTab].loading && !previewState[activePreviewTab].html && !previewState[activePreviewTab].error && (
<p className="text-sm text-gray-500">Click Preview to render the latest template for this user.</p>
)}
</div>
</div>