Fix backend compile errors after schema migrations
- employees.rs: rewrite for new standalone schema (email/password_hash, no user_id/role_id FK — matches 20260402030000 migration) - migration: DROP old employees table before CREATE (old schema incompatible) - pricing.rs: merge if-else sqlx::query! branches into single nullable param query - kb.rs: fix target_roles Option<Vec<String>> unwrap, category_id Some() wrapping - support.rs: fix .or() call with non-optional user_email (use Some()) - roles.rs: fix employees JOIN from role_id (deleted) to role_code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
73629fa935
commit
3935277fb7
6 changed files with 163 additions and 237 deletions
|
|
@ -9,11 +9,10 @@ use axum::{
|
||||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::types::Uuid;
|
use sqlx::types::Uuid;
|
||||||
use sqlx::Row;
|
use auth::crypto::hash_password;
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/provision", axum::routing::post(provision_employee))
|
|
||||||
.route("/", get(list_employees).post(create_employee))
|
.route("/", get(list_employees).post(create_employee))
|
||||||
.route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee))
|
.route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee))
|
||||||
}
|
}
|
||||||
|
|
@ -21,27 +20,24 @@ pub fn router() -> Router<AppState> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ListQuery {
|
struct ListQuery {
|
||||||
q: Option<String>,
|
q: Option<String>,
|
||||||
status: Option<String>, // ACTIVE | INACTIVE | SUSPENDED
|
status: Option<String>,
|
||||||
page: Option<i64>,
|
page: Option<i64>,
|
||||||
per_page: Option<i64>,
|
per_page: Option<i64>,
|
||||||
sort: Option<String>, // name_asc | name_desc | joined_asc | joined_desc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct EmployeeRow {
|
struct EmployeeRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
user_id: Uuid,
|
first_name: String,
|
||||||
name: String,
|
last_name: String,
|
||||||
email: String,
|
email: String,
|
||||||
phone: Option<String>,
|
employee_code: Option<String>,
|
||||||
employee_id: Option<String>,
|
|
||||||
department_name: Option<String>,
|
department_name: Option<String>,
|
||||||
designation_name: Option<String>,
|
designation_name: Option<String>,
|
||||||
role_name: Option<String>,
|
role_code: String,
|
||||||
role_key: Option<String>,
|
|
||||||
joining_date: String,
|
|
||||||
status: String,
|
status: String,
|
||||||
updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
joined_at: String,
|
||||||
|
created_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -65,82 +61,65 @@ async fn list_employees(
|
||||||
let offset = (page - 1) * per_page;
|
let offset = (page - 1) * per_page;
|
||||||
let search = q.q.unwrap_or_default().to_lowercase();
|
let search = q.q.unwrap_or_default().to_lowercase();
|
||||||
let status = q.status.unwrap_or_default().to_uppercase();
|
let status = q.status.unwrap_or_default().to_uppercase();
|
||||||
let sort_sql = match q.sort.as_deref() {
|
|
||||||
Some("name_asc") => "u.full_name ASC",
|
let rows = sqlx::query!(
|
||||||
Some("name_desc") => "u.full_name DESC",
|
r#"
|
||||||
Some("joined_asc") => "e.created_at ASC",
|
|
||||||
_ => "e.created_at DESC",
|
|
||||||
};
|
|
||||||
let sql = format!(r#"
|
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
e.user_id,
|
e.first_name,
|
||||||
|
e.last_name,
|
||||||
|
e.email,
|
||||||
e.employee_code,
|
e.employee_code,
|
||||||
e.created_at AS joining_date,
|
e.role_code,
|
||||||
u.full_name,
|
e.status,
|
||||||
u.email,
|
e.joined_at,
|
||||||
u.phone,
|
e.created_at,
|
||||||
u.status AS user_status,
|
d.name AS "department_name?",
|
||||||
r.name AS role_name,
|
des.name AS "designation_name?"
|
||||||
r.key AS role_key,
|
|
||||||
d.name AS department_name,
|
|
||||||
des.name AS designation_name,
|
|
||||||
e.created_at AS updated_at
|
|
||||||
FROM employees e
|
FROM employees e
|
||||||
JOIN users u ON u.id = e.user_id
|
|
||||||
LEFT JOIN roles r ON r.id = e.role_id
|
|
||||||
LEFT JOIN departments d ON d.id = e.department_id
|
LEFT JOIN departments d ON d.id = e.department_id
|
||||||
LEFT JOIN designations des ON des.id = e.designation_id
|
LEFT JOIN designations des ON des.id = e.designation_id
|
||||||
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%'
|
||||||
AND (
|
OR LOWER(e.email) LIKE '%' || $1 || '%'
|
||||||
$2 = ''
|
OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
||||||
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE')
|
AND ($2 = '' OR e.status = $2)
|
||||||
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE')
|
ORDER BY e.created_at DESC
|
||||||
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
|
|
||||||
)
|
|
||||||
ORDER BY {sort}
|
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
"#, sort = sort_sql);
|
"#,
|
||||||
let rows = sqlx::query(&sql)
|
search,
|
||||||
.bind(&search)
|
status,
|
||||||
.bind(&status)
|
per_page,
|
||||||
.bind(per_page)
|
offset
|
||||||
.bind(offset)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||||
|
|
||||||
let employees = rows.into_iter().map(|row| {
|
let employees = rows
|
||||||
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date");
|
.into_iter()
|
||||||
EmployeeRow {
|
.map(|r| EmployeeRow {
|
||||||
id: row.get("id"),
|
id: r.id,
|
||||||
user_id: row.get("user_id"),
|
first_name: r.first_name,
|
||||||
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(),
|
last_name: r.last_name,
|
||||||
email: row.get("email"),
|
email: r.email,
|
||||||
phone: row.get::<Option<String>, _>("phone"),
|
employee_code: r.employee_code,
|
||||||
employee_id: row.get::<Option<String>, _>("employee_code"),
|
department_name: r.department_name,
|
||||||
department_name: row.get::<Option<String>, _>("department_name"),
|
designation_name: r.designation_name,
|
||||||
designation_name: row.get::<Option<String>, _>("designation_name"),
|
role_code: r.role_code,
|
||||||
role_name: row.get::<Option<String>, _>("role_name"),
|
status: r.status,
|
||||||
role_key: row.get::<Option<String>, _>("role_key"),
|
joined_at: r.joined_at.to_string(),
|
||||||
joining_date: joining.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()),
|
})
|
||||||
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")),
|
.collect::<Vec<_>>();
|
||||||
}
|
|
||||||
}).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let total: i64 = sqlx::query_scalar!(
|
let total: i64 = sqlx::query_scalar!(
|
||||||
r#"
|
r#"
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM employees e
|
FROM employees e
|
||||||
JOIN users u ON u.id = e.user_id
|
WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%'
|
||||||
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
OR LOWER(e.email) LIKE '%' || $1 || '%'
|
||||||
AND (
|
OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
||||||
$2 = ''
|
AND ($2 = '' OR e.status = $2)
|
||||||
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE')
|
|
||||||
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE')
|
|
||||||
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
|
|
||||||
)
|
|
||||||
"#,
|
"#,
|
||||||
search,
|
search,
|
||||||
status
|
status
|
||||||
|
|
@ -161,52 +140,50 @@ async fn get_employee(
|
||||||
if let Err(_e) = require_admin(&auth) {
|
if let Err(_e) = require_admin(&auth) {
|
||||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||||
}
|
}
|
||||||
let sql = r#"
|
let r = sqlx::query!(
|
||||||
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.user_id, e.employee_code,
|
e.id, e.first_name, e.last_name, e.email, e.employee_code,
|
||||||
e.created_at AS joining_date, e.created_at AS updated_at,
|
e.role_code, e.status, e.joined_at, e.created_at,
|
||||||
u.full_name, u.email, u.phone, u.status AS user_status,
|
d.name AS "department_name?",
|
||||||
r.name AS role_name, r.key AS role_key,
|
des.name AS "designation_name?"
|
||||||
d.name AS department_name,
|
|
||||||
des.name AS designation_name
|
|
||||||
FROM employees e
|
FROM employees e
|
||||||
JOIN users u ON u.id = e.user_id
|
|
||||||
LEFT JOIN roles r ON r.id = e.role_id
|
|
||||||
LEFT JOIN departments d ON d.id = e.department_id
|
LEFT JOIN departments d ON d.id = e.department_id
|
||||||
LEFT JOIN designations des ON des.id = e.designation_id
|
LEFT JOIN designations des ON des.id = e.designation_id
|
||||||
WHERE e.id = $1
|
WHERE e.id = $1
|
||||||
"#;
|
"#,
|
||||||
let row = sqlx::query(sql)
|
id
|
||||||
.bind(id)
|
)
|
||||||
.fetch_optional(&state.pool)
|
.fetch_optional(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||||
.ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?;
|
.ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?;
|
||||||
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date");
|
|
||||||
Ok(Json(EmployeeRow {
|
Ok(Json(EmployeeRow {
|
||||||
id: row.get("id"),
|
id: r.id,
|
||||||
user_id: row.get("user_id"),
|
first_name: r.first_name,
|
||||||
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(),
|
last_name: r.last_name,
|
||||||
email: row.get("email"),
|
email: r.email,
|
||||||
phone: row.get::<Option<String>, _>("phone"),
|
employee_code: r.employee_code,
|
||||||
employee_id: row.get::<Option<String>, _>("employee_code"),
|
department_name: r.department_name,
|
||||||
department_name: row.get::<Option<String>, _>("department_name"),
|
designation_name: r.designation_name,
|
||||||
designation_name: row.get::<Option<String>, _>("designation_name"),
|
role_code: r.role_code,
|
||||||
role_name: row.get::<Option<String>, _>("role_name"),
|
status: r.status,
|
||||||
role_key: row.get::<Option<String>, _>("role_key"),
|
joined_at: r.joined_at.to_string(),
|
||||||
joining_date: joining.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()),
|
|
||||||
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CreateEmployeePayload {
|
struct CreateEmployeePayload {
|
||||||
user_id: Uuid,
|
first_name: String,
|
||||||
role_id: Uuid,
|
last_name: String,
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
employee_code: Option<String>,
|
||||||
department_id: Option<Uuid>,
|
department_id: Option<Uuid>,
|
||||||
designation_id: Option<Uuid>,
|
designation_id: Option<Uuid>,
|
||||||
employee_code: Option<String>,
|
role_code: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_employee(
|
async fn create_employee(
|
||||||
|
|
@ -217,103 +194,49 @@ async fn create_employee(
|
||||||
if let Err(_e) = require_admin(&auth) {
|
if let Err(_e) = require_admin(&auth) {
|
||||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||||
}
|
}
|
||||||
let row = sqlx::query!(
|
let password_hash = hash_password(&p.password)
|
||||||
r#"
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Crypto error: {e}")))?;
|
||||||
INSERT INTO employees (user_id, role_id, department_id, designation_id, employee_code)
|
let role_code = p.role_code.unwrap_or_else(|| "STAFF".to_string());
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
role_id = EXCLUDED.role_id,
|
|
||||||
department_id = EXCLUDED.department_id,
|
|
||||||
designation_id = EXCLUDED.designation_id,
|
|
||||||
employee_code = EXCLUDED.employee_code
|
|
||||||
RETURNING id
|
|
||||||
"#,
|
|
||||||
p.user_id, p.role_id, p.department_id, p.designation_id, p.employee_code
|
|
||||||
)
|
|
||||||
.fetch_one(&state.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
|
||||||
get_employee(auth, State(state), Path(row.id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ProvisionEmployeePayload {
|
|
||||||
email: String,
|
|
||||||
full_name: String,
|
|
||||||
role_id: Uuid,
|
|
||||||
department_id: Option<Uuid>,
|
|
||||||
designation_id: Option<Uuid>,
|
|
||||||
employee_code: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
generate_login: bool,
|
|
||||||
password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn provision_employee(
|
|
||||||
auth: AuthUser,
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Json(p): Json<ProvisionEmployeePayload>,
|
|
||||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
||||||
if let Err(_e) = require_admin(&auth) {
|
|
||||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
|
||||||
}
|
|
||||||
use db::models::user::{CreateUserPayload, UserRepository};
|
|
||||||
use auth::crypto::hash_password;
|
|
||||||
|
|
||||||
let email = p.email.trim().to_lowercase();
|
let email = p.email.trim().to_lowercase();
|
||||||
|
|
||||||
// 1. Resolve User
|
let r = sqlx::query!(
|
||||||
let user_id = match UserRepository::get_by_email(&state.pool, &email).await {
|
|
||||||
Ok(user) => user.id,
|
|
||||||
Err(_) => {
|
|
||||||
if !p.generate_login {
|
|
||||||
return Err((StatusCode::BAD_REQUEST, "User not found for this email".to_string()));
|
|
||||||
}
|
|
||||||
let plain_password = p.password.unwrap_or_else(|| format!("{:08}", rand::random::<u32>() % 100_000_000));
|
|
||||||
if plain_password.len() < 8 {
|
|
||||||
return Err((StatusCode::BAD_REQUEST, "Password must be at least 8 characters".to_string()));
|
|
||||||
}
|
|
||||||
let password_hash = hash_password(&plain_password)
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Crypto error: {e}")))?;
|
|
||||||
|
|
||||||
let new_user = UserRepository::create(&state.pool, CreateUserPayload {
|
|
||||||
full_name: p.full_name.clone(),
|
|
||||||
email: email.clone(),
|
|
||||||
phone: None,
|
|
||||||
password_hash,
|
|
||||||
}).await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
|
||||||
|
|
||||||
new_user.id
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Link Employee
|
|
||||||
let row = sqlx::query!(
|
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO employees (user_id, role_id, department_id, designation_id, employee_code)
|
INSERT INTO employees (first_name, last_name, email, password_hash, employee_code,
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
department_id, designation_id, role_code)
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
role_id = EXCLUDED.role_id,
|
|
||||||
department_id = EXCLUDED.department_id,
|
|
||||||
designation_id = EXCLUDED.designation_id,
|
|
||||||
employee_code = EXCLUDED.employee_code
|
|
||||||
RETURNING id
|
RETURNING id
|
||||||
"#,
|
"#,
|
||||||
user_id, p.role_id, p.department_id, p.designation_id, p.employee_code
|
p.first_name,
|
||||||
|
p.last_name,
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
p.employee_code,
|
||||||
|
p.department_id,
|
||||||
|
p.designation_id,
|
||||||
|
role_code,
|
||||||
)
|
)
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
.map_err(|e| {
|
||||||
|
if e.to_string().contains("unique") {
|
||||||
|
(StatusCode::CONFLICT, "Email already exists".to_string())
|
||||||
|
} else {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}"))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
get_employee(auth, State(state), Path(row.id)).await
|
get_employee(auth, State(state), Path(r.id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct UpdateEmployeePayload {
|
struct UpdateEmployeePayload {
|
||||||
role_id: Option<Uuid>,
|
first_name: Option<String>,
|
||||||
|
last_name: Option<String>,
|
||||||
|
employee_code: Option<String>,
|
||||||
department_id: Option<Uuid>,
|
department_id: Option<Uuid>,
|
||||||
designation_id: Option<Uuid>,
|
designation_id: Option<Uuid>,
|
||||||
employee_code: Option<String>,
|
role_code: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_employee(
|
async fn update_employee(
|
||||||
|
|
@ -329,17 +252,29 @@ async fn update_employee(
|
||||||
r#"
|
r#"
|
||||||
UPDATE employees
|
UPDATE employees
|
||||||
SET
|
SET
|
||||||
role_id = COALESCE($1, role_id),
|
first_name = COALESCE($1, first_name),
|
||||||
department_id = COALESCE($2, department_id),
|
last_name = COALESCE($2, last_name),
|
||||||
designation_id = COALESCE($3, designation_id),
|
employee_code = COALESCE($3, employee_code),
|
||||||
employee_code = COALESCE($4, employee_code)
|
department_id = COALESCE($4, department_id),
|
||||||
WHERE id = $5
|
designation_id = COALESCE($5, designation_id),
|
||||||
|
role_code = COALESCE($6, role_code),
|
||||||
|
status = COALESCE($7, status),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $8
|
||||||
"#,
|
"#,
|
||||||
p.role_id, p.department_id, p.designation_id, p.employee_code, id
|
p.first_name,
|
||||||
|
p.last_name,
|
||||||
|
p.employee_code,
|
||||||
|
p.department_id,
|
||||||
|
p.designation_id,
|
||||||
|
p.role_code,
|
||||||
|
p.status,
|
||||||
|
id,
|
||||||
)
|
)
|
||||||
.execute(&state.pool)
|
.execute(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||||
|
|
||||||
get_employee(auth, State(state), Path(id)).await
|
get_employee(auth, State(state), Path(id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ async fn public_list_articles(
|
||||||
let articles: Vec<_> = rows
|
let articles: Vec<_> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let role = derive_role(&r.target_roles);
|
let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
|
||||||
PublicArticleDto {
|
PublicArticleDto {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
|
|
@ -245,7 +245,7 @@ async fn public_get_article(
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Ok(Some(r)) => {
|
Ok(Some(r)) => {
|
||||||
let role = derive_role(&r.target_roles);
|
let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
|
||||||
let dto = PublicArticleDto {
|
let dto = PublicArticleDto {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
|
|
@ -528,11 +528,11 @@ async fn admin_list_articles(
|
||||||
title: r.title,
|
title: r.title,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
category_id: r.category_id,
|
category_id: Some(r.category_id),
|
||||||
category: Some(r.category_name),
|
category: Some(r.category_name),
|
||||||
content: r.body,
|
content: r.body,
|
||||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||||
target_roles: r.target_roles,
|
target_roles: r.target_roles.unwrap_or_default(),
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
views: r.views,
|
views: r.views,
|
||||||
created_at: r.created_at.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
|
@ -606,11 +606,11 @@ async fn admin_create_article(
|
||||||
title: r.title,
|
title: r.title,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
category_id: r.category_id,
|
category_id: Some(r.category_id),
|
||||||
category: None,
|
category: None,
|
||||||
content: r.body,
|
content: r.body,
|
||||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||||
target_roles: r.target_roles,
|
target_roles: r.target_roles.unwrap_or_default(),
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
views: r.views,
|
views: r.views,
|
||||||
created_at: r.created_at.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
|
@ -668,11 +668,11 @@ async fn admin_get_article(
|
||||||
title: r.title,
|
title: r.title,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
category_id: r.category_id,
|
category_id: Some(r.category_id),
|
||||||
category: Some(r.category_name),
|
category: Some(r.category_name),
|
||||||
content: r.body,
|
content: r.body,
|
||||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||||
target_roles: r.target_roles,
|
target_roles: r.target_roles.unwrap_or_default(),
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
views: r.views,
|
views: r.views,
|
||||||
created_at: r.created_at.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
|
@ -752,11 +752,11 @@ async fn admin_update_article(
|
||||||
title: r.title,
|
title: r.title,
|
||||||
slug: r.slug,
|
slug: r.slug,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
category_id: r.category_id,
|
category_id: Some(r.category_id),
|
||||||
category: None,
|
category: None,
|
||||||
content: r.body,
|
content: r.body,
|
||||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||||
target_roles: r.target_roles,
|
target_roles: r.target_roles.unwrap_or_default(),
|
||||||
tags: r.tags,
|
tags: r.tags,
|
||||||
views: r.views,
|
views: r.views,
|
||||||
created_at: r.created_at.to_rfc3339(),
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
|
|
||||||
|
|
@ -94,30 +94,18 @@ async fn public_list_packages(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<PackageQuery>,
|
Query(params): Query<PackageQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let rows = if let Some(role) = params.role {
|
let rows = sqlx::query!(
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
|
|
||||||
FROM pricing_packages
|
|
||||||
WHERE is_active = true AND (role_key = $1 OR role_key = 'ALL')
|
|
||||||
ORDER BY price_inr
|
|
||||||
"#,
|
|
||||||
role
|
|
||||||
)
|
|
||||||
.fetch_all(&state.pool)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
r#"
|
||||||
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
|
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
|
||||||
FROM pricing_packages
|
FROM pricing_packages
|
||||||
WHERE is_active = true
|
WHERE is_active = true
|
||||||
|
AND ($1::text IS NULL OR role_key = $1 OR role_key = 'ALL')
|
||||||
ORDER BY role_key, price_inr
|
ORDER BY role_key, price_inr
|
||||||
"#
|
"#,
|
||||||
|
params.role as Option<String>
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await;
|
||||||
};
|
|
||||||
|
|
||||||
match rows {
|
match rows {
|
||||||
Ok(rows) => {
|
Ok(rows) => {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ async fn list_roles(
|
||||||
COUNT(DISTINCT rp.id) AS permissions_count
|
COUNT(DISTINCT rp.id) AS permissions_count
|
||||||
FROM roles r
|
FROM roles r
|
||||||
LEFT JOIN departments d ON d.id = r.department_id
|
LEFT JOIN departments d ON d.id = r.department_id
|
||||||
LEFT JOIN employees e ON e.role_id = r.id
|
LEFT JOIN employees e ON e.role_code = r.key
|
||||||
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
||||||
WHERE ($1 = '' OR r.audience = $1)
|
WHERE ($1 = '' OR r.audience = $1)
|
||||||
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%')
|
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%')
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ async fn admin_list_cases(
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
// Use user info if available, fall back to requester fields
|
// Use user info if available, fall back to requester fields
|
||||||
let requester_name = r.requester_name.or(r.user_name);
|
let requester_name = r.requester_name.or(r.user_name);
|
||||||
let requester_email = r.requester_email.or(r.user_email);
|
let requester_email = r.requester_email.or(Some(r.user_email));
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"id": r.id,
|
"id": r.id,
|
||||||
"title": r.subject,
|
"title": r.subject,
|
||||||
|
|
@ -562,7 +562,7 @@ async fn admin_get_case(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let requester_name = t.requester_name.or(t.user_name);
|
let requester_name = t.requester_name.or(t.user_name);
|
||||||
let requester_email = t.requester_email.or(t.user_email);
|
let requester_email = t.requester_email.or(Some(t.user_email));
|
||||||
|
|
||||||
(StatusCode::OK, Json(serde_json::json!({
|
(StatusCode::OK, Json(serde_json::json!({
|
||||||
"ticket": {
|
"ticket": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
-- UP: 20260402030000_strict_employee_separation.up.sql
|
-- UP: 20260402030000_strict_employee_separation.up.sql
|
||||||
|
|
||||||
|
-- Drop old employees table (was linked to users — replacing with standalone auth)
|
||||||
|
DROP TABLE IF EXISTS employees CASCADE;
|
||||||
|
|
||||||
-- 1. EMPLOYEES (Standalone Table - Not Linked to 'users')
|
-- 1. EMPLOYEES (Standalone Table - Not Linked to 'users')
|
||||||
CREATE TABLE IF NOT EXISTS employees (
|
CREATE TABLE employees (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
first_name VARCHAR(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name VARCHAR(100) NOT NULL,
|
||||||
|
|
@ -10,14 +13,14 @@ CREATE TABLE IF NOT EXISTS employees (
|
||||||
employee_code VARCHAR(50) UNIQUE,
|
employee_code VARCHAR(50) UNIQUE,
|
||||||
department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
||||||
designation_id UUID REFERENCES designations(id) ON DELETE SET NULL,
|
designation_id UUID REFERENCES designations(id) ON DELETE SET NULL,
|
||||||
role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF', -- ADMIN, MANAGER, STAFF
|
role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF',
|
||||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, INACTIVE, SUSPENDED
|
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||||
joined_at DATE NOT NULL DEFAULT CURRENT_DATE,
|
joined_at DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 4. EMPLOYEE SESSIONS (Standalone Auth)
|
-- 2. EMPLOYEE SESSIONS (Standalone Auth)
|
||||||
CREATE TABLE IF NOT EXISTS employee_sessions (
|
CREATE TABLE IF NOT EXISTS employee_sessions (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue