Skip to content

Commit 5e38e51

Browse files
authored
Merge pull request #589 from CodebilityDev/myteamboard/juryyy
fix(leaderboard): calculate weekly/monthly points
2 parents 4a6e372 + 9c41744 commit 5e38e51

2 files changed

Lines changed: 230 additions & 152 deletions

File tree

‎apps/codebility/app/api/project-leaderboard/route.ts‎

Lines changed: 120 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -32,62 +32,32 @@ export async function GET(request: NextRequest) {
3232
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
3333
}
3434

35-
// Try to use the optimized database function first
36-
const { data: leaders, error } = await supabase
37-
.rpc('get_project_leaderboard', {
38-
time_filter: timeFilter,
39-
result_limit: limit
40-
});
41-
42-
if (error) {
43-
// Fallback to manual query if RPC doesn't exist
44-
console.warn("RPC function not found, using fallback query:", error);
45-
46-
// Optimized fallback - single query with joins
47-
let baseQuery = `
48-
projects!inner(*),
49-
project_members!inner(
50-
codev_id,
51-
codev_points!inner(
52-
points,
53-
created_at,
54-
skill_category:skill_category_id!inner(name)
55-
)
56-
)
57-
`;
35+
let processedLeaders: ProjectLeader[] = [];
5836

59-
let query = supabase
37+
if (timeFilter === "all") {
38+
// All-time leaderboard uses the codev_points/project_members relationship for historical data
39+
const { data: rawData, error: fallbackError } = await supabase
6040
.from("projects")
61-
.select(baseQuery);
62-
63-
// Apply time filters through the relationship
64-
if (timeFilter === "weekly") {
65-
const weekStart = new Date();
66-
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
67-
weekStart.setHours(0, 0, 0, 0);
68-
query = query.gte("project_members.codev_points.created_at", weekStart.toISOString());
69-
} else if (timeFilter === "monthly") {
70-
const monthStart = new Date();
71-
monthStart.setDate(1);
72-
monthStart.setHours(0, 0, 0, 0);
73-
query = query.gte("project_members.codev_points.created_at", monthStart.toISOString());
74-
}
75-
76-
const { data: rawData, error: fallbackError } = await query.limit(100); // Get more projects to filter later
41+
.select(`
42+
id,
43+
name,
44+
project_members(
45+
codev_id,
46+
codev_points(
47+
points,
48+
skill_category:skill_category_id(name)
49+
)
50+
)
51+
`);
7752

7853
if (fallbackError) {
79-
console.error("Error fetching project leaderboard:", fallbackError);
54+
console.error("Error fetching all-time project leaderboard:", fallbackError);
8055
return NextResponse.json(
8156
{ error: "Failed to fetch leaderboard data", details: fallbackError.message },
8257
{ status: 500 }
8358
);
8459
}
8560

86-
if (!rawData || rawData.length === 0) {
87-
return NextResponse.json({ leaders: [], totalCount: 0 });
88-
}
89-
90-
// Process the complex nested data structure
9161
const projectMap = new Map<string, {
9262
project_id: string;
9363
project_name: string;
@@ -96,11 +66,8 @@ export async function GET(request: NextRequest) {
9666
skill_breakdown: Record<string, number>;
9767
}>();
9868

99-
rawData.forEach((project: any) => {
100-
if (!project.project_members || project.project_members.length === 0) return;
101-
69+
rawData?.forEach((project: any) => {
10270
const projectId = project.id;
103-
10471
if (!projectMap.has(projectId)) {
10572
projectMap.set(projectId, {
10673
project_id: projectId,
@@ -110,63 +77,126 @@ export async function GET(request: NextRequest) {
11077
skill_breakdown: {}
11178
});
11279
}
113-
11480
const projectData = projectMap.get(projectId)!;
11581

116-
project.project_members.forEach((member: any) => {
82+
project.project_members?.forEach((member: any) => {
11783
projectData.members.add(member.codev_id);
118-
119-
if (member.codev_points && member.codev_points.length > 0) {
120-
member.codev_points.forEach((point: any) => {
121-
const skillName = point.skill_category?.name || "Other";
122-
const pointValue = point.points || 0;
123-
124-
projectData.total_points += pointValue;
125-
projectData.skill_breakdown[skillName] =
126-
(projectData.skill_breakdown[skillName] || 0) + pointValue;
127-
});
128-
}
84+
member.codev_points?.forEach((point: any) => {
85+
const skillName = point.skill_category?.name || "Other";
86+
const val = point.points || 0;
87+
projectData.total_points += val;
88+
projectData.skill_breakdown[skillName] = (projectData.skill_breakdown[skillName] || 0) + val;
89+
});
12990
});
13091
});
13192

132-
// Convert to array and sort
133-
const processedLeaders: ProjectLeader[] = Array.from(projectMap.values())
134-
.map(project => ({
135-
project_id: project.project_id,
136-
project_name: project.project_name,
137-
total_points: project.total_points,
138-
member_count: project.members.size,
139-
skill_breakdown: project.skill_breakdown
93+
processedLeaders = Array.from(projectMap.values())
94+
.map(p => ({
95+
project_id: p.project_id,
96+
project_name: p.project_name,
97+
total_points: p.total_points,
98+
member_count: p.members.size,
99+
skill_breakdown: p.skill_breakdown
140100
}))
141-
.filter(project => project.total_points > 0)
142-
.sort((a, b) => {
143-
if (b.total_points === a.total_points) {
144-
return a.project_name.localeCompare(b.project_name);
145-
}
146-
return b.total_points - a.total_points;
147-
})
101+
.filter(p => p.total_points > 0)
102+
.sort((a, b) => b.total_points - a.total_points)
148103
.slice(0, limit);
149104

150-
return NextResponse.json({
151-
leaders: processedLeaders,
152-
totalCount: processedLeaders.length
105+
} else {
106+
// Weekly/Monthly leaderboard calculates points from completed tasks based on approval date (updated_at)
107+
const startDate = new Date();
108+
if (timeFilter === "weekly") {
109+
startDate.setDate(startDate.getDate() - startDate.getDay());
110+
startDate.setHours(0, 0, 0, 0);
111+
} else {
112+
startDate.setDate(1);
113+
startDate.setHours(0, 0, 0, 0);
114+
}
115+
116+
const { data: rawTasks, error: tasksError } = await supabase
117+
.from("tasks")
118+
.select(`
119+
points,
120+
updated_at,
121+
project_id,
122+
codev_id,
123+
sidekick_ids,
124+
project:project_id!inner(name),
125+
skill_category:skill_category_id!inner(name)
126+
`)
127+
.eq("is_archive", true)
128+
.gte("updated_at", startDate.toISOString());
129+
130+
if (tasksError) {
131+
console.error(`Error fetching ${timeFilter} project leaderboard tasks:`, tasksError);
132+
return NextResponse.json(
133+
{ error: "Failed to fetch tasks data", details: tasksError.message },
134+
{ status: 500 }
135+
);
136+
}
137+
138+
const projectMap = new Map<string, {
139+
project_id: string;
140+
project_name: string;
141+
total_points: number;
142+
members: Set<string>;
143+
skill_breakdown: Record<string, number>;
144+
}>();
145+
146+
rawTasks?.forEach((task: any) => {
147+
const projectId = task.project_id;
148+
if (!projectId) return;
149+
150+
if (!projectMap.has(projectId)) {
151+
projectMap.set(projectId, {
152+
project_id: projectId,
153+
project_name: task.project?.name || "Unknown Project",
154+
total_points: 0,
155+
members: new Set(),
156+
skill_breakdown: {}
157+
});
158+
}
159+
const projectData = projectMap.get(projectId)!;
160+
const skillName = task.skill_category?.name || "Other";
161+
const points = task.points || 0;
162+
const sidekickPoints = Math.floor(points * 0.5);
163+
164+
// Add primary assignee points
165+
if (task.codev_id) {
166+
projectData.members.add(task.codev_id);
167+
projectData.total_points += points;
168+
projectData.skill_breakdown[skillName] = (projectData.skill_breakdown[skillName] || 0) + points;
169+
}
170+
171+
// Add sidekick points
172+
if (task.sidekick_ids && Array.isArray(task.sidekick_ids)) {
173+
task.sidekick_ids.forEach((sid: string) => {
174+
projectData.members.add(sid);
175+
projectData.total_points += sidekickPoints;
176+
projectData.skill_breakdown[skillName] = (projectData.skill_breakdown[skillName] || 0) + sidekickPoints;
177+
});
178+
}
153179
});
154-
}
155180

156-
// Process RPC results
157-
const processedLeaders = leaders?.map((leader: any) => ({
158-
project_id: leader.project_id,
159-
project_name: leader.project_name || "Unknown Project",
160-
total_points: leader.total_points || 0,
161-
member_count: leader.member_count || 0,
162-
skill_breakdown: leader.skill_breakdown || {}
163-
})) || [];
181+
processedLeaders = Array.from(projectMap.values())
182+
.map(p => ({
183+
project_id: p.project_id,
184+
project_name: p.project_name,
185+
total_points: p.total_points,
186+
member_count: p.members.size,
187+
skill_breakdown: p.skill_breakdown
188+
}))
189+
.filter(p => p.total_points > 0)
190+
.sort((a, b) => b.total_points - a.total_points)
191+
.slice(0, limit);
192+
}
164193

165194
return NextResponse.json({
166195
leaders: processedLeaders,
167196
totalCount: processedLeaders.length
168197
});
169198

199+
170200
} catch (error) {
171201
console.error("API error:", error);
172202

0 commit comments

Comments
 (0)