use crate::AppState; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router, }; use contracts::auth_middleware::{AuthUser, require_admin}; use serde::{Deserialize, Serialize}; use sqlx::types::Uuid; use auth::crypto::hash_password; pub fn router() -> Router { Router::new() .route("/", get(list_employees).post(create_employee)) .route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee)) } #[derive(Deserialize)] struct ListQuery { q: Option, status: Option, page: Option, per_page: Option, } #[derive(Serialize)] struct EmployeeRow { id: Uuid, first_name: String, last_name: String, email: String, employee_code: Option, department_name: Option, designation_name: Option, role_code: String, status: String, joined_at: String, created_at: String, } #[derive(Serialize)] struct ListResponse { employees: Vec, total: i64, page: i64, per_page: i64, } async fn list_employees( auth: AuthUser, State(state): State, Query(q): Query, ) -> Result { if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } let page = q.page.unwrap_or(1).max(1); let per_page = q.per_page.unwrap_or(20).clamp(1, 100); let offset = (page - 1) * per_page; let search = q.q.unwrap_or_default().to_lowercase(); let status = q.status.unwrap_or_default().to_uppercase(); let rows = sqlx::query!( r#" SELECT e.id, e.first_name, e.last_name, e.email, e.employee_code, e.role_code, e.status, e.joined_at, e.created_at, d.name AS "department_name?", des.name AS "designation_name?" FROM employees e LEFT JOIN departments d ON d.id = e.department_id LEFT JOIN designations des ON des.id = e.designation_id WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%' OR LOWER(e.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%') AND ($2 = '' OR e.status = $2) ORDER BY e.created_at DESC LIMIT $3 OFFSET $4 "#, search, status, per_page, offset ) .fetch_all(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let employees = rows .into_iter() .map(|r| EmployeeRow { id: r.id, first_name: r.first_name, last_name: r.last_name, email: r.email, employee_code: r.employee_code, department_name: r.department_name, designation_name: r.designation_name, role_code: r.role_code, status: r.status, joined_at: r.joined_at.to_string(), created_at: r.created_at.to_rfc3339(), }) .collect::>(); let total: i64 = sqlx::query_scalar!( r#" SELECT COUNT(*) FROM employees e WHERE ($1 = '' OR LOWER(e.first_name || ' ' || e.last_name) LIKE '%' || $1 || '%' OR LOWER(e.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%') AND ($2 = '' OR e.status = $2) "#, search, status ) .fetch_one(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .unwrap_or(0); Ok(Json(ListResponse { employees, total, page, per_page })) } async fn get_employee( auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } let r = sqlx::query!( r#" SELECT e.id, e.first_name, e.last_name, e.email, e.employee_code, e.role_code, e.status, e.joined_at, e.created_at, d.name AS "department_name?", des.name AS "designation_name?" FROM employees e LEFT JOIN departments d ON d.id = e.department_id LEFT JOIN designations des ON des.id = e.designation_id WHERE e.id = $1 "#, id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?; Ok(Json(EmployeeRow { id: r.id, first_name: r.first_name, last_name: r.last_name, email: r.email, employee_code: r.employee_code, department_name: r.department_name, designation_name: r.designation_name, role_code: r.role_code, status: r.status, joined_at: r.joined_at.to_string(), created_at: r.created_at.to_rfc3339(), })) } #[derive(Deserialize)] struct CreateEmployeePayload { first_name: String, last_name: String, email: String, password: String, employee_code: Option, department_id: Option, designation_id: Option, role_code: Option, } async fn create_employee( auth: AuthUser, State(state): State, Json(p): Json, ) -> Result { if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } let password_hash = hash_password(&p.password) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Crypto error: {e}")))?; let role_code = p.role_code.unwrap_or_else(|| "STAFF".to_string()); let email = p.email.trim().to_lowercase(); let r = sqlx::query!( r#" INSERT INTO employees (first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id "#, p.first_name, p.last_name, email, password_hash, p.employee_code, p.department_id, p.designation_id, role_code, ) .fetch_one(&state.pool) .await .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(r.id)).await } #[derive(Deserialize)] struct UpdateEmployeePayload { first_name: Option, last_name: Option, employee_code: Option, department_id: Option, designation_id: Option, role_code: Option, status: Option, } async fn update_employee( auth: AuthUser, State(state): State, Path(id): Path, Json(p): Json, ) -> Result { if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } sqlx::query!( r#" UPDATE employees SET first_name = COALESCE($1, first_name), last_name = COALESCE($2, last_name), employee_code = COALESCE($3, employee_code), department_id = COALESCE($4, department_id), designation_id = COALESCE($5, designation_id), role_code = COALESCE($6, role_code), status = COALESCE($7, status), updated_at = NOW() WHERE id = $8 "#, p.first_name, p.last_name, p.employee_code, p.department_id, p.designation_id, p.role_code, p.status, id, ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; get_employee(auth, State(state), Path(id)).await } async fn delete_employee( auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } let result = sqlx::query!("DELETE FROM employees WHERE id = $1", id) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Employee not found".to_string())); } Ok(StatusCode::NO_CONTENT) }