Skip to content

Commit e75f96a

Browse files
committed
fix: handle potential null values for user name and email in AdminPerformancePage and AdminUserManagementPage
1 parent 069afa9 commit e75f96a

File tree

2 files changed

+113
-24
lines changed

2 files changed

+113
-24
lines changed

client/pages/AdminPerformancePage.tsx

Lines changed: 110 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,22 @@ export const AdminPerformancePage: React.FC = () => {
3838
usersApi.getAll()
3939
]);
4040

41-
// 1. Initialize stats map with ALL users using ID as key
42-
const statsMap: Record<number, UserStats> = {};
41+
console.log('Leaderboard Data - Users:', users.length, 'Tasks:', tasks.length);
42+
43+
// 1. Initialize stats map with ALL users using email as key (for backward compatibility)
44+
const statsMap: Record<string, UserStats> = {};
4345

4446
users.forEach(u => {
45-
const formattedRole = u.role.replace('ROLE_', '').split('_')
47+
const formattedRole = u.role ? u.role.replace('ROLE_', '').split('_')
4648
.map(word => word.charAt(0) + word.slice(1).toLowerCase())
47-
.join(' ');
49+
.join(' ') : 'Unknown';
50+
51+
if (!u.email) {
52+
console.warn('Skipping user without email:', u.name || u.id);
53+
return; // Skip users without email
54+
}
4855

49-
statsMap[u.id] = {
56+
statsMap[u.email] = {
5057
id: u.id,
5158
name: u.name,
5259
email: u.email,
@@ -68,8 +75,8 @@ export const AdminPerformancePage: React.FC = () => {
6875
if (task.assignedToList && task.assignedToList.length > 0) {
6976
// For multi-assignee tasks, count for ALL assignees
7077
task.assignedToList.forEach(assigneeEmail => {
71-
// Find user by email
72-
const matchedStats = Object.values(statsMap).find(s => s.email === assigneeEmail);
78+
// Use email as key to find user stats directly
79+
const matchedStats = statsMap[assigneeEmail];
7380

7481
if (matchedStats) {
7582
matchedStats.total++;
@@ -86,11 +93,20 @@ export const AdminPerformancePage: React.FC = () => {
8693
// Fallback to old single-assignee logic for backward compatibility
8794
let matchedStats: UserStats | undefined;
8895

89-
if (task.assigneeId && statsMap[task.assigneeId]) {
90-
matchedStats = statsMap[task.assigneeId];
91-
} else if (task.assignedTo && task.assignedTo !== 'Unassigned') {
92-
// Fallback for legacy tasks without ID
93-
matchedStats = Object.values(statsMap).find(s => s.name === task.assignedTo);
96+
// Try to find by assigneeId first (look up email by id)
97+
if (task.assigneeId) {
98+
matchedStats = Object.values(statsMap).find(s => s.id === task.assigneeId);
99+
}
100+
101+
// If not found, try matching by name
102+
if (!matchedStats && task.assignedTo && task.assignedTo !== 'Unassigned') {
103+
// Try exact email match first (in case assignedTo is an email)
104+
matchedStats = statsMap[task.assignedTo];
105+
106+
// If not found, try name match as final fallback
107+
if (!matchedStats) {
108+
matchedStats = Object.values(statsMap).find(s => s.name === task.assignedTo);
109+
}
94110
}
95111

96112
if (matchedStats) {
@@ -113,6 +129,9 @@ export const AdminPerformancePage: React.FC = () => {
113129
completionRate: s.total > 0 ? (s.completed / s.total) * 100 : 0
114130
}));
115131

132+
console.log('Leaderboard - Total users in statsMap:', Object.keys(statsMap).length);
133+
console.log('Leaderboard - Final stats array:', finalStats.length);
134+
116135
// 4. SORTING LOGIC: Completed Volume (Desc) -> Efficiency Score (Desc) -> Total Assigned (Desc)
117136
// This ensures users with high output rank higher than users with low volume but 100% rate.
118137
finalStats.sort((a, b) => {
@@ -128,6 +147,7 @@ export const AdminPerformancePage: React.FC = () => {
128147
return b.total - a.total;
129148
});
130149

150+
console.log('Leaderboard - Setting stats with', finalStats.length, 'users');
131151
setStats(finalStats);
132152
} catch (e) {
133153
console.error(e);
@@ -239,11 +259,23 @@ export const AdminPerformancePage: React.FC = () => {
239259
</div>
240260
) : (
241261
<>
242-
{/* Podium - Only show if we have data */}
243-
{topPerformers.length > 0 && stats.length >= 3 && (
244-
<div className="mb-16 lg:mb-24 px-4">
245-
<div className="flex flex-col md:flex-row justify-center items-end gap-6 md:gap-10 max-w-5xl mx-auto">
246-
{[topPerformers[1], topPerformers[0], topPerformers[2]].map((user, i) => {
262+
{stats.length === 0 ? (
263+
<div className="text-center py-20">
264+
<div className="inline-flex items-center justify-center h-20 w-20 rounded-full bg-slate-100 mb-6">
265+
<Trophy className="h-10 w-10 text-slate-300" />
266+
</div>
267+
<h3 className="text-xl font-black text-slate-900 mb-2">No Performance Data</h3>
268+
<p className="text-slate-500 text-sm">No users or tasks found. Check your data or try refreshing.</p>
269+
</div>
270+
) : (
271+
<>
272+
{/* Podium - Show if we have at least 1 user */}
273+
{topPerformers.length > 0 && (
274+
<div className="mb-16 lg:mb-24 px-4">
275+
<div className="flex flex-col md:flex-row justify-center items-end gap-6 md:gap-10 max-w-5xl mx-auto">
276+
{/* Show top 3 if available, otherwise show what we have */}
277+
{stats.length >= 3 ? (
278+
[topPerformers[1], topPerformers[0], topPerformers[2]].map((user, i) => {
247279
if (!user) return null;
248280
const rank = i === 1 ? 0 : i === 0 ? 1 : 2;
249281
const config = getRankConfig(rank);
@@ -295,7 +327,62 @@ export const AdminPerformancePage: React.FC = () => {
295327
</div>
296328
</div>
297329
);
298-
})}
330+
})
331+
) : (
332+
// Show available users in order for less than 3 users
333+
topPerformers.map((user, idx) => {
334+
if (!user) return null;
335+
const config = getRankConfig(idx);
336+
337+
return (
338+
<div key={user.name} className={`w-full md:w-80 flex flex-col ${config.containerClass}`}>
339+
<div
340+
onClick={() => setSelectedUser(user)}
341+
className={`relative p-8 rounded-[2.5rem] border backdrop-blur-xl flex flex-col items-center text-center transition-all duration-500 hover:-translate-y-2 cursor-pointer group ${config.cardClass}`}
342+
>
343+
<div className="absolute -top-6">
344+
<div className="bg-white p-3 rounded-2xl shadow-lg border border-slate-100">
345+
{config.icon}
346+
</div>
347+
</div>
348+
349+
<div className="mt-8 mb-5 relative">
350+
<div className={`p-1.5 rounded-[1.5rem] bg-white shadow-xl ring-4 ${config.ringColor}`}>
351+
{user.avatarUrl ? (
352+
<img src={user.avatarUrl} alt={user.name} referrerPolicy="no-referrer" className="h-20 w-20 rounded-[1.2rem] object-cover" />
353+
) : (
354+
<div className="h-20 w-20 rounded-[1.2rem] bg-gradient-to-tr from-slate-100 to-slate-200 flex items-center justify-center text-2xl font-black text-slate-400 uppercase tracking-tighter">
355+
{user.name.slice(0, 2)}
356+
</div>
357+
)}
358+
</div>
359+
<div className="absolute -bottom-3 inset-x-0 flex justify-center">
360+
<span className="bg-slate-900 text-white text-[9px] font-black px-3 py-1 rounded-xl shadow-lg border-2 border-white uppercase tracking-widest">
361+
{user.completed} Completed
362+
</span>
363+
</div>
364+
</div>
365+
366+
<h3 className={`text-xl font-black mb-2 truncate w-full tracking-tight ${config.titleColor}`}>{user.name}</h3>
367+
<span className={`text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-lg mb-6 ${config.badgeClass}`}>
368+
{user.role}
369+
</span>
370+
371+
<div className="grid grid-cols-2 gap-3 w-full mt-auto">
372+
<div className="bg-white/60 p-3 rounded-2xl border border-white shadow-inner">
373+
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Assigned</p>
374+
<p className="text-xl font-black text-slate-900">{user.total}</p>
375+
</div>
376+
<div className="bg-white/60 p-3 rounded-2xl border border-white shadow-inner">
377+
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Efficiency</p>
378+
<p className="text-xl font-black text-slate-900">{user.completionRate.toFixed(0)}%</p>
379+
</div>
380+
</div>
381+
</div>
382+
</div>
383+
);
384+
})
385+
)}
299386
</div>
300387
</div>
301388
)}
@@ -389,14 +476,16 @@ export const AdminPerformancePage: React.FC = () => {
389476
</div>
390477
</div>
391478
</td>
392-
</tr>
479+
</tr>
393480
))}
394481
</tbody>
395482
</table>
396483
</div>
397484
</div>
398-
</>
399-
)}
485+
</>
486+
)}
487+
</>
488+
)}
400489
</main>
401490
</div>
402491

client/pages/AdminUserManagementPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export const AdminUserManagementPage: React.FC = () => {
6666
}, []);
6767

6868
const filteredUsers = users.filter(u => {
69-
const matchesSearch = u.name.toLowerCase().includes(search.toLowerCase()) ||
70-
u.email.toLowerCase().includes(search.toLowerCase());
69+
const matchesSearch = (u.name && u.name.toLowerCase().includes(search.toLowerCase())) ||
70+
(u.email && u.email.toLowerCase().includes(search.toLowerCase()));
7171
const matchesRole = filterRole === '' || u.role === filterRole;
7272
return matchesSearch && matchesRole;
7373
}).sort((a, b) => {
@@ -203,7 +203,7 @@ export const AdminUserManagementPage: React.FC = () => {
203203
user.role === 'ROLE_CLIENT' ? 'bg-emerald-50 text-emerald-700 border-emerald-100' :
204204
'bg-slate-50 text-slate-600 border-slate-100'
205205
}`}>
206-
{user.role.replace('ROLE_', '').replace('_', ' ')}
206+
{user.role ? user.role.replace('ROLE_', '').replace('_', ' ') : 'Unknown'}
207207
</span>
208208
</td>
209209
<td className="px-6 lg:px-8 py-4 lg:py-5">

0 commit comments

Comments
 (0)