Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -681,5 +681,63 @@
"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_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_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.",
"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_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_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",
"admin_contests_card_view_user_stats": "View User Stats",
"contest_user_stats_task_breakdown": "Task Breakdown",
"contest_user_stats_task_title": "Task",
"contest_user_stats_task_score": "Score",
"contest_user_stats_task_attempts": "Attempts",
"contest_user_stats_task_status": "Status",
"contest_user_stats_task_solved": "Solved",
"contest_user_stats_task_partial": "Partial",
"contest_user_stats_task_failed": "Failed",
"contest_user_stats_task_not_attempted": "Not Attempted",
"contest_user_stats_expand_details": "Click to expand task breakdown",
"contest_user_stats_collapse_details": "Click to collapse task breakdown"
}
60 changes: 59 additions & 1 deletion messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -681,5 +681,63 @@
"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_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_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.",
"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_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_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",
"admin_contests_card_view_user_stats": "Zobacz statystyki użytkowników",
"contest_user_stats_task_breakdown": "Rozbicie na zadania",
"contest_user_stats_task_title": "Zadanie",
"contest_user_stats_task_score": "Wynik",
"contest_user_stats_task_attempts": "Próby",
"contest_user_stats_task_status": "Status",
"contest_user_stats_task_solved": "Rozwiązane",
"contest_user_stats_task_partial": "Częściowe",
"contest_user_stats_task_failed": "Nieudane",
"contest_user_stats_task_not_attempted": "Nie próbowano",
"contest_user_stats_expand_details": "Kliknij, aby rozwinąć rozbicie na zadania",
"contest_user_stats_collapse_details": "Kliknij, aby zwinąć rozbicie na zadania"
}
8 changes: 8 additions & 0 deletions src/lib/components/dashboard/contests/AdminContestCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@
<Users class="mr-2 h-4 w-4" />
{m.admin_contests_card_view_collaborators()}
</Button>
<Button
variant="default"
class="w-full transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md"
href={localizeHref(`${AppRoutes.TeacherContests}/${contest.id}/user-stats`)}
>
<Trophy class="mr-2 h-4 w-4" />
{m.admin_contests_card_view_user_stats()}
</Button>
</div>
</Card.Content>
</Card.Root>
Expand Down
10 changes: 10 additions & 0 deletions src/lib/components/dashboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions src/lib/dto/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 19 additions & 1 deletion src/lib/services/ContestsManagementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -260,6 +261,23 @@ export class ContestsManagementService {
throw error;
}
}

async getTaskUserStats(contestId: number, taskId: number): Promise<TaskUserStats[]> {
try {
const url = `/contests-management/contests/${contestId}/tasks/${taskId}/user-stats`;

const response = await this.apiClient.get<ApiResponse<TaskUserStats[]>>({
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
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';
import Calendar from '@lucide/svelte/icons/calendar';
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 {
Expand All @@ -23,6 +25,14 @@
<h1 class="text-3xl font-bold text-foreground">
{m.admin_contest_tasks_page_title({ contestId: data.contestId })}
</h1>
<Button
href="/dashboard/teacher/contests/{data.contestId}/user-stats"
variant="default"
class="gap-2"
>
<ChartBar class="h-4 w-4" />
{m.admin_contest_view_all_user_stats()}
</Button>
</div>

<!-- Contest Tasks Section -->
Expand Down Expand Up @@ -72,6 +82,15 @@
{m.admin_contest_tasks_submission_open_no()}
</span>
{/if}
<Button
href={`/dashboard/teacher/contests/${data.contestId}/tasks/${contestTask.task.id}/user-stats`}
variant="outline"
size="sm"
class="gap-2"
>
<ChartBar class="h-4 w-4" />
{m.admin_contest_tasks_view_user_stats()}
</Button>
<RemoveTaskFromContestButton
contestId={data.contestId}
taskId={contestTask.task.id}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { error } from '@sveltejs/kit';

export const load = async ({
params,
parent
}: {
params: { contestId: string; taskId: string };
parent: () => 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
};
};
Loading