diff --git a/apps/companies/src/handlers/admin.rs b/apps/companies/src/handlers/admin.rs index ad8d45f..d9c1c89 100644 --- a/apps/companies/src/handlers/admin.rs +++ b/apps/companies/src/handlers/admin.rs @@ -6,6 +6,7 @@ use axum::{ routing::{get, patch}, Json, Router, }; +use chrono::{DateTime, Utc}; use contracts::auth_middleware::AuthUser; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -17,6 +18,8 @@ pub fn router() -> Router { .route("/{id}/approve", patch(approve_company)) .route("/{id}/reject", patch(reject_company)) .route("/{id}/suspend", patch(suspend_company)) + .route("/jobs", get(list_jobs)) + .route("/applications", get(list_applications)) } #[derive(Deserialize)] @@ -69,6 +72,43 @@ pub struct ApproveRejectRequest { pub reason: Option, } +#[derive(Serialize)] +pub struct AdminJobRow { + pub id: Uuid, + pub title: String, + pub description: Option, + pub company_id: Uuid, + pub company_name: String, + pub location: Option, + pub job_type: Option, + pub salary_min: Option, + pub salary_max: Option, + pub status: String, + pub is_featured: bool, + pub applications_count: i64, + pub posted_at: Option>, + pub expires_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[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, + pub resume_url: Option, + pub applied_at: DateTime, + pub created_at: DateTime, +} + async fn list_companies( _auth: AuthUser, State(state): State, @@ -245,3 +285,70 @@ async fn suspend_company( "message": "Company suspended" }))) } + +async fn list_jobs( + _auth: AuthUser, + State(state): State, + Query(q): Query, +) -> Result { + 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, + Query(q): Query, +) -> Result { + 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)) +}