|
1 | 1 | import { NextRequest, NextResponse } from "next/server"; |
2 | 2 | import { createClientServerComponent } from "@/utils/supabase/server"; |
3 | 3 | import { z } from "zod"; |
| 4 | +import { getWeekRange, getMonthRange } from "@/lib/leaderboard-utils"; |
4 | 5 |
|
5 | 6 | interface ProjectLeader { |
6 | 7 | project_id: string; |
@@ -34,169 +35,99 @@ export async function GET(request: NextRequest) { |
34 | 35 |
|
35 | 36 | let processedLeaders: ProjectLeader[] = []; |
36 | 37 |
|
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 | + } |
60 | 70 |
|
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: {} |
90 | 90 | }); |
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; |
114 | 102 | } |
115 | 103 |
|
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 | + }); |
136 | 111 | } |
| 112 | + }); |
137 | 113 |
|
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); |
193 | 125 |
|
194 | 126 | return NextResponse.json({ |
195 | 127 | leaders: processedLeaders, |
196 | 128 | totalCount: processedLeaders.length |
197 | 129 | }); |
198 | 130 |
|
199 | | - |
200 | 131 | } catch (error) { |
201 | 132 | console.error("API error:", error); |
202 | 133 |
|
|
0 commit comments