11"use client" ;
22
3- import { useState , useEffect } from "react" ;
3+ import { useState , useEffect , useRef } from "react" ;
44import Image from "next/image" ;
55import { SimpleMemberData } from "@/app/home/projects/actions" ;
66import DefaultAvatar from "@/components/DefaultAvatar" ;
7- import { Mail , Phone , Calendar , MapPin , Briefcase , Trophy , RefreshCw } from "lucide-react" ;
7+ import { Mail , Calendar , Trophy , RefreshCw , AlertCircle } from "lucide-react" ;
88import { multipleMoveApplicantToOnboardingAction } from "@/app/home/applicants/_service/action" ;
99const ATTENDANCE_POINTS_PER_DAY = 2 ;
1010import { getTeamMonthlyAttendancePoints } from "../actions" ;
@@ -23,23 +23,31 @@ interface MemberPoints {
2323 } ;
2424}
2525
26+ // Fix #4: Track per-member fetch errors separately from points data
27+ interface MemberErrors {
28+ [ codevId : string ] : boolean ;
29+ }
30+
2631const CompactMemberGrid = ( { members, teamLead, projectId } : CompactMemberGridProps ) => {
2732 const allMembers = teamLead ? [ teamLead , ...members ] : members ;
33+ const memberKey = allMembers . map ( ( m ) => m . id ) . join ( "," ) ;
2834 const [ memberPoints , setMemberPoints ] = useState < MemberPoints > ( { } ) ;
29- const [ isLoadingPoints , setIsLoadingPoints ] = useState ( false ) ; // Automatically loads on mount
35+ const [ memberErrors , setMemberErrors ] = useState < MemberErrors > ( { } ) ;
36+ const [ isLoadingPoints , setIsLoadingPoints ] = useState ( false ) ;
3037 const [ monthlyAttendancePoints , setMonthlyAttendancePoints ] = useState < { uniqueCodevIdsPresentDays : { codevId : string ; presentDays : number } [ ] ; } > ( { uniqueCodevIdsPresentDays : [ ] } ) ;
3138 const [ selectedMember , setSelectedMember ] = useState < SimpleMemberData | null > ( null ) ;
3239 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
40+ const loadedMemberIdsRef = useRef < string > ( "" ) ;
3341
3442 // Load points for all members - runs automatically on mount and can be refreshed
3543 const loadAllPoints = async ( ) => {
36- if ( isLoadingPoints ) return ; // Prevent multiple simultaneous calls
37-
44+ if ( isLoadingPoints ) return ;
45+
3846 setIsLoadingPoints ( true ) ;
3947 const pointsData : MemberPoints = { } ;
48+ const errorsData : MemberErrors = { } ;
4049
4150 try {
42- // Load points in smaller batches to improve perceived performance
4351 const batchSize = 3 ;
4452 for ( let i = 0 ; i < allMembers . length ; i += batchSize ) {
4553 const batch = allMembers . slice ( i , i + batchSize ) ;
@@ -48,24 +56,29 @@ const CompactMemberGrid = ({ members, teamLead, projectId }: CompactMemberGridPr
4856 const response = await fetch ( `/api/codev/${ member . id } /points` ) ;
4957 if ( ! response . ok ) throw new Error ( 'Failed to fetch' ) ;
5058 const data = await response . json ( ) as { totalPoints ?: number ; attendancePoints ?: number } ;
51- return { memberId : member . id , data } ;
59+ return { memberId : member . id , data, failed : false } ;
5260 } catch ( error ) {
61+ // Fix #4: Record the failure instead of silently substituting 0
5362 console . warn ( `Failed to load points for ${ member . id } :` , error ) ;
54- return { memberId : member . id , data : { totalPoints : 0 , attendancePoints : 0 } } ;
63+ return { memberId : member . id , data : null , failed : true } ;
5564 }
5665 } ) ;
5766
5867 const results = await Promise . all ( pointsPromises ) ;
59-
60- results . forEach ( ( { memberId, data } ) => {
61- pointsData [ memberId ] = {
62- totalPoints : data . totalPoints || 0 ,
63- attendancePoints : data . attendancePoints || 0
64- } ;
68+
69+ results . forEach ( ( { memberId, data, failed } ) => {
70+ if ( failed || ! data ) {
71+ errorsData [ memberId ] = true ;
72+ } else {
73+ pointsData [ memberId ] = {
74+ totalPoints : data . totalPoints || 0 ,
75+ attendancePoints : data . attendancePoints || 0 ,
76+ } ;
77+ }
6578 } ) ;
6679
67- // Update state after each batch for progressive loading
6880 setMemberPoints ( prev => ( { ...prev , ...pointsData } ) ) ;
81+ setMemberErrors ( prev => ( { ...prev , ...errorsData } ) ) ;
6982 }
7083 } catch ( error ) {
7184 console . error ( "Error loading points:" , error ) ;
@@ -74,6 +87,26 @@ const CompactMemberGrid = ({ members, teamLead, projectId }: CompactMemberGridPr
7487 }
7588 } ;
7689
90+ // Retry a single member's points fetch
91+ const retryMemberPoints = async ( memberId : string ) => {
92+ setMemberErrors ( prev => ( { ...prev , [ memberId ] : false } ) ) ;
93+ try {
94+ const response = await fetch ( `/api/codev/${ memberId } /points` ) ;
95+ if ( ! response . ok ) throw new Error ( 'Failed to fetch' ) ;
96+ const data = await response . json ( ) as { totalPoints ?: number ; attendancePoints ?: number } ;
97+ setMemberPoints ( prev => ( {
98+ ...prev ,
99+ [ memberId ] : {
100+ totalPoints : data . totalPoints || 0 ,
101+ attendancePoints : data . attendancePoints || 0 ,
102+ } ,
103+ } ) ) ;
104+ } catch ( error ) {
105+ console . warn ( `Retry failed for ${ memberId } :` , error ) ;
106+ setMemberErrors ( prev => ( { ...prev , [ memberId ] : true } ) ) ;
107+ }
108+ } ;
109+
77110 useEffect ( ( ) => {
78111 const loadAttendancePoints = async ( ) => {
79112 const currentDate = new Date ( ) ;
@@ -85,23 +118,25 @@ const CompactMemberGrid = ({ members, teamLead, projectId }: CompactMemberGridPr
85118
86119 if ( result . success ) {
87120 setMonthlyAttendancePoints ( {
88- uniqueCodevIdsPresentDays : result . uniqueCodevIdsPresentDays || [ ]
121+ uniqueCodevIdsPresentDays : ( result . uniqueCodevIdsPresentDays || [ ] ) . map ( item => ( {
122+ codevId : item . codevId ,
123+ presentDays : item . presentDays ?? 0
124+ } ) )
89125 } ) ;
90126 }
91127 } ;
92128
93129 loadAttendancePoints ( ) ;
94130 } , [ projectId ] ) ;
95131
96- // Automatically load points when component mounts
97132 useEffect ( ( ) => {
98- if ( allMembers . length > 0 ) {
99- loadAllPoints ( ) ;
100- }
101- // eslint-disable-next-line react-hooks/exhaustive-deps
102- } , [ ] ) ; // Only run once on mount
133+ if ( allMembers . length === 0 ) return ;
134+ if ( memberKey === loadedMemberIdsRef . current ) return ;
135+ loadedMemberIdsRef . current = memberKey ;
136+ loadAllPoints ( ) ;
137+ } , [ memberKey ] ) ;
103138
104- const hasPointsData = Object . keys ( memberPoints ) . length > 0 ;
139+ const hasPointsData = Object . keys ( memberPoints ) . length > 0 || Object . keys ( memberErrors ) . length > 0 ;
105140
106141 const handleMemberClick = ( member : SimpleMemberData ) => {
107142 setSelectedMember ( member ) ;
@@ -115,7 +150,7 @@ const CompactMemberGrid = ({ members, teamLead, projectId }: CompactMemberGridPr
115150
116151 return (
117152 < div className = "space-y-4" >
118- { /* Refresh Points Button - Shows when data is loaded */ }
153+ { /* Refresh Points Button */ }
119154 { hasPointsData && ! isLoadingPoints && allMembers . length > 0 && (
120155 < div className = "flex justify-end" >
121156 < button
@@ -142,104 +177,125 @@ const CompactMemberGrid = ({ members, teamLead, projectId }: CompactMemberGridPr
142177 const isLead = teamLead ?. id === member . id ;
143178 const imageUrl = member . image_url || "/assets/images/default-avatar-200x200.jpg" ;
144179 const memberHasPoints = memberPoints [ member . id ] ;
180+ const memberHasError = memberErrors [ member . id ] ;
145181
146182 return (
147183 < div
148184 key = { member . id }
149185 onClick = { ( ) => handleMemberClick ( member ) }
150186 className = "relative rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all duration-200 hover:shadow-md hover:scale-105 hover:border-blue-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-blue-600 overflow-hidden cursor-pointer"
151187 >
152- { /* Status indicator */ }
153- < div className = "absolute top-2 right-2 h-2 w-2 rounded-full bg-green-500" title = "Active" />
154-
155- < div className = "flex items-start gap-3" >
156- { /* Avatar */ }
157- < div className = "relative flex-shrink-0" >
158- { member . image_url ? (
159- < Image
160- src = { imageUrl }
161- alt = { `${ member . first_name } ${ member . last_name } ` }
162- width = { 48 }
163- height = { 48 }
164- className = "h-12 w-12 rounded-full object-cover"
165- />
166- ) : (
167- < DefaultAvatar size = { 48 } />
168- ) }
169- { isLead && (
170- < div className = "absolute -bottom-1 -right-1 rounded-full bg-blue-500 px-1.5 py-0.5" >
171- < span className = "text-xs font-bold text-white" > TL</ span >
172- </ div >
173- ) }
174- </ div >
175-
176- { /* Info */ }
177- < div className = "min-w-0 flex-1" >
178- < h4 className = "text-sm sm:text-base font-semibold text-gray-900 dark:text-white truncate" >
179- { member . first_name } { member . last_name }
188+ { /* Status indicator */ }
189+ < div className = "absolute top-2 right-2 h-2 w-2 rounded-full bg-green-500" title = "Active" />
180190
191+ < div className = "flex items-start gap-3" >
192+ { /* Avatar */ }
193+ < div className = "relative flex-shrink-0" >
194+ { member . image_url ? (
195+ < Image
196+ src = { imageUrl }
197+ alt = { `${ member . first_name } ${ member . last_name } ` }
198+ width = { 48 }
199+ height = { 48 }
200+ className = "h-12 w-12 rounded-full object-cover"
201+ />
202+ ) : (
203+ < DefaultAvatar size = { 48 } />
204+ ) }
205+ { isLead && (
206+ < div className = "absolute -bottom-1 -right-1 rounded-full bg-blue-500 px-1.5 py-0.5" >
207+ < span className = "text-xs font-bold text-white" > TL</ span >
208+ </ div >
209+ ) }
210+ </ div >
181211
182- </ h4 >
212+ { /* Info */ }
213+ < div className = "min-w-0 flex-1" >
214+ < h4 className = "text-sm sm:text-base font-semibold text-gray-900 dark:text-white truncate" >
215+ { member . first_name } { member . last_name }
216+ </ h4 >
183217
184- < p className = "text-xs sm:text-sm text-gray-600 dark:text-gray-400 truncate" >
185- { member . display_position || "Team Member" }
186- </ p >
218+ < p className = "text-xs sm:text-sm text-gray-600 dark:text-gray-400 truncate" >
219+ { member . display_position || "Team Member" }
220+ </ p >
187221
188- { /* Quick stats */ }
189- < div className = "mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 text-xs text-gray-500 dark:text-gray-400" >
190- < span className = "flex items-center gap-1" >
191- < Calendar className = "h-3 w-3 flex-shrink-0" />
192- { new Date ( member . joined_at ) . getFullYear ( ) }
193- </ span >
194- < span className = "flex items-center gap-1 min-w-0" >
195- < Mail className = "h-3 w-3 flex-shrink-0" />
196- < span className = "truncate" > { member . email_address . split ( '@' ) [ 0 ] } </ span >
197- </ span >
222+ { /* Quick stats */ }
223+ < div className = "mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 text-xs text-gray-500 dark:text-gray-400" >
224+ < span className = "flex items-center gap-1" >
225+ < Calendar className = "h-3 w-3 flex-shrink-0" />
226+ { new Date ( member . joined_at ) . getFullYear ( ) }
227+ </ span >
228+ < span className = "flex items-center gap-1 min-w-0" >
229+ < Mail className = "h-3 w-3 flex-shrink-0" />
230+ < span className = "truncate" > { member . email_address . split ( '@' ) [ 0 ] } </ span >
231+ </ span >
232+ </ div >
198233 </ div >
199234 </ div >
200- </ div >
201235
202- { /* Points and Attendance summary */ }
203- { ( memberHasPoints || hasPointsData ) && (
204- < div className = "mt-3 border-t border-gray-100 pt-2 dark:border-gray-700" >
205- < div className = "flex items-center justify-between" >
206- < div className = "flex items-center gap-2 text-xs" >
207- < Trophy className = "h-3 w-3 text-yellow-500" />
208- < span className = "text-gray-500" > Total Points:</ span >
209- { memberHasPoints ? (
210- < span className = "font-semibold text-gray-900 dark:text-white" >
211- { memberPoints [ member . id ] ?. totalPoints || 0 }
236+ { /* Points and Attendance summary */ }
237+ { ( memberHasPoints || memberHasError || hasPointsData ) && (
238+ < div className = "mt-3 border-t border-gray-100 pt-2 dark:border-gray-700" >
239+ { /* Fix #4: Show error state with retry instead of silent 0 */ }
240+ { memberHasError ? (
241+ < div className = "flex items-center justify-between text-xs" >
242+ < span className = "flex items-center gap-1 text-amber-600 dark:text-amber-400" >
243+ < AlertCircle className = "h-3 w-3" />
244+ Stats unavailable
212245 </ span >
213- ) : (
214- < div className = "w-8 h-3 bg-gray-200 animate-pulse rounded" > </ div >
215- ) }
216- </ div >
217- < div className = "text-xs" >
218- < span className = "text-gray-500" > Attendance:</ span >
219- < span className = "ml-1 font-medium text-green-600 dark:text-green-400" >
220- +{ ( ( ) => {
221- const found = monthlyAttendancePoints . uniqueCodevIdsPresentDays . find (
222- item => item . codevId === member . id
223- ) ;
224- return found ? found . presentDays * ATTENDANCE_POINTS_PER_DAY : 0 ;
225- } ) ( ) }
226- </ span >
227- </ div >
246+ < button
247+ onClick = { ( e ) => {
248+ e . stopPropagation ( ) ;
249+ retryMemberPoints ( member . id ) ;
250+ } }
251+ className = "flex items-center gap-1 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium"
252+ >
253+ < RefreshCw className = "h-3 w-3" />
254+ Retry
255+ </ button >
256+ </ div >
257+ ) : (
258+ < >
259+ < div className = "flex items-center justify-between" >
260+ < div className = "flex items-center gap-2 text-xs" >
261+ < Trophy className = "h-3 w-3 text-yellow-500" />
262+ < span className = "text-gray-500" > Total Points:</ span >
263+ { memberHasPoints ? (
264+ < span className = "font-semibold text-gray-900 dark:text-white" >
265+ { memberPoints [ member . id ] ?. totalPoints || 0 }
266+ </ span >
267+ ) : (
268+ < div className = "w-8 h-3 bg-gray-200 animate-pulse rounded" > </ div >
269+ ) }
270+ </ div >
271+ < div className = "text-xs" >
272+ < span className = "text-gray-500" > Attendance:</ span >
273+ < span className = "ml-1 font-medium text-green-600 dark:text-green-400" >
274+ +{ ( ( ) => {
275+ const found = monthlyAttendancePoints . uniqueCodevIdsPresentDays . find (
276+ item => item . codevId === member . id
277+ ) ;
278+ return found ? found . presentDays * ATTENDANCE_POINTS_PER_DAY : 0 ;
279+ } ) ( ) }
280+ </ span >
281+ </ div >
282+ </ div >
283+ < div className = "mt-1 flex items-center gap-2 text-xs text-gray-500" >
284+ < span > This month: </ span >
285+ < span className = "font-medium text-green-600 dark:text-green-400" >
286+ { monthlyAttendancePoints . uniqueCodevIdsPresentDays . find (
287+ item => item . codevId === member . id
288+ ) ?. presentDays || 0 } d
289+ </ span >
290+ < span > present</ span >
291+ </ div >
292+ </ >
293+ ) }
228294 </ div >
229- < div className = "mt-1 flex items-center gap-2 text-xs text-gray-500" >
230- < span > This month: </ span >
231- < span className = "font-medium text-green-600 dark:text-green-400" >
232- { monthlyAttendancePoints . uniqueCodevIdsPresentDays . find (
233- item => item . codevId === member . id
234- ) ?. presentDays || 0 } d
235- </ span >
236- < span > present</ span >
237- </ div >
238- </ div >
239- ) }
240- </ div >
241- ) ;
242- } ) }
295+ ) }
296+ </ div >
297+ ) ;
298+ } ) }
243299 </ div >
244300
245301 { /* Member Detail Modal */ }
0 commit comments