Skip to content

Commit 8bbc78d

Browse files
authored
Merge pull request #597 from CodebilityDev/leaderboardaccuracy/juryyy
fix(leaderboard): resolve accuracy issues with weekly dates and total…
2 parents 4d1949f + ce3a3d4 commit 8bbc78d

6 files changed

Lines changed: 212 additions & 296 deletions

File tree

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

Lines changed: 82 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { createClientServerComponent } from "@/utils/supabase/server";
33
import { z } from "zod";
4+
import { getWeekRange, getMonthRange } from "@/lib/leaderboard-utils";
45

56
interface ProjectLeader {
67
project_id: string;
@@ -34,169 +35,99 @@ export async function GET(request: NextRequest) {
3435

3536
let processedLeaders: ProjectLeader[] = [];
3637

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
40-
.from("projects")
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-
`);
52-
53-
if (fallbackError) {
54-
console.error("Error fetching all-time project leaderboard:", fallbackError);
55-
return NextResponse.json(
56-
{ error: "Failed to fetch leaderboard data", details: fallbackError.message },
57-
{ status: 500 }
58-
);
59-
}
38+
// The tasks table is the single source of truth for points to ensure consistency
39+
// across all-time, monthly, and weekly leaderboards.
40+
let query = supabase
41+
.from("tasks")
42+
.select(`
43+
points,
44+
approved_at,
45+
project_id,
46+
codev_id,
47+
sidekick_ids,
48+
project:project_id!inner(name),
49+
skill_category:skill_category_id!inner(name)
50+
`)
51+
.eq("is_archive", true);
52+
53+
if (timeFilter === "weekly") {
54+
const { startDate, endDate } = getWeekRange();
55+
query = query.gte("approved_at", startDate.toISOString()).lte("approved_at", endDate.toISOString());
56+
} else if (timeFilter === "monthly") {
57+
const { startDate, endDate } = getMonthRange();
58+
query = query.gte("approved_at", startDate.toISOString()).lte("approved_at", endDate.toISOString());
59+
}
60+
61+
const { data: rawTasks, error: tasksError } = await query;
62+
63+
if (tasksError) {
64+
console.error(`Error fetching ${timeFilter} project leaderboard tasks:`, tasksError);
65+
return NextResponse.json(
66+
{ error: "Failed to fetch tasks data", details: tasksError.message },
67+
{ status: 500 }
68+
);
69+
}
6070

61-
const projectMap = new Map<string, {
62-
project_id: string;
63-
project_name: string;
64-
total_points: number;
65-
members: Set<string>;
66-
skill_breakdown: Record<string, number>;
67-
}>();
68-
69-
rawData?.forEach((project: any) => {
70-
const projectId = project.id;
71-
if (!projectMap.has(projectId)) {
72-
projectMap.set(projectId, {
73-
project_id: projectId,
74-
project_name: project.name,
75-
total_points: 0,
76-
members: new Set(),
77-
skill_breakdown: {}
78-
});
79-
}
80-
const projectData = projectMap.get(projectId)!;
81-
82-
project.project_members?.forEach((member: any) => {
83-
projectData.members.add(member.codev_id);
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-
});
71+
const projectMap = new Map<string, {
72+
project_id: string;
73+
project_name: string;
74+
total_points: number;
75+
members: Set<string>;
76+
skill_breakdown: Record<string, number>;
77+
}>();
78+
79+
rawTasks?.forEach((task: any) => {
80+
const projectId = task.project_id;
81+
if (!projectId) return;
82+
83+
if (!projectMap.has(projectId)) {
84+
projectMap.set(projectId, {
85+
project_id: projectId,
86+
project_name: task.project?.name || "Unknown Project",
87+
total_points: 0,
88+
members: new Set(),
89+
skill_breakdown: {}
9090
});
91-
});
92-
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
100-
}))
101-
.filter(p => p.total_points > 0)
102-
.sort((a, b) => b.total_points - a.total_points)
103-
.slice(0, limit);
104-
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);
91+
}
92+
const projectData = projectMap.get(projectId)!;
93+
const skillName = task.skill_category?.name || "Other";
94+
const points = task.points || 0;
95+
const sidekickPoints = Math.floor(points * 0.5);
96+
97+
// Add primary assignee points
98+
if (task.codev_id) {
99+
projectData.members.add(task.codev_id);
100+
projectData.total_points += points;
101+
projectData.skill_breakdown[skillName] = (projectData.skill_breakdown[skillName] || 0) + points;
114102
}
115103

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-
);
104+
// Add sidekick points
105+
if (task.sidekick_ids && Array.isArray(task.sidekick_ids)) {
106+
task.sidekick_ids.forEach((sid: string) => {
107+
projectData.members.add(sid);
108+
projectData.total_points += sidekickPoints;
109+
projectData.skill_breakdown[skillName] = (projectData.skill_breakdown[skillName] || 0) + sidekickPoints;
110+
});
136111
}
112+
});
137113

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-
}
179-
});
180-
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-
}
114+
processedLeaders = Array.from(projectMap.values())
115+
.map(p => ({
116+
project_id: p.project_id,
117+
project_name: p.project_name,
118+
total_points: p.total_points,
119+
member_count: p.members.size,
120+
skill_breakdown: p.skill_breakdown
121+
}))
122+
.filter(p => p.total_points > 0)
123+
.sort((a, b) => b.total_points - a.total_points)
124+
.slice(0, limit);
193125

194126
return NextResponse.json({
195127
leaders: processedLeaders,
196128
totalCount: processedLeaders.length
197129
});
198130

199-
200131
} catch (error) {
201132
console.error("API error:", error);
202133

0 commit comments

Comments
 (0)