profit-planet-frontend/src/app/referral-management/components/registeredUserList.tsx
DeathKaioken 646c293bc1 .
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 06:22:10 +02:00

341 lines
17 KiB
TypeScript

'use client'
import React, { useMemo, useState } from 'react'
import { UsersIcon } from '@heroicons/react/24/outline'
import { useTranslation } from '../../i18n/useTranslation'
type UserType = 'personal' | 'company'
type UserStatus = 'active' | 'inactive' | 'pending' | 'blocked'
export interface RegisteredUser {
id: string | number
name: string
email: string
userType: UserType
registeredAt: string | Date
refCode?: string
status: UserStatus
}
interface Props {
users?: RegisteredUser[]
loading?: boolean
}
function statusBadgeClass(s: UserStatus) {
switch (s) {
case 'active': return 'bg-green-100 text-green-800'
case 'inactive': return 'bg-gray-100 text-gray-800'
case 'pending': return 'bg-amber-100 text-amber-800'
case 'blocked': return 'bg-rose-100 text-rose-800'
default: return 'bg-slate-100 text-slate-800'
}
}
function typeBadgeClass(t: UserType) {
return t === 'company' ? 'bg-indigo-100 text-indigo-800' : 'bg-blue-100 text-blue-800'
}
// CSV export helper
function exportCsv(rows: RegisteredUser[]) {
const header = ['Name', 'Email', 'Type', 'Registered', 'Status']
const csv = [
header.join(','),
...rows.map(r => [
`"${r.name.replace(/"/g, '""')}"`,
`"${r.email}"`,
r.userType,
`"${new Date(r.registeredAt).toLocaleString()}"`,
r.status
].join(','))
].join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `registered-users-${Date.now()}.csv`
a.click()
URL.revokeObjectURL(url)
}
export default function RegisteredUserList({ users, loading }: Props) {
const { t } = useTranslation()
// Normalize backend shape to local RegisteredUser shape
const normalizedUsers = useMemo<RegisteredUser[]>(() => {
if (!users || users.length === 0) return []
return users.map((u: any) => ({
id: u.id ?? u._id ?? u.userId ?? '',
name: u.name ?? u.fullName ?? u.companyName ?? u.email ?? '',
email: u.email ?? '',
userType: (u.userType ?? u.type ?? 'personal') as UserType,
registeredAt: u.registeredAt ?? u.createdAt ?? new Date().toISOString(),
status: (u.status ?? 'inactive') as UserStatus,
}))
}, [users])
// Latest 5 rows only from backend
const sorted = useMemo(() => {
const data = normalizedUsers.slice()
return data.sort((a, b) => new Date(b.registeredAt).getTime() - new Date(a.registeredAt).getTime())
}, [normalizedUsers])
const rows = sorted.slice(0, 5)
// Total badge: from backend list length
const totalRegistered = normalizedUsers.length
// Modal state
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<'all' | UserType>('all')
const [statusFilter, setStatusFilter] = useState<'all' | UserStatus>('all')
const [page, setPage] = useState(1)
const pageSize = 10
// Full dataset for modal (backend only)
const allRows = useMemo(() => {
return normalizedUsers.slice().sort((a, b) => new Date(b.registeredAt).getTime() - new Date(a.registeredAt).getTime())
}, [normalizedUsers])
const filtered = useMemo(() => {
return allRows.filter(r => {
if (typeFilter !== 'all' && r.userType !== typeFilter) return false
if (statusFilter !== 'all' && r.status !== statusFilter) return false
if (!query.trim()) return true
const q = query.toLowerCase()
return r.name.toLowerCase().includes(q) || r.email.toLowerCase().includes(q)
})
}, [allRows, query, typeFilter, statusFilter])
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
const pageRows = filtered.slice((page - 1) * pageSize, page * pageSize)
const resetAndOpen = () => {
setQuery('')
setTypeFilter('all')
setStatusFilter('all')
setPage(1)
setOpen(true)
}
// Lock scroll when modal open
React.useEffect(() => {
if (!open) return
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = prev }
}, [open])
return (
<>
<div className="mt-8 mb-8 rounded-[28px] border border-white/80 bg-white/85 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur overflow-hidden">
<div className="p-6 border-b border-slate-200/60 bg-white/40 flex items-start justify-between gap-4 flex-wrap">
<div>
<h2 className="text-xl font-bold text-slate-950 break-words">{t('referralManagement.registeredUsersTitle')}</h2>
<div className="mt-2 inline-flex items-center gap-2">
<span className="inline-flex items-center gap-2 rounded-full bg-violet-100 text-violet-800 px-3 py-1 text-[11px] font-semibold tracking-wide">
<UsersIcon className="h-4 w-4" />
{t('referralManagement.totalRefBadge')}
</span>
<span className="inline-flex items-center rounded-full bg-violet-600 text-white px-2 py-1 text-[11px] font-bold">
{totalRegistered}
</span>
</div>
<p className="text-sm text-slate-600 mt-2 break-words">
{t('referralManagement.registeredUsersSubtitle')}
</p>
<p className="text-xs text-slate-500 mt-2 break-words">
{t('referralManagement.showingLatest5')}
</p>
</div>
<button
onClick={resetAndOpen}
className="inline-flex items-center rounded-2xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-500"
>
{t('referralManagement.viewAll')}
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50/80">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colUser')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colEmail')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colType')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colRegistered')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colStatus')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 bg-white/75">
{loading ? (
<>
<tr><td className="px-6 py-4" colSpan={5}><div className="h-4 w-40 bg-gray-200 animate-pulse rounded" /></td></tr>
<tr><td className="px-6 py-4" colSpan={5}><div className="h-4 w-3/4 bg-gray-200 animate-pulse rounded" /></td></tr>
<tr><td className="px-6 py-4" colSpan={5}><div className="h-4 w-1/2 bg-gray-200 animate-pulse rounded" /></td></tr>
</>
) : rows.length === 0 ? (
<tr>
<td className="px-6 py-6 text-sm text-slate-500 break-words" colSpan={5}>
{t('referralManagement.noRegisteredUsers')}
</td>
</tr>
) : (
rows.map(u => {
const date = new Date(u.registeredAt).toLocaleString()
return (
<tr key={u.id}>
<td className="px-6 py-4 text-sm text-slate-900 break-words">{u.name}</td>
<td className="px-6 py-4 text-sm text-slate-700 break-words">{u.email}</td>
<td className="px-6 py-4 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${typeBadgeClass(u.userType)}`}>
{u.userType === 'company' ? t('referralManagement.typeCompany') : t('referralManagement.typePersonal')}
</span>
</td>
<td className="px-6 py-4 text-sm text-slate-700 break-words">{date}</td>
<td className="px-6 py-4 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${statusBadgeClass(u.status)}`}>
{u.status.charAt(0).toUpperCase() + u.status.slice(1)}
</span>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
</div>
{/* Modal with full list */}
{open && (
<div className="fixed inset-0 z-50" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setOpen(false)} aria-hidden />
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-6xl rounded-[28px] border border-white/80 bg-white/90 backdrop-blur shadow-[0_32px_80px_-40px_rgba(15,23,42,0.42)] overflow-hidden">
<div className="px-6 py-4 border-b border-slate-200/70 bg-white/45 flex items-center justify-between gap-3 flex-wrap">
<div>
<h3 className="text-lg font-bold text-slate-900 break-words">{t('referralManagement.allRegisteredUsersTitle')}</h3>
<p className="text-xs text-slate-600 break-words">{t('referralManagement.allRegisteredUsersSubtitle')}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => exportCsv(filtered)}
className="inline-flex items-center rounded-2xl border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50"
>
{t('referralManagement.exportCsv')}
</button>
<button
onClick={() => setOpen(false)}
className="inline-flex items-center rounded-2xl bg-slate-900 px-3 py-1.5 text-sm font-semibold text-white hover:bg-slate-800"
>
{t('common.close')}
</button>
</div>
</div>
<div className="px-6 py-4 border-b border-slate-100 grid grid-cols-1 md:grid-cols-4 gap-3">
<input
value={query}
onChange={e => { setQuery(e.target.value); setPage(1) }}
placeholder={t('referralManagement.searchPlaceholder')}
className="md:col-span-2 w-full rounded-2xl border border-slate-200 px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900/20 focus:border-slate-300 placeholder:text-slate-500"
/>
<select
value={typeFilter}
onChange={e => { setTypeFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-2xl border border-slate-200 px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900/20 focus:border-slate-300"
>
<option value="all">{t('referralManagement.filterAllTypes')}</option>
<option value="personal">{t('referralManagement.typePersonal')}</option>
<option value="company">{t('referralManagement.typeCompany')}</option>
</select>
<select
value={statusFilter}
onChange={e => { setStatusFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-2xl border border-slate-200 px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900/20 focus:border-slate-300"
>
<option value="all">{t('referralManagement.filterAllStatus')}</option>
<option value="active">{t('referralManagement.filterActive')}</option>
<option value="inactive">{t('referralManagement.filterInactive')}</option>
<option value="pending">{t('referralManagement.filterPending')}</option>
<option value="blocked">{t('referralManagement.filterBlocked')}</option>
</select>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50/80">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colUser')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colEmail')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colType')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colRegistered')}</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wide whitespace-normal">{t('referralManagement.colStatus')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 bg-white/75">
{pageRows.length === 0 ? (
<tr>
<td className="px-6 py-6 text-sm text-slate-500 break-words" colSpan={5}>
{t('referralManagement.noUsersMatchFilters')}
</td>
</tr>
) : (
pageRows.map(u => {
const date = new Date(u.registeredAt).toLocaleString()
return (
<tr key={u.id}>
<td className="px-6 py-3 text-sm text-slate-900 break-words">{u.name}</td>
<td className="px-6 py-3 text-sm text-slate-700 break-words">{u.email}</td>
<td className="px-6 py-3 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${typeBadgeClass(u.userType)}`}>
{u.userType === 'company' ? t('referralManagement.typeCompany') : t('referralManagement.typePersonal')}
</span>
</td>
<td className="px-6 py-3 text-sm text-slate-700 break-words">{date}</td>
<td className="px-6 py-3 text-sm">
<span className={`inline-flex items-center rounded px-2 py-1 text-xs font-medium ${statusBadgeClass(u.status)}`}>
{u.status.charAt(0).toUpperCase() + u.status.slice(1)}
</span>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
<div className="px-6 py-4 flex items-center justify-between gap-3 flex-wrap">
<span className="text-xs text-slate-600 break-words">
{t('referralManagement.showing')} {pageRows.length} {t('referralManagement.of')} {filtered.length} {t('referralManagement.colUser').toLowerCase()}s
</span>
<div className="flex items-center gap-2">
<button
disabled={page <= 1}
onClick={() => setPage(p => Math.max(1, p - 1))}
className="rounded-xl border border-slate-200 px-3 py-1.5 text-sm text-slate-700 disabled:opacity-50 hover:bg-slate-50"
>
{t('referralManagement.pagePrev')}
</button>
<span className="text-sm text-slate-700 break-words">
{t('referralManagement.pageOf').replace('{page}', String(page)).replace('{total}', String(totalPages))}
</span>
<button
disabled={page >= totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className="rounded-xl border border-slate-200 px-3 py-1.5 text-sm text-slate-700 disabled:opacity-50 hover:bg-slate-50"
>
{t('referralManagement.pageNext')}
</button>
</div>
</div>
</div>
</div>
</div>
)}
</>
)
}