feat: update DB schema - split users.first_name, users.last_name, roles split
This commit is contained in:
parent
92ded2b43d
commit
a3076ed526
38 changed files with 1324 additions and 1595 deletions
|
|
@ -258,7 +258,7 @@ async fn submit_job(
|
|||
Ok(updated) => {
|
||||
// Fire email to company user (ignore failures)
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await {
|
||||
let _ = state.mail.send_job_submitted_email(&user.email, user.name.as_deref().unwrap_or("User"), &updated.title).await;
|
||||
let _ = state.mail.send_job_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await;
|
||||
}
|
||||
|
||||
// Create verification case so the request appears in Verification Management first.
|
||||
|
|
@ -367,7 +367,7 @@ async fn update_application_status(
|
|||
Ok(updated) => {
|
||||
// Notify applicant of status change (ignore failures)
|
||||
let applicant_info = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT u.name, u.email FROM users u WHERE u.id = $1",
|
||||
"SELECT CONCAT(u.first_name, ' ', u.last_name) AS name, u.email, u.phone FROM users u WHERE u.id = $1",
|
||||
)
|
||||
.bind(app.applicant_user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -439,7 +439,7 @@ async fn view_contact(
|
|||
|
||||
let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>(
|
||||
r#"
|
||||
SELECT u.name, u.email, u.phone
|
||||
SELECT CONCAT(u.first_name, ' ', u.last_name) AS name, u.email, u.phone
|
||||
FROM users u
|
||||
WHERE u.id = $1
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub async fn expire_stale_jobs(
|
|||
WHERE jobs.company_id = c.id
|
||||
AND jobs.status = 'LIVE'
|
||||
AND jobs.expires_at < $1
|
||||
RETURNING jobs.id as job_id, jobs.title, u.email, u.name
|
||||
RETURNING jobs.id as job_id, jobs.title, u.email, CONCAT(u.first_name, ' ', u.last_name) AS name
|
||||
"#
|
||||
)
|
||||
.bind(now)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub async fn expire_stale_lead_requests(
|
|||
lr.tracecoins_reserved,
|
||||
urp.user_id,
|
||||
u.email,
|
||||
u.name
|
||||
CONCAT(u.first_name, ' ', u.last_name) AS name
|
||||
FROM lead_requests lr
|
||||
INNER JOIN user_role_profiles urp ON urp.id = lr.user_role_profile_id
|
||||
INNER JOIN users u ON u.id = urp.user_id
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub async fn expire_stale_leads(
|
|||
WHERE leads.created_by_user_id = u.id
|
||||
AND leads.status = 'OPEN'
|
||||
AND leads.expires_at < $1
|
||||
RETURNING leads.id as lead_id, leads.title, u.email, u.name
|
||||
RETURNING leads.id as lead_id, leads.title, u.email, CONCAT(u.first_name, ' ', u.last_name) AS name
|
||||
"#
|
||||
)
|
||||
.bind(now)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pub struct AdminLeadRow {
|
|||
pub description: Option<String>,
|
||||
pub profession_key: String,
|
||||
pub location: String,
|
||||
pub budget: Option<i32>,
|
||||
pub budget_inr: Option<i32>,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
|
|
@ -25,7 +25,7 @@ impl From<Requirement> for AdminLeadRow {
|
|||
description: Some(r.description),
|
||||
profession_key: r.profession_key,
|
||||
location: r.location,
|
||||
budget: r.budget,
|
||||
budget_inr: r.budget_inr,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ async fn create_requirement(
|
|||
title: payload.title,
|
||||
description: payload.description,
|
||||
location: payload.location,
|
||||
budget: payload.budget,
|
||||
preferred_date: p_date,
|
||||
budget_inr: payload.budget,
|
||||
required_date: p_date,
|
||||
extra_data_json: payload.extra_data_json,
|
||||
};
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ async fn submit_requirement(
|
|||
Ok(updated) => {
|
||||
// Fire email to customer (ignore failures)
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await {
|
||||
let _ = state.mail.send_requirement_submitted_email(&user.email, user.name.as_deref().unwrap_or("User"), &updated.title).await;
|
||||
let _ = state.mail.send_requirement_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await;
|
||||
}
|
||||
|
||||
// Create verification case so this request enters Verification Management first.
|
||||
|
|
@ -200,7 +200,7 @@ async fn submit_requirement(
|
|||
"title": updated.title,
|
||||
"profession_key": updated.profession_key,
|
||||
"location": updated.location,
|
||||
"budget": updated.budget,
|
||||
"budget_inr": updated.budget_inr,
|
||||
"status": updated.status,
|
||||
"created_by_user_id": updated.created_by_user_id,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -245,19 +245,19 @@ async fn apply_to_job(
|
|||
// Send email notification to company
|
||||
// Get company user details via raw query
|
||||
let company_user = sqlx::query_as::<_, (String, Option<String>)>(
|
||||
"SELECT u.email, u.name FROM users u INNER JOIN companies c ON c.user_id = u.id WHERE c.id = $1"
|
||||
"SELECT u.email, CONCAT(u.first_name, ' ', u.last_name) AS name FROM users u INNER JOIN companies c ON c.user_id = u.id WHERE c.id = $1"
|
||||
)
|
||||
.bind(job.company_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
if let Ok(Some((email, name))) = company_user {
|
||||
let seeker_name = seeker.full_name.as_deref().unwrap_or("A candidate");
|
||||
let seeker_name = format!("{} {}", seeker.first_name.unwrap_or_default(), seeker.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_new_application_email(
|
||||
&email,
|
||||
name.as_deref().unwrap_or("Company"),
|
||||
&job.title,
|
||||
seeker_name
|
||||
&seeker_name
|
||||
).await;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ pub struct ListQuery {
|
|||
pub struct AdminUserRow {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub roles: Vec<String>,
|
||||
|
|
@ -49,12 +50,12 @@ async fn list_users(
|
|||
// Generic list: users + their approved roles
|
||||
r#"
|
||||
SELECT
|
||||
u.id, u.email, u.name, u.status, u.created_at,
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
COALESCE(array_agg(r.key) FILTER (WHERE r.key IS NOT NULL), '{}') as roles
|
||||
FROM users u
|
||||
LEFT JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
LEFT JOIN roles r ON r.id = ur.role_id
|
||||
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
GROUP BY u.id
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 100
|
||||
|
|
@ -80,11 +81,11 @@ async fn list_users(
|
|||
format!(
|
||||
r#"
|
||||
SELECT
|
||||
u.id, u.email, u.name, p.status, u.created_at,
|
||||
u.id, u.email, u.first_name, u.last_name, p.status, u.created_at,
|
||||
ARRAY['{}']::text[] as roles
|
||||
FROM users u
|
||||
JOIN {} p ON p.user_id = u.id
|
||||
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -110,12 +111,12 @@ async fn list_customers(
|
|||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
u.id, u.email, u.name, u.status, u.created_at,
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
ARRAY['CUSTOMER']::text[] as roles
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN roles r ON r.id = ur.role_id AND r.key = 'CUSTOMER'
|
||||
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 50
|
||||
"#;
|
||||
|
|
@ -138,12 +139,12 @@ async fn list_candidates(
|
|||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
u.id, u.email, u.name, u.status, u.created_at,
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
ARRAY['JOB_SEEKER']::text[] as roles
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN roles r ON r.id = ur.role_id AND r.key = 'JOB_SEEKER'
|
||||
WHERE ($1 = '' OR LOWER(u.name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 50
|
||||
"#;
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ async fn get_submission(
|
|||
Json(serde_json::json!({
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"name": format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()),
|
||||
"email": user.email,
|
||||
"phone": null,
|
||||
"status": user.status,
|
||||
|
|
@ -218,7 +218,7 @@ async fn activate_profile_after_final_approval(
|
|||
};
|
||||
|
||||
let query = format!(
|
||||
"UPDATE {} SET verification_status = 'APPROVED', updated_at = NOW() WHERE id = $1",
|
||||
"UPDATE {} SET status = 'APPROVED', updated_at = NOW() WHERE id = $1",
|
||||
table
|
||||
);
|
||||
sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
|
||||
|
|
@ -243,13 +243,10 @@ async fn activate_profile_after_final_approval(
|
|||
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
let display = role_key_to_display(&role_key);
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state
|
||||
.mail
|
||||
.send_approval_approved_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&display,
|
||||
)
|
||||
.send_approval_approved_email(&user.email, &user_name, &display)
|
||||
.await;
|
||||
}
|
||||
|
||||
|
|
@ -292,18 +289,19 @@ async fn reject_profile_after_final_approval(
|
|||
};
|
||||
|
||||
let query = format!(
|
||||
"UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
table
|
||||
);
|
||||
sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
|
||||
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
let display = role_key_to_display(&role_key);
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state
|
||||
.mail
|
||||
.send_approval_rejected_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&user_name,
|
||||
&display,
|
||||
reason.unwrap_or("Rejected by final approval"),
|
||||
)
|
||||
|
|
@ -440,7 +438,7 @@ async fn approve_job(
|
|||
.await;
|
||||
|
||||
let company_info = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT u.name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
||||
"SELECT CONCAT(u.first_name, ' ', u.last_name) AS u_full_name, u.email FROM company_profiles c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
||||
)
|
||||
.bind(existing.company_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -490,7 +488,7 @@ async fn reject_job(
|
|||
.await;
|
||||
|
||||
let company_info = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT u.name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
||||
"SELECT CONCAT(u.first_name, ' ', u.last_name) AS u_full_name, u.email FROM company_profiles c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
||||
)
|
||||
.bind(existing.company_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@ pub struct RegisterPayload {
|
|||
#[serde(default)]
|
||||
pub last_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub full_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
pub email: String,
|
||||
pub phone: Option<String>,
|
||||
|
|
@ -179,7 +177,7 @@ async fn ensure_role_exists(pool: &sqlx::PgPool, role_code: &str) -> Option<Uuid
|
|||
return None;
|
||||
}
|
||||
|
||||
if let Ok(found) = sqlx::query_scalar::<_, Uuid>("SELECT id FROM roles WHERE code = $1")
|
||||
if let Ok(found) = sqlx::query_scalar::<_, Uuid>("SELECT id FROM roles WHERE key = $1")
|
||||
.bind(&normalized)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
|
|
@ -190,20 +188,29 @@ async fn ensure_role_exists(pool: &sqlx::PgPool, role_code: &str) -> Option<Uuid
|
|||
}
|
||||
|
||||
let display_name = role_display_name_from_code(&normalized);
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
let role_id = sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO roles (name, code, audience, is_active)
|
||||
VALUES ($1, $2, 'USER', true)
|
||||
ON CONFLICT (code)
|
||||
DO UPDATE SET updated_at = NOW()
|
||||
INSERT INTO roles (key, name, audience, is_active)
|
||||
VALUES ($1, $2, 'EXTERNAL', true)
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET is_active = true
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(&normalized)
|
||||
.bind(display_name)
|
||||
.bind(normalized)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()
|
||||
.ok()?;
|
||||
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO external_roles (role_id) VALUES ($1) ON CONFLICT (role_id) DO NOTHING",
|
||||
)
|
||||
.bind(role_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
Some(role_id)
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -264,15 +271,12 @@ async fn register(
|
|||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "INTERNAL_ERROR"))?;
|
||||
|
||||
let full_name = match (&payload.first_name, &payload.last_name, &payload.full_name, &payload.name) {
|
||||
(Some(fn_), Some(ln_), _, _) => format!("{} {}", fn_.trim(), ln_.trim()).trim().to_string(),
|
||||
(_, _, Some(fn_), _) => fn_.trim().to_string(),
|
||||
(_, _, _, Some(n)) => n.trim().to_string(),
|
||||
_ => return Err(err(StatusCode::UNPROCESSABLE_ENTITY, "Name is required (full_name or first_name+last_name)", "VALIDATION_ERROR")),
|
||||
};
|
||||
let first_name = payload.first_name.unwrap_or_default().trim().to_string();
|
||||
let last_name = payload.last_name.unwrap_or_default().trim().to_string();
|
||||
|
||||
let user = UserRepository::create(&state.pool, CreateUserPayload {
|
||||
name: full_name,
|
||||
first_name: Some(first_name),
|
||||
last_name: Some(last_name),
|
||||
email: email.clone(),
|
||||
password_hash,
|
||||
})
|
||||
|
|
@ -332,13 +336,14 @@ async fn register(
|
|||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
|
||||
cache::otp::record_resend(&mut redis, &user.id.to_string()).await.ok();
|
||||
|
||||
let _ = state.mail.send_verification_email(&user.email, &user.name.clone().unwrap_or_default(), &otp).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_verification_email(&user.email, &user_name, &otp).await;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(RegisterResponse {
|
||||
user_id: user.id.to_string(),
|
||||
email: user.email,
|
||||
phone: None,
|
||||
name: user.name.unwrap_or_default(),
|
||||
name: user_name,
|
||||
status: user.status,
|
||||
email_verified: user.email_verified,
|
||||
created_at: user.created_at.to_rfc3339(),
|
||||
|
|
@ -400,6 +405,7 @@ async fn login(
|
|||
);
|
||||
let active_role = user_roles.first().cloned();
|
||||
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
Ok((StatusCode::OK, [(SET_COOKIE, cookie)], Json(serde_json::json!({
|
||||
"access_token": tokens.access_token,
|
||||
"token_type": "Bearer",
|
||||
|
|
@ -407,7 +413,7 @@ async fn login(
|
|||
"user": {
|
||||
"id": user.id.to_string(),
|
||||
"email": user.email,
|
||||
"full_name": user.name.unwrap_or_default(),
|
||||
"name": user_name,
|
||||
"email_verified": user.email_verified,
|
||||
"active_role": active_role,
|
||||
"roles": user_roles,
|
||||
|
|
@ -516,10 +522,11 @@ async fn session(
|
|||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
Ok(Json(SessionUser {
|
||||
id: user.id.to_string(),
|
||||
email: user.email,
|
||||
name: user.name.unwrap_or_default(),
|
||||
name: user_name,
|
||||
email_verified: user.email_verified,
|
||||
active_role: user_roles.first().cloned(),
|
||||
roles: user_roles,
|
||||
|
|
@ -549,7 +556,8 @@ async fn verify_email(
|
|||
|
||||
// Get user details for welcome email
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
let _ = state.mail.send_welcome_email(&user.email, &user.name.unwrap_or_default()).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_welcome_email(&user.email, &user_name).await;
|
||||
}
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Email verified successfully" }))))
|
||||
|
|
@ -585,7 +593,8 @@ async fn resend_otp(
|
|||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
|
||||
cache::otp::record_resend(&mut redis, &user.id.to_string()).await.ok();
|
||||
|
||||
let _ = state.mail.send_verification_email(&user.email, &user.name.unwrap_or_default(), &otp).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_verification_email(&user.email, &user_name, &otp).await;
|
||||
|
||||
Ok(silent_ok)
|
||||
}
|
||||
|
|
@ -610,7 +619,8 @@ async fn forgot_password(
|
|||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "CACHE_ERROR"))?;
|
||||
|
||||
let _ = state.mail.send_password_reset_email(&user.email, &user.name.unwrap_or_default(), &token).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_password_reset_email(&user.email, &user_name, &token).await;
|
||||
|
||||
Ok(silent_ok)
|
||||
}
|
||||
|
|
@ -644,7 +654,8 @@ async fn reset_password(
|
|||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
|
||||
|
||||
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
let _ = state.mail.send_password_changed_email(&user.email, user.name.as_deref().unwrap_or_default()).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_password_changed_email(&user.email, &user_name).await;
|
||||
}
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Password reset successfully" }))))
|
||||
|
|
@ -677,7 +688,8 @@ async fn change_password(
|
|||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
|
||||
|
||||
let _ = state.mail.send_password_changed_email(&user.email, user.name.as_deref().unwrap_or_default()).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_password_changed_email(&user.email, &user_name).await;
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Password changed successfully" }))))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ async fn get_my_runtime_config(
|
|||
"user".to_string(),
|
||||
serde_json::json!({
|
||||
"id": user.id.to_string(),
|
||||
"name": user.name.unwrap_or_default(),
|
||||
"name": format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()),
|
||||
"email": user.email,
|
||||
"roles": roles,
|
||||
"active_role": role_key,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
|
|||
let recent_leads = sqlx::query_as::<_, LeadRow>(
|
||||
r#"
|
||||
SELECT r.id, r.title, r.status, r.created_at,
|
||||
u.name AS requester_name
|
||||
CONCAT(u.first_name, ' ', u.last_name) AS requester_name
|
||||
FROM leads r
|
||||
LEFT JOIN users u ON u.id = r.created_by_user_id
|
||||
WHERE r.status IN ('PENDING_APPROVAL', 'APPROVED')
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ pub fn router() -> Router<AppState> {
|
|||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
q: Option<String>,
|
||||
status: Option<String>, // ACTIVE | INACTIVE
|
||||
vertical: Option<String>, // jobs | marketplace
|
||||
category: Option<String>, // provider | employer | consumer | specialist
|
||||
status: Option<String>,
|
||||
vertical: Option<String>,
|
||||
category: Option<String>,
|
||||
page: Option<i64>,
|
||||
per_page: Option<i64>,
|
||||
}
|
||||
|
|
@ -83,7 +83,6 @@ async fn list_external_roles(
|
|||
let vertical = q.vertical.unwrap_or_default().to_lowercase();
|
||||
let category = q.category.unwrap_or_default().to_lowercase();
|
||||
|
||||
// Join roles with active runtime_config for that role (optional) and count assigned user_roles
|
||||
let rows = sqlx::query_as::<_, ExternalRoleListRow>(
|
||||
r#"
|
||||
SELECT
|
||||
|
|
@ -95,8 +94,8 @@ async fn list_external_roles(
|
|||
rc.updated_at as "updated_at",
|
||||
rc.config_json as "config_json"
|
||||
FROM roles r
|
||||
LEFT JOIN runtime_configs rc
|
||||
ON rc.role_id = r.id AND rc.is_active = true
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
LEFT JOIN runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
|
||||
|
|
@ -112,11 +111,11 @@ async fn list_external_roles(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Compute total with same filters
|
||||
let total: i64 = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM roles r
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
|
||||
|
|
@ -149,14 +148,12 @@ async fn list_external_roles(
|
|||
assigned_user_types = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
|
||||
}
|
||||
}
|
||||
// Additional filters by vertical/category after extracting from config
|
||||
if !vertical.is_empty() && vertical_v.as_deref() != Some(vertical.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if !category.is_empty() && category_v.as_deref() != Some(category.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// Count assigned users from user_roles (approved)
|
||||
let assigned_users: i64 = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM user_roles WHERE role_id = $1 AND status = 'APPROVED'",
|
||||
)
|
||||
|
|
@ -223,8 +220,10 @@ async fn get_external_role(
|
|||
}
|
||||
let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
|
||||
r#"
|
||||
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at, rc.updated_at as updated_at, rc.config_json as config_json
|
||||
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
|
||||
rc.updated_at as updated_at, rc.config_json as config_json
|
||||
FROM roles r
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
LEFT JOIN runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.id = $1 AND r.audience = 'EXTERNAL'
|
||||
"#,
|
||||
|
|
@ -252,7 +251,7 @@ struct CreateExternalRolePayload {
|
|||
name: String,
|
||||
code: String,
|
||||
is_active: Option<bool>,
|
||||
runtime: JsonValue, // carries vertical/category/modules/permissions/assigned_user_types/requires/feature_limits/onboarding_schema_id
|
||||
runtime: JsonValue,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
|
@ -279,7 +278,6 @@ async fn create_external_role(
|
|||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let is_active = payload.is_active.unwrap_or(true);
|
||||
// Insert role
|
||||
let role = sqlx::query_as::<_, InsertedRole>(
|
||||
r#"
|
||||
INSERT INTO roles (key, name, audience, is_active)
|
||||
|
|
@ -294,7 +292,14 @@ async fn create_external_role(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Create runtime config version 1
|
||||
sqlx::query(
|
||||
"INSERT INTO external_roles (role_id) VALUES ($1)",
|
||||
)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
let rc = sqlx::query_as::<_, InsertedRc>(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
|
|
@ -339,7 +344,6 @@ async fn update_external_role(
|
|||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
// Update role basic fields
|
||||
if payload.name.is_some() || payload.is_active.is_some() {
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
|
@ -356,7 +360,6 @@ async fn update_external_role(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
}
|
||||
// Create a new runtime config version if provided
|
||||
if let Some(runtime) = payload.runtime {
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ struct AdminArticleRow {
|
|||
category_id: Uuid,
|
||||
target_roles: Option<Vec<String>>,
|
||||
tags: Vec<String>,
|
||||
is_published: bool,
|
||||
status: String,
|
||||
views: i32,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
|
|
@ -149,7 +149,7 @@ struct InsertedArticleRow {
|
|||
category_id: Uuid,
|
||||
target_roles: Option<Vec<String>>,
|
||||
tags: Vec<String>,
|
||||
is_published: bool,
|
||||
status: String,
|
||||
views: i32,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
|
|
@ -227,7 +227,7 @@ async fn public_list_articles(
|
|||
c.name AS category_name, c.slug AS category_slug
|
||||
FROM kb_articles a
|
||||
JOIN kb_categories c ON c.id = a.category_id
|
||||
WHERE a.is_published = true
|
||||
WHERE a.status = 'PUBLISHED'
|
||||
AND c.is_active = true
|
||||
AND ($1 = '' OR c.slug = $1)
|
||||
AND ($2 = '' OR $2 = 'ALL'
|
||||
|
|
@ -294,7 +294,7 @@ async fn public_get_article(
|
|||
c.name AS category_name, c.slug AS category_slug
|
||||
FROM kb_articles a
|
||||
JOIN kb_categories c ON c.id = a.category_id
|
||||
WHERE a.slug = $1 AND a.is_published = true AND c.is_active = true
|
||||
WHERE a.slug = $1 AND a.status = 'PUBLISHED' AND c.is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(&slug)
|
||||
|
|
@ -569,26 +569,26 @@ async fn admin_list_articles(
|
|||
Query(params): Query<AdminArticleQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let q = params.q.as_deref().unwrap_or("").to_lowercase();
|
||||
let published_filter: Option<bool> = params.status.as_deref().map(|s| s == "PUBLISHED");
|
||||
let status_filter: Option<String> = params.status.as_deref().map(|s| s.to_string());
|
||||
|
||||
let rows = sqlx::query_as::<_, AdminArticleRow>(
|
||||
r#"
|
||||
SELECT
|
||||
a.id, a.title, a.slug, a.summary, a.body, a.target_roles, a.tags,
|
||||
a.is_published, a.views, a.category_id, a.created_at, a.updated_at,
|
||||
a.status, a.views, a.category_id, a.created_at, a.updated_at,
|
||||
c.name AS category_name
|
||||
FROM kb_articles a
|
||||
JOIN kb_categories c ON c.id = a.category_id
|
||||
WHERE ($1 = '' OR LOWER(a.title) LIKE '%' || $1 || '%')
|
||||
AND ($2::uuid IS NULL OR a.category_id = $2)
|
||||
AND ($3::bool IS NULL OR a.is_published = $3)
|
||||
AND ($3::text IS NULL OR a.status = $3)
|
||||
ORDER BY a.updated_at DESC
|
||||
LIMIT 200
|
||||
"#,
|
||||
)
|
||||
.bind(&q)
|
||||
.bind(params.category_id)
|
||||
.bind(published_filter)
|
||||
.bind(status_filter)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
|
|
@ -604,7 +604,7 @@ async fn admin_list_articles(
|
|||
category_id: Some(r.category_id),
|
||||
category: Some(r.category_name),
|
||||
content: r.body,
|
||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||
status: r.status,
|
||||
target_roles: r.target_roles.unwrap_or_default(),
|
||||
tags: r.tags,
|
||||
views: r.views,
|
||||
|
|
@ -646,16 +646,16 @@ async fn admin_create_article(
|
|||
.slug
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| slugify(&body.title));
|
||||
let is_published = body.status.as_deref() == Some("PUBLISHED");
|
||||
let status = body.status.as_deref().unwrap_or("DRAFT").to_string();
|
||||
let roles: Vec<String> = body.target_roles.unwrap_or_default();
|
||||
let tags: Vec<String> = body.tags.unwrap_or_default();
|
||||
|
||||
let result = sqlx::query_as::<_, InsertedArticleRow>(
|
||||
r#"
|
||||
INSERT INTO kb_articles
|
||||
(title, slug, summary, body, category_id, is_published, target_roles, tags, created_by)
|
||||
(title, slug, summary, body, category_id, status, target_roles, tags, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, title, slug, summary, body, category_id, is_published,
|
||||
RETURNING id, title, slug, summary, body, category_id, status,
|
||||
target_roles, tags, views, created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
|
|
@ -664,7 +664,7 @@ async fn admin_create_article(
|
|||
.bind(&body.summary)
|
||||
.bind(&body.content)
|
||||
.bind(body.category_id)
|
||||
.bind(is_published)
|
||||
.bind(&status)
|
||||
.bind(&roles)
|
||||
.bind(&tags)
|
||||
.bind(auth.user_id)
|
||||
|
|
@ -682,7 +682,7 @@ async fn admin_create_article(
|
|||
category_id: Some(r.category_id),
|
||||
category: None,
|
||||
content: r.body,
|
||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||
status: r.status,
|
||||
target_roles: r.target_roles.unwrap_or_default(),
|
||||
tags: r.tags,
|
||||
views: r.views,
|
||||
|
|
@ -721,7 +721,7 @@ async fn admin_get_article(
|
|||
r#"
|
||||
SELECT
|
||||
a.id, a.title, a.slug, a.summary, a.body, a.category_id,
|
||||
a.target_roles, a.tags, a.is_published, a.views,
|
||||
a.target_roles, a.tags, a.status, a.views,
|
||||
a.created_at, a.updated_at,
|
||||
c.name AS category_name
|
||||
FROM kb_articles a
|
||||
|
|
@ -744,7 +744,7 @@ async fn admin_get_article(
|
|||
category_id: Some(r.category_id),
|
||||
category: Some(r.category_name),
|
||||
content: r.body,
|
||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||
status: r.status,
|
||||
target_roles: r.target_roles.unwrap_or_default(),
|
||||
tags: r.tags,
|
||||
views: r.views,
|
||||
|
|
@ -787,7 +787,7 @@ async fn admin_update_article(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateArticleBody>,
|
||||
) -> impl IntoResponse {
|
||||
let is_published: Option<bool> = body.status.as_deref().map(|s| s == "PUBLISHED");
|
||||
let status: Option<String> = body.status.as_deref().map(|s| s.to_string());
|
||||
let result = sqlx::query_as::<_, InsertedArticleRow>(
|
||||
r#"
|
||||
UPDATE kb_articles SET
|
||||
|
|
@ -796,13 +796,13 @@ async fn admin_update_article(
|
|||
summary = COALESCE($4, summary),
|
||||
body = COALESCE($5, body),
|
||||
category_id = COALESCE($6, category_id),
|
||||
is_published = COALESCE($7, is_published),
|
||||
status = COALESCE($7, status),
|
||||
target_roles = COALESCE($8, target_roles),
|
||||
tags = COALESCE($9, tags),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, title, slug, summary, body, category_id,
|
||||
target_roles, tags, is_published, views, created_at, updated_at
|
||||
target_roles, tags, status, views, created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -811,7 +811,7 @@ async fn admin_update_article(
|
|||
.bind(&body.summary)
|
||||
.bind(&body.content)
|
||||
.bind(body.category_id)
|
||||
.bind(is_published)
|
||||
.bind(&status)
|
||||
.bind(body.target_roles.as_deref())
|
||||
.bind(body.tags.as_deref())
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -828,7 +828,7 @@ async fn admin_update_article(
|
|||
category_id: Some(r.category_id),
|
||||
category: None,
|
||||
content: r.body,
|
||||
status: if r.is_published { "PUBLISHED".into() } else { "DRAFT".into() },
|
||||
status: r.status,
|
||||
target_roles: r.target_roles.unwrap_or_default(),
|
||||
tags: r.tags,
|
||||
views: r.views,
|
||||
|
|
|
|||
|
|
@ -173,12 +173,11 @@ async fn submit(
|
|||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {} (id, "profileData", verification_status, submitted_at, updated_at)
|
||||
VALUES ($1, $2, 'PENDING', NOW(), NOW())
|
||||
INSERT INTO {} (id, custom_data, status, updated_at)
|
||||
VALUES ($1, $2, 'PENDING', NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
"profileData" = EXCLUDED."profileData",
|
||||
verification_status = 'PENDING',
|
||||
submitted_at = NOW(),
|
||||
custom_data = EXCLUDED.custom_data,
|
||||
status = 'PENDING',
|
||||
updated_at = NOW()
|
||||
"#,
|
||||
tbl
|
||||
|
|
@ -194,11 +193,11 @@ async fn submit(
|
|||
// Simple companies upsert (using basic fields if possible)
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO companies ("userId", status, "updatedAt")
|
||||
INSERT INTO company_profiles (user_id, status, updated_at)
|
||||
VALUES ($1, 'PENDING', NOW())
|
||||
ON CONFLICT ("userId") DO UPDATE SET
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
status = 'PENDING',
|
||||
"updatedAt" = NOW()
|
||||
updated_at = NOW()
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
|
|
@ -211,7 +210,7 @@ async fn submit(
|
|||
sqlx::query(
|
||||
r#"
|
||||
UPDATE user_roles
|
||||
SET status = 'PENDING', updated_at = NOW()
|
||||
SET status = 'PENDING'
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
"#,
|
||||
)
|
||||
|
|
@ -283,15 +282,14 @@ async fn get_or_create_user_role_profile_id(
|
|||
|
||||
sqlx::query_scalar::<_, uuid::Uuid>(
|
||||
r#"
|
||||
INSERT INTO user_role_profiles (user_id, role_key, role_id, status)
|
||||
VALUES ($1, $2, $3, 'DRAFT')
|
||||
INSERT INTO user_role_profiles (user_id, role_key, status)
|
||||
VALUES ($1, $2, 'DRAFT')
|
||||
ON CONFLICT (user_id, role_key) DO UPDATE SET updated_at = NOW()
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role_key)
|
||||
.bind(role_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ async fn get_profile(
|
|||
|
||||
if role_key == "COMPANY" {
|
||||
let row = sqlx::query(
|
||||
r#"SELECT name, status, "updatedAt" FROM companies WHERE "userId" = $1"#,
|
||||
r#"SELECT company_name, status, updated_at FROM company_profiles WHERE user_id = $1"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -124,7 +124,7 @@ async fn get_profile(
|
|||
return match row {
|
||||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let name: Option<String> = r.try_get("name").ok();
|
||||
let name: Option<String> = r.try_get("company_name").ok();
|
||||
let status: String = r.try_get("status").unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
|
|
@ -161,7 +161,7 @@ async fn get_profile(
|
|||
};
|
||||
|
||||
let query = format!(
|
||||
r#"SELECT "profileData", verification_status FROM {} WHERE id = $1"#,
|
||||
r#"SELECT custom_data, status FROM {} WHERE id = $1"#,
|
||||
table
|
||||
);
|
||||
|
||||
|
|
@ -189,10 +189,10 @@ async fn get_profile(
|
|||
Ok(Some(row)) => {
|
||||
use sqlx::Row;
|
||||
let profile_data: serde_json::Value = row
|
||||
.try_get("profileData")
|
||||
.try_get("custom_data")
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let verification_status: String =
|
||||
row.try_get("verification_status").unwrap_or_default();
|
||||
row.try_get("status").unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
|
|
@ -234,11 +234,11 @@ async fn save_profile(
|
|||
|
||||
return match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO companies ("userId", name, status, "updatedAt")
|
||||
INSERT INTO company_profiles (user_id, company_name, status, updated_at)
|
||||
VALUES ($1, $2, 'DRAFT', NOW())
|
||||
ON CONFLICT ("userId") DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
"updatedAt" = NOW()
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
company_name = EXCLUDED.company_name,
|
||||
updated_at = NOW()
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
|
|
@ -268,10 +268,10 @@ async fn save_profile(
|
|||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {table} (id, "profileData", verification_status, updated_at)
|
||||
INSERT INTO {table} (id, custom_data, status, updated_at)
|
||||
VALUES ($1, $2, 'DRAFT', NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
"profileData" = EXCLUDED."profileData",
|
||||
custom_data = EXCLUDED.custom_data,
|
||||
updated_at = NOW()
|
||||
"#
|
||||
);
|
||||
|
|
@ -441,14 +441,14 @@ async fn fetch_saved_profile(
|
|||
role_key: &str,
|
||||
) -> serde_json::Value {
|
||||
if role_key == "COMPANY" {
|
||||
return match sqlx::query(r#"SELECT name FROM companies WHERE "userId" = $1"#)
|
||||
return match sqlx::query(r#"SELECT company_name FROM company_profiles WHERE user_id = $1"#)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let name: Option<String> = r.try_get("name").ok();
|
||||
let name: Option<String> = r.try_get("company_name").ok();
|
||||
serde_json::json!({ "company_name": name })
|
||||
}
|
||||
_ => serde_json::Value::Object(Default::default()),
|
||||
|
|
@ -465,7 +465,7 @@ async fn fetch_saved_profile(
|
|||
async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, status: &str) {
|
||||
if role_key == "COMPANY" {
|
||||
sqlx::query(
|
||||
r#"UPDATE companies SET status = $1, "updatedAt" = NOW() WHERE "userId" = $2"#,
|
||||
r#"UPDATE company_profiles SET status = $1, updated_at = NOW() WHERE user_id = $2"#,
|
||||
)
|
||||
.bind(status)
|
||||
.bind(user_id)
|
||||
|
|
@ -483,7 +483,7 @@ async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, sta
|
|||
|
||||
if let Some(table) = role_to_table(role_key) {
|
||||
let q = format!(
|
||||
"UPDATE {} SET verification_status = $1, submitted_at = NOW(), updated_at = NOW() WHERE id = $2",
|
||||
"UPDATE {} SET status = $1, updated_at = NOW() WHERE id = $2",
|
||||
table
|
||||
);
|
||||
sqlx::query(&q)
|
||||
|
|
@ -525,15 +525,14 @@ async fn get_or_create_user_role_profile_id(
|
|||
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO user_role_profiles (user_id, role_key, role_id, status)
|
||||
VALUES ($1, $2, $3, 'DRAFT')
|
||||
INSERT INTO user_role_profiles (user_id, role_key, status)
|
||||
VALUES ($1, $2, 'DRAFT')
|
||||
ON CONFLICT (user_id, role_key) DO UPDATE SET updated_at = NOW()
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role_key)
|
||||
.bind(role.id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
|
@ -544,7 +543,7 @@ async fn fetch_saved_profile_by_urp_id(
|
|||
role_key: &str,
|
||||
) -> serde_json::Value {
|
||||
if let Some(table) = role_to_table(role_key) {
|
||||
let q = format!(r#"SELECT "profileData" FROM {} WHERE id = $1"#, table);
|
||||
let q = format!(r#"SELECT custom_data FROM {} WHERE id = $1"#, table);
|
||||
if let Ok(Some(row)) = sqlx::query(&q)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -552,7 +551,7 @@ async fn fetch_saved_profile_by_urp_id(
|
|||
{
|
||||
use sqlx::Row;
|
||||
return row
|
||||
.try_get::<serde_json::Value, _>("profileData")
|
||||
.try_get::<serde_json::Value, _>("custom_data")
|
||||
.unwrap_or(serde_json::Value::Object(Default::default()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ struct ReviewDto {
|
|||
title: Option<String>,
|
||||
comment: Option<String>,
|
||||
status: String,
|
||||
is_published: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +47,6 @@ struct CreateReviewBody {
|
|||
#[derive(Deserialize)]
|
||||
struct PatchReviewBody {
|
||||
status: Option<String>,
|
||||
is_published: Option<bool>,
|
||||
}
|
||||
|
||||
// ── FromRow structs ──────────────────────────────────────────────────────────
|
||||
|
|
@ -64,7 +62,6 @@ struct ReviewRow {
|
|||
title: Option<String>,
|
||||
comment: Option<String>,
|
||||
status: String,
|
||||
is_published: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -81,12 +78,11 @@ async fn admin_list_reviews(
|
|||
r.subject_type,
|
||||
r.subject_id,
|
||||
r.reviewer_name,
|
||||
r.customer_id AS reviewer_id,
|
||||
r.reviewer_user_id AS reviewer_id,
|
||||
r.rating,
|
||||
r.title,
|
||||
r.comment,
|
||||
r.status,
|
||||
r.is_published,
|
||||
r.created_at
|
||||
FROM reviews r
|
||||
ORDER BY r.created_at DESC
|
||||
|
|
@ -109,7 +105,6 @@ async fn admin_list_reviews(
|
|||
title: r.title,
|
||||
comment: r.comment,
|
||||
status: r.status,
|
||||
is_published: r.is_published,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -136,10 +131,10 @@ async fn admin_create_review(
|
|||
|
||||
let row = sqlx::query_as::<_, ReviewRow>(
|
||||
r#"
|
||||
INSERT INTO reviews (subject_type, subject_id, reviewer_name, rating, title, comment, status, is_published)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, true)
|
||||
RETURNING id, subject_type, subject_id, reviewer_name, customer_id AS reviewer_id,
|
||||
rating, title, comment, status, is_published, created_at
|
||||
INSERT INTO reviews (subject_type, subject_id, reviewer_name, rating, title, comment, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, subject_type, subject_id, reviewer_name, reviewer_user_id AS reviewer_id,
|
||||
rating, title, comment, status, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&subject_type)
|
||||
|
|
@ -164,7 +159,6 @@ async fn admin_create_review(
|
|||
title: r.title,
|
||||
comment: r.comment,
|
||||
status: r.status,
|
||||
is_published: r.is_published,
|
||||
created_at: r.created_at,
|
||||
};
|
||||
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
||||
|
|
@ -182,24 +176,13 @@ async fn admin_update_review(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<PatchReviewBody>,
|
||||
) -> impl IntoResponse {
|
||||
// Derive is_published from status string, or use explicit field
|
||||
let (status, published) = match (body.status.as_deref(), body.is_published) {
|
||||
(Some("PUBLISHED"), _) => ("PUBLISHED".to_string(), true),
|
||||
(Some("HIDDEN"), _) => ("HIDDEN".to_string(), false),
|
||||
(Some(s), _) => (s.to_string(), false),
|
||||
(None, Some(p)) => {
|
||||
if p { ("PUBLISHED".to_string(), true) } else { ("HIDDEN".to_string(), false) }
|
||||
}
|
||||
(None, None) => {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Provide status or is_published" }))).into_response();
|
||||
}
|
||||
};
|
||||
let status = body.status.as_deref().unwrap_or("PUBLISHED").to_string();
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE reviews SET status = $1, is_published = $2, updated_at = NOW() WHERE id = $3",
|
||||
"UPDATE reviews SET status = $1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&status)
|
||||
.bind(published)
|
||||
.bind(id)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -15,18 +15,13 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/{id}", get(get_role).patch(update_role).delete(delete_role))
|
||||
}
|
||||
|
||||
// ── Query params ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
audience: Option<String>,
|
||||
q: Option<String>,
|
||||
page: Option<i64>,
|
||||
per_page: Option<i64>,
|
||||
}
|
||||
|
||||
// ── Response types ───────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RoleRow {
|
||||
id: Uuid,
|
||||
|
|
@ -68,13 +63,10 @@ struct RoleDetail {
|
|||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
// ── Request types ────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateRolePayload {
|
||||
key: String,
|
||||
name: String,
|
||||
audience: String,
|
||||
description: Option<String>,
|
||||
department_id: Option<Uuid>,
|
||||
is_active: Option<bool>,
|
||||
|
|
@ -94,8 +86,6 @@ struct UpdateRolePayload {
|
|||
permission_keys: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// ── FromRow structs ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RoleListRow {
|
||||
id: Uuid,
|
||||
|
|
@ -134,11 +124,7 @@ struct InsertedRoleRow {
|
|||
key: String,
|
||||
name: String,
|
||||
audience: String,
|
||||
description: Option<String>,
|
||||
department_id: Option<Uuid>,
|
||||
is_active: bool,
|
||||
can_approve_requests: bool,
|
||||
can_manage_system_settings: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -152,8 +138,6 @@ struct CurrentRoleRow {
|
|||
can_manage_system_settings: bool,
|
||||
}
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn list_roles(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ListQuery>,
|
||||
|
|
@ -162,36 +146,35 @@ async fn list_roles(
|
|||
let per_page = params.per_page.unwrap_or(20).min(100);
|
||||
let offset = (page - 1) * per_page;
|
||||
let search = params.q.as_deref().unwrap_or("").to_lowercase();
|
||||
let audience = params.audience.as_deref().unwrap_or("").to_string();
|
||||
|
||||
let rows = sqlx::query_as::<_, RoleListRow>(
|
||||
r#"
|
||||
SELECT
|
||||
r.id,
|
||||
r.code AS key,
|
||||
r.key,
|
||||
r.name,
|
||||
r.audience,
|
||||
r.description,
|
||||
r.department_id,
|
||||
ir.description,
|
||||
ir.department_id,
|
||||
d.name AS department_name,
|
||||
r.is_active,
|
||||
r.can_approve_requests,
|
||||
r.can_manage_system_settings,
|
||||
COALESCE(ir.can_approve_requests, false) AS can_approve_requests,
|
||||
COALESCE(ir.can_manage_system_settings, false) AS can_manage_system_settings,
|
||||
r.created_at,
|
||||
COUNT(DISTINCT e.id) AS users_assigned,
|
||||
COUNT(DISTINCT rp.id) AS permissions_count
|
||||
FROM roles r
|
||||
LEFT JOIN departments d ON d.id = r.department_id
|
||||
LEFT JOIN employees e ON e.role_code = r.code
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
LEFT JOIN departments d ON d.id = ir.department_id
|
||||
LEFT JOIN employees e ON e.role_code = r.key
|
||||
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
||||
WHERE ($1 = '' OR r.audience = $1)
|
||||
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.code) LIKE '%' || $2 || '%')
|
||||
GROUP BY r.id, d.name
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
GROUP BY r.id, ir.description, ir.department_id, ir.can_approve_requests, ir.can_manage_system_settings, d.name
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(&audience)
|
||||
.bind(&search)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
|
|
@ -202,11 +185,11 @@ async fn list_roles(
|
|||
let total: i64 = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM roles r
|
||||
WHERE ($1 = '' OR r.audience = $1)
|
||||
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.code) LIKE '%' || $2 || '%')
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
"#,
|
||||
)
|
||||
.bind(&audience)
|
||||
.bind(&search)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
|
|
@ -241,13 +224,17 @@ async fn get_role(
|
|||
let row = sqlx::query_as::<_, RoleDetailRow>(
|
||||
r#"
|
||||
SELECT
|
||||
r.id, r.code AS key, r.name, r.audience, r.description,
|
||||
r.department_id, d.name AS department_name,
|
||||
r.is_active, r.can_approve_requests, r.can_manage_system_settings,
|
||||
r.id, r.key, r.name, r.audience,
|
||||
ir.description,
|
||||
ir.department_id, d.name AS department_name,
|
||||
r.is_active,
|
||||
COALESCE(ir.can_approve_requests, false) AS can_approve_requests,
|
||||
COALESCE(ir.can_manage_system_settings, false) AS can_manage_system_settings,
|
||||
r.created_at
|
||||
FROM roles r
|
||||
LEFT JOIN departments d ON d.id = r.department_id
|
||||
WHERE r.id = $1
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
LEFT JOIN departments d ON d.id = ir.department_id
|
||||
WHERE r.id = $1 AND r.audience = 'INTERNAL'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -290,24 +277,33 @@ async fn create_role(
|
|||
|
||||
let role = sqlx::query_as::<_, InsertedRoleRow>(
|
||||
r#"
|
||||
INSERT INTO roles (code, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, code AS key, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings, created_at
|
||||
INSERT INTO roles (key, name, audience, is_active)
|
||||
VALUES ($1, $2, 'INTERNAL', $3)
|
||||
RETURNING id, key, name, audience, is_active, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.key)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.audience)
|
||||
.bind(&payload.description)
|
||||
.bind(payload.department_id)
|
||||
.bind(is_active)
|
||||
.bind(can_approve)
|
||||
.bind(can_manage)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Insert permission keys
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO internal_roles (role_id, description, department_id, can_approve_requests, can_manage_system_settings)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
.bind(role.id)
|
||||
.bind(&payload.description)
|
||||
.bind(payload.department_id)
|
||||
.bind(can_approve)
|
||||
.bind(can_manage)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
if let Some(keys) = &payload.permission_keys {
|
||||
for key in keys {
|
||||
sqlx::query(
|
||||
|
|
@ -336,12 +332,12 @@ async fn create_role(
|
|||
key: role.key,
|
||||
name: role.name,
|
||||
audience: role.audience,
|
||||
description: role.description,
|
||||
department_id: role.department_id,
|
||||
description: payload.description,
|
||||
department_id: payload.department_id,
|
||||
department_name: None,
|
||||
is_active: role.is_active,
|
||||
can_approve_requests: role.can_approve_requests,
|
||||
can_manage_system_settings: role.can_manage_system_settings,
|
||||
can_approve_requests: can_approve,
|
||||
can_manage_system_settings: can_manage,
|
||||
permission_keys,
|
||||
created_at: role.created_at,
|
||||
}),
|
||||
|
|
@ -353,9 +349,15 @@ async fn update_role(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateRolePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
// Fetch current values first
|
||||
let current = sqlx::query_as::<_, CurrentRoleRow>(
|
||||
"SELECT name, description, department_id, is_active, can_approve_requests, can_manage_system_settings FROM roles WHERE id = $1",
|
||||
r#"
|
||||
SELECT r.name, ir.description, ir.department_id, r.is_active,
|
||||
COALESCE(ir.can_approve_requests, false) AS can_approve_requests,
|
||||
COALESCE(ir.can_manage_system_settings, false) AS can_manage_system_settings
|
||||
FROM roles r
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
WHERE r.id = $1 AND r.audience = 'INTERNAL'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -364,28 +366,35 @@ async fn update_role(
|
|||
.ok_or((StatusCode::NOT_FOUND, "Role not found".to_string()))?;
|
||||
|
||||
let name = payload.name.unwrap_or(current.name);
|
||||
let is_active = payload.is_active.unwrap_or(current.is_active);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE roles SET name = $1, is_active = $2 WHERE id = $3",
|
||||
)
|
||||
.bind(&name)
|
||||
.bind(is_active)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
let description = payload.description.or(current.description);
|
||||
let department_id = payload.department_id.or(current.department_id);
|
||||
let is_active = payload.is_active.unwrap_or(current.is_active);
|
||||
let can_approve = payload.can_approve_requests.unwrap_or(current.can_approve_requests);
|
||||
let can_manage = payload.can_manage_system_settings.unwrap_or(current.can_manage_system_settings);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE roles SET
|
||||
name = $1,
|
||||
description = $2,
|
||||
department_id = $3,
|
||||
is_active = $4,
|
||||
can_approve_requests = $5,
|
||||
can_manage_system_settings = $6
|
||||
WHERE id = $7
|
||||
UPDATE internal_roles SET
|
||||
description = $1,
|
||||
department_id = $2,
|
||||
can_approve_requests = $3,
|
||||
can_manage_system_settings = $4
|
||||
WHERE role_id = $5
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(description)
|
||||
.bind(&description)
|
||||
.bind(department_id)
|
||||
.bind(is_active)
|
||||
.bind(can_approve)
|
||||
.bind(can_manage)
|
||||
.bind(id)
|
||||
|
|
@ -393,7 +402,6 @@ async fn update_role(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Replace permissions if provided
|
||||
if let Some(keys) = &payload.permission_keys {
|
||||
sqlx::query("DELETE FROM role_permissions WHERE role_id = $1")
|
||||
.bind(id)
|
||||
|
|
@ -413,7 +421,6 @@ async fn update_role(
|
|||
}
|
||||
}
|
||||
|
||||
// Return updated role
|
||||
get_role(State(state), Path(id)).await
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +428,7 @@ async fn delete_role(
|
|||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let result = sqlx::query("DELETE FROM roles WHERE id = $1")
|
||||
let result = sqlx::query("DELETE FROM roles WHERE id = $1 AND audience = 'INTERNAL'")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ async fn create_delete_account_request(
|
|||
.mail
|
||||
.send_account_deleted_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()),
|
||||
)
|
||||
.await;
|
||||
let _ = sqlx::query(
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ async fn user_create_ticket(
|
|||
};
|
||||
let _ = state.mail.send_support_ticket_created_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()),
|
||||
&r.id.to_string(),
|
||||
&body.subject,
|
||||
&category,
|
||||
|
|
@ -444,14 +444,10 @@ async fn admin_list_cases(
|
|||
t.id, t.subject, t.description, t.category, t.priority, t.status,
|
||||
t.requester_name, t.requester_email, t.assigned_to,
|
||||
t.created_at, t.updated_at,
|
||||
u.name AS user_name, u.email AS user_email
|
||||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||
FROM support_tickets t
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
WHERE ($1 = '' OR t.status = $1)
|
||||
AND ($2 = '' OR t.priority = $2)
|
||||
AND ($3 = '' OR t.category = $3)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
WHERE t.id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&status_filter)
|
||||
|
|
@ -586,10 +582,14 @@ async fn admin_get_case(
|
|||
t.id, t.subject, t.description, t.category, t.priority, t.status,
|
||||
t.requester_name, t.requester_email, t.assigned_to,
|
||||
t.created_at, t.updated_at,
|
||||
u.name AS user_name, u.email AS user_email
|
||||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||
FROM support_tickets t
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
WHERE t.id = $1
|
||||
WHERE ($1 = '' OR t.status = $1)
|
||||
AND ($2 = '' OR t.priority = $2)
|
||||
AND ($3 = '' OR t.category = $3)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -832,7 +832,7 @@ async fn admin_add_message(
|
|||
if let Some(user_email) = ticket.requester_email {
|
||||
// Try to get user name from user table
|
||||
let user_name = if let Ok(user) = db::models::user::UserRepository::get_by_email(&state.pool, &user_email).await {
|
||||
user.name.unwrap_or_default()
|
||||
format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default())
|
||||
} else {
|
||||
ticket.requester_name.unwrap_or_default()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ async fn trigger_rejection(
|
|||
};
|
||||
|
||||
let query = format!(
|
||||
"UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
table
|
||||
);
|
||||
sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
|
||||
|
|
@ -144,12 +144,8 @@ async fn trigger_rejection(
|
|||
// Send Email
|
||||
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
let display = role_key_to_display(&role_key);
|
||||
let _ = state.mail.send_approval_rejected_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&display,
|
||||
reason_str
|
||||
).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_approval_rejected_email(&user.email, &user_name, &display, reason_str).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,11 +176,8 @@ async fn approve_verification(
|
|||
// Send approval email
|
||||
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, v.user_id).await {
|
||||
let display = role_key_to_display(&v.role_key);
|
||||
let _ = state.mail.send_approval_approved_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&display
|
||||
).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_approval_approved_email(&user.email, &user_name, &display).await;
|
||||
}
|
||||
(StatusCode::OK, Json(v)).into_response()
|
||||
}
|
||||
|
|
@ -294,12 +287,8 @@ async fn request_documents(
|
|||
// Send email notification
|
||||
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, v.user_id).await {
|
||||
let display = role_key_to_display(&v.role_key);
|
||||
let _ = state.mail.send_documents_requested_email(
|
||||
&user.email,
|
||||
user.name.as_deref().unwrap_or_default(),
|
||||
&display,
|
||||
&payload.message
|
||||
).await;
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
let _ = state.mail.send_documents_requested_email(&user.email, &user_name, &display, &payload.message).await;
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(v)).into_response()
|
||||
|
|
|
|||
1
companies.pid
Normal file
1
companies.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
9692
|
||||
|
|
@ -183,7 +183,7 @@ async fn send_lead_request(
|
|||
Err(_) => return (StatusCode::BAD_REQUEST, "Wallet not found").into_response(),
|
||||
};
|
||||
|
||||
if wallet.current_balance < 25 {
|
||||
if wallet.balance < 25 {
|
||||
return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response();
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ async fn my_requests(
|
|||
sqlx::query_as::<_, RichLeadReq>(
|
||||
r#"
|
||||
SELECT lr.*, r.title as req_title, r.profession_key as req_profession_key, r.location as req_location, r.budget as req_budget,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.name ELSE NULL END as customer_name,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN CONCAT(u.first_name, ' ', u.last_name) AS name ELSE NULL END as customer_name,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.email ELSE NULL END as customer_email,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.phone ELSE NULL END as customer_phone
|
||||
FROM lead_requests lr
|
||||
|
|
@ -390,7 +390,7 @@ async fn my_requests(
|
|||
sqlx::query_as::<_, RichLeadReq>(
|
||||
r#"
|
||||
SELECT lr.*, r.title as req_title, r.profession_key as req_profession_key, r.location as req_location, r.budget as req_budget,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.name ELSE NULL END as customer_name,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN CONCAT(u.first_name, ' ', u.last_name) AS name ELSE NULL END as customer_name,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.email ELSE NULL END as customer_email,
|
||||
CASE WHEN lr.status = 'ACCEPTED' THEN u.phone ELSE NULL END as customer_phone
|
||||
FROM lead_requests lr
|
||||
|
|
@ -567,7 +567,7 @@ async fn accepted_lead_detail(
|
|||
r.location AS requirement_location,
|
||||
r.profession_key,
|
||||
r.custom_fields,
|
||||
u.name AS customer_name,
|
||||
CONCAT(u.first_name, ' ', u.last_name) AS name AS customer_name,
|
||||
u.email AS customer_email,
|
||||
u.phone AS customer_phone
|
||||
FROM lead_requests lr
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ pub struct CreateOnboardingConfigPayload {
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct DashboardConfigListItem {
|
||||
pub id: Uuid,
|
||||
pub role_id: Uuid,
|
||||
pub role_key: String,
|
||||
pub audience: String,
|
||||
pub config_json: serde_json::Value,
|
||||
|
|
@ -42,7 +43,7 @@ pub struct DashboardConfigListItem {
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct DashboardConfig {
|
||||
pub id: Uuid,
|
||||
pub role_key: String,
|
||||
pub role_id: Uuid,
|
||||
pub audience: String,
|
||||
pub config_json: serde_json::Value,
|
||||
pub is_active: bool,
|
||||
|
|
@ -172,40 +173,31 @@ impl ConfigRepository {
|
|||
pool: &PgPool,
|
||||
payload: CreateDashboardConfigPayload,
|
||||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let role_key = sqlx::query_scalar::<_, String>(
|
||||
"SELECT code FROM roles WHERE id = $1",
|
||||
)
|
||||
.bind(payload.role_id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
// Soft-disable previous active configs for this role
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE dashboard_configs
|
||||
SET is_active = false
|
||||
WHERE UPPER(role_key) = UPPER($1) AND audience = $2::text AND is_active = true
|
||||
WHERE role_id = $1 AND audience = $2::text AND is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(&role_key)
|
||||
.bind(payload.role_id)
|
||||
.bind(&payload.audience)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
// Insert new config
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
INSERT INTO dashboard_configs (role_key, audience, widgets, is_active)
|
||||
INSERT INTO dashboard_configs (role_id, audience, config_json, is_active)
|
||||
VALUES (
|
||||
$1,
|
||||
$2::text,
|
||||
$3,
|
||||
true
|
||||
)
|
||||
RETURNING id, role_key, audience, widgets as config_json, is_active, updated_at
|
||||
RETURNING id, role_id, audience, config_json, is_active, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(&role_key)
|
||||
.bind(payload.role_id)
|
||||
.bind(&payload.audience)
|
||||
.bind(payload.config_json)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -219,21 +211,14 @@ impl ConfigRepository {
|
|||
role_id: Uuid,
|
||||
audience: &str,
|
||||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let role_key = sqlx::query_scalar::<_, String>(
|
||||
"SELECT code FROM roles WHERE id = $1",
|
||||
)
|
||||
.bind(role_id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT id, role_key, audience, widgets as config_json, is_active, updated_at
|
||||
SELECT id, role_id, audience, config_json, is_active, updated_at
|
||||
FROM dashboard_configs
|
||||
WHERE UPPER(role_key) = UPPER($1) AND audience = $2 AND is_active = true
|
||||
WHERE role_id = $1 AND audience = $2 AND is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(role_key)
|
||||
.bind(role_id)
|
||||
.bind(audience)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
@ -247,8 +232,9 @@ impl ConfigRepository {
|
|||
let configs = sqlx::query_as::<_, DashboardConfigListItem>(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.role_key, c.audience, c.widgets as config_json, c.is_active, c.updated_at
|
||||
c.id, c.role_id, r.key as role_key, c.audience, c.config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
ORDER BY c.updated_at DESC
|
||||
"#,
|
||||
)
|
||||
|
|
@ -265,9 +251,10 @@ impl ConfigRepository {
|
|||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT c.id, c.role_key, c.audience, c.widgets as config_json, c.is_active, c.updated_at
|
||||
SELECT c.id, c.role_id, c.audience, c.config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
WHERE UPPER(c.role_key) = UPPER($1) AND c.audience = $2 AND c.is_active = true
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
WHERE r.key = $1 AND c.audience = $2 AND c.is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(role_key)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ use uuid::Uuid;
|
|||
pub struct CustomerProfile {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub area: Option<String>,
|
||||
|
|
@ -15,7 +16,6 @@ pub struct CustomerProfile {
|
|||
pub active_requirement_count: i32,
|
||||
pub status: String,
|
||||
pub bio: Option<String>,
|
||||
pub experience_years: Option<i32>,
|
||||
pub custom_data: Option<serde_json::Value>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
|
@ -23,7 +23,8 @@ pub struct CustomerProfile {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpsertCustomerProfilePayload {
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub area: Option<String>,
|
||||
|
|
@ -42,8 +43,8 @@ impl CustomerRepository {
|
|||
let profile = sqlx::query_as::<_, CustomerProfile>(
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, full_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, experience_years, custom_data,
|
||||
id, user_id, first_name, last_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
FROM customer_profiles
|
||||
WHERE user_id = $1
|
||||
|
|
@ -64,11 +65,12 @@ impl CustomerRepository {
|
|||
let profile = sqlx::query_as::<_, CustomerProfile>(
|
||||
r#"
|
||||
INSERT INTO customer_profiles (
|
||||
user_id, full_name, phone, city, area, preferred_professions, bio, custom_data, status
|
||||
user_id, first_name, last_name, phone, city, area, preferred_professions, bio, custom_data, status
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'PENDING')
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
full_name = EXCLUDED.full_name,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
phone = EXCLUDED.phone,
|
||||
city = EXCLUDED.city,
|
||||
area = EXCLUDED.area,
|
||||
|
|
@ -81,13 +83,14 @@ impl CustomerRepository {
|
|||
END,
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
id, user_id, full_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, experience_years, custom_data,
|
||||
id, user_id, first_name, last_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(payload.full_name)
|
||||
.bind(payload.first_name)
|
||||
.bind(payload.last_name)
|
||||
.bind(payload.phone)
|
||||
.bind(payload.city)
|
||||
.bind(payload.area)
|
||||
|
|
@ -125,8 +128,8 @@ impl CustomerRepository {
|
|||
SET status = 'PENDING_REVIEW', updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
RETURNING
|
||||
id, user_id, full_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, experience_years, custom_data,
|
||||
id, user_id, first_name, last_name, phone, city, area, preferred_professions,
|
||||
active_requirement_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ pub struct Department {
|
|||
pub department_head: Option<String>,
|
||||
pub department_email: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub visibility: String,
|
||||
pub transfers_enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -25,9 +23,7 @@ pub struct CreateDepartmentPayload {
|
|||
pub description: Option<String>,
|
||||
pub department_head: Option<String>,
|
||||
pub department_email: Option<String>,
|
||||
pub status: Option<String>, // ACTIVE | INACTIVE
|
||||
pub visibility: Option<String>, // INTERNAL | EXTERNAL
|
||||
pub transfers_enabled: Option<bool>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
pub struct DepartmentRepository;
|
||||
|
|
@ -35,17 +31,15 @@ pub struct DepartmentRepository;
|
|||
impl DepartmentRepository {
|
||||
pub async fn create(pool: &PgPool, payload: CreateDepartmentPayload) -> Result<Department, sqlx::Error> {
|
||||
let is_active = payload.status.map(|s| s.to_uppercase() == "ACTIVE").unwrap_or(true);
|
||||
let visibility = payload.visibility.unwrap_or_else(|| "INTERNAL".to_string());
|
||||
let transfers_enabled = payload.transfers_enabled.unwrap_or(false);
|
||||
|
||||
sqlx::query_as::<_, Department>(
|
||||
r#"
|
||||
INSERT INTO departments (
|
||||
name, code, description, department_head, department_email,
|
||||
is_active, visibility, transfers_enabled
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, name, code, description, department_head, department_email, is_active, visibility, transfers_enabled, created_at, updated_at
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, name, code, description, department_head, department_email, is_active, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(payload.name)
|
||||
|
|
@ -54,8 +48,6 @@ impl DepartmentRepository {
|
|||
.bind(payload.department_head)
|
||||
.bind(payload.department_email)
|
||||
.bind(is_active)
|
||||
.bind(visibility)
|
||||
.bind(transfers_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
|
@ -68,8 +60,6 @@ impl DepartmentRepository {
|
|||
let department_email = payload.get("department_email").map(|v| v.as_str().unwrap_or_default());
|
||||
let status = payload.get("status").and_then(|v| v.as_str());
|
||||
let is_active = status.map(|s| s.to_uppercase() == "ACTIVE");
|
||||
let visibility = payload.get("visibility").and_then(|v| v.as_str());
|
||||
let transfers_enabled = payload.get("transfers_enabled").and_then(|v| v.as_bool());
|
||||
|
||||
sqlx::query_as::<_, Department>(
|
||||
r#"
|
||||
|
|
@ -80,11 +70,9 @@ impl DepartmentRepository {
|
|||
department_head = COALESCE($5, department_head),
|
||||
department_email = COALESCE($6, department_email),
|
||||
is_active = COALESCE($7, is_active),
|
||||
visibility = COALESCE($8, visibility),
|
||||
transfers_enabled = COALESCE($9, transfers_enabled),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, name, code, description, department_head, department_email, is_active, visibility, transfers_enabled, created_at, updated_at
|
||||
RETURNING id, name, code, description, department_head, department_email, is_active, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -94,8 +82,6 @@ impl DepartmentRepository {
|
|||
.bind(department_head)
|
||||
.bind(department_email)
|
||||
.bind(is_active)
|
||||
.bind(visibility)
|
||||
.bind(transfers_enabled)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
|
@ -110,7 +96,7 @@ impl DepartmentRepository {
|
|||
|
||||
pub async fn list(pool: &PgPool) -> Result<Vec<Department>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Department>(
|
||||
"SELECT id, name, code, description, department_head, department_email, is_active, visibility, transfers_enabled, created_at, updated_at FROM departments ORDER BY name ASC"
|
||||
"SELECT id, name, code, description, department_head, department_email, is_active, created_at, updated_at FROM departments ORDER BY name ASC"
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
|
|
@ -118,7 +104,7 @@ impl DepartmentRepository {
|
|||
|
||||
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Department>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Department>(
|
||||
"SELECT id, name, code, description, department_head, department_email, is_active, visibility, transfers_enabled, created_at, updated_at FROM departments WHERE id = $1"
|
||||
"SELECT id, name, code, description, department_head, department_email, is_active, created_at, updated_at FROM departments WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub struct Employee {
|
|||
pub designation_id: Option<Uuid>,
|
||||
pub role_code: String,
|
||||
pub status: String,
|
||||
pub joined_at: NaiveDate,
|
||||
pub joining_date: NaiveDate,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ impl EmployeeRepository {
|
|||
r#"
|
||||
INSERT INTO employees (first_name, last_name, email, password_hash, department_id, designation_id, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joined_at, created_at, updated_at
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(payload.first_name)
|
||||
|
|
@ -88,7 +88,7 @@ impl EmployeeRepository {
|
|||
status = COALESCE($7, status),
|
||||
updated_at = NOW()
|
||||
WHERE id = $8
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joined_at, created_at, updated_at
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(first_name)
|
||||
|
|
@ -113,7 +113,7 @@ impl EmployeeRepository {
|
|||
|
||||
pub async fn get_by_email(pool: &PgPool, email: &str) -> Result<Option<Employee>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Employee>(
|
||||
"SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joined_at, created_at, updated_at FROM employees WHERE email = $1"
|
||||
"SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at FROM employees WHERE email = $1"
|
||||
)
|
||||
.bind(email.to_lowercase())
|
||||
.fetch_optional(pool)
|
||||
|
|
@ -124,7 +124,7 @@ impl EmployeeRepository {
|
|||
let search = q.unwrap_or_default().to_lowercase();
|
||||
sqlx::query_as::<_, Employee>(
|
||||
r#"
|
||||
SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joined_at, created_at, updated_at
|
||||
SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
FROM employees
|
||||
WHERE ($1 = '' OR LOWER(first_name) LIKE '%' || $1 || '%' OR LOWER(last_name) LIKE '%' || $1 || '%' OR LOWER(email) LIKE '%' || $1 || '%')
|
||||
ORDER BY last_name, first_name
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ use uuid::Uuid;
|
|||
pub struct JobSeekerProfile {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub experience_years: Option<i32>,
|
||||
|
|
@ -23,7 +24,8 @@ pub struct JobSeekerProfile {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpsertJobSeekerProfilePayload {
|
||||
pub full_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub experience_years: Option<i32>,
|
||||
|
|
@ -43,7 +45,7 @@ impl JobSeekerRepository {
|
|||
let profile = sqlx::query_as::<_, JobSeekerProfile>(
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, full_name, location, summary, experience_years,
|
||||
id, user_id, first_name, last_name, location, summary, experience_years,
|
||||
skills, resume_url, active_application_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
FROM job_seeker_profiles
|
||||
|
|
@ -65,12 +67,13 @@ impl JobSeekerRepository {
|
|||
let profile = sqlx::query_as::<_, JobSeekerProfile>(
|
||||
r#"
|
||||
INSERT INTO job_seeker_profiles (
|
||||
user_id, full_name, location, summary, experience_years,
|
||||
user_id, first_name, last_name, location, summary, experience_years,
|
||||
skills, resume_url, bio, custom_data
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
full_name = EXCLUDED.full_name,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
location = EXCLUDED.location,
|
||||
summary = EXCLUDED.summary,
|
||||
experience_years = EXCLUDED.experience_years,
|
||||
|
|
@ -80,13 +83,14 @@ impl JobSeekerRepository {
|
|||
custom_data = EXCLUDED.custom_data,
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
id, user_id, full_name, location, summary, experience_years,
|
||||
id, user_id, first_name, last_name, location, summary, experience_years,
|
||||
skills, resume_url, active_application_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(payload.full_name)
|
||||
.bind(payload.first_name)
|
||||
.bind(payload.last_name)
|
||||
.bind(payload.location)
|
||||
.bind(payload.summary)
|
||||
.bind(payload.experience_years.unwrap_or(0))
|
||||
|
|
@ -125,7 +129,7 @@ impl JobSeekerRepository {
|
|||
SET status = 'PENDING_REVIEW', updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
RETURNING
|
||||
id, user_id, full_name, location, summary, experience_years,
|
||||
id, user_id, first_name, last_name, location, summary, experience_years,
|
||||
skills, resume_url, active_application_count, status, bio, custom_data,
|
||||
created_at, updated_at
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ pub struct Requirement {
|
|||
pub title: String,
|
||||
pub description: String,
|
||||
pub location: String,
|
||||
pub budget: Option<i32>,
|
||||
pub preferred_date: Option<chrono::NaiveDate>,
|
||||
pub budget_inr: Option<i32>,
|
||||
pub required_date: Option<chrono::NaiveDate>,
|
||||
pub extra_data_json: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
pub rejection_reason: Option<String>,
|
||||
|
|
@ -22,7 +22,6 @@ pub struct Requirement {
|
|||
pub approved_by: Option<Uuid>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub created_by_user_id: Option<Uuid>,
|
||||
pub required_date: Option<chrono::NaiveDate>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -32,8 +31,8 @@ pub struct CreateRequirementPayload {
|
|||
pub title: String,
|
||||
pub description: String,
|
||||
pub location: String,
|
||||
pub budget: Option<i32>,
|
||||
pub preferred_date: Option<chrono::NaiveDate>,
|
||||
pub budget_inr: Option<i32>,
|
||||
pub required_date: Option<chrono::NaiveDate>,
|
||||
pub extra_data_json: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +41,8 @@ pub struct UpdateRequirementPayload {
|
|||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub budget: Option<i32>,
|
||||
pub preferred_date: Option<chrono::NaiveDate>,
|
||||
pub budget_inr: Option<i32>,
|
||||
pub required_date: Option<chrono::NaiveDate>,
|
||||
pub extra_data_json: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ impl RequirementRepository {
|
|||
r#"
|
||||
INSERT INTO leads (
|
||||
profession_key, title, description, location,
|
||||
budget, preferred_date, extra_data_json
|
||||
budget_inr, required_date, extra_data_json
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *
|
||||
|
|
@ -68,8 +67,8 @@ impl RequirementRepository {
|
|||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
.bind(payload.location)
|
||||
.bind(payload.budget)
|
||||
.bind(payload.preferred_date)
|
||||
.bind(payload.budget_inr)
|
||||
.bind(payload.required_date)
|
||||
.bind(payload.extra_data_json)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
@ -119,8 +118,8 @@ impl RequirementRepository {
|
|||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
location = COALESCE($3, location),
|
||||
budget = COALESCE($4, budget),
|
||||
preferred_date = COALESCE($5, preferred_date),
|
||||
budget_inr = COALESCE($4, budget_inr),
|
||||
required_date = COALESCE($5, required_date),
|
||||
extra_data_json = COALESCE($6, extra_data_json),
|
||||
updated_at = NOW()
|
||||
WHERE id = $7
|
||||
|
|
@ -130,8 +129,8 @@ impl RequirementRepository {
|
|||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
.bind(payload.location)
|
||||
.bind(payload.budget)
|
||||
.bind(payload.preferred_date)
|
||||
.bind(payload.budget_inr)
|
||||
.bind(payload.required_date)
|
||||
.bind(payload.extra_data_json)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use uuid::Uuid;
|
|||
pub struct Wallet {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub current_balance: i32,
|
||||
pub balance: i32,
|
||||
pub reserved: i32,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ impl TracecoinWalletRepository {
|
|||
pub async fn ensure_wallet(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tracecoin_wallets (user_id, current_balance, reserved)
|
||||
INSERT INTO tracecoin_wallets (user_id, balance, reserved)
|
||||
VALUES ($1, 0, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING
|
||||
"#,
|
||||
|
|
@ -61,7 +61,7 @@ impl TracecoinWalletRepository {
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tracecoin_wallets (user_id, current_balance, reserved)
|
||||
INSERT INTO tracecoin_wallets (user_id, balance, reserved)
|
||||
VALUES ($1, 0, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING
|
||||
"#,
|
||||
|
|
@ -77,7 +77,7 @@ impl TracecoinWalletRepository {
|
|||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if wallet.current_balance < amount {
|
||||
if wallet.balance < amount {
|
||||
tx.rollback().await?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ impl TracecoinWalletRepository {
|
|||
sqlx::query(
|
||||
r#"
|
||||
UPDATE tracecoin_wallets
|
||||
SET current_balance = current_balance - $1, reserved = reserved + $1, updated_at = NOW()
|
||||
SET balance = balance - $1, reserved = reserved + $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
"#,
|
||||
)
|
||||
|
|
@ -192,7 +192,7 @@ impl TracecoinWalletRepository {
|
|||
sqlx::query(
|
||||
r#"
|
||||
UPDATE tracecoin_wallets
|
||||
SET reserved = reserved - $1, current_balance = current_balance + $1, updated_at = NOW()
|
||||
SET reserved = reserved - $1, balance = balance + $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
"#,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ pub struct User {
|
|||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
pub name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub email_verified: bool,
|
||||
pub phone_verified: bool,
|
||||
pub status: String, // ACTIVE, SUSPENDED, BANNED
|
||||
|
|
@ -26,7 +27,8 @@ pub struct User {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateUserPayload {
|
||||
pub name: String,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub email: String,
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
|
@ -49,17 +51,18 @@ impl UserRepository {
|
|||
pub async fn create(pool: &PgPool, payload: CreateUserPayload) -> Result<User, sqlx::Error> {
|
||||
let user = sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
INSERT INTO users (name, email, password_hash, email_verified, phone_verified)
|
||||
INSERT INTO users (first_name, last_name, email, password_hash, email_verified, phone_verified)
|
||||
VALUES ($1, $2, $3, false, false)
|
||||
RETURNING
|
||||
id, email, password_hash, name,
|
||||
id, email, password_hash, first_name, last_name,
|
||||
email_verified, phone_verified, status,
|
||||
email_verification_token, email_verification_expires_at,
|
||||
reset_password_token, reset_password_expires_at,
|
||||
created_at, updated_at, deleted_at
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.first_name)
|
||||
.bind(&payload.last_name)
|
||||
.bind(payload.email.to_lowercase())
|
||||
.bind(payload.password_hash)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -71,7 +74,7 @@ impl UserRepository {
|
|||
pub async fn get_by_email(pool: &PgPool, email: &str) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
SELECT id, email, password_hash, name,
|
||||
SELECT id, email, password_hash, first_name, last_name,
|
||||
email_verified, phone_verified, status,
|
||||
email_verification_token, email_verification_expires_at,
|
||||
reset_password_token, reset_password_expires_at,
|
||||
|
|
@ -88,7 +91,7 @@ impl UserRepository {
|
|||
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
SELECT id, email, password_hash, name,
|
||||
SELECT id, email, password_hash, first_name, last_name,
|
||||
email_verified, phone_verified, status,
|
||||
email_verification_token, email_verification_expires_at,
|
||||
reset_password_token, reset_password_expires_at,
|
||||
|
|
@ -106,7 +109,7 @@ impl UserRepository {
|
|||
pub async fn get_user_role_keys(pool: &PgPool, user_id: Uuid) -> Result<Vec<String>, sqlx::Error> {
|
||||
let rows = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT r.code
|
||||
SELECT r.key
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = $1 AND ur.status = 'APPROVED'
|
||||
|
|
@ -145,7 +148,7 @@ impl UserRepository {
|
|||
pub async fn get_by_verification_token(pool: &PgPool, token: &str) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
SELECT id, email, password_hash, name,
|
||||
SELECT id, email, password_hash, first_name, last_name,
|
||||
email_verified, phone_verified, status,
|
||||
email_verification_token, email_verification_expires_at,
|
||||
reset_password_token, reset_password_expires_at,
|
||||
|
|
@ -193,7 +196,7 @@ impl UserRepository {
|
|||
pub async fn get_by_reset_token(pool: &PgPool, token: &str) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
SELECT id, email, password_hash, name,
|
||||
SELECT id, email, password_hash, first_name, last_name,
|
||||
email_verified, phone_verified, status,
|
||||
email_verification_token, email_verification_expires_at,
|
||||
reset_password_token, reset_password_expires_at,
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ pub struct Verification {
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct VerificationLog {
|
||||
pub id: Uuid,
|
||||
pub verification_id: Uuid,
|
||||
pub verification_request_id: Uuid,
|
||||
pub action: String,
|
||||
pub actor_id: Option<Uuid>,
|
||||
pub acted_by_user_id: Option<Uuid>,
|
||||
pub old_status: Option<String>,
|
||||
pub new_status: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub remarks: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -81,9 +81,8 @@ impl VerificationRepository {
|
|||
if status.is_some() { query.push_str(" AND status = $1"); }
|
||||
if case_type.is_some() { query.push_str(if status.is_some() { " AND case_type = $2" } else { " AND case_type = $1" }); }
|
||||
|
||||
query.push_str(" ORDER BY created_at DESC LIMIT $3 OFFSET $4"); // This simplified query string concatenation is for readability, handle properly in prod.
|
||||
query.push_str(" ORDER BY created_at DESC LIMIT $3 OFFSET $4");
|
||||
|
||||
// Actually implementing with sqlx properly:
|
||||
sqlx::query_as::<_, Verification>(
|
||||
r#"
|
||||
SELECT * FROM verifications
|
||||
|
|
@ -135,7 +134,7 @@ impl VerificationRepository {
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO verification_logs (verification_id, action, actor_id, old_status, new_status, message)
|
||||
INSERT INTO verification_logs (verification_request_id, action, acted_by_user_id, old_status, new_status, remarks)
|
||||
VALUES ($1, 'STATUS_CHANGE', $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
|
|
|
|||
1
customers.pid
Normal file
1
customers.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
9694
|
||||
|
|
@ -1 +1 @@
|
|||
97314
|
||||
9690
|
||||
|
|
|
|||
1
job_seekers.pid
Normal file
1
job_seekers.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
9693
|
||||
2155
scripts/init-db.sql
2155
scripts/init-db.sql
File diff suppressed because it is too large
Load diff
|
|
@ -4,10 +4,26 @@
|
|||
|
||||
-- ── 1. Roles ─────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Internal roles
|
||||
INSERT INTO roles (key, name, audience) VALUES
|
||||
('SUPER_ADMIN', 'Super Admin', 'INTERNAL'),
|
||||
('ADMIN', 'Admin', 'INTERNAL'),
|
||||
('SUPPORT', 'Support Agent', 'INTERNAL'),
|
||||
('SUPPORT', 'Support Agent', 'INTERNAL')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Internal role extensions
|
||||
INSERT INTO internal_roles (role_id, description)
|
||||
SELECT id, 'Full system administrator with all permissions' FROM roles WHERE key = 'SUPER_ADMIN'
|
||||
ON CONFLICT (role_id) DO NOTHING;
|
||||
INSERT INTO internal_roles (role_id, description, can_approve_requests, can_manage_system_settings)
|
||||
SELECT id, 'Standard administrator', true, true FROM roles WHERE key = 'ADMIN'
|
||||
ON CONFLICT (role_id) DO NOTHING;
|
||||
INSERT INTO internal_roles (role_id, description)
|
||||
SELECT id, 'Customer support agent' FROM roles WHERE key = 'SUPPORT'
|
||||
ON CONFLICT (role_id) DO NOTHING;
|
||||
|
||||
-- External roles
|
||||
INSERT INTO roles (key, name, audience) VALUES
|
||||
('COMPANY', 'Company', 'EXTERNAL'),
|
||||
('JOB_SEEKER', 'Job Seeker', 'EXTERNAL'),
|
||||
('CUSTOMER', 'Customer', 'EXTERNAL'),
|
||||
|
|
@ -22,6 +38,11 @@ INSERT INTO roles (key, name, audience) VALUES
|
|||
('CATERING_SERVICES', 'Catering Services', 'EXTERNAL')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- External role extensions
|
||||
INSERT INTO external_roles (role_id)
|
||||
SELECT id FROM roles WHERE audience = 'EXTERNAL'
|
||||
ON CONFLICT (role_id) DO NOTHING;
|
||||
|
||||
-- ── 2. Super Admin User ──────────────────────────────────────────────────────
|
||||
-- Default password: Admin@nxtgauge1 (bcrypt hash)
|
||||
-- CHANGE THIS PASSWORD IMMEDIATELY AFTER FIRST LOGIN
|
||||
|
|
@ -33,13 +54,14 @@ DECLARE
|
|||
BEGIN
|
||||
SELECT id INTO super_admin_role_id FROM roles WHERE key = 'SUPER_ADMIN';
|
||||
|
||||
INSERT INTO users (email, password_hash, status, role_id, full_name, email_verified)
|
||||
INSERT INTO users (email, password_hash, status, role_id, first_name, last_name, email_verified)
|
||||
VALUES (
|
||||
'admin@nxtgauge.com',
|
||||
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TiGniB9GSmJBGp0K7RqUi/4hY/Ii',
|
||||
'ACTIVE',
|
||||
super_admin_role_id,
|
||||
'Super Admin',
|
||||
'Super',
|
||||
'Admin',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
96200
|
||||
9691
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue