From bce79bfce3178c9cb9e5ab0f8240493d01606638 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:14:22 +0000 Subject: [PATCH 1/8] Initial plan From e2281d7bb375691b8ede7476b3bfd6d2d33b45d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:24:50 +0000 Subject: [PATCH 2/8] Add task user stats feature with UI and API integration Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- messages/en.json | 26 +- messages/pl.json | 26 +- src/lib/dto/contest.ts | 7 + src/lib/services/ContestsManagementService.ts | 20 +- .../contests/[contestId]/tasks/+page.svelte | 12 + .../[taskId]/user-stats/+layout.server.ts | 21 ++ .../tasks/[taskId]/user-stats/+page.svelte | 353 ++++++++++++++++++ .../[taskId]/user-stats/user-stats.remote.ts | 31 ++ 8 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+layout.server.ts create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/user-stats.remote.ts diff --git a/messages/en.json b/messages/en.json index d7cb273..15cdb81 100644 --- a/messages/en.json +++ b/messages/en.json @@ -681,5 +681,29 @@ "contest_results_no_results_description": "No participants have submitted solutions yet.", "contest_results_solved_partial": "{solved} solved · {partial} partial", "contest_results_score_submissions": "Score / Submissions", - "contest_results_error_generic": "Failed to load contest results" + "contest_results_error_generic": "Failed to load contest results", + "admin_contest_tasks_view_user_stats": "View User Stats", + "task_user_stats_title": "Task User Statistics", + "task_user_stats_subtitle": "User performance statistics for Task #{taskId} in Contest #{contestId}", + "task_user_stats_loading": "Loading user statistics...", + "task_user_stats_load_error": "Failed to load user statistics", + "task_user_stats_total_users": "Total Users", + "task_user_stats_average_score": "Average Score", + "task_user_stats_perfect_scores": "Perfect Scores", + "task_user_stats_total_submissions": "Total Submissions", + "task_user_stats_top_performers": "Top 3 Performers", + "task_user_stats_all_users": "All User Statistics", + "task_user_stats_participants": "{count} participants", + "task_user_stats_rank": "Rank", + "task_user_stats_name": "Name", + "task_user_stats_username": "Username", + "task_user_stats_best_score": "Best Score", + "task_user_stats_submissions": "Submissions", + "task_user_stats_submissions_count": "{count} submissions", + "task_user_stats_previous": "Previous", + "task_user_stats_next": "Next", + "task_user_stats_page_info": "Page {current} of {total}", + "task_user_stats_showing": "Showing {start} - {end} of {total}", + "task_user_stats_no_data_title": "No statistics available", + "task_user_stats_no_data_description": "No users have attempted this task yet." } diff --git a/messages/pl.json b/messages/pl.json index 696772d..24f4de9 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -681,5 +681,29 @@ "contest_results_no_results_description": "Żaden uczestnik nie przesłał jeszcze rozwiązań.", "contest_results_solved_partial": "{solved} rozwiązane · {partial} częściowo", "contest_results_score_submissions": "Wynik / Zgłoszenia", - "contest_results_error_generic": "Nie udało się załadować wyników konkursu" + "contest_results_error_generic": "Nie udało się załadować wyników konkursu", + "admin_contest_tasks_view_user_stats": "Zobacz statystyki użytkowników", + "task_user_stats_title": "Statystyki użytkowników zadania", + "task_user_stats_subtitle": "Statystyki wydajności użytkowników dla zadania #{taskId} w konkursie #{contestId}", + "task_user_stats_loading": "Ładowanie statystyk użytkowników...", + "task_user_stats_load_error": "Nie udało się załadować statystyk użytkowników", + "task_user_stats_total_users": "Liczba użytkowników", + "task_user_stats_average_score": "Średni wynik", + "task_user_stats_perfect_scores": "Idealne wyniki", + "task_user_stats_total_submissions": "Wszystkie zgłoszenia", + "task_user_stats_top_performers": "Najlepsi 3", + "task_user_stats_all_users": "Wszystkie statystyki użytkowników", + "task_user_stats_participants": "{count} uczestników", + "task_user_stats_rank": "Miejsce", + "task_user_stats_name": "Imię i nazwisko", + "task_user_stats_username": "Nazwa użytkownika", + "task_user_stats_best_score": "Najlepszy wynik", + "task_user_stats_submissions": "Zgłoszenia", + "task_user_stats_submissions_count": "{count} zgłoszeń", + "task_user_stats_previous": "Poprzednia", + "task_user_stats_next": "Następna", + "task_user_stats_page_info": "Strona {current} z {total}", + "task_user_stats_showing": "Pokazywanie {start} - {end} z {total}", + "task_user_stats_no_data_title": "Brak dostępnych statystyk", + "task_user_stats_no_data_description": "Żaden użytkownik nie podjął jeszcze próby rozwiązania tego zadania." } diff --git a/src/lib/dto/contest.ts b/src/lib/dto/contest.ts index 775bbf4..b916399 100644 --- a/src/lib/dto/contest.ts +++ b/src/lib/dto/contest.ts @@ -214,6 +214,13 @@ export interface UserContestStats { taskBreakdown: UserTaskPerformance[]; } +export interface TaskUserStats { + user: UserInfo; + bestScore: number; + submissionCount: number; + bestSubmissionId: number; +} + export interface ContestDetailed { id: number; name: string; diff --git a/src/lib/services/ContestsManagementService.ts b/src/lib/services/ContestsManagementService.ts index 821492f..3737e1d 100644 --- a/src/lib/services/ContestsManagementService.ts +++ b/src/lib/services/ContestsManagementService.ts @@ -7,7 +7,8 @@ import type { AddContestTaskDto, ContestTask as ContestTaskRelation, ManagedContest, - UserContestStats + UserContestStats, + TaskUserStats } from '$lib/dto/contest'; import type { Task, ContestTask } from '$lib/dto/task'; import type { Cookies } from '@sveltejs/kit'; @@ -260,6 +261,23 @@ export class ContestsManagementService { throw error; } } + + async getTaskUserStats(contestId: number, taskId: number): Promise { + try { + const url = `/contests-management/contests/${contestId}/tasks/${taskId}/user-stats`; + + const response = await this.apiClient.get>({ + url + }); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + console.error('Failed to get task user stats:', error.toJSON()); + throw error; + } + throw error; + } + } } export function createContestsManagementService(cookies: Cookies): ContestsManagementService { diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte index a9423ea..da71d16 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte @@ -2,6 +2,7 @@ import { getContestTasks, removeTaskFromContest } from './tasks.remote'; import { LoadingSpinner, ErrorCard, EmptyState } from '$lib/components/common'; import { RemoveTaskFromContestButton } from '$lib/components/dashboard/admin/contests'; + import { Button } from '$lib/components/ui/button'; import * as m from '$lib/paraglide/messages'; import ClipboardList from '@lucide/svelte/icons/clipboard-list'; import User from '@lucide/svelte/icons/user'; @@ -9,6 +10,7 @@ import Clock from '@lucide/svelte/icons/clock'; import CheckCircle from '@lucide/svelte/icons/check-circle'; import XCircle from '@lucide/svelte/icons/x-circle'; + import ChartBar from '@lucide/svelte/icons/chart-bar'; import { formatDate } from '$lib/utils'; interface Props { @@ -72,6 +74,16 @@ {m.admin_contest_tasks_submission_open_no()} {/if} + Promise<{ contestId: number }>; +}) => { + const taskId = parseInt(params.taskId, 10); + const parentData = await parent(); + + if (isNaN(taskId)) { + throw error(400, 'Invalid task ID'); + } + + return { + contestId: parentData.contestId, + taskId + }; +}; diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte new file mode 100644 index 0000000..7dea325 --- /dev/null +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte @@ -0,0 +1,353 @@ + + +
+
+

+ {m.task_user_stats_title()} +

+

+ {m.task_user_stats_subtitle({ contestId: data.contestId, taskId: data.taskId })} +

+
+ + {#if statsQuery.error} + statsQuery.refresh()} + /> + {:else if statsQuery.loading} + + {:else if statsQuery.current} + +
+ + + + + {m.task_user_stats_total_users()} + + + +

{sortedStats.length}

+
+
+ + + + + + {m.task_user_stats_average_score()} + + + +

{averageScore.toFixed(1)}%

+
+
+ + + + + + {m.task_user_stats_perfect_scores()} + + + +

{perfectScores}

+
+
+ + + + + + {m.task_user_stats_total_submissions()} + + + +

{totalSubmissions}

+
+
+
+ + + {#if sortedStats.length >= 3} + + + + + {m.task_user_stats_top_performers()} + + + +
+ + {#if sortedStats[1]} +
+
+ +
+
+

+ {sortedStats[1].user.name} + {sortedStats[1].user.surname} +

+

@{sortedStats[1].user.username}

+

+ {sortedStats[1].bestScore.toFixed(1)}% +

+

+ {m.task_user_stats_submissions_count({ + count: sortedStats[1].submissionCount + })} +

+
+
+ {/if} + + + {#if sortedStats[0]} +
+
+ +
+
+

+ {sortedStats[0].user.name} + {sortedStats[0].user.surname} +

+

@{sortedStats[0].user.username}

+

+ {sortedStats[0].bestScore.toFixed(1)}% +

+

+ {m.task_user_stats_submissions_count({ + count: sortedStats[0].submissionCount + })} +

+
+
+ {/if} + + + {#if sortedStats[2]} +
+
+ +
+
+

+ {sortedStats[2].user.name} + {sortedStats[2].user.surname} +

+

@{sortedStats[2].user.username}

+

+ {sortedStats[2].bestScore.toFixed(1)}% +

+

+ {m.task_user_stats_submissions_count({ + count: sortedStats[2].submissionCount + })} +

+
+
+ {/if} +
+
+
+ {/if} + + + {#if sortedStats.length > 0} + + +
+ + + {m.task_user_stats_all_users()} + +

+ {m.task_user_stats_participants({ count: sortedStats.length })} +

+
+
+ +
+ + + + {m.task_user_stats_rank()} + {m.task_user_stats_name()} + + {m.task_user_stats_best_score()} + {m.task_user_stats_submissions()} + + + + {#each paginatedStats as userStat, index (userStat.user.id)} + {@const rank = (currentPage - 1) * pageSize + index + 1} + {@const RankIcon = getRankIcon(rank)} + + +
+ {#if RankIcon} +
+ +
+ {:else} + {rank} + {/if} +
+
+ + {userStat.user.name} + {userStat.user.surname} + + + + + {userStat.bestScore.toFixed(1)}% + + + + {userStat.submissionCount} + +
+ {/each} +
+
+
+ + + {#if totalPages > 1} +
+

+ {m.task_user_stats_showing({ + start: (currentPage - 1) * pageSize + 1, + end: Math.min(currentPage * pageSize, sortedStats.length), + total: sortedStats.length + })} +

+
+ + + {m.task_user_stats_page_info({ current: currentPage, total: totalPages })} + + +
+
+ {/if} +
+
+ {:else} + + {/if} + {/if} +
diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/user-stats.remote.ts b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/user-stats.remote.ts new file mode 100644 index 0000000..ded3258 --- /dev/null +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/user-stats.remote.ts @@ -0,0 +1,31 @@ +import { query, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import { createContestsManagementService } from '$lib/services/ContestsManagementService'; +import { ApiError } from '$lib/services/ApiService'; +import { error } from '@sveltejs/kit'; +import type { TaskUserStats } from '$lib/dto/contest'; + +const paramsSchema = v.object({ + contestId: v.number(), + taskId: v.number() +}); + +export const getTaskUserStats = query( + paramsSchema, + async (params: { contestId: number; taskId: number }): Promise => { + const { cookies } = getRequestEvent(); + + try { + const svc = createContestsManagementService(cookies); + return await svc.getTaskUserStats(params.contestId, params.taskId); + } catch (err) { + console.error('Failed to load task user stats:', err); + + if (err instanceof ApiError) { + throw error(err.getStatus(), err.getApiMessage()); + } + + throw error(500, 'Failed to load task user stats'); + } + } +); From 8d3eb2661db7a64b8cb8858438df1f6c681f078f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 22:51:55 +0000 Subject: [PATCH 3/8] Add contest-level user statistics page for teachers Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- messages/en.json | 28 +- messages/pl.json | 28 +- .../contests/[contestId]/tasks/+page.svelte | 8 + .../[contestId]/user-stats/+page.server.ts | 11 + .../[contestId]/user-stats/+page.svelte | 368 ++++++++++++++++++ .../user-stats/user-stats.remote.ts | 30 ++ 6 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.server.ts create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte create mode 100644 src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts diff --git a/messages/en.json b/messages/en.json index 15cdb81..ffbddf8 100644 --- a/messages/en.json +++ b/messages/en.json @@ -705,5 +705,31 @@ "task_user_stats_page_info": "Page {current} of {total}", "task_user_stats_showing": "Showing {start} - {end} of {total}", "task_user_stats_no_data_title": "No statistics available", - "task_user_stats_no_data_description": "No users have attempted this task yet." + "task_user_stats_no_data_description": "No users have attempted this task yet.", + "contest_user_stats_title": "Contest User Statistics", + "contest_user_stats_subtitle": "Overall user performance statistics for Contest #{contestId}", + "contest_user_stats_loading": "Loading contest statistics...", + "contest_user_stats_load_error": "Failed to load contest statistics", + "contest_user_stats_total_users": "Total Users", + "contest_user_stats_average_score": "Average Score", + "contest_user_stats_total_solved": "Total Tasks Solved", + "contest_user_stats_total_attempted": "Total Tasks Attempted", + "contest_user_stats_top_performers": "Top 3 Performers", + "contest_user_stats_all_users": "All User Statistics", + "contest_user_stats_participants": "{count} participants", + "contest_user_stats_rank": "Rank", + "contest_user_stats_name": "Name", + "contest_user_stats_username": "Username", + "contest_user_stats_total_score": "Total Score", + "contest_user_stats_solved": "Solved", + "contest_user_stats_partial": "Partial", + "contest_user_stats_attempted": "Attempted", + "contest_user_stats_tasks_info": "{solved} solved · {partial} partial", + "contest_user_stats_previous": "Previous", + "contest_user_stats_next": "Next", + "contest_user_stats_page_info": "Page {current} of {total}", + "contest_user_stats_showing": "Showing {start} - {end} of {total}", + "contest_user_stats_no_data_title": "No statistics available", + "contest_user_stats_no_data_description": "No users have participated in this contest yet.", + "admin_contest_view_all_user_stats": "View Contest Stats" } diff --git a/messages/pl.json b/messages/pl.json index 24f4de9..c1954cc 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -705,5 +705,31 @@ "task_user_stats_page_info": "Strona {current} z {total}", "task_user_stats_showing": "Pokazywanie {start} - {end} z {total}", "task_user_stats_no_data_title": "Brak dostępnych statystyk", - "task_user_stats_no_data_description": "Żaden użytkownik nie podjął jeszcze próby rozwiązania tego zadania." + "task_user_stats_no_data_description": "Żaden użytkownik nie podjął jeszcze próby rozwiązania tego zadania.", + "contest_user_stats_title": "Statystyki użytkowników konkursu", + "contest_user_stats_subtitle": "Ogólne statystyki wydajności użytkowników dla konkursu #{contestId}", + "contest_user_stats_loading": "Ładowanie statystyk konkursu...", + "contest_user_stats_load_error": "Nie udało się załadować statystyk konkursu", + "contest_user_stats_total_users": "Liczba użytkowników", + "contest_user_stats_average_score": "Średni wynik", + "contest_user_stats_total_solved": "Łącznie rozwiązanych zadań", + "contest_user_stats_total_attempted": "Łącznie podjętych prób", + "contest_user_stats_top_performers": "Najlepsi 3", + "contest_user_stats_all_users": "Wszystkie statystyki użytkowników", + "contest_user_stats_participants": "{count} uczestników", + "contest_user_stats_rank": "Miejsce", + "contest_user_stats_name": "Imię i nazwisko", + "contest_user_stats_username": "Nazwa użytkownika", + "contest_user_stats_total_score": "Całkowity wynik", + "contest_user_stats_solved": "Rozwiązane", + "contest_user_stats_partial": "Częściowe", + "contest_user_stats_attempted": "Podjęte", + "contest_user_stats_tasks_info": "{solved} rozwiązane · {partial} częściowe", + "contest_user_stats_previous": "Poprzednia", + "contest_user_stats_next": "Następna", + "contest_user_stats_page_info": "Strona {current} z {total}", + "contest_user_stats_showing": "Pokazywanie {start} - {end} z {total}", + "contest_user_stats_no_data_title": "Brak dostępnych statystyk", + "contest_user_stats_no_data_description": "Żaden użytkownik nie wziął jeszcze udziału w tym konkursie.", + "admin_contest_view_all_user_stats": "Zobacz statystyki konkursu" } diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte index da71d16..6d91c49 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte @@ -25,6 +25,14 @@

{m.admin_contest_tasks_page_title({ contestId: data.contestId })}

+ diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.server.ts b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.server.ts new file mode 100644 index 0000000..b49ec23 --- /dev/null +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.server.ts @@ -0,0 +1,11 @@ +export const load = async ({ + parent +}: { + parent: () => Promise<{ contestId: number }>; +}) => { + const parentData = await parent(); + + return { + contestId: parentData.contestId + }; +}; diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte new file mode 100644 index 0000000..ecd3eae --- /dev/null +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte @@ -0,0 +1,368 @@ + + +
+
+

+ {m.contest_user_stats_title()} +

+

+ {m.contest_user_stats_subtitle({ contestId: data.contestId })} +

+
+ + {#if statsQuery.error} + statsQuery.refresh()} + /> + {:else if statsQuery.loading} + + {:else if statsQuery.current} + +
+ + + + + {m.contest_user_stats_total_users()} + + + +

{sortedStats.length}

+
+
+ + + + + + {m.contest_user_stats_average_score()} + + + +

{averageScore.toFixed(1)}%

+
+
+ + + + + + {m.contest_user_stats_total_solved()} + + + +

{totalTasksSolved}

+
+
+ + + + + + {m.contest_user_stats_total_attempted()} + + + +

{totalTasksAttempted}

+
+
+
+ + + {#if sortedStats.length >= 3} + + + + + {m.contest_user_stats_top_performers()} + + + +
+ + {#if sortedStats[1]} +
+
+ +
+
+

+ {sortedStats[1].user.name} + {sortedStats[1].user.surname} +

+

@{sortedStats[1].user.username}

+

+ {sortedStats[1].totalScore.toFixed(1)}% +

+

+ {m.contest_user_stats_tasks_info({ + solved: sortedStats[1].tasksSolved, + partial: sortedStats[1].tasksPartiallySolved + })} +

+
+
+ {/if} + + + {#if sortedStats[0]} +
+
+ +
+
+

+ {sortedStats[0].user.name} + {sortedStats[0].user.surname} +

+

@{sortedStats[0].user.username}

+

+ {sortedStats[0].totalScore.toFixed(1)}% +

+

+ {m.contest_user_stats_tasks_info({ + solved: sortedStats[0].tasksSolved, + partial: sortedStats[0].tasksPartiallySolved + })} +

+
+
+ {/if} + + + {#if sortedStats[2]} +
+
+ +
+
+

+ {sortedStats[2].user.name} + {sortedStats[2].user.surname} +

+

@{sortedStats[2].user.username}

+

+ {sortedStats[2].totalScore.toFixed(1)}% +

+

+ {m.contest_user_stats_tasks_info({ + solved: sortedStats[2].tasksSolved, + partial: sortedStats[2].tasksPartiallySolved + })} +

+
+
+ {/if} +
+
+
+ {/if} + + + {#if sortedStats.length > 0} + + +
+ + + {m.contest_user_stats_all_users()} + +

+ {m.contest_user_stats_participants({ count: sortedStats.length })} +

+
+
+ +
+ + + + {m.contest_user_stats_rank()} + {m.contest_user_stats_name()} + + {m.contest_user_stats_total_score()} + {m.contest_user_stats_solved()} + + + + + + {#each paginatedStats as userStat, index (userStat.user.id)} + {@const rank = (currentPage - 1) * pageSize + index + 1} + {@const RankIcon = getRankIcon(rank)} + + +
+ {#if RankIcon} +
+ +
+ {:else} + {rank} + {/if} +
+
+ + {userStat.user.name} + {userStat.user.surname} + + + + {userStat.totalScore.toFixed(1)}% + + + {userStat.tasksSolved} + + + +
+ {/each} +
+
+
+ + + {#if totalPages > 1} +
+

+ {m.contest_user_stats_showing({ + start: (currentPage - 1) * pageSize + 1, + end: Math.min(currentPage * pageSize, sortedStats.length), + total: sortedStats.length + })} +

+
+ + + {m.contest_user_stats_page_info({ current: currentPage, total: totalPages })} + + +
+
+ {/if} +
+
+ {:else} + + {/if} + {/if} +
diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts new file mode 100644 index 0000000..14d591f --- /dev/null +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts @@ -0,0 +1,30 @@ +import { query, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import { createContestsManagementService } from '$lib/services/ContestsManagementService'; +import { ApiError } from '$lib/services/ApiService'; +import { error } from '@sveltejs/kit'; +import type { UserContestStats } from '$lib/dto/contest'; + +const paramsSchema = v.object({ + contestId: v.number() +}); + +export const getContestUserStats = query( + paramsSchema, + async (params: { contestId: number }): Promise => { + const { cookies } = getRequestEvent(); + + try { + const svc = createContestsManagementService(cookies); + return await svc.getUserStats(params.contestId); + } catch (err) { + console.error('Failed to load contest user stats:', err); + + if (err instanceof ApiError) { + throw error(err.getStatus(), err.getApiMessage()); + } + + throw error(500, 'Failed to load contest user stats'); + } + } +); From 1a06ded8b9f73a2836757770dabdcd231d6d11c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:04:03 +0000 Subject: [PATCH 4/8] Address PR review feedback: improve validation, routing, and add contest card button Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- messages/en.json | 3 ++- messages/pl.json | 3 ++- .../dashboard/contests/AdminContestCard.svelte | 8 ++++++++ src/lib/components/dashboard/utils.ts | 10 ++++++++++ .../teacher/contests/[contestId]/tasks/+page.svelte | 3 +-- .../tasks/[taskId]/user-stats/user-stats.remote.ts | 4 ++-- .../contests/[contestId]/user-stats/+page.server.ts | 6 +----- 7 files changed, 26 insertions(+), 11 deletions(-) diff --git a/messages/en.json b/messages/en.json index ffbddf8..d9d004e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -731,5 +731,6 @@ "contest_user_stats_showing": "Showing {start} - {end} of {total}", "contest_user_stats_no_data_title": "No statistics available", "contest_user_stats_no_data_description": "No users have participated in this contest yet.", - "admin_contest_view_all_user_stats": "View Contest Stats" + "admin_contest_view_all_user_stats": "View Contest Stats", + "admin_contests_card_view_user_stats": "View User Stats" } diff --git a/messages/pl.json b/messages/pl.json index c1954cc..5a63308 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -731,5 +731,6 @@ "contest_user_stats_showing": "Pokazywanie {start} - {end} z {total}", "contest_user_stats_no_data_title": "Brak dostępnych statystyk", "contest_user_stats_no_data_description": "Żaden użytkownik nie wziął jeszcze udziału w tym konkursie.", - "admin_contest_view_all_user_stats": "Zobacz statystyki konkursu" + "admin_contest_view_all_user_stats": "Zobacz statystyki konkursu", + "admin_contests_card_view_user_stats": "Zobacz statystyki użytkowników" } diff --git a/src/lib/components/dashboard/contests/AdminContestCard.svelte b/src/lib/components/dashboard/contests/AdminContestCard.svelte index 736ee30..11cc0b5 100644 --- a/src/lib/components/dashboard/contests/AdminContestCard.svelte +++ b/src/lib/components/dashboard/contests/AdminContestCard.svelte @@ -171,6 +171,14 @@ {m.admin_contests_card_view_collaborators()} + diff --git a/src/lib/components/dashboard/utils.ts b/src/lib/components/dashboard/utils.ts index ba85a90..5aaf73b 100644 --- a/src/lib/components/dashboard/utils.ts +++ b/src/lib/components/dashboard/utils.ts @@ -25,6 +25,16 @@ export function getDashboardTitleTranslationFromPathname(pathname: string): stri return m.header_task_details(); } + // Check for teacher contest task user stats (e.g., /dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats) + if (path.match(/^\/dashboard\/teacher\/contests\/\d+\/tasks\/\d+\/user-stats/)) { + return m.task_user_stats_title(); + } + + // Check for teacher contest user stats (e.g., /dashboard/teacher/contests/[contestId]/user-stats) + if (path.match(/^\/dashboard\/teacher\/contests\/\d+\/user-stats/)) { + return m.contest_user_stats_title(); + } + // Check for admin contest registration requests (e.g., /dashboard/admin/contests/[contestId]/registration-requests) if (path.match(/^\/dashboard\/admin\/contests\/\d+\/registration-requests/)) { return m.admin_registration_requests_title(); diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte index 6d91c49..2c8ba5b 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte @@ -83,8 +83,7 @@ {/if} +
{#if RankIcon} @@ -319,6 +284,67 @@ {userStat.tasksAttempted} + + {#if isExpanded} + + +
+

+ {m.contest_user_stats_task_breakdown()} +

+
+ + + + + + + + + + + {#each userStat.taskBreakdown as task (task.taskId)} + {@const statusBadge = getTaskStatusBadge(task)} + + + + + + + {/each} + +
+ {m.contest_user_stats_task_title()} + + {m.contest_user_stats_task_score()} + + {m.contest_user_stats_task_attempts()} + + {m.contest_user_stats_task_status()} +
+ {task.taskTitle} + + 0} + > + {task.bestScore.toFixed(1)}% + + + {task.attemptCount} + + + {statusBadge.text} + +
+
+
+
+
+ {/if} {/each} From d93ba3fccf4d0064dc34439e8afbb08c943b18c5 Mon Sep 17 00:00:00 2001 From: TheRealSeber Date: Sun, 21 Dec 2025 13:09:21 +0100 Subject: [PATCH 6/8] chore: adjust some stuff --- .../teacher/contests/[contestId]/user-stats/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte index 1443f4a..a87cb84 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte @@ -82,7 +82,7 @@ return { text: m.contest_user_stats_task_failed(), class: - 'bg-muted text-muted-foreground border-borderbg-muted text-muted-foreground border-border' + 'bg-muted text-muted-foreground border-border' }; } else { return { From b8d34f2cb7c47aacc4fc50b0bd1e20022e3998eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:14:39 +0000 Subject: [PATCH 7/8] Fix schema validation to use integer pipe for contestId Co-authored-by: TheRealSeber <111927572+TheRealSeber@users.noreply.github.com> --- .../teacher/contests/[contestId]/user-stats/+page.svelte | 3 +-- .../contests/[contestId]/user-stats/user-stats.remote.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte index a87cb84..8611a12 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/+page.svelte @@ -81,8 +81,7 @@ } else if (task.attemptCount > 0) { return { text: m.contest_user_stats_task_failed(), - class: - 'bg-muted text-muted-foreground border-border' + class: 'bg-muted text-muted-foreground border-border' }; } else { return { diff --git a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts index 14d591f..1a79513 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts +++ b/src/routes/dashboard/teacher/contests/[contestId]/user-stats/user-stats.remote.ts @@ -6,7 +6,7 @@ import { error } from '@sveltejs/kit'; import type { UserContestStats } from '$lib/dto/contest'; const paramsSchema = v.object({ - contestId: v.number() + contestId: v.pipe(v.number(), v.integer()) }); export const getContestUserStats = query( From 610dd6f28157d5c6d8149372c4bf2fd0a85994f8 Mon Sep 17 00:00:00 2001 From: TheRealSeber Date: Sun, 21 Dec 2025 13:27:45 +0100 Subject: [PATCH 8/8] remove top 3 from task statistics --- messages/en.json | 2 - messages/pl.json | 2 - .../tasks/[taskId]/user-stats/+page.svelte | 99 ------------------- 3 files changed, 103 deletions(-) diff --git a/messages/en.json b/messages/en.json index 6dfa6c5..7765dbf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -691,7 +691,6 @@ "task_user_stats_average_score": "Average Score", "task_user_stats_perfect_scores": "Perfect Scores", "task_user_stats_total_submissions": "Total Submissions", - "task_user_stats_top_performers": "Top 3 Performers", "task_user_stats_all_users": "All User Statistics", "task_user_stats_participants": "{count} participants", "task_user_stats_rank": "Rank", @@ -699,7 +698,6 @@ "task_user_stats_username": "Username", "task_user_stats_best_score": "Best Score", "task_user_stats_submissions": "Submissions", - "task_user_stats_submissions_count": "{count} submissions", "task_user_stats_previous": "Previous", "task_user_stats_next": "Next", "task_user_stats_page_info": "Page {current} of {total}", diff --git a/messages/pl.json b/messages/pl.json index 4c0bd28..2b4a689 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -691,7 +691,6 @@ "task_user_stats_average_score": "Średni wynik", "task_user_stats_perfect_scores": "Idealne wyniki", "task_user_stats_total_submissions": "Wszystkie zgłoszenia", - "task_user_stats_top_performers": "Najlepsi 3", "task_user_stats_all_users": "Wszystkie statystyki użytkowników", "task_user_stats_participants": "{count} uczestników", "task_user_stats_rank": "Miejsce", @@ -699,7 +698,6 @@ "task_user_stats_username": "Nazwa użytkownika", "task_user_stats_best_score": "Najlepszy wynik", "task_user_stats_submissions": "Zgłoszenia", - "task_user_stats_submissions_count": "{count} zgłoszeń", "task_user_stats_previous": "Poprzednia", "task_user_stats_next": "Następna", "task_user_stats_page_info": "Strona {current} z {total}", diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte index 7dea325..8549d12 100644 --- a/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte +++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/[taskId]/user-stats/+page.svelte @@ -140,105 +140,6 @@
- - {#if sortedStats.length >= 3} - - - - - {m.task_user_stats_top_performers()} - - - -
- - {#if sortedStats[1]} -
-
- -
-
-

- {sortedStats[1].user.name} - {sortedStats[1].user.surname} -

-

@{sortedStats[1].user.username}

-

- {sortedStats[1].bestScore.toFixed(1)}% -

-

- {m.task_user_stats_submissions_count({ - count: sortedStats[1].submissionCount - })} -

-
-
- {/if} - - - {#if sortedStats[0]} -
-
- -
-
-

- {sortedStats[0].user.name} - {sortedStats[0].user.surname} -

-

@{sortedStats[0].user.username}

-

- {sortedStats[0].bestScore.toFixed(1)}% -

-

- {m.task_user_stats_submissions_count({ - count: sortedStats[0].submissionCount - })} -

-
-
- {/if} - - - {#if sortedStats[2]} -
-
- -
-
-

- {sortedStats[2].user.name} - {sortedStats[2].user.surname} -

-

@{sortedStats[2].user.username}

-

- {sortedStats[2].bestScore.toFixed(1)}% -

-

- {m.task_user_stats_submissions_count({ - count: sortedStats[2].submissionCount - })} -

-
-
- {/if} -
-
-
- {/if} - {#if sortedStats.length > 0}