11'use client' ;
22
3- import React , { useState , useEffect , useCallback } from 'react' ;
3+ import React , { useState , useEffect , useCallback , useMemo , useRef } from 'react' ;
44import type { UserProfile , Registration } from '@/lib/supabase' ;
55import { getSupabase , fetchSessionFromDatabase , getCookie } from '@/lib/supabase' ;
6- import { csvCell , formatReferralCode , GENDER_OPTIONS , safeHttpUrl } from '@/lib/validation' ;
6+ import { csvCell , formatReferralCode , GENDER_OPTIONS , matchesRegistrationSearch , safeHttpUrl , searchTerm } from '@/lib/validation' ;
77
88const genderLabel = ( value : string ) => GENDER_OPTIONS . find ( ( g ) => g . value === value ) ?. label ?? value ;
99import EventsManager from './admin/EventsManager' ;
@@ -19,6 +19,9 @@ import { ID_CARD_REUPLOAD_REASONS, idCardReuploadLabel, type IdCardReuploadReaso
1919/** A registration that still needs its college ID card (supabase/schema.sql section 19); rejected ones never do. */
2020const idCardMissing = ( r : Registration ) => r . status !== 'rejected' && ! r . id_card_path ;
2121
22+ /** A filter or search change reloads the list in the background only if it was last loaded longer ago than this. */
23+ const REGS_FRESH_MS = 60_000 ;
24+
2225/** Roles an admin can give from the USERS tab (/api/admin/users); 'admin' is set in the database only. */
2326type AssignableRole = 'user' | 'it-team' | 'registration-team' | 'viewer' ;
2427
@@ -47,7 +50,10 @@ export default function AdminDashboard({
4750
4851 const defaultTab = canViewRegistrations ? 'registrations' : 'events' ;
4952 const [ tab , setTab ] = useState < 'registrations' | 'referrals' | 'ambassadors' | 'events' | 'gallery' | 'users' | 'entry' > ( defaultTab ) ;
50- const [ registrations , setRegistrations ] = useState < Registration [ ] > ( [ ] ) ;
53+ // Every registration, loaded once: the filters and search below work over it without asking the server again.
54+ const [ allRegistrations , setRegistrations ] = useState < Registration [ ] > ( [ ] ) ;
55+ // Each row's status when loaded: the status filter uses it, so a row just reviewed stays in view until the next load.
56+ const [ loadedStatus , setLoadedStatus ] = useState < Map < string , string > > ( new Map ( ) ) ;
5157 const [ users , setUsers ] = useState < UserProfile [ ] > ( [ ] ) ;
5258 const [ loadingRegs , setLoadingRegs ] = useState ( false ) ;
5359 const [ loadingUsers , setLoadingUsers ] = useState ( false ) ;
@@ -80,6 +86,16 @@ export default function AdminDashboard({
8086 const [ searchReg , setSearchReg ] = useState < string > ( '' ) ;
8187 const [ searchUser , setSearchUser ] = useState < string > ( '' ) ;
8288
89+ const registrations = useMemo ( ( ) => {
90+ const search = searchTerm ( searchReg ) ;
91+ return allRegistrations . filter (
92+ ( r ) =>
93+ ( statusFilter === 'all' || ( loadedStatus . get ( r . id ?? '' ) ?? r . status ) === statusFilter ) &&
94+ ( typeFilter === 'all' || r . student_type === typeFilter ) &&
95+ ( ! search || matchesRegistrationSearch ( r , search ) )
96+ ) ;
97+ } , [ allRegistrations , loadedStatus , statusFilter , typeFilter , searchReg ] ) ;
98+
8399 // Pagination states (30 items per page)
84100 const [ regPage , setRegPage ] = useState < number > ( 1 ) ;
85101 const [ userPage , setUserPage ] = useState < number > ( 1 ) ;
@@ -156,39 +172,48 @@ export default function AdminDashboard({
156172 }
157173 } ;
158174
159- // Fetch registrations
160- const fetchRegistrations = useCallback ( async ( ) => {
161- if ( ! canViewRegistrations ) return ;
175+ const regsRequest = useRef ( 0 ) ;
176+ const regsLoading = useRef ( false ) ;
177+ const regsLoadedAt = useRef ( 0 ) ;
178+
179+ // Fetch every registration. `background` keeps the table on screen; `opening`: the SEAT COUNTER card loads by itself as
180+ // the dashboard opens, so it needn't be told to reload.
181+ const fetchRegistrations = useCallback ( async ( { background = false , opening = false } = { } ) => {
182+ if ( ! canViewRegistrations || ( background && regsLoading . current ) ) return ;
183+ const request = ++ regsRequest . current ;
184+ regsLoading . current = true ;
162185 try {
163- setLoadingRegs ( true ) ;
186+ if ( ! background ) setLoadingRegs ( true ) ;
164187 setErrorMsg ( '' ) ;
165188 const token = await getAuthToken ( ) ;
166189
167- const params = new URLSearchParams ( ) ;
168- if ( statusFilter !== 'all' ) params . set ( 'status' , statusFilter ) ;
169- if ( typeFilter !== 'all' ) params . set ( 'student_type' , typeFilter ) ;
170- if ( searchReg . trim ( ) ) params . set ( 'q' , searchReg . trim ( ) ) ;
171-
172190 const headers : Record < string , string > = { } ;
173191 if ( token ) {
174192 headers [ 'Authorization' ] = `Bearer ${ token } ` ;
175193 }
176194
177- const res = await fetch ( ` /api/admin/registrations? ${ params . toString ( ) } ` , {
195+ const res = await fetch ( ' /api/admin/registrations' , {
178196 headers,
179197 credentials : 'include' ,
180198 } ) ;
181199 const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
200+ if ( request !== regsRequest . current ) return ; // a newer request superseded this one
182201 if ( ! res . ok ) throw new Error ( httpErrorMessage ( res ) || data . error || 'Failed to fetch registrations' ) ;
183202 setRegistrations ( data . registrations || [ ] ) ;
184- setSeatsKey ( ( k ) => k + 1 ) ;
203+ setLoadedStatus ( new Map ( ( data . registrations || [ ] ) . map ( ( r : Registration ) => [ r . id ?? '' , r . status ] ) ) ) ;
204+ regsLoadedAt . current = Date . now ( ) ;
205+ if ( ! opening ) setSeatsKey ( ( k ) => k + 1 ) ;
185206 } catch ( err : unknown ) {
207+ if ( request !== regsRequest . current ) return ;
186208 const message = err instanceof Error ? err . message : 'Error loading registrations' ;
187209 setErrorMsg ( message ) ;
188210 } finally {
189- setLoadingRegs ( false ) ;
211+ if ( request === regsRequest . current ) {
212+ regsLoading . current = false ;
213+ setLoadingRegs ( false ) ;
214+ }
190215 }
191- } , [ statusFilter , typeFilter , searchReg , canViewRegistrations ] ) ;
216+ } , [ canViewRegistrations ] ) ;
192217
193218 // Fetch users (Admin only: IT-Team cannot access users)
194219 const fetchUsers = useCallback ( async ( ) => {
@@ -221,11 +246,33 @@ export default function AdminDashboard({
221246 useEffect ( ( ) => {
222247 if ( ! isOpen ) return ;
223248 const timer = setTimeout ( ( ) => {
224- if ( canViewRegistrations ) fetchRegistrations ( ) ;
225- if ( isAdmin ) fetchUsers ( ) ;
249+ if ( canViewRegistrations ) fetchRegistrations ( { opening : true } ) ;
250+ } , 0 ) ;
251+ return ( ) => clearTimeout ( timer ) ;
252+ } , [ isOpen , fetchRegistrations , canViewRegistrations ] ) ;
253+
254+ // Filtering costs no request, but changing a filter used to bring in other staff's reviews too: still does, if the
255+ // list is more than a minute old.
256+ useEffect ( ( ) => {
257+ if ( ! isOpen || ! regsLoadedAt . current || Date . now ( ) - regsLoadedAt . current < REGS_FRESH_MS ) return ;
258+ const timer = setTimeout ( ( ) => fetchRegistrations ( { background : true } ) , 0 ) ;
259+ return ( ) => clearTimeout ( timer ) ;
260+ } , [ isOpen , statusFilter , typeFilter , searchReg , fetchRegistrations ] ) ;
261+
262+ // The user directory loads when the USERS tab first opens, and again the next time the dashboard is opened.
263+ const usersLoaded = useRef ( false ) ;
264+ useEffect ( ( ) => {
265+ if ( ! isOpen ) {
266+ usersLoaded . current = false ;
267+ return ;
268+ }
269+ if ( ! isAdmin || tab !== 'users' || usersLoaded . current ) return ;
270+ const timer = setTimeout ( ( ) => {
271+ usersLoaded . current = true ;
272+ fetchUsers ( ) ;
226273 } , 0 ) ;
227274 return ( ) => clearTimeout ( timer ) ;
228- } , [ isOpen , fetchRegistrations , fetchUsers , isAdmin , canViewRegistrations ] ) ;
275+ } , [ isOpen , isAdmin , tab , fetchUsers ] ) ;
229276
230277 // Active tab: 'users' is strictly admin-only; staff have their own tabs
231278 let activeTab = tab ;
@@ -1109,7 +1156,7 @@ export default function AdminDashboard({
11091156 cursor : 'pointer' ,
11101157 } }
11111158 >
1112- USERS & ROLES ( { users . length } )
1159+ USERS & ROLES { users . length > 0 ? ` ( $ {users . length } )` : '' }
11131160 </ button >
11141161 ) }
11151162
@@ -1375,7 +1422,7 @@ export default function AdminDashboard({
13751422
13761423 < button
13771424 type = "button"
1378- onClick = { fetchRegistrations }
1425+ onClick = { ( ) => fetchRegistrations ( ) }
13791426 style = { {
13801427 height : '40px' ,
13811428 padding : '0 16px' ,
0 commit comments