feat(companies): add admin jobs and applications list endpoints
- Add AdminJobRow and AdminApplicationRow response structs - Implement GET /api/admin/jobs with company join and applications count - Implement GET /api/admin/applications with applicant and job details - Gateway routing update will follow
This commit is contained in:
parent
d996131890
commit
13643ffb1b
1 changed files with 107 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ use axum::{
|
||||||
routing::{get, patch},
|
routing::{get, patch},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use contracts::auth_middleware::AuthUser;
|
use contracts::auth_middleware::AuthUser;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -17,6 +18,8 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/{id}/approve", patch(approve_company))
|
.route("/{id}/approve", patch(approve_company))
|
||||||
.route("/{id}/reject", patch(reject_company))
|
.route("/{id}/reject", patch(reject_company))
|
||||||
.route("/{id}/suspend", patch(suspend_company))
|
.route("/{id}/suspend", patch(suspend_company))
|
||||||
|
.route("/jobs", get(list_jobs))
|
||||||
|
.route("/applications", get(list_applications))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -69,6 +72,43 @@ pub struct ApproveRejectRequest {
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdminJobRow {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub company_id: Uuid,
|
||||||
|
pub company_name: String,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub job_type: Option<String>,
|
||||||
|
pub salary_min: Option<i32>,
|
||||||
|
pub salary_max: Option<i32>,
|
||||||
|
pub status: String,
|
||||||
|
pub is_featured: bool,
|
||||||
|
pub applications_count: i64,
|
||||||
|
pub posted_at: Option<DateTime<Utc>>,
|
||||||
|
pub expires_at: Option<DateTime<Utc>>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdminApplicationRow {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub job_id: Uuid,
|
||||||
|
pub job_title: String,
|
||||||
|
pub company_id: Uuid,
|
||||||
|
pub company_name: String,
|
||||||
|
pub applicant_id: Uuid,
|
||||||
|
pub applicant_name: String,
|
||||||
|
pub applicant_email: String,
|
||||||
|
pub status: String,
|
||||||
|
pub cover_letter: Option<String>,
|
||||||
|
pub resume_url: Option<String>,
|
||||||
|
pub applied_at: DateTime<Utc>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_companies(
|
async fn list_companies(
|
||||||
_auth: AuthUser,
|
_auth: AuthUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -245,3 +285,70 @@ async fn suspend_company(
|
||||||
"message": "Company suspended"
|
"message": "Company suspended"
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_jobs(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(q): Query<ListQuery>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let search = q.q.as_deref().unwrap_or_default().to_lowercase();
|
||||||
|
|
||||||
|
let jobs = sqlx::query_as!(
|
||||||
|
AdminJobRow,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
j.id, j.title, j.description, j.company_id, cp.company_name,
|
||||||
|
j.location, j.job_type, j.salary_min, j.salary_max,
|
||||||
|
j.status, j.is_featured,
|
||||||
|
COUNT(a.id) AS "applications_count!",
|
||||||
|
j.posted_at, j.expires_at,
|
||||||
|
j.created_at, j.updated_at
|
||||||
|
FROM jobs j
|
||||||
|
JOIN company_profiles cp ON j.company_id = cp.id
|
||||||
|
LEFT JOIN applications a ON a.job_id = j.id
|
||||||
|
WHERE ($1 = '' OR LOWER(j.title) LIKE '%' || $1 || '%')
|
||||||
|
GROUP BY j.id, cp.company_name
|
||||||
|
ORDER BY j.created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
"#,
|
||||||
|
search
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||||
|
|
||||||
|
Ok(Json(jobs))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_applications(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(q): Query<ListQuery>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let search = q.q.as_deref().unwrap_or_default().to_lowercase();
|
||||||
|
|
||||||
|
let applications = sqlx::query_as!(
|
||||||
|
AdminApplicationRow,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
a.id, a.job_id, j.title AS "job_title!", a.company_id, cp.company_name,
|
||||||
|
a.user_id AS "applicant_id!", COALESCE(u.first_name, '') || ' ' || COALESCE(u.last_name, '') AS "applicant_name!",
|
||||||
|
u.email AS "applicant_email!",
|
||||||
|
a.status, a.cover_letter, a.resume_url,
|
||||||
|
a.applied_at, a.created_at
|
||||||
|
FROM applications a
|
||||||
|
JOIN jobs j ON a.job_id = j.id
|
||||||
|
JOIN company_profiles cp ON j.company_id = cp.id
|
||||||
|
JOIN users u ON a.user_id = u.id
|
||||||
|
WHERE ($1 = '' OR LOWER(j.title) LIKE '%' || $1 || '%' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%')
|
||||||
|
ORDER BY a.applied_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
"#,
|
||||||
|
search
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||||
|
|
||||||
|
Ok(Json(applications))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue