diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index 8de69b6..c316fb4 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -120,12 +120,13 @@ pub async fn create_candidate( } #[allow(unused_variables)] -#[get("/candidates?&")] +#[get("/candidates?&&")] pub async fn list_candidates( conn: Connection<'_, Db>, session: AdminAuth, field: Option, page: Option, + sort: Option, ) -> Result>, Custom> { let db = conn.into_inner(); let private_key = session.get_private_key(); @@ -135,7 +136,7 @@ pub async fn list_candidates( } } - let candidates = ApplicationService::list_applications(&private_key, db, field, page) + let candidates = ApplicationService::list_applications(&private_key, db, field, page, sort) .await.map_err(to_custom_error)?; Ok( diff --git a/core/src/database/query/application.rs b/core/src/database/query/application.rs index 95705de..b4b32bd 100644 --- a/core/src/database/query/application.rs +++ b/core/src/database/query/application.rs @@ -1,3 +1,4 @@ +use chrono::NaiveDateTime; use entity::{application, candidate}; use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait}; @@ -13,10 +14,32 @@ pub struct ApplicationCandidateJoin { pub email: Option, pub telephone: Option, pub field_of_study: Option, + pub created_at: NaiveDateTime, } use crate::{Query}; +fn get_ordering(sort: String) -> (application::Column, sea_orm::Order) +{ + let mut split = sort.split("_"); + let column = split.next(); + let order = split.next(); + + let column = match column { + Some("id") => application::Column::Id, + Some("createdAt") => application::Column::CreatedAt, + _ => application::Column::Id + }; + + let order = match order { + Some("asc") => sea_orm::Order::Asc, + Some("desc") => sea_orm::Order::Desc, + _ => sea_orm::Order::Asc, + }; + + (column, order) +} + impl Query { pub async fn find_application_by_id( db: &DbConn, @@ -41,14 +64,20 @@ impl Query { db: &DbConn, field_of_study: Option, page: Option, + sort: Option, ) -> Result, DbErr> { let select = application::Entity::find(); + let (column, order) = if let Some(sort) = sort { + get_ordering(sort) + } else { + (application::Column::Id, sea_orm::Order::Asc) + }; let query = if let Some(field) = field_of_study { select.filter(application::Column::FieldOfStudy.eq(field)) } else { select } - .order_by(application::Column::Id, sea_orm::Order::Asc) + .order_by(column, order) .join(JoinType::InnerJoin, application::Relation::Candidate.def()) .column_as(application::Column::Id, "application_id") .column_as(candidate::Column::Id, "candidate_id") @@ -56,6 +85,7 @@ impl Query { .column_as(candidate::Column::Surname, "surname") .column_as(candidate::Column::Email, "email") .column_as(candidate::Column::Telephone, "telephone") + .column_as(application::Column::CreatedAt, "created_at") .into_model::(); if let Some(page) = page { diff --git a/core/src/models/application.rs b/core/src/models/application.rs index a4b436b..271b008 100644 --- a/core/src/models/application.rs +++ b/core/src/models/application.rs @@ -1,3 +1,4 @@ +use chrono::NaiveDateTime; use serde::{Serialize, Deserialize}; use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError}; @@ -16,6 +17,7 @@ pub struct ApplicationResponse { pub email: String, pub telephone: String, pub field_of_study: Option, + pub created_at: NaiveDateTime, } impl ApplicationResponse { @@ -40,6 +42,7 @@ impl ApplicationResponse { email: email.unwrap_or_default(), telephone: telephone.unwrap_or_default(), field_of_study: c.field_of_study, + created_at: c.created_at, } ) } diff --git a/core/src/services/application_service.rs b/core/src/services/application_service.rs index 0176dae..6a7c970 100644 --- a/core/src/services/application_service.rs +++ b/core/src/services/application_service.rs @@ -267,8 +267,9 @@ impl ApplicationService { db: &DbConn, field_of_study: Option, page: Option, + sort: Option, ) -> Result, ServiceError> { - let applications = Query::list_applications(db, field_of_study, page).await?; + let applications = Query::list_applications(db, field_of_study, page, sort).await?; futures::future::try_join_all( applications diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 628b6be..a1970ef 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -79,12 +79,12 @@ pub mod tests { let db = get_memory_sqlite_connection().await; let admin = create_admin(&db).await; let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap(); - let candidates = ApplicationService::list_applications(&private_key, &db, None, None).await.unwrap(); + let candidates = ApplicationService::list_applications(&private_key, &db, None, None, None).await.unwrap(); assert_eq!(candidates.len(), 0); put_user_data(&db).await; - let candidates = ApplicationService::list_applications(&private_key, &db, None, None).await.unwrap(); + let candidates = ApplicationService::list_applications(&private_key, &db, None, None, None).await.unwrap(); assert_eq!(candidates.len(), 1); } diff --git a/frontend/src/lib/@api/admin.ts b/frontend/src/lib/@api/admin.ts index 7972315..4345b60 100644 --- a/frontend/src/lib/@api/admin.ts +++ b/frontend/src/lib/@api/admin.ts @@ -101,15 +101,18 @@ export const apiLogout = async (fetchSsr?: Fetch) => { // List all candidates /admin/list/candidates export const apiListCandidates = async ( fetchSsr?: Fetch, - field?: string + params: { field?: string; column?: 'createdAt' | 'application', order?: 'asc' | 'desc' } = {column: 'createdAt', order: 'desc'} ): Promise> => { const apiFetch = fetchSsr || fetch; - const params = new URLSearchParams(); - if (field) { - params.append('field', field); + const searchParams = new URLSearchParams(); + if (params.field) { + searchParams.append('field', params.field); + } + if (params.column) { + searchParams.append('sort', `${params.column}_${params.order}`); } try { - const res = await apiFetch(API_URL + '/admin/list/candidates?' + params.toString(), { + const res = await apiFetch(API_URL + '/admin/list/candidates?' + searchParams.toString(), { method: 'GET', credentials: 'include' }); diff --git a/frontend/src/lib/components/admin/table/Table.svelte b/frontend/src/lib/components/admin/table/Table.svelte index a54199f..e4803b5 100644 --- a/frontend/src/lib/components/admin/table/Table.svelte +++ b/frontend/src/lib/components/admin/table/Table.svelte @@ -3,6 +3,15 @@ import type { CandidatePreview } from '$lib/stores/candidate'; export let candidates: Array = []; + export let showCreatedAt: boolean; + + const formatRustChronoDateTime = (date?: string) => { + if (!date) return ''; + const [datePart, timePart] = date.split('T'); + const [_, month, day] = datePart.split('-'); + const [hour, minute, second] = timePart.split(':'); + return `${day}. ${month}. ${hour}:${minute}:${Number(second).toFixed(0).padStart(2, '0')}`; + };
@@ -16,6 +25,9 @@ Obor Rodné číslo Link + {#if showCreatedAt} + Vytvořeno + {/if} @@ -38,6 +50,11 @@ {candidate.relatedApplications?.filter((a) => a !== candidate.applicationId)} + {#if showCreatedAt} + + {formatRustChronoDateTime(candidate.createdAt)} + + {/if} diff --git a/frontend/src/lib/stores/candidate.ts b/frontend/src/lib/stores/candidate.ts index e90b133..fc5eb3e 100644 --- a/frontend/src/lib/stores/candidate.ts +++ b/frontend/src/lib/stores/candidate.ts @@ -47,6 +47,7 @@ export interface CandidatePreview { surname?: string; email?: string; fieldOfStudy?: string; + createdAt?: string; } export interface CandidateLogin { diff --git a/frontend/src/routes/(admin)/admin/(authenticated)/dashboard/+page.svelte b/frontend/src/routes/(admin)/admin/(authenticated)/dashboard/+page.svelte index ba4f391..354cc62 100644 --- a/frontend/src/routes/(admin)/admin/(authenticated)/dashboard/+page.svelte +++ b/frontend/src/routes/(admin)/admin/(authenticated)/dashboard/+page.svelte @@ -20,13 +20,17 @@ const getCandidates = async () => { try { - candidates = await apiListCandidates(undefined, activeFilter.filter); + // TODO: more generic implementation + candidates = await apiListCandidates( + undefined, + activeFilter.filter !== undefined ? { field: activeFilter.filter } : undefined + ); } catch { pushErrorText('Nepodařilo se načíst uchazeče'); } }; - type Class = 'Vše' | 'KBB' | 'IT' | 'GYM'; + type Class = 'Chronologicky' | 'KBB' | 'IT' | 'GYM'; type Filter = { class: Class; @@ -35,7 +39,7 @@ let filters: Array = [ { - class: 'Vše', + class: 'Chronologicky', filter: undefined }, { @@ -155,7 +159,11 @@
{/if} - deleteCandidate(event.detail.id)} /> +
deleteCandidate(event.detail.id)} + />