feat: enhance user verification filters and update UI elements in AdminUserVerifyPage
This commit is contained in:
parent
8307654458
commit
c5327d54d5
@ -13,6 +13,8 @@ import { PendingUser } from '../../utils/api'
|
|||||||
|
|
||||||
type UserType = 'personal' | 'company'
|
type UserType = 'personal' | 'company'
|
||||||
type UserRole = 'user' | 'admin'
|
type UserRole = 'user' | 'admin'
|
||||||
|
type VerificationReadyFilter = 'all' | 'ready' | 'not_ready'
|
||||||
|
type StatusFilter = 'all' | 'pending' | 'verifying' | 'active'
|
||||||
|
|
||||||
export default function AdminUserVerifyPage() {
|
export default function AdminUserVerifyPage() {
|
||||||
const {
|
const {
|
||||||
@ -31,6 +33,8 @@ export default function AdminUserVerifyPage() {
|
|||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [fType, setFType] = useState<'all' | UserType>('all')
|
const [fType, setFType] = useState<'all' | UserType>('all')
|
||||||
const [fRole, setFRole] = useState<'all' | UserRole>('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 [perPage, setPerPage] = useState(10)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
@ -41,10 +45,18 @@ export default function AdminUserVerifyPage() {
|
|||||||
const lastName = u.last_name || ''
|
const lastName = u.last_name || ''
|
||||||
const companyName = u.company_name || ''
|
const companyName = u.company_name || ''
|
||||||
const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}`
|
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 (
|
return (
|
||||||
(fType === 'all' || u.user_type === fType) &&
|
(fType === 'all' || u.user_type === fType) &&
|
||||||
(fRole === 'all' || u.role === fRole) &&
|
(fRole === 'all' || u.role === fRole) &&
|
||||||
|
(fStatus === 'all' || u.status === fStatus) &&
|
||||||
|
(
|
||||||
|
fReady === 'all' ||
|
||||||
|
(fReady === 'ready' && isReadyToVerify) ||
|
||||||
|
(fReady === 'not_ready' && !isReadyToVerify)
|
||||||
|
) &&
|
||||||
(
|
(
|
||||||
!search.trim() ||
|
!search.trim() ||
|
||||||
u.email.toLowerCase().includes(search.toLowerCase()) ||
|
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 totalPages = Math.max(1, Math.ceil(filtered.length / perPage))
|
||||||
const current = filtered.slice((page - 1) * perPage, page * 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">
|
<h2 className="text-lg font-semibold text-blue-900">
|
||||||
Search & Filter Pending Users
|
Search & Filter Pending Users
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-7 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<label className="sr-only">Search</label>
|
<label className="block text-xs font-semibold text-blue-900 mb-1">Search</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-blue-300" />
|
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-blue-300" />
|
||||||
<input
|
<input
|
||||||
@ -189,6 +201,7 @@ export default function AdminUserVerifyPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-blue-900 mb-1">User Type</label>
|
||||||
<select
|
<select
|
||||||
value={fType}
|
value={fType}
|
||||||
onChange={e => { setFType(e.target.value as any); setPage(1) }}
|
onChange={e => { setFType(e.target.value as any); setPage(1) }}
|
||||||
@ -200,6 +213,7 @@ export default function AdminUserVerifyPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-blue-900 mb-1">Role</label>
|
||||||
<select
|
<select
|
||||||
value={fRole}
|
value={fRole}
|
||||||
onChange={e => { setFRole(e.target.value as any); setPage(1) }}
|
onChange={e => { setFRole(e.target.value as any); setPage(1) }}
|
||||||
@ -211,6 +225,32 @@ export default function AdminUserVerifyPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<select
|
||||||
value={perPage}
|
value={perPage}
|
||||||
onChange={e => { setPerPage(parseInt(e.target.value, 10)); setPage(1) }}
|
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>)}
|
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -355,6 +387,9 @@ export default function AdminUserVerifyPage() {
|
|||||||
setSelectedUserId(null)
|
setSelectedUserId(null)
|
||||||
}}
|
}}
|
||||||
userId={selectedUserId}
|
userId={selectedUserId}
|
||||||
|
onUserUpdated={() => {
|
||||||
|
fetchPendingUsers()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
const token = useAuthStore(state => state.accessToken)
|
const token = useAuthStore(state => state.accessToken)
|
||||||
|
|
||||||
// Contract preview state (lazy-loaded, per contract type)
|
// Contract preview state (lazy-loaded, per contract type)
|
||||||
const [previewLoading, setPreviewLoading] = useState(false)
|
|
||||||
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
|
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
|
||||||
const [previewState, setPreviewState] = useState({
|
const [previewState, setPreviewState] = useState({
|
||||||
contract: { loading: false, html: null as string | null, error: null as string | null },
|
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) => {
|
const handleStatusChange = async (newStatus: UserStatus) => {
|
||||||
if (!userId || !token || newStatus === selectedStatus) return
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => Promise.all([loadContractPreview('contract'), loadContractPreview('gdpr')])}
|
onClick={() => loadContractPreview(activePreviewTab)}
|
||||||
disabled={previewLoading || previewState.contract.loading || previewState.gdpr.loading}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -467,12 +455,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
{previewState[activePreviewTab].error}
|
{previewState[activePreviewTab].error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(previewLoading || previewState[activePreviewTab].loading) && (
|
{previewState[activePreviewTab].loading && (
|
||||||
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
|
<div className="flex items-center justify-center h-40 text-sm text-gray-500">
|
||||||
Loading preview…
|
Loading preview…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!previewLoading && !previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
|
{!previewState[activePreviewTab].loading && previewState[activePreviewTab].html && (
|
||||||
<div className="rounded-md border border-gray-200 overflow-hidden">
|
<div className="rounded-md border border-gray-200 overflow-hidden">
|
||||||
<iframe
|
<iframe
|
||||||
title={`Contract Preview ${activePreviewTab}`}
|
title={`Contract Preview ${activePreviewTab}`}
|
||||||
@ -481,8 +469,8 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!previewLoading && !previewState[activePreviewTab].loading && !previewState[activePreviewTab].html && !previewState[activePreviewTab].error && (
|
{!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>
|
<p className="text-sm text-gray-500">Click “Preview” to render the latest template for this user.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user