feat: added level tracker and registereed users via your referral

This commit is contained in:
DeathKaioken 2025-10-22 20:25:16 +02:00
parent 12e0aa4fd4
commit 6fa4f02fb2
5 changed files with 647 additions and 3 deletions

View File

@ -0,0 +1,112 @@
'use client'
import React, { useEffect, useMemo, useState } from 'react'
interface Props {
// total points = total registered users via your referral
points?: number
className?: string
}
// NEW: thresholds with names (Level 1+)
// Level 0 (<5) will be handled separately as "Starter"
const LEVELS = [
{ threshold: 5, name: 'Novice' }, // Level 1
{ threshold: 25, name: 'Hustler' }, // Level 2
{ threshold: 125, name: 'Entrepreneur' },// Level 3
{ threshold: 625, name: 'Prestige' }, // Level 4
{ threshold: 3125, name: 'MAX' }, // Level 5+
]
// ...existing calc helpers...
function calcLevel(points: number) {
// Level increases when meeting thresholds 5, 25, 125, ...
// level = count of thresholds <= points
let lvl = 0
let threshold = 5
while (points >= threshold) {
lvl++
threshold *= 5
}
return lvl
}
function nextThreshold(points: number) {
let t = 5
while (points >= t) t *= 5
return t
}
export default function LevelTrackerWidget({ points = 3, className = '' }: Props) {
const safePoints = Math.max(0, Math.floor(points))
// NEW: derive level index and name
const levelIndex = useMemo(() => {
// index of last level whose threshold is met; -1 if below level 1
let idx = -1
for (let i = 0; i < LEVELS.length; i++) {
if (safePoints >= LEVELS[i].threshold) idx = i
else break
}
return idx
}, [safePoints])
const level = useMemo(() => calcLevel(safePoints), [safePoints])
const displayLevel = Math.min(level, LEVELS.length) // cap at 5 for display
const levelName = level === 0
? 'Starter'
: LEVELS[Math.min(level - 1, LEVELS.length - 1)].name
// For progress to next target: keep growing in powers of 5, even after MAX (name stays MAX)
const target = useMemo(() => nextThreshold(safePoints), [safePoints])
// Progress towards the current target
const targetProgress = Math.min(safePoints, target)
const percent = target === 0 ? 0 : Math.min(100, Math.round((targetProgress / target) * 100))
// Animate on mount
const [animPercent, setAnimPercent] = useState(0)
useEffect(() => {
const id = setTimeout(() => setAnimPercent(percent), 120)
return () => clearTimeout(id)
}, [percent])
return (
<div className={`w-full rounded-xl border border-gray-200 bg-white shadow-sm ${className}`}>
<div className="p-4 sm:p-5">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="flex items-baseline gap-2">
<span className="text-sm font-semibold text-gray-900">Level</span>
<span className="text-lg font-bold text-indigo-700">#{displayLevel}</span>
</div>
{/* NEW: level name badge */}
<span className="inline-flex items-center rounded-full bg-indigo-50 text-indigo-700 px-2 py-[2px] text-[11px] font-semibold">
{levelName}
</span>
</div>
<div className="text-xs text-gray-600">
{targetProgress} of {target} referrals
</div>
</div>
{/* Slim progress bar */}
<div className="relative h-3 w-full overflow-hidden rounded-full bg-gray-100 ring-1 ring-gray-200">
<div
className="absolute inset-y-0 left-0 h-full rounded-full bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500 shadow-[0_0_10px_rgba(99,102,241,0.35)] transition-[width] duration-800 ease-out"
style={{ width: `${animPercent}%` }}
/>
</div>
<div className="mt-2 flex items-center justify-between">
<span className="text-[11px] font-medium text-gray-600">
{displayLevel >= LEVELS.length
? 'Max level reached'
: `Next milestone: ${target} total referrals`}
</span>
<span className="text-[11px] font-semibold text-indigo-700">{animPercent}%</span>
</div>
</div>
</div>
)
}

View File

@ -8,6 +8,7 @@ import {
UsersIcon, UsersIcon,
BuildingOffice2Icon, BuildingOffice2Icon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import LevelTrackerWidget from './levelTrackerWidget' // NEW
type Stats = { type Stats = {
activeLinks: number activeLinks: number
@ -19,6 +20,7 @@ type Stats = {
interface Props { interface Props {
stats: Stats stats: Stats
totalReferredFromBackend?: number // NEW
} }
const renderStatCard = ( const renderStatCard = (
@ -36,7 +38,7 @@ const renderStatCard = (
</div> </div>
) )
export default function ReferralStatisticWidget({ stats }: Props) { export default function ReferralStatisticWidget({ stats, totalReferredFromBackend }: Props) {
const topStats = [ const topStats = [
{ label: 'Active Links', value: stats.activeLinks, icon: CheckCircleIcon, color: 'bg-green-500' }, { label: 'Active Links', value: stats.activeLinks, icon: CheckCircleIcon, color: 'bg-green-500' },
{ label: 'Links Used', value: stats.linksUsed, icon: ChartBarIcon, color: 'bg-indigo-500' }, { label: 'Links Used', value: stats.linksUsed, icon: ChartBarIcon, color: 'bg-indigo-500' },
@ -47,8 +49,16 @@ export default function ReferralStatisticWidget({ stats }: Props) {
{ label: 'Total Links', value: stats.totalLinks, icon: LinkIcon, color: 'bg-yellow-500' }, { label: 'Total Links', value: stats.totalLinks, icon: LinkIcon, color: 'bg-yellow-500' },
] ]
// NEW: prefer backend total_referred_users if provided
const totalReferred =
typeof totalReferredFromBackend === 'number'
? totalReferredFromBackend
: (stats.personalUsersReferred || 0) + (stats.companyUsersReferred || 0)
return ( return (
<> <>
<LevelTrackerWidget points={totalReferred} className="mb-6" />
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 mb-5"> <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 mb-5">
{topStats.map((c, i) => renderStatCard(c, `top-${i}`))} {topStats.map((c, i) => renderStatCard(c, `top-${i}`))}
</div> </div>

View File

@ -0,0 +1,383 @@
'use client'
import React, { useMemo, useState } from 'react'
import { UsersIcon } from '@heroicons/react/24/outline'
type UserType = 'personal' | 'company'
type UserStatus = 'active' | 'pending' | 'blocked'
export interface RegisteredUser {
id: string | number
name: string
email: string
userType: UserType
registeredAt: string | Date
refCode?: string // CHANGED: optional, not used in UI
status: UserStatus
}
interface Props {
users?: RegisteredUser[]
loading?: boolean
}
// Base dummy set for the widget preview
const baseUsers: RegisteredUser[] = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', userType: 'personal', registeredAt: '2025-03-22T10:04:00Z', refCode: 'REF-9A2F1C', status: 'active' },
{ id: 2, name: 'Beta GmbH', email: 'office@beta-gmbh.de', userType: 'company', registeredAt: '2025-03-20T08:31:00Z', refCode: 'REF-9A2F1C', status: 'pending' },
{ id: 3, name: 'Carlos Diaz', email: 'carlos@sample.io', userType: 'personal', registeredAt: '2025-03-19T14:22:00Z', refCode: 'REF-77XZQ1', status: 'active' },
{ id: 4, name: 'Delta Solutions AG', email: 'contact@delta.ag', userType: 'company', registeredAt: '2025-03-18T09:12:00Z', refCode: 'REF-77XZQ1', status: 'active' },
{ id: 5, name: 'Emily Nguyen', email: 'emily.ng@ex.com', userType: 'personal', registeredAt: '2025-03-17T19:44:00Z', refCode: 'REF-9A2F1C', status: 'blocked' },
]
// Expanded dummy list for the modal (pagination/search/filter/export demo)
function buildDummyAll(): RegisteredUser[] {
const list: RegisteredUser[] = []
let id = 1
const refCodes = ['REF-9A2F1C', 'REF-77XZQ1', 'REF-55PLK9']
const names = [
'Alice Johnson', 'Beta GmbH', 'Carlos Diaz', 'Delta Solutions AG', 'Emily Nguyen',
'Foxtrot LLC', 'Green Innovations', 'Helios Corp', 'Ivy Partners', 'Jonas Weber'
]
for (let i = 0; i < 60; i++) {
const name = names[i % names.length]
const isCompany = /GmbH|AG|LLC|Corp|Partners|Innovations/.test(name) ? 'company' : 'personal'
const status: UserStatus = (i % 9 === 0) ? 'blocked' : (i % 3 === 0 ? 'pending' : 'active')
const refCode = refCodes[i % refCodes.length]
const date = new Date(2025, 2, 28 - (i % 25), 10 + (i % 12), (i * 7) % 60, 0).toISOString()
list.push({
id: id++,
name,
email: `${name.toLowerCase().replace(/[^a-z]+/g, '.')}@example.com`,
userType: isCompany as UserType,
registeredAt: date,
refCode,
status
})
}
// newest first
return list.sort((a, b) => new Date(b.registeredAt).getTime() - new Date(a.registeredAt).getTime())
}
const allDummyUsers = buildDummyAll()
function statusBadgeClass(s: UserStatus) {
switch (s) {
case 'active': return 'bg-green-100 text-green-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 (no ref code)
function exportCsv(rows: RegisteredUser[]) {
const header = ['Name', 'Email', 'Type', 'Registered', 'Status'] // CHANGED
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) {
// Main widget rows (latest 5)
const sorted = useMemo(() => {
const data = (users && users.length > 0 ? users : baseUsers).slice()
return data.sort((a, b) => new Date(b.registeredAt).getTime() - new Date(a.registeredAt).getTime())
}, [users])
const rows = sorted.slice(0, 5)
// NEW: total registered count for badge (from provided users or dummy full list)
const totalRegistered = useMemo(() => {
return users && users.length ? users.length : allDummyUsers.length
}, [users])
// 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 (dummy for now)
const allRows = useMemo(() => {
const data = users && users.length ? users : allDummyUsers
return data.sort((a, b) => new Date(b.registeredAt).getTime() - new Date(a.registeredAt).getTime())
}, [users])
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)
// CHANGED: remove refCode match (not provided by backend)
)
})
}, [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)
}
// NEW: lock page scroll while modal is 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 bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-100 flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-semibold text-gray-900">Registered Users via Your Referral</h2>
{/* Moved badge directly under the title */}
<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" />
TOTAL REGISTERED USER WITH YOUR REF LINK
</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-gray-600 mt-2">
Users who signed up using one of your referral links.
</p>
<p className="text-xs text-gray-500 mt-2">
Showing the latest 5 users. Use View all to see the complete list.
</p>
</div>
{/* ...existing code... */}
<button
onClick={resetAndOpen}
className="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500"
>
View all
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Registered</th>
{/* REMOVED: Ref Code column */}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{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-gray-500" colSpan={5}>
No registered users found for your referral links.
</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-gray-900">{u.name}</td>
<td className="px-6 py-4 text-sm text-gray-700">{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' ? 'Company' : 'Personal'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-700">{date}</td>
{/* REMOVED: Ref Code cell */}
<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"
onClick={() => setOpen(false)}
aria-hidden
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-6xl bg-white rounded-xl shadow-2xl ring-1 ring-black/10 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-gray-900">All Registered Users via Your Referral</h3>
<p className="text-xs text-gray-600">Search, filter, paginate, or export the full list.</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => exportCsv(filtered)}
className="inline-flex items-center rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
>
Export CSV
</button>
<button
onClick={() => setOpen(false)}
className="inline-flex items-center rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white hover:bg-gray-800"
>
Close
</button>
</div>
</div>
{/* Controls */}
<div className="px-6 py-4 border-b border-gray-100 grid grid-cols-1 md:grid-cols-4 gap-3">
<input
value={query}
onChange={e => { setQuery(e.target.value); setPage(1) }}
placeholder="Search name or email…" // CHANGED
className="md:col-span-2 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder:text-gray-700 placeholder:opacity-100"
/>
<select
value={typeFilter}
onChange={e => { setTypeFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900"
>
<option value="all">All Types</option>
<option value="personal">Personal</option>
<option value="company">Company</option>
</select>
<select
value={statusFilter}
onChange={e => { setStatusFilter(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900"
>
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="blocked">Blocked</option>
</select>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Registered</th>
{/* REMOVED: Ref Code column */}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{pageRows.length === 0 ? (
<tr>
<td className="px-6 py-6 text-sm text-gray-500" colSpan={5}>
No users match your filters.
</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-gray-900">{u.name}</td>
<td className="px-6 py-3 text-sm text-gray-700">{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' ? 'Company' : 'Personal'}
</span>
</td>
<td className="px-6 py-3 text-sm text-gray-700">{date}</td>
{/* REMOVED: Ref Code cell */}
<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>
{/* Pagination */}
<div className="px-6 py-4 flex items-center justify-between gap-3">
<span className="text-xs text-gray-600">
Showing {pageRows.length} of {filtered.length} users
</span>
<div className="flex items-center gap-2">
<button
disabled={page <= 1}
onClick={() => setPage(p => Math.max(1, p - 1))}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 disabled:opacity-50 hover:bg-gray-50"
>
Previous
</button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
</span>
<button
disabled={page >= totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 disabled:opacity-50 hover:bg-gray-50"
>
Next
</button>
</div>
</div>
</div>
</div>
</div>
)}
</>
)
}

View File

@ -0,0 +1,120 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import useAuthStore from '../../store/authStore'
export type ReferredUserType = 'personal' | 'company'
export type ReferredUserStatus = 'active' | 'inactive' | null
export interface ReferredUser {
id: string | number
name: string
email: string
type: ReferredUserType
registeredAt: string | null
status: ReferredUserStatus
}
interface ApiResponseShape {
success?: boolean
total_referred_users?: number
users?: any[]
[key: string]: any
}
export function useRegisteredUsers() {
const accessToken = useAuthStore(s => s.accessToken)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>('')
const [total, setTotal] = useState<number>(0)
const [users, setUsers] = useState<ReferredUser[]>([])
const inFlight = useRef<AbortController | null>(null)
const normalize = (raw: any): ReferredUser => ({
id: raw?.id ?? raw?._id ?? raw?.userId ?? '',
name: raw?.name ?? raw?.fullName ?? raw?.companyName ?? raw?.email ?? '',
email: raw?.email ?? '',
type: (raw?.type === 'company' ? 'company' : 'personal') as ReferredUserType,
registeredAt: typeof raw?.registeredAt === 'string' ? raw.registeredAt : (raw?.createdAt || null),
status: (raw?.status === 'active' || raw?.status === 'inactive') ? raw.status : null
})
const fetchRegisteredUsers = useCallback(async () => {
setError('')
if (!accessToken) {
console.warn('useRegisteredUsers: no access token available')
setError('Not authenticated')
return
}
// Abort any in-flight request
inFlight.current?.abort()
const controller = new AbortController()
inFlight.current = controller
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
const url = `${base}/api/referral/referred-users`
console.log('🌐 GET referred-users:', url)
setLoading(true)
try {
const res = await fetch(url, {
method: 'GET',
credentials: 'include',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
signal: controller.signal,
})
const body: ApiResponseShape = await res.json().catch(() => ({}))
console.log('📡 referred-users status:', res.status)
console.log('📦 referred-users body:', body)
if (!res.ok) {
const msg = (body as any)?.message || `Failed to load referred users (${res.status})`
setError(msg)
setUsers([])
setTotal(0)
return
}
const list = Array.isArray(body.users) ? body.users : []
setUsers(list.map(normalize))
setTotal(typeof body.total_referred_users === 'number' ? body.total_referred_users : list.length)
} catch (e: any) {
if (e?.name === 'AbortError') {
console.log('⏹️ referred-users request aborted')
return
}
console.error('❌ referred-users fetch error:', e)
setError('Network error')
setUsers([])
setTotal(0)
} finally {
setLoading(false)
if (inFlight.current === controller) inFlight.current = null
}
}, [accessToken])
// Auto-load when token becomes available
useEffect(() => {
if (accessToken) {
fetchRegisteredUsers()
}
return () => {
inFlight.current?.abort()
}
}, [accessToken, fetchRegisteredUsers])
return {
loading,
error,
total,
users,
reload: fetchRegisteredUsers,
}
}

View File

@ -8,8 +8,10 @@ import DeactivateReferralLinkModal from './components/deactivateReferralLinkModa
import ReferralStatisticWidget from './components/referralStatisticWidget' import ReferralStatisticWidget from './components/referralStatisticWidget'
import GenerateReferralLinkWidget from './components/generateReferralLinkWidget' import GenerateReferralLinkWidget from './components/generateReferralLinkWidget'
import ReferralLinksListWidget from './components/referralLinksListWidget' import ReferralLinksListWidget from './components/referralLinksListWidget'
import RegisteredUserList from './components/registeredUserList'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import { useRegisteredUsers } from './hooks/registeredUsers'
export default function ReferralManagementPage() { export default function ReferralManagementPage() {
const router = useRouter() const router = useRouter()
@ -204,12 +206,23 @@ export default function ReferralManagementPage() {
} }
} }
// NEW: fetch referred users via hook (auto fetch on token)
const {
users: referredUsers,
total: referredTotal,
loading: usersLoading,
error: usersError,
reload: reloadReferredUsers,
} = useRegisteredUsers()
// Load data only when permission is granted // Load data only when permission is granted
useEffect(() => { useEffect(() => {
if (isPermChecked && hasReferralPerm) { if (isPermChecked && hasReferralPerm) {
loadData() loadData()
// Ensure referred users are refreshed when arriving at this page
reloadReferredUsers()
} }
}, [isPermChecked, hasReferralPerm]) }, [isPermChecked, hasReferralPerm]) // eslint-disable-line react-hooks/exhaustive-deps
// Gate rendering until auth + permission resolved // Gate rendering until auth + permission resolved
if (!isAuthReady || !user || !isPermChecked || !hasReferralPerm) { if (!isAuthReady || !user || !isPermChecked || !hasReferralPerm) {
@ -238,7 +251,13 @@ export default function ReferralManagementPage() {
</div> </div>
{/* Stats overview */} {/* Stats overview */}
<ReferralStatisticWidget stats={stats} /> <ReferralStatisticWidget stats={stats} totalReferredFromBackend={referredTotal} /> {/* NEW */}
{/* Registered users (hook data) */}
<RegisteredUserList
users={referredUsers as any} // CHANGED: pass directly; no refCode mapping
loading={usersLoading}
/>
{/* Generator */} {/* Generator */}
<GenerateReferralLinkWidget onCreated={loadData} /> <GenerateReferralLinkWidget onCreated={loadData} />