nxtgauge-backend-rust/apps/companies/src/handlers/admin.rs

355 lines
11 KiB
Rust
Raw Normal View History

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<AppState> {
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<String>,
}
#[derive(Serialize)]
pub struct AdminCompanyRow {
pub id: Uuid,
pub user_id: Uuid,
pub company_name: String,
pub registration_number: Option<String>,
pub industry: Option<String>,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize)]
pub struct AdminCompanyDetail {
pub id: Uuid,
pub user_id: Uuid,
pub company_name: String,
pub registration_number: Option<String>,
pub industry: Option<String>,
pub website_url: Option<String>,
pub employee_count: Option<i32>,
pub business_type: Option<String>,
pub gst_number: Option<String>,
pub contact_name: Option<String>,
pub contact_email: Option<String>,
pub contact_phone: Option<String>,
pub address_line1: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub country: String,
pub postal_code: Option<String>,
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<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct ApproveRejectRequest {
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(
_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 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<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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<AppState>,
Path(id): Path<Uuid>,
Json(_payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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<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))
}