diff --git a/messages/en.json b/messages/en.json
index 79d9a7b..a5b0ca4 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -9,9 +9,8 @@
"admin_contests_card_start": "Start:",
"admin_contests_card_tasks": "Tasks",
"admin_contests_card_user_prefix": "User #",
- "admin_contests_card_view_tasks": "View Contest Tasks",
+ "admin_contests_card_manage_tasks": "Manage Tasks",
"admin_contests_card_view_requests": "Registration Requests",
- "admin_contests_card_add_tasks": "Add Tasks to Contest",
"admin_contests_create_description": "Set up a new programming contest",
"admin_contests_create_error": "Failed to create contest",
"admin_contests_create_success": "Contest created successfully!",
@@ -80,6 +79,30 @@
"admin_contest_tasks_remove_cancel": "Cancel",
"admin_contest_tasks_remove_success": "Task removed from contest successfully!",
"admin_contest_tasks_remove_error": "Failed to remove task from contest",
+ "admin_contest_tasks_manage_title": "Manage Contest Tasks",
+ "admin_contest_tasks_added": "Added Tasks",
+ "admin_contest_tasks_search_placeholder": "Search tasks...",
+ "admin_contest_tasks_no_results_title": "No tasks found",
+ "admin_contest_tasks_column_creator": "Creator",
+ "admin_contest_tasks_edit_schedule_title": "Edit Schedule",
+ "admin_contest_tasks_edit_dialog_title": "Edit Task Schedule",
+ "admin_contest_tasks_edit_dialog_description": "Update when this task is available in the contest.",
+ "admin_contest_tasks_update_button": "Save Changes",
+ "admin_contest_tasks_update_success": "Task schedule updated successfully!",
+ "admin_contest_tasks_update_error": "Failed to update task schedule",
+ "admin_contest_tasks_column_id": "ID",
+ "admin_contest_tasks_column_title": "Title",
+ "admin_contest_tasks_column_start_at": "Available From",
+ "admin_contest_tasks_column_end_at": "Available Until",
+ "admin_contest_tasks_column_created_at": "Created At",
+ "admin_contest_tasks_column_actions": "Actions",
+ "admin_contest_tasks_pagination_page": "Page",
+ "admin_contest_tasks_pagination_of": "of",
+ "admin_contest_tasks_pagination_rows_per_page": "Rows per page",
+ "admin_contest_tasks_pagination_total": "{total} total tasks",
+ "admin_contest_tasks_pagination_prev": "Previous",
+ "admin_contest_tasks_pagination_next": "Next",
+ "admin_contest_tasks_pagination_showing_range": "Showing {from}-{to} of {total} tasks",
"admin_registration_requests_approve": "Approve",
"admin_registration_requests_email": "Email",
"admin_registration_requests_load_error": "Failed to load registration requests",
diff --git a/messages/pl.json b/messages/pl.json
index 87befe4..93fd026 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -9,9 +9,8 @@
"admin_contests_card_start": "Początek:",
"admin_contests_card_tasks": "Zadania",
"admin_contests_card_user_prefix": "Użytkownik #",
- "admin_contests_card_view_tasks": "Zobacz Zadania Konkursu",
+ "admin_contests_card_manage_tasks": "Zarządzaj Zadania",
"admin_contests_card_view_requests": "Prośby o Rejestrację",
- "admin_contests_card_add_tasks": "Dodaj Zadania do Konkursu",
"admin_contests_create_description": "Skonfiguruj nowy konkurs programistyczny",
"admin_contests_create_error": "Nie udało się utworzyć konkursu",
"admin_contests_create_success": "Konkurs został pomyślnie utworzony!",
@@ -80,6 +79,30 @@
"admin_contest_tasks_remove_cancel": "Anuluj",
"admin_contest_tasks_remove_success": "Zadanie zostało pomyślnie usunięte z konkursu!",
"admin_contest_tasks_remove_error": "Nie udało się usunąć zadania z konkursu",
+ "admin_contest_tasks_manage_title": "Zarządzaj Zadaniami Konkursu",
+ "admin_contest_tasks_added": "Dodane Zadania",
+ "admin_contest_tasks_search_placeholder": "Szukaj zadań...",
+ "admin_contest_tasks_no_results_title": "Nie znaleziono zadań",
+ "admin_contest_tasks_column_creator": "Autor",
+ "admin_contest_tasks_edit_schedule_title": "Edytuj Harmonogram",
+ "admin_contest_tasks_edit_dialog_title": "Edytuj Harmonogram Zadania",
+ "admin_contest_tasks_edit_dialog_description": "Zaktualizuj, kiedy to zadanie będzie dostępne w konkursie.",
+ "admin_contest_tasks_update_button": "Zapisz Zmiany",
+ "admin_contest_tasks_update_success": "Harmonogram zadania zaktualizowany pomyślnie!",
+ "admin_contest_tasks_update_error": "Nie udało się zaktualizować harmonogramu zadania",
+ "admin_contest_tasks_column_id": "ID",
+ "admin_contest_tasks_column_title": "Tytuł",
+ "admin_contest_tasks_column_start_at": "Dostępne od",
+ "admin_contest_tasks_column_end_at": "Dostępne do",
+ "admin_contest_tasks_column_created_at": "Utworzono",
+ "admin_contest_tasks_column_actions": "Akcje",
+ "admin_contest_tasks_pagination_page": "Strona",
+ "admin_contest_tasks_pagination_of": "z",
+ "admin_contest_tasks_pagination_rows_per_page": "Wierszy na stronę",
+ "admin_contest_tasks_pagination_total": "{total} zadań łącznie",
+ "admin_contest_tasks_pagination_prev": "Poprzednia",
+ "admin_contest_tasks_pagination_next": "Następna",
+ "admin_contest_tasks_pagination_showing_range": "Wyświetlanie {from}-{to} z {total} zadań",
"admin_registration_requests_approve": "Zatwierdź",
"admin_registration_requests_email": "Email",
"admin_registration_requests_load_error": "Nie udało się załadować próśb o rejestrację",
diff --git a/src/lib/components/dashboard/admin/contests/AddedContestTasksList.svelte b/src/lib/components/dashboard/admin/contests/AddedContestTasksList.svelte
new file mode 100644
index 0000000..8839ef8
--- /dev/null
+++ b/src/lib/components/dashboard/admin/contests/AddedContestTasksList.svelte
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {m.admin_contest_tasks_pagination_showing_range({
+ from: total === 0 ? 0 : offset + 1,
+ to: Math.min(offset + contestTasks.length, total),
+ total
+ })}
+
+
+ {m.admin_contest_tasks_pagination_rows_per_page()}:
+ handleLimitChange(Number((e.target as HTMLSelectElement).value))}
+ >
+ {#each perPageOptions as opt (opt)}
+ {opt}
+ {/each}
+
+
+
+ {m.admin_contest_tasks_pagination_page()}
+ {currentPage}
+ {m.admin_contest_tasks_pagination_of()}
+ {totalPages}
+
+
+
+
+
+
+
+
+
+ handleHeaderSort(ContestTaskSortKey.Id)}
+ >
+ {m.admin_contest_tasks_column_id()}
+ {#if sortKey === ContestTaskSortKey.Id}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+
+ handleHeaderSort(ContestTaskSortKey.Title)}
+ >
+ {m.admin_contest_tasks_column_title()}
+ {#if sortKey === ContestTaskSortKey.Title}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+
+ {m.admin_contest_tasks_column_creator()}
+
+
+ handleHeaderSort(ContestTaskSortKey.StartAt)}
+ >
+ {m.admin_contest_tasks_column_start_at()}
+ {#if sortKey === ContestTaskSortKey.StartAt}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+
+ handleHeaderSort(ContestTaskSortKey.EndAt)}
+ >
+ {m.admin_contest_tasks_column_end_at()}
+ {#if sortKey === ContestTaskSortKey.EndAt}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+ {m.admin_contest_tasks_column_actions()}
+
+
+
+
+ {#if contestTasks.length === 0}
+
+
+ {m.admin_contest_tasks_no_results_title()}
+
+
+ {:else}
+ {#each contestTasks as contestTask (contestTask.task.id)}
+
+ {contestTask.task.id}
+
+
+
+
+
+
+ {contestTask.task.title}
+
+
+ {contestTask.task.title}
+
+
+
+
+
+
+ {contestTask.creatorName}
+
+
+
+
+
+ {formatDate(contestTask.startAt)}
+
+
+
+ {#if contestTask.endAt}
+
+
+ {formatDate(contestTask.endAt)}
+
+ {:else}
+ {m.admin_contest_tasks_no_end_date()}
+ {/if}
+
+
+
+
onEdit(contestTask)}
+ >
+
+
+
+
+
+
+ {/each}
+ {/if}
+
+
+
+ {#if total > 0}
+
+
{
+ if (p && p !== currentPage) offset = getOffset(p, limit);
+ }}
+ >
+
+
+ {m.admin_contest_tasks_pagination_prev()}
+
+
+ {#each paginationPages as p (p)}
+ {#if p === 'ellipsis'}
+
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+
+
+ {m.admin_contest_tasks_pagination_next()}
+
+
+
+
+
+ {m.admin_contest_tasks_pagination_total({ total })}
+
+
+ {/if}
+
diff --git a/src/lib/components/dashboard/admin/contests/AssignableContestTasksList.svelte b/src/lib/components/dashboard/admin/contests/AssignableContestTasksList.svelte
new file mode 100644
index 0000000..d5b650c
--- /dev/null
+++ b/src/lib/components/dashboard/admin/contests/AssignableContestTasksList.svelte
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {m.admin_contest_tasks_pagination_showing_range({
+ from: total === 0 ? 0 : offset + 1,
+ to: Math.min(offset + tasks.length, total),
+ total
+ })}
+
+
+ {m.admin_contest_tasks_pagination_rows_per_page()}:
+ handleLimitChange(Number((e.target as HTMLSelectElement).value))}
+ >
+ {#each perPageOptions as opt (opt)}
+ {opt}
+ {/each}
+
+
+
+ {m.admin_contest_tasks_pagination_page()}
+ {currentPage}
+ {m.admin_contest_tasks_pagination_of()}
+ {totalPages}
+
+
+
+
+
+
+
+
+
+ handleHeaderSort(AssignableTaskSortKey.Id)}
+ >
+ {m.admin_contest_tasks_column_id()}
+ {#if sortKey === AssignableTaskSortKey.Id}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+
+ handleHeaderSort(AssignableTaskSortKey.Title)}
+ >
+ {m.admin_contest_tasks_column_title()}
+ {#if sortKey === AssignableTaskSortKey.Title}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+
+ {m.admin_contest_tasks_column_creator()}
+
+
+ handleHeaderSort(AssignableTaskSortKey.CreatedAt)}
+ >
+ {m.admin_contest_tasks_column_created_at()}
+ {#if sortKey === AssignableTaskSortKey.CreatedAt}{#if sortDir === SortDirection.Asc} {:else} {/if}{/if}
+
+
+ {m.admin_contest_tasks_column_actions()}
+
+
+
+
+ {#if tasks.length === 0}
+
+
+ {m.admin_contest_tasks_no_results_title()}
+
+
+ {:else}
+ {#each tasks as task (task.id)}
+
+ {task.id}
+
+
+
+
+
+
+ {task.title}
+
+
+ {task.title}
+
+
+
+
+
+
+ {task.creatorName}
+
+
+
+
+
+ {formatDate(task.createdAt)}
+
+
+
+ onAdd(task.id)}
+ >
+
+
+
+
+ {/each}
+ {/if}
+
+
+
+ {#if total > 0}
+
+
{
+ if (p && p !== currentPage) offset = getOffset(p, limit);
+ }}
+ >
+
+
+ {m.admin_contest_tasks_pagination_prev()}
+
+
+ {#each paginationPages as p (p)}
+ {#if p === 'ellipsis'}
+
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+
+
+ {m.admin_contest_tasks_pagination_next()}
+
+
+
+
+
+ {m.admin_contest_tasks_pagination_total({ total })}
+
+
+ {/if}
+
diff --git a/src/lib/components/dashboard/admin/contests/index.ts b/src/lib/components/dashboard/admin/contests/index.ts
index 233ffa3..eaa35cd 100644
--- a/src/lib/components/dashboard/admin/contests/index.ts
+++ b/src/lib/components/dashboard/admin/contests/index.ts
@@ -10,3 +10,5 @@ export { default as RemoveGroupFromContestButton } from './RemoveGroupFromContes
export { default as AddParticipantsToContestButton } from './AddParticipantsToContestButton.svelte';
export { default as RemoveParticipantFromContestButton } from './RemoveParticipantFromContestButton.svelte';
export { default as ParticipantsList } from './ParticipantsList.svelte';
+export { default as AddedContestTasksList } from './AddedContestTasksList.svelte';
+export { default as AssignableContestTasksList } from './AssignableContestTasksList.svelte';
diff --git a/src/lib/components/dashboard/contests/AdminContestCard.svelte b/src/lib/components/dashboard/contests/AdminContestCard.svelte
index 55b804b..6e13c28 100644
--- a/src/lib/components/dashboard/contests/AdminContestCard.svelte
+++ b/src/lib/components/dashboard/contests/AdminContestCard.svelte
@@ -10,7 +10,6 @@
import UserCheck from '@lucide/svelte/icons/user-check';
import FileText from '@lucide/svelte/icons/file-text';
import Edit from '@lucide/svelte/icons/pencil';
- import Plus from '@lucide/svelte/icons/plus';
import type { CreatedContest } from '$lib/dto/contest';
import * as m from '$lib/paraglide/messages';
import { localizeHref } from '$lib/paraglide/runtime';
@@ -138,15 +137,7 @@
href={localizeHref(`${AppRoutes.TeacherContests}/${contest.id}/tasks`)}
>
- {m.admin_contests_card_view_tasks()}
-
-
-
- {m.admin_contests_card_add_tasks()}
+ {m.admin_contests_card_manage_tasks()}
{
+ async getAssignableTasks(
+ contestId: number,
+ params?: { limit?: number; offset?: number; sort?: string; search?: string }
+ ): Promise> {
try {
- const response = await this.apiClient.get>({
- url: `/contests-management/contests/${contestId}/tasks/assignable-tasks`
+ const queryParams = new URLSearchParams();
+ if (params?.limit) queryParams.append('limit', params.limit.toString());
+ if (params?.offset) queryParams.append('offset', params.offset.toString());
+ if (params?.sort) queryParams.append('sort', params.sort);
+ if (params?.search) queryParams.append('search', params.search);
+
+ const url = `/contests-management/contests/${contestId}/tasks/assignable-tasks${
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
+ }`;
+ const response = await this.apiClient.get>>({
+ url
});
return response.data;
} catch (error) {
@@ -165,6 +177,25 @@ export class ContestsManagementService {
}
}
+ async updateTaskInContest(
+ contestId: number,
+ taskId: number,
+ data: { startAt: string; endAt: string | null }
+ ): Promise {
+ try {
+ await this.apiClient.put>({
+ url: `/contests-management/contests/${contestId}/tasks/${taskId}`,
+ body: JSON.stringify({ startAt: data.startAt, endAt: data.endAt })
+ });
+ } catch (error) {
+ if (error instanceof ApiError) {
+ console.error('Failed to update task in contest:', error.toJSON());
+ throw error;
+ }
+ throw error;
+ }
+ }
+
async removeTaskFromContest(contestId: number, taskIds: number | number[]): Promise {
try {
const requestData = {
@@ -183,10 +214,23 @@ export class ContestsManagementService {
}
}
- async getContestTasks(contestId: number): Promise {
+ async getContestTasks(
+ contestId: number,
+ params?: { limit?: number; offset?: number; sort?: string; search?: string }
+ ): Promise> {
try {
- const response = await this.apiClient.get>({
- url: `/contests-management/contests/${contestId}/tasks`
+ const queryParams = new URLSearchParams();
+ if (params?.limit) queryParams.append('limit', params.limit.toString());
+ if (params?.offset) queryParams.append('offset', params.offset.toString());
+ if (params?.sort) queryParams.append('sort', params.sort);
+ if (params?.search) queryParams.append('search', params.search);
+
+ const url = `/contests-management/contests/${contestId}/tasks${
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
+ }`;
+
+ const response = await this.apiClient.get>>({
+ url
});
return response.data;
} catch (error) {
diff --git a/src/routes/dashboard/teacher/contests/[contestId]/assignable-tasks/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/assignable-tasks/+page.svelte
deleted file mode 100644
index e66adec..0000000
--- a/src/routes/dashboard/teacher/contests/[contestId]/assignable-tasks/+page.svelte
+++ /dev/null
@@ -1,331 +0,0 @@
-
-
-
-
-
-
- {m.admin_contest_tasks_title()}
-
-
- {m.admin_contest_tasks_subtitle()}
-
-
-
-
-
-
-
{m.admin_contest_tasks_available()}
-
- {#if tasksQuery.error}
-
tasksQuery.refresh()}
- />
- {:else if tasksQuery.loading}
-
- {:else if tasksQuery.current && tasksQuery.current.length === 0}
-
- {:else if tasksQuery.current}
-
- {#each tasksQuery.current as task (task.id)}
-
-
-
-
-
-
- #{task.id}
-
-
-
-
- {task.title}
-
-
-
-
-
-
{m.admin_contest_tasks_created()}: {formatDate(task.createdAt)}
-
-
-
-
openDialog(task.id)}
- class="w-full transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md"
- >
-
- {m.admin_contest_tasks_add_button()}
-
-
-
-
- {/each}
-
- {/if}
-
-
-
-
-
-
-
- {m.admin_contest_tasks_dialog_title()}
-
- {m.admin_contest_tasks_dialog_description()}
-
-
-
-
-
-
diff --git a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte
index 8261cd0..d020596 100644
--- a/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte
+++ b/src/routes/dashboard/teacher/contests/[contestId]/tasks/+page.svelte
@@ -1,33 +1,230 @@
@@ -45,155 +242,202 @@
-
-
- {#if tasksQuery.error}
-
tasksQuery.refresh()}
- />
- {:else if tasksQuery.loading}
-
- {:else if tasksQuery.current && tasksQuery.current.length === 0}
-
- {:else if tasksQuery.current}
-
- {#each tasksQuery.current as contestTask (contestTask.task.id)}
-
-
-
-
-
-
{contestTask.task.title}
-
- {m.admin_contest_tasks_task_id()}: {contestTask.task.id}
-
-
-
- {#if contestTask.isSubmissionOpen}
-
-
- {m.admin_contest_tasks_submission_open_yes()}
-
- {:else}
-
-
- {m.admin_contest_tasks_submission_open_no()}
-
- {/if}
-
-
- {m.admin_contest_tasks_view_user_stats()}
-
- tasksQuery.refresh()}
- />
-
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_creator_name()}
-
-
- {contestTask.creatorName}
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_created_at()}
-
-
- {formatDate(contestTask.task.createdAt)}
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_updated_at()}
-
-
- {formatDate(contestTask.task.updatedAt)}
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_start_at()}
-
-
- {formatDate(contestTask.startAt)}
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_end_at()}
-
-
- {#if contestTask.endAt}
- {formatDate(contestTask.endAt)}
- {:else}
- {m.admin_contest_tasks_no_end_date()}
- {/if}
-
-
-
-
-
-
-
-
- {m.admin_contest_tasks_created_by_id()}
-
-
- {contestTask.task.createdBy}
-
-
-
-
-
-
- {/each}
-
- {/if}
+
+
+
+ {m.admin_contest_tasks_added()}
+
+ {#if addedTasksQuery.error}
+ addedTasksQuery.refresh()}
+ />
+ {:else if !addedTasksQuery.current && addedTasksQuery.loading}
+
+ {:else if addedTasksQuery.current}
+
+ {/if}
+
+
+
+
+ {m.admin_contest_tasks_available()}
+
+ {#if assignableTasksQuery.error}
+ assignableTasksQuery.refresh()}
+ />
+ {:else if !assignableTasksQuery.current && assignableTasksQuery.loading}
+
+ {:else if assignableTasksQuery.current}
+
+ {/if}
+
+
+
+
+
+
+
+ {dialogMode === 'edit'
+ ? m.admin_contest_tasks_edit_dialog_title()
+ : m.admin_contest_tasks_dialog_title()}
+
+
+ {dialogMode === 'edit'
+ ? m.admin_contest_tasks_edit_dialog_description()
+ : m.admin_contest_tasks_dialog_description()}
+
+
+
+
+
+