230 lines
6.5 KiB
Rust
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)
|
|
}
|
|
}
|