nxtgauge-backend-rust/crates/db/src/models/job.rs

230 lines
6.5 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Job {
pub id: Uuid,
pub company_id: Uuid,
pub title: String,
pub category: Option<String>,
pub description: String,
pub location: String,
pub job_type: String, // FULL_TIME, PART_TIME, CONTRACT
pub salary_min: Option<i32>,
pub salary_max: Option<i32>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
pub status: String, // DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED
pub rejection_reason: Option<String>,
pub expires_at: Option<DateTime<Utc>>,
pub approved_at: Option<DateTime<Utc>>,
pub approved_by: Option<Uuid>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateJobPayload {
pub company_id: Uuid,
pub title: String,
pub category: Option<String>,
pub description: String,
pub location: String,
pub job_type: Option<String>,
pub salary_min: Option<i32>,
pub salary_max: Option<i32>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateJobPayload {
pub title: Option<String>,
pub category: Option<String>,
pub description: Option<String>,
pub location: Option<String>,
pub job_type: Option<String>,
pub salary_min: Option<i32>,
pub salary_max: Option<i32>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
}
pub struct JobRepository;
impl JobRepository {
pub async fn create(pool: &PgPool, payload: CreateJobPayload) -> Result<Job, sqlx::Error> {
let job = sqlx::query_as::<_, Job>(
r#"
INSERT INTO jobs (
company_id, title, category, description, location,
job_type, salary_min, salary_max, experience_years, skills
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
"#,
)
.bind(payload.company_id)
.bind(payload.title)
.bind(payload.category)
.bind(payload.description)
.bind(payload.location)
.bind(payload.job_type.unwrap_or_else(|| "FULL_TIME".to_string()))
.bind(payload.salary_min)
.bind(payload.salary_max)
.bind(payload.experience_years)
.bind(payload.skills.unwrap_or_default())
.fetch_one(pool)
.await?;
Ok(job)
}
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Job>, sqlx::Error> {
sqlx::query_as::<_, Job>("SELECT * FROM jobs WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await
}
pub async fn list_by_company_id(
pool: &PgPool,
company_id: Uuid,
status: Option<String>,
page: i64,
limit: i64,
) -> Result<Vec<Job>, sqlx::Error> {
let offset = (page - 1) * limit;
let jobs = sqlx::query_as::<_, Job>(
r#"
SELECT * FROM jobs
WHERE company_id = $1 AND ($2::VARCHAR IS NULL OR status = $2)
ORDER BY created_at DESC
LIMIT $3 OFFSET $4
"#,
)
.bind(company_id)
.bind(status)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await?;
Ok(jobs)
}
pub async fn update(
pool: &PgPool,
id: Uuid,
payload: UpdateJobPayload,
) -> Result<Job, sqlx::Error> {
let job = sqlx::query_as::<_, Job>(
r#"
UPDATE jobs SET
title = COALESCE($1, title),
category = COALESCE($2, category),
description = COALESCE($3, description),
location = COALESCE($4, location),
job_type = COALESCE($5, job_type),
salary_min = COALESCE($6, salary_min),
salary_max = COALESCE($7, salary_max),
experience_years = COALESCE($8, experience_years),
skills = COALESCE($9, skills),
updated_at = NOW()
WHERE id = $10
RETURNING *
"#,
)
.bind(payload.title)
.bind(payload.category)
.bind(payload.description)
.bind(payload.location)
.bind(payload.job_type)
.bind(payload.salary_min)
.bind(payload.salary_max)
.bind(payload.experience_years)
.bind(payload.skills)
.bind(id)
.fetch_one(pool)
.await?;
Ok(job)
}
pub async fn update_status(
pool: &PgPool,
id: Uuid,
status: &str,
) -> Result<Job, sqlx::Error> {
let job = sqlx::query_as::<_, Job>(
"UPDATE jobs SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *",
)
.bind(status)
.bind(id)
.fetch_one(pool)
.await?;
Ok(job)
}
pub async fn approve(
pool: &PgPool,
id: Uuid,
admin_user_id: Uuid,
) -> Result<Job, sqlx::Error> {
let job = sqlx::query_as::<_, Job>(
r#"
UPDATE jobs
SET status = 'LIVE', approved_at = NOW(), approved_by = $1, rejection_reason = NULL, updated_at = NOW()
WHERE id = $2
RETURNING *
"#,
)
.bind(admin_user_id)
.bind(id)
.fetch_one(pool)
.await?;
Ok(job)
}
pub async fn reject(
pool: &PgPool,
id: Uuid,
reason: Option<String>,
) -> Result<Job, sqlx::Error> {
let job = sqlx::query_as::<_, Job>(
r#"
UPDATE jobs
SET status = 'REJECTED', rejection_reason = $1, approved_at = NULL, approved_by = NULL, updated_at = NOW()
WHERE id = $2
RETURNING *
"#,
)
.bind(reason)
.bind(id)
.fetch_one(pool)
.await?;
Ok(job)
}
pub async fn count_by_company_id_this_month(
pool: &PgPool,
company_id: Uuid,
) -> Result<i64, sqlx::Error> {
let count = sqlx::query_scalar::<_, i64>(
r#"
SELECT COUNT(*)
FROM jobs
WHERE company_id = $1
AND created_at >= date_trunc('month', now())
AND status != 'REJECTED'
"#,
)
.bind(company_id)
.fetch_one(pool)
.await?;
Ok(count)
}
}