use crate::AppState; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, patch}, Json, Router, }; use chrono::{DateTime, Utc}; use contracts::auth_middleware::AuthUser; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/", get(list_companies)) .route("/{id}", get(get_company)) .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)] pub struct ListQuery { pub q: Option, } #[derive(Serialize)] pub struct AdminCompanyRow { pub id: Uuid, pub user_id: Uuid, pub company_name: String, pub registration_number: Option, pub industry: Option, pub status: String, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } #[derive(Serialize)] pub struct AdminCompanyDetail { pub id: Uuid, pub user_id: Uuid, pub company_name: String, pub registration_number: Option, pub industry: Option, pub website_url: Option, pub employee_count: Option, pub business_type: Option, pub gst_number: Option, pub contact_name: Option, pub contact_email: Option, pub contact_phone: Option, pub address_line1: Option, pub city: Option, pub state: Option, pub country: String, pub postal_code: Option, pub status: String, pub free_job_slots: i32, pub purchased_job_slots: i32, pub free_contact_views: i32, pub purchased_contact_views: i32, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } #[derive(Deserialize)] 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, Query(q): Query, ) -> Result { let search = q.q.as_deref().unwrap_or_default().to_lowercase(); let companies = sqlx::query_as!( AdminCompanyRow, r#" SELECT id, user_id, company_name, registration_number, industry, status, created_at, updated_at FROM company_profiles WHERE ($1 = '' OR LOWER(company_name) LIKE '%' || $1 || '%') ORDER BY created_at DESC LIMIT 100 "#, search ) .fetch_all(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; Ok(Json(companies)) } async fn get_company( _auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { let company = sqlx::query_as!( AdminCompanyDetail, r#" SELECT id, user_id, company_name, registration_number, industry, website_url, employee_count, business_type, gst_number, contact_name, contact_email, contact_phone, address_line1, city, state, country, postal_code, status, free_job_slots, purchased_job_slots, free_contact_views, purchased_contact_views, created_at, updated_at FROM company_profiles WHERE id = $1 "#, id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; match company { Some(c) => Ok(Json(c)), None => Err((StatusCode::NOT_FOUND, "Company not found".to_string())), } } async fn approve_company( _auth: AuthUser, State(state): State, Path(id): Path, Json(_payload): Json, ) -> Result { let company = sqlx::query!( "SELECT id, user_id, status FROM company_profiles WHERE id = $1", id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let company = match company { Some(c) => c, None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())), }; if company.status == "APPROVED" { return Err((StatusCode::BAD_REQUEST, "Company is already approved".to_string())); } sqlx::query!( "UPDATE company_profiles SET status = 'APPROVED', updated_at = NOW() WHERE id = $1", id ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; Ok(Json(serde_json::json!({ "id": id, "status": "APPROVED", "message": "Company approved successfully" }))) } async fn reject_company( _auth: AuthUser, State(state): State, Path(id): Path, Json(payload): Json, ) -> Result { let reason = payload.reason.as_deref().unwrap_or("No reason provided"); let company = sqlx::query!( "SELECT id, user_id, status FROM company_profiles WHERE id = $1", id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let company = match company { Some(c) => c, None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())), }; if company.status == "REJECTED" { return Err((StatusCode::BAD_REQUEST, "Company is already rejected".to_string())); } sqlx::query!( "UPDATE company_profiles SET status = 'REJECTED', updated_at = NOW() WHERE id = $1", id ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; Ok(Json(serde_json::json!({ "id": id, "status": "REJECTED", "reason": reason, "message": "Company rejected" }))) } async fn suspend_company( _auth: AuthUser, State(state): State, Path(id): Path, Json(payload): Json, ) -> Result { let reason = payload.reason.as_deref().unwrap_or("No reason provided"); let company = sqlx::query!( "SELECT id, user_id, status FROM company_profiles WHERE id = $1", id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let company = match company { Some(c) => c, None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())), }; if company.status == "SUSPENDED" { return Err((StatusCode::BAD_REQUEST, "Company is already suspended".to_string())); } sqlx::query!( "UPDATE company_profiles SET status = 'SUSPENDED', updated_at = NOW() WHERE id = $1", id ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; Ok(Json(serde_json::json!({ "id": id, "status": "SUSPENDED", "reason": reason, "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)) }