mirror of
https://github.com/danbulant/Portfolio
synced 2026-07-07 03:50:36 +00:00
Merge pull request #205 from EETagent/application_list_ordering
Application list ordering
This commit is contained in:
commit
94015aa042
9 changed files with 79 additions and 15 deletions
|
|
@ -120,12 +120,13 @@ pub async fn create_candidate(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[get("/candidates?<field>&<page>")]
|
#[get("/candidates?<field>&<page>&<sort>")]
|
||||||
pub async fn list_candidates(
|
pub async fn list_candidates(
|
||||||
conn: Connection<'_, Db>,
|
conn: Connection<'_, Db>,
|
||||||
session: AdminAuth,
|
session: AdminAuth,
|
||||||
field: Option<String>,
|
field: Option<String>,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
|
sort: Option<String>,
|
||||||
) -> Result<Json<Vec<ApplicationResponse>>, Custom<String>> {
|
) -> Result<Json<Vec<ApplicationResponse>>, Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
let private_key = session.get_private_key();
|
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)?;
|
.await.map_err(to_custom_error)?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use entity::{application, candidate};
|
use entity::{application, candidate};
|
||||||
use sea_orm::{EntityTrait, DbErr, DbConn, ModelTrait, FromQueryResult, QuerySelect, JoinType, RelationTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
|
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<String>,
|
pub email: Option<String>,
|
||||||
pub telephone: Option<String>,
|
pub telephone: Option<String>,
|
||||||
pub field_of_study: Option<String>,
|
pub field_of_study: Option<String>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{Query};
|
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 {
|
impl Query {
|
||||||
pub async fn find_application_by_id(
|
pub async fn find_application_by_id(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
|
|
@ -41,14 +64,20 @@ impl Query {
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
field_of_study: Option<String>,
|
field_of_study: Option<String>,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
|
sort: Option<String>,
|
||||||
) -> Result<Vec<ApplicationCandidateJoin>, DbErr> {
|
) -> Result<Vec<ApplicationCandidateJoin>, DbErr> {
|
||||||
let select = application::Entity::find();
|
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 {
|
let query = if let Some(field) = field_of_study {
|
||||||
select.filter(application::Column::FieldOfStudy.eq(field))
|
select.filter(application::Column::FieldOfStudy.eq(field))
|
||||||
} else {
|
} else {
|
||||||
select
|
select
|
||||||
}
|
}
|
||||||
.order_by(application::Column::Id, sea_orm::Order::Asc)
|
.order_by(column, order)
|
||||||
.join(JoinType::InnerJoin, application::Relation::Candidate.def())
|
.join(JoinType::InnerJoin, application::Relation::Candidate.def())
|
||||||
.column_as(application::Column::Id, "application_id")
|
.column_as(application::Column::Id, "application_id")
|
||||||
.column_as(candidate::Column::Id, "candidate_id")
|
.column_as(candidate::Column::Id, "candidate_id")
|
||||||
|
|
@ -56,6 +85,7 @@ impl Query {
|
||||||
.column_as(candidate::Column::Surname, "surname")
|
.column_as(candidate::Column::Surname, "surname")
|
||||||
.column_as(candidate::Column::Email, "email")
|
.column_as(candidate::Column::Email, "email")
|
||||||
.column_as(candidate::Column::Telephone, "telephone")
|
.column_as(candidate::Column::Telephone, "telephone")
|
||||||
|
.column_as(application::Column::CreatedAt, "created_at")
|
||||||
.into_model::<ApplicationCandidateJoin>();
|
.into_model::<ApplicationCandidateJoin>();
|
||||||
|
|
||||||
if let Some(page) = page {
|
if let Some(page) = page {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError};
|
use crate::{database::query::application::ApplicationCandidateJoin, error::ServiceError};
|
||||||
|
|
@ -16,6 +17,7 @@ pub struct ApplicationResponse {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub telephone: String,
|
pub telephone: String,
|
||||||
pub field_of_study: Option<String>,
|
pub field_of_study: Option<String>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationResponse {
|
impl ApplicationResponse {
|
||||||
|
|
@ -40,6 +42,7 @@ impl ApplicationResponse {
|
||||||
email: email.unwrap_or_default(),
|
email: email.unwrap_or_default(),
|
||||||
telephone: telephone.unwrap_or_default(),
|
telephone: telephone.unwrap_or_default(),
|
||||||
field_of_study: c.field_of_study,
|
field_of_study: c.field_of_study,
|
||||||
|
created_at: c.created_at,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,8 +267,9 @@ impl ApplicationService {
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
field_of_study: Option<String>,
|
field_of_study: Option<String>,
|
||||||
page: Option<u64>,
|
page: Option<u64>,
|
||||||
|
sort: Option<String>,
|
||||||
) -> Result<Vec<ApplicationResponse>, ServiceError> {
|
) -> Result<Vec<ApplicationResponse>, 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(
|
futures::future::try_join_all(
|
||||||
applications
|
applications
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,12 @@ pub mod tests {
|
||||||
let db = get_memory_sqlite_connection().await;
|
let db = get_memory_sqlite_connection().await;
|
||||||
let admin = create_admin(&db).await;
|
let admin = create_admin(&db).await;
|
||||||
let private_key = crypto::decrypt_password(admin.private_key, "admin".to_string()).await.unwrap();
|
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);
|
assert_eq!(candidates.len(), 0);
|
||||||
|
|
||||||
put_user_data(&db).await;
|
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);
|
assert_eq!(candidates.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,15 +101,18 @@ export const apiLogout = async (fetchSsr?: Fetch) => {
|
||||||
// List all candidates /admin/list/candidates
|
// List all candidates /admin/list/candidates
|
||||||
export const apiListCandidates = async (
|
export const apiListCandidates = async (
|
||||||
fetchSsr?: Fetch,
|
fetchSsr?: Fetch,
|
||||||
field?: string
|
params: { field?: string; column?: 'createdAt' | 'application', order?: 'asc' | 'desc' } = {column: 'createdAt', order: 'desc'}
|
||||||
): Promise<Array<CandidatePreview>> => {
|
): Promise<Array<CandidatePreview>> => {
|
||||||
const apiFetch = fetchSsr || fetch;
|
const apiFetch = fetchSsr || fetch;
|
||||||
const params = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
if (field) {
|
if (params.field) {
|
||||||
params.append('field', field);
|
searchParams.append('field', params.field);
|
||||||
|
}
|
||||||
|
if (params.column) {
|
||||||
|
searchParams.append('sort', `${params.column}_${params.order}`);
|
||||||
}
|
}
|
||||||
try {
|
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',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,15 @@
|
||||||
import type { CandidatePreview } from '$lib/stores/candidate';
|
import type { CandidatePreview } from '$lib/stores/candidate';
|
||||||
|
|
||||||
export let candidates: Array<CandidatePreview> = [];
|
export let candidates: Array<CandidatePreview> = [];
|
||||||
|
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')}`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
@ -16,6 +25,9 @@
|
||||||
<th scope="col"> Obor </th>
|
<th scope="col"> Obor </th>
|
||||||
<th scope="col"> Rodné číslo </th>
|
<th scope="col"> Rodné číslo </th>
|
||||||
<th scope="col"> Link </th>
|
<th scope="col"> Link </th>
|
||||||
|
{#if showCreatedAt}
|
||||||
|
<th scope="col"> Vytvořeno </th>
|
||||||
|
{/if}
|
||||||
<th scope="col" />
|
<th scope="col" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -38,6 +50,11 @@
|
||||||
<td class="text-gray-900">
|
<td class="text-gray-900">
|
||||||
{candidate.relatedApplications?.filter((a) => a !== candidate.applicationId)}
|
{candidate.relatedApplications?.filter((a) => a !== candidate.applicationId)}
|
||||||
</td>
|
</td>
|
||||||
|
{#if showCreatedAt}
|
||||||
|
<td class="text-gray-900">
|
||||||
|
{formatRustChronoDateTime(candidate.createdAt)}
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
<td class="text-sm">
|
<td class="text-sm">
|
||||||
<Delete id={candidate.applicationId} on:delete value="Odstranit" />
|
<Delete id={candidate.applicationId} on:delete value="Odstranit" />
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export interface CandidatePreview {
|
||||||
surname?: string;
|
surname?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
fieldOfStudy?: string;
|
fieldOfStudy?: string;
|
||||||
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CandidateLogin {
|
export interface CandidateLogin {
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,17 @@
|
||||||
|
|
||||||
const getCandidates = async () => {
|
const getCandidates = async () => {
|
||||||
try {
|
try {
|
||||||
candidates = await apiListCandidates(undefined, activeFilter.filter);
|
// TODO: more generic implementation
|
||||||
|
candidates = await apiListCandidates(
|
||||||
|
undefined,
|
||||||
|
activeFilter.filter !== undefined ? { field: activeFilter.filter } : undefined
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
pushErrorText('Nepodařilo se načíst uchazeče');
|
pushErrorText('Nepodařilo se načíst uchazeče');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type Class = 'Vše' | 'KBB' | 'IT' | 'GYM';
|
type Class = 'Chronologicky' | 'KBB' | 'IT' | 'GYM';
|
||||||
|
|
||||||
type Filter = {
|
type Filter = {
|
||||||
class: Class;
|
class: Class;
|
||||||
|
|
@ -35,7 +39,7 @@
|
||||||
|
|
||||||
let filters: Array<Filter> = [
|
let filters: Array<Filter> = [
|
||||||
{
|
{
|
||||||
class: 'Vše',
|
class: 'Chronologicky',
|
||||||
filter: undefined
|
filter: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -155,7 +159,11 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Table candidates={candidatesTable} on:delete={(event) => deleteCandidate(event.detail.id)} />
|
<Table
|
||||||
|
showCreatedAt={activeFilter.class === 'Chronologicky'}
|
||||||
|
candidates={candidatesTable}
|
||||||
|
on:delete={(event) => deleteCandidate(event.detail.id)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue