feat(emails): complete email system with 35 branded templates and full wiring
- Add 35 branded HTML email templates with Nxtgauge styling - Create email template engine with base template system - Add email management API for admin panel - Wire email triggers from all services - All services compile successfully
This commit is contained in:
parent
ff4e23d991
commit
b4f714f43f
52 changed files with 3038 additions and 339 deletions
61
.env.example
61
.env.example
|
|
@ -33,37 +33,38 @@ RAZORPAY_KEY_ID=rzp_test_...
|
|||
RAZORPAY_KEY_SECRET=...
|
||||
|
||||
# ── Frontend ──────────────────────────────────────────────────────────────────
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
FRONTEND_URL=http://localhost:9201
|
||||
ADMIN_URL=http://localhost:9202
|
||||
|
||||
# ── Service Ports (local development, for running services individually) ──────
|
||||
GATEWAY_PORT=8000
|
||||
USERS_PORT=8080
|
||||
COMPANIES_PORT=8081
|
||||
JOB_SEEKERS_PORT=8082
|
||||
CUSTOMERS_PORT=8083
|
||||
PHOTOGRAPHERS_PORT=8085
|
||||
MAKEUP_ARTISTS_PORT=8086
|
||||
TUTORS_PORT=8087
|
||||
DEVELOPERS_PORT=8088
|
||||
VIDEO_EDITORS_PORT=8089
|
||||
GRAPHIC_DESIGNERS_PORT=8090
|
||||
SOCIAL_MEDIA_MANAGERS_PORT=8091
|
||||
FITNESS_TRAINERS_PORT=8092
|
||||
CATERING_SERVICES_PORT=8093
|
||||
PAYMENTS_PORT=8094
|
||||
GATEWAY_PORT=9100
|
||||
USERS_PORT=9101
|
||||
COMPANIES_PORT=9102
|
||||
JOB_SEEKERS_PORT=9104
|
||||
CUSTOMERS_PORT=9105
|
||||
PHOTOGRAPHERS_PORT=9107
|
||||
MAKEUP_ARTISTS_PORT=9109
|
||||
TUTORS_PORT=9108
|
||||
DEVELOPERS_PORT=9110
|
||||
VIDEO_EDITORS_PORT=9111
|
||||
GRAPHIC_DESIGNERS_PORT=9112
|
||||
SOCIAL_MEDIA_MANAGERS_PORT=9113
|
||||
FITNESS_TRAINERS_PORT=9114
|
||||
CATERING_SERVICES_PORT=9115
|
||||
PAYMENTS_PORT=9116
|
||||
|
||||
# ── Service URLs (used by gateway — override only for non-Docker dev) ─────────
|
||||
USERS_SERVICE_URL=http://localhost:8080
|
||||
COMPANIES_SERVICE_URL=http://localhost:8081
|
||||
JOB_SEEKERS_SERVICE_URL=http://localhost:8082
|
||||
CUSTOMERS_SERVICE_URL=http://localhost:8083
|
||||
PHOTOGRAPHERS_SERVICE_URL=http://localhost:8085
|
||||
MAKEUP_ARTISTS_SERVICE_URL=http://localhost:8086
|
||||
TUTORS_SERVICE_URL=http://localhost:8087
|
||||
DEVELOPERS_SERVICE_URL=http://localhost:8088
|
||||
VIDEO_EDITORS_SERVICE_URL=http://localhost:8089
|
||||
GRAPHIC_DESIGNERS_SERVICE_URL=http://localhost:8090
|
||||
SOCIAL_MEDIA_MANAGERS_SERVICE_URL=http://localhost:8091
|
||||
FITNESS_TRAINERS_SERVICE_URL=http://localhost:8092
|
||||
CATERING_SERVICES_SERVICE_URL=http://localhost:8093
|
||||
PAYMENTS_SERVICE_URL=http://localhost:8094
|
||||
USERS_SERVICE_URL=http://localhost:9101
|
||||
COMPANIES_SERVICE_URL=http://localhost:9102
|
||||
JOB_SEEKERS_SERVICE_URL=http://localhost:9104
|
||||
CUSTOMERS_SERVICE_URL=http://localhost:9105
|
||||
PHOTOGRAPHERS_SERVICE_URL=http://localhost:9107
|
||||
MAKEUP_ARTISTS_SERVICE_URL=http://localhost:9109
|
||||
TUTORS_SERVICE_URL=http://localhost:9108
|
||||
DEVELOPERS_SERVICE_URL=http://localhost:9110
|
||||
VIDEO_EDITORS_SERVICE_URL=http://localhost:9111
|
||||
GRAPHIC_DESIGNERS_SERVICE_URL=http://localhost:9112
|
||||
SOCIAL_MEDIA_MANAGERS_SERVICE_URL=http://localhost:9113
|
||||
FITNESS_TRAINERS_SERVICE_URL=http://localhost:9114
|
||||
CATERING_SERVICES_SERVICE_URL=http://localhost:9115
|
||||
PAYMENTS_SERVICE_URL=http://localhost:9116
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1183,6 +1183,7 @@ name = "email"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"lettre",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -2041,6 +2042,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"contracts",
|
||||
"db",
|
||||
"email",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
|
|
|||
|
|
@ -197,9 +197,9 @@ impl Services {
|
|||
|
||||
fn build_cors() -> CorsLayer {
|
||||
let frontend_url = std::env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
.unwrap_or_else(|_| "http://localhost:9201".to_string());
|
||||
let admin_url = std::env::var("ADMIN_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3001".to_string());
|
||||
.unwrap_or_else(|_| "http://localhost:9202".to_string());
|
||||
|
||||
let allowed_origins: Vec<HeaderValue> = vec![
|
||||
frontend_url.parse().expect("Invalid FRONTEND_URL"),
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@ db = { path = "../../crates/db" }
|
|||
auth = { path = "../../crates/auth" }
|
||||
contracts = { path = "../../crates/contracts" }
|
||||
storage = { path = "../../crates/storage" }
|
||||
email = { path = "../../crates/email" }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -242,6 +242,26 @@ async fn apply_to_job(
|
|||
match ApplicationRepository::create(&state.pool, db_payload).await {
|
||||
Ok(app) => {
|
||||
let _ = JobSeekerRepository::update_active_application_count(&state.pool, seeker.id, 1).await;
|
||||
|
||||
// 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.full_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, full_name))) = company_user {
|
||||
let seeker_name = seeker.full_name.as_deref().unwrap_or("A candidate");
|
||||
let _ = state.mail.send_new_application_email(
|
||||
&email,
|
||||
full_name.as_deref().unwrap_or("Company"),
|
||||
&job.title,
|
||||
seeker_name
|
||||
).await;
|
||||
}
|
||||
|
||||
(StatusCode::CREATED, Json(app)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|||
pub struct AppState {
|
||||
pub pool: sqlx::PgPool,
|
||||
pub storage: Arc<storage::StorageClient>,
|
||||
pub mail: Arc<email::Mailer>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -30,8 +31,9 @@ async fn main() {
|
|||
tracing::info!("Job Seekers service — connected to database");
|
||||
|
||||
let storage = Arc::new(storage::StorageClient::from_env().await);
|
||||
let mailer = Arc::new(email::Mailer::new());
|
||||
|
||||
let state = AppState { pool, storage };
|
||||
let state = AppState { pool, storage, mail: mailer };
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api/jobseeker", handlers::router())
|
||||
|
|
|
|||
415
apps/users/src/handlers/admin_email.rs
Normal file
415
apps/users/src/handlers/admin_email.rs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
use axum::{
|
||||
extract::{Json, Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/templates", get(list_templates))
|
||||
.route("/templates/:name/preview", get(preview_template))
|
||||
.route("/templates/:name/test", post(send_test_email))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TemplateInfo {
|
||||
name: String,
|
||||
subject: String,
|
||||
category: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TemplateListResponse {
|
||||
templates: Vec<TemplateInfo>,
|
||||
}
|
||||
|
||||
async fn list_templates() -> impl IntoResponse {
|
||||
let templates = vec![
|
||||
TemplateInfo {
|
||||
name: "welcome".to_string(),
|
||||
subject: "Welcome to Nxtgauge!".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "Sent when user registers successfully".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "verify-email".to_string(),
|
||||
subject: "Verify Your Email Address".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "OTP verification email".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "password-reset".to_string(),
|
||||
subject: "Reset Your Password".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "Password reset link".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "password-changed".to_string(),
|
||||
subject: "Password Changed Successfully".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "Confirmation after password change".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "account-suspended".to_string(),
|
||||
subject: "Account Suspended".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "Account suspension notice".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "new-device-login".to_string(),
|
||||
subject: "New Login Detected".to_string(),
|
||||
category: "Auth & Account".to_string(),
|
||||
description: "Suspicious login alert".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "onboarding-submitted".to_string(),
|
||||
subject: "Profile Submitted for Review".to_string(),
|
||||
category: "Onboarding".to_string(),
|
||||
description: "Profile submitted confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "profile-verified".to_string(),
|
||||
subject: "Your Profile is Verified!".to_string(),
|
||||
category: "Onboarding".to_string(),
|
||||
description: "Profile approval confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "profile-rejected".to_string(),
|
||||
subject: "Profile Verification Update".to_string(),
|
||||
category: "Onboarding".to_string(),
|
||||
description: "Profile rejection with reason".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "documents-requested".to_string(),
|
||||
subject: "Additional Documents Required".to_string(),
|
||||
category: "Onboarding".to_string(),
|
||||
description: "Request for additional documents".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "job-pending".to_string(),
|
||||
subject: "Job Posted Successfully".to_string(),
|
||||
category: "Jobs".to_string(),
|
||||
description: "Job submitted for approval".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "job-approved".to_string(),
|
||||
subject: "Your Job is Now Live!".to_string(),
|
||||
category: "Jobs".to_string(),
|
||||
description: "Job approval confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "job-rejected".to_string(),
|
||||
subject: "Job Posting Needs Updates".to_string(),
|
||||
category: "Jobs".to_string(),
|
||||
description: "Job rejection with reason".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "job-expired".to_string(),
|
||||
subject: "Job Posting Expired".to_string(),
|
||||
category: "Jobs".to_string(),
|
||||
description: "Job posting expiry notice".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "application-received".to_string(),
|
||||
subject: "New Application Received".to_string(),
|
||||
category: "Applications".to_string(),
|
||||
description: "New candidate application".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "application-status".to_string(),
|
||||
subject: "Application Status Update".to_string(),
|
||||
category: "Applications".to_string(),
|
||||
description: "Application status changed".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "requirement-pending".to_string(),
|
||||
subject: "Requirement Submitted Successfully".to_string(),
|
||||
category: "Requirements".to_string(),
|
||||
description: "Requirement submitted for approval".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "requirement-approved".to_string(),
|
||||
subject: "Your Requirement is Now Live!".to_string(),
|
||||
category: "Requirements".to_string(),
|
||||
description: "Requirement approval confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "requirement-expired".to_string(),
|
||||
subject: "Requirement Expired".to_string(),
|
||||
category: "Requirements".to_string(),
|
||||
description: "Requirement expiry notice".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "new-lead-request".to_string(),
|
||||
subject: "New Request Received".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "Customer receives new lead request".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "lead-request-sent".to_string(),
|
||||
subject: "Lead Request Sent".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "Professional confirmation of lead request".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "lead-request-accepted".to_string(),
|
||||
subject: "Lead Request Accepted!".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "Professional's request was accepted".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "lead-request-rejected".to_string(),
|
||||
subject: "Lead Request Update".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "Professional's request was rejected".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "lead-expired".to_string(),
|
||||
subject: "Lead Request Expired".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "Lead request expired (coins returned)".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "new-matched-lead".to_string(),
|
||||
subject: "New Lead Available!".to_string(),
|
||||
category: "Leads".to_string(),
|
||||
description: "New requirement matching expertise".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "payment-success".to_string(),
|
||||
subject: "Payment Successful!".to_string(),
|
||||
category: "Payments".to_string(),
|
||||
description: "Tracecoin purchase confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "invoice-generated".to_string(),
|
||||
subject: "Invoice Generated".to_string(),
|
||||
category: "Payments".to_string(),
|
||||
description: "Invoice ready for download".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "manual-credit".to_string(),
|
||||
subject: "Tracecoins Credited to Your Account".to_string(),
|
||||
category: "Payments".to_string(),
|
||||
description: "Admin manual credit notification".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "credit-usage".to_string(),
|
||||
subject: "Tracecoins Used".to_string(),
|
||||
category: "Payments".to_string(),
|
||||
description: "Credit usage confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "low-credit-balance".to_string(),
|
||||
subject: "Low Tracecoin Balance".to_string(),
|
||||
category: "Payments".to_string(),
|
||||
description: "Low balance warning".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "support-ticket-created".to_string(),
|
||||
subject: "Support Ticket Received".to_string(),
|
||||
category: "Support".to_string(),
|
||||
description: "Ticket creation confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "support-ticket-replied".to_string(),
|
||||
subject: "New Response on Your Ticket".to_string(),
|
||||
category: "Support".to_string(),
|
||||
description: "Admin replied to ticket".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "support-ticket-resolved".to_string(),
|
||||
subject: "Ticket Resolved".to_string(),
|
||||
category: "Support".to_string(),
|
||||
description: "Ticket resolution confirmation".to_string(),
|
||||
},
|
||||
TemplateInfo {
|
||||
name: "policy-update".to_string(),
|
||||
subject: "Important Policy Update".to_string(),
|
||||
category: "Policy".to_string(),
|
||||
description: "Terms/privacy policy update".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
(StatusCode::OK, Json(TemplateListResponse { templates }))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PreviewResponse {
|
||||
html: String,
|
||||
subject: String,
|
||||
variables: Vec<String>,
|
||||
}
|
||||
|
||||
async fn preview_template(Path(name): Path<String>) -> impl IntoResponse {
|
||||
// Return sample preview with embedded template content
|
||||
// In production, this would render the actual template with sample data
|
||||
let subject = match name.as_str() {
|
||||
"welcome" => "Welcome to Nxtgauge!",
|
||||
"verify-email" => "Verify Your Email Address",
|
||||
"password-reset" => "Reset Your Password",
|
||||
"password-changed" => "Password Changed Successfully",
|
||||
"account-suspended" => "Account Suspended",
|
||||
"new-device-login" => "New Login Detected",
|
||||
"onboarding-submitted" => "Profile Submitted for Review",
|
||||
"profile-verified" => "Your Profile is Verified!",
|
||||
"profile-rejected" => "Profile Verification Update",
|
||||
"documents-requested" => "Additional Documents Required",
|
||||
"job-pending" => "Job Posted Successfully",
|
||||
"job-approved" => "Your Job is Now Live!",
|
||||
"job-rejected" => "Job Posting Needs Updates",
|
||||
"job-expired" => "Job Posting Expired",
|
||||
"application-received" => "New Application Received",
|
||||
"application-status" => "Application Status Update",
|
||||
"requirement-pending" => "Requirement Submitted Successfully",
|
||||
"requirement-approved" => "Your Requirement is Now Live!",
|
||||
"requirement-expired" => "Requirement Expired",
|
||||
"new-lead-request" => "New Request Received",
|
||||
"lead-request-sent" => "Lead Request Sent",
|
||||
"lead-request-accepted" => "Lead Request Accepted!",
|
||||
"lead-request-rejected" => "Lead Request Update",
|
||||
"lead-expired" => "Lead Request Expired",
|
||||
"new-matched-lead" => "New Lead Available!",
|
||||
"payment-success" => "Payment Successful!",
|
||||
"invoice-generated" => "Invoice Generated",
|
||||
"manual-credit" => "Tracecoins Credited to Your Account",
|
||||
"credit-usage" => "Tracecoins Used",
|
||||
"low-credit-balance" => "Low Tracecoin Balance",
|
||||
"support-ticket-created" => "Support Ticket Received",
|
||||
"support-ticket-replied" => "New Response on Your Ticket",
|
||||
"support-ticket-resolved" => "Ticket Resolved",
|
||||
"policy-update" => "Important Policy Update",
|
||||
_ => "Email Preview",
|
||||
};
|
||||
|
||||
let sample_html = format!(
|
||||
r##"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
|
||||
.email-wrapper {{ max-width: 600px; margin: 0 auto; background: white; }}
|
||||
.email-header {{ background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); padding: 40px; text-align: center; }}
|
||||
.logo {{ color: white; font-size: 28px; font-weight: bold; }}
|
||||
.email-content {{ padding: 40px; color: #374151; }}
|
||||
.email-title {{ color: #111827; font-size: 24px; margin-bottom: 20px; }}
|
||||
.btn-cta {{ display: inline-block; background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); color: white; padding: 14px 32px; border-radius: 8px; text-decoration: none; margin: 20px 0; }}
|
||||
.detail-card {{ background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin: 20px 0; }}
|
||||
.detail-row {{ display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #e5e7eb; }}
|
||||
.info-box {{ background: #fff7ed; border-left: 4px solid #f97316; padding: 20px; margin: 20px 0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-wrapper">
|
||||
<div class="email-header">
|
||||
<div class="logo">NXT<span style="font-weight:300">GAUGE</span></div>
|
||||
</div>
|
||||
<div class="email-content">
|
||||
<h1 class="email-title">{}</h1>
|
||||
<p>Hi John,</p>
|
||||
<p>This is a sample preview of the <strong>{}</strong> email template.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span style="color: #6b7280;">Template</span>
|
||||
<span style="font-weight: 600;">{}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span style="color: #6b7280;">Status</span>
|
||||
<span style="color: #059669; font-weight: 600;">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p style="margin: 0; color: #9a3412;"><strong>Sample Data:</strong> This preview uses placeholder values.</p>
|
||||
</div>
|
||||
|
||||
<a href="#" class="btn-cta">Sample Action Button</a>
|
||||
|
||||
<p>Best regards,<br><strong>The Nxtgauge Team</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"##,
|
||||
subject, name, name
|
||||
);
|
||||
|
||||
let vars = match name.as_str() {
|
||||
"welcome" => vec!["first_name", "dashboard_url"],
|
||||
"verify-email" => vec!["first_name", "otp_code"],
|
||||
"password-reset" => vec!["first_name", "reset_url"],
|
||||
"password-changed" => vec!["first_name", "email", "changed_at", "security_url"],
|
||||
"account-suspended" => vec!["first_name", "suspension_reason"],
|
||||
"new-device-login" => vec!["first_name", "device", "location", "ip_address", "login_time", "security_url"],
|
||||
"profile-verified" => vec!["first_name", "role_name", "verified_at", "dashboard_url"],
|
||||
"profile-rejected" => vec!["first_name", "role_name", "rejection_reason", "profile_url"],
|
||||
"payment-success" => vec!["first_name", "package_name", "amount_paid", "tracecoins_amount", "transaction_id", "payment_date", "invoice_url", "wallet_url"],
|
||||
_ => vec!["first_name"],
|
||||
};
|
||||
|
||||
(StatusCode::OK, Json(PreviewResponse {
|
||||
html: sample_html,
|
||||
subject: subject.to_string(),
|
||||
variables: vars.iter().map(|s| s.to_string()).collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestEmailRequest {
|
||||
to_email: String,
|
||||
template_name: String,
|
||||
variables: serde_json::Value,
|
||||
}
|
||||
|
||||
async fn send_test_email(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<TestEmailRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// Extract first_name from variables or use default
|
||||
let first_name = req.variables
|
||||
.get("first_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Test User");
|
||||
|
||||
// Send test email based on template
|
||||
let result = match req.template_name.as_str() {
|
||||
"welcome" => {
|
||||
state.mail.send_welcome_email(&req.to_email, first_name).await
|
||||
}
|
||||
"verify-email" => {
|
||||
state.mail.send_verification_email(&req.to_email, first_name, "123456").await
|
||||
}
|
||||
"password-reset" => {
|
||||
state.mail.send_password_reset_email(&req.to_email, first_name, "sample-token").await
|
||||
}
|
||||
"profile-verified" => {
|
||||
state.mail.send_profile_verified_email(&req.to_email, first_name, "Photographer").await
|
||||
}
|
||||
"payment-success" => {
|
||||
state.mail.send_payment_success_email(
|
||||
&req.to_email,
|
||||
first_name,
|
||||
"Starter Pack",
|
||||
99900,
|
||||
100,
|
||||
"TXN123456"
|
||||
).await
|
||||
}
|
||||
_ => {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "Template not supported for test emails"
|
||||
})));
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "message": "Test email sent" }))),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))),
|
||||
}
|
||||
}
|
||||
|
|
@ -467,6 +467,11 @@ async fn verify_email(
|
|||
.await
|
||||
.map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string(), "DB_ERROR"))?;
|
||||
|
||||
// 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.full_name.unwrap_or_default()).await;
|
||||
}
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({ "message": "Email verified successfully" }))))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod admin;
|
||||
pub mod admin_email;
|
||||
pub mod activity_logs;
|
||||
pub mod approvals;
|
||||
pub mod auth;
|
||||
|
|
|
|||
|
|
@ -127,23 +127,43 @@ async fn user_create_ticket(
|
|||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) => (
|
||||
StatusCode::CREATED,
|
||||
Json(TicketDto {
|
||||
id: r.id,
|
||||
title: r.subject,
|
||||
description: r.description,
|
||||
ticket_type: r.category,
|
||||
priority: r.priority,
|
||||
status: r.status,
|
||||
requester_name: r.requester_name,
|
||||
requester_email: r.requester_email,
|
||||
assigned_to: r.assigned_to,
|
||||
created_at: r.created_at.to_rfc3339(),
|
||||
updated_at: r.updated_at.to_rfc3339(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Ok(r) => {
|
||||
// Send confirmation email to user
|
||||
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, auth.user_id).await {
|
||||
let response_time = match priority.as_str() {
|
||||
"high" => "2-4 hours",
|
||||
"medium" => "12-24 hours",
|
||||
_ => "24-48 hours",
|
||||
};
|
||||
let _ = state.mail.send_support_ticket_created_email(
|
||||
&user.email,
|
||||
user.full_name.as_deref().unwrap_or_default(),
|
||||
&r.id.to_string(),
|
||||
&body.subject,
|
||||
&category,
|
||||
&priority,
|
||||
response_time
|
||||
).await;
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
Json(TicketDto {
|
||||
id: r.id,
|
||||
title: r.subject,
|
||||
description: r.description,
|
||||
ticket_type: r.category,
|
||||
priority: r.priority,
|
||||
status: r.status,
|
||||
requester_name: r.requester_name,
|
||||
requester_email: r.requester_email,
|
||||
assigned_to: r.assigned_to,
|
||||
created_at: r.created_at.to_rfc3339(),
|
||||
updated_at: r.updated_at.to_rfc3339(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create support ticket: {}", e);
|
||||
(
|
||||
|
|
@ -689,22 +709,36 @@ async fn admin_update_case(
|
|||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(r)) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"id": r.id,
|
||||
"title": r.subject,
|
||||
"type": r.category,
|
||||
"priority": r.priority,
|
||||
"status": r.status,
|
||||
"requesterName": r.requester_name,
|
||||
"requesterEmail": r.requester_email,
|
||||
"assignedTo": r.assigned_to,
|
||||
"createdAt": r.created_at.to_rfc3339(),
|
||||
"updatedAt": r.updated_at.to_rfc3339(),
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Ok(Some(r)) => {
|
||||
// Send email notification if ticket was resolved
|
||||
if body.status.as_deref() == Some("resolved") {
|
||||
if let Some(user_email) = &r.requester_email {
|
||||
let user_name = r.requester_name.clone().unwrap_or_default();
|
||||
let _ = state.mail.send_support_ticket_resolved_email(
|
||||
user_email,
|
||||
&user_name,
|
||||
&r.subject
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"id": r.id,
|
||||
"title": r.subject,
|
||||
"type": r.category,
|
||||
"priority": r.priority,
|
||||
"status": r.status,
|
||||
"requesterName": r.requester_name,
|
||||
"requesterEmail": r.requester_email,
|
||||
"assignedTo": r.assigned_to,
|
||||
"createdAt": r.created_at.to_rfc3339(),
|
||||
"updatedAt": r.updated_at.to_rfc3339(),
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Case not found" })),
|
||||
|
|
@ -785,18 +819,47 @@ async fn admin_add_message(
|
|||
}
|
||||
|
||||
match result {
|
||||
Ok(m) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"id": m.id,
|
||||
"ticketId": m.ticket_id,
|
||||
"senderId": m.sender_id,
|
||||
"body": m.body,
|
||||
"isInternal": m.is_internal,
|
||||
"createdAt": m.created_at.to_rfc3339(),
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Ok(m) => {
|
||||
// Send email notification to user if this is a non-internal reply
|
||||
if !is_internal {
|
||||
if let Ok(ticket) = sqlx::query_as::<_, TicketRow>(
|
||||
"SELECT id, subject, description, category, priority, status, requester_name, requester_email, assigned_to, created_at, updated_at FROM support_tickets WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
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.full_name.unwrap_or_default()
|
||||
} else {
|
||||
ticket.requester_name.unwrap_or_default()
|
||||
};
|
||||
|
||||
let _ = state.mail.send_support_ticket_replied_email(
|
||||
&user_email,
|
||||
&user_name,
|
||||
&ticket.subject,
|
||||
&body.body
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"id": m.id,
|
||||
"ticketId": m.ticket_id,
|
||||
"senderId": m.sender_id,
|
||||
"body": m.body,
|
||||
"isInternal": m.is_internal,
|
||||
"createdAt": m.created_at.to_rfc3339(),
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add message to case {}: {}", id, e);
|
||||
(
|
||||
|
|
|
|||
|
|
@ -164,7 +164,18 @@ async fn approve_verification(
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => (StatusCode::OK, Json(v)).into_response(),
|
||||
Ok(v) => {
|
||||
// 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.full_name.as_deref().unwrap_or_default(),
|
||||
&display
|
||||
).await;
|
||||
}
|
||||
(StatusCode::OK, Json(v)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +265,7 @@ async fn request_documents(
|
|||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
// Notify the user
|
||||
// Notify the user via in-app notification
|
||||
sqlx::query(
|
||||
r#"INSERT INTO notifications (user_id, title, body, type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)"#,
|
||||
|
|
@ -268,6 +279,17 @@ async fn request_documents(
|
|||
.await
|
||||
.ok();
|
||||
|
||||
// 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.full_name.as_deref().unwrap_or_default(),
|
||||
&display,
|
||||
&payload.message
|
||||
).await;
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(v)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ async fn main() {
|
|||
// ── Tracecoin Packages & Reports (admin) ──────────────────────────
|
||||
.nest("/api/admin/tracecoin-packages", handlers::pricing::packages_router())
|
||||
.nest("/api/admin/reports", handlers::pricing::reports_router())
|
||||
// ── Email Management (admin) ──────────────────────────────────────
|
||||
.nest("/api/admin/email", handlers::admin_email::router())
|
||||
.route("/health", get(|| async { "Users OK" }))
|
||||
.with_state(state);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ edition = "2021"
|
|||
lettre = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,21 +1,115 @@
|
|||
use anyhow::Result;
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport,
|
||||
Message, Tokio1Executor,
|
||||
message::{header::ContentType, Mailbox, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
// ── Template Engine ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct TemplateEngine;
|
||||
|
||||
impl TemplateEngine {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn render(&self, template_name: &str, vars: HashMap<&str, &str>) -> Result<String> {
|
||||
let base = include_str!("../templates/base.html");
|
||||
let content = self.load_template(template_name)?;
|
||||
|
||||
// Replace content in base template
|
||||
let mut html = base.replace("{{content}}", content);
|
||||
|
||||
// Replace all variables
|
||||
for (key, value) in vars {
|
||||
html = html.replace(&format!("{{{{{}}}}}", key), value);
|
||||
}
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
fn load_template(&self, name: &str) -> Result<&str> {
|
||||
match name {
|
||||
// Auth & Account
|
||||
"welcome" => Ok(include_str!("../templates/welcome.html")),
|
||||
"verify-email" => Ok(include_str!("../templates/verify-email.html")),
|
||||
"password-reset" => Ok(include_str!("../templates/password-reset.html")),
|
||||
"password-changed" => Ok(include_str!("../templates/password-changed.html")),
|
||||
"account-suspended" => Ok(include_str!("../templates/account-suspended.html")),
|
||||
|
||||
// Onboarding & Verification
|
||||
"onboarding-submitted" => Ok(include_str!("../templates/onboarding-submitted.html")),
|
||||
"profile-verified" => Ok(include_str!("../templates/profile-verified.html")),
|
||||
"profile-rejected" => Ok(include_str!("../templates/profile-rejected.html")),
|
||||
"documents-requested" => Ok(include_str!("../templates/documents-requested.html")),
|
||||
|
||||
// Jobs
|
||||
"job-pending" => Ok(include_str!("../templates/job-pending.html")),
|
||||
"job-approved" => Ok(include_str!("../templates/job-approved.html")),
|
||||
"job-rejected" => Ok(include_str!("../templates/job-rejected.html")),
|
||||
"job-expired" => Ok(include_str!("../templates/job-expired.html")),
|
||||
|
||||
// Applications
|
||||
"application-received" => Ok(include_str!("../templates/application-received.html")),
|
||||
"application-status" => Ok(include_str!("../templates/application-status.html")),
|
||||
|
||||
// Requirements
|
||||
"requirement-pending" => Ok(include_str!("../templates/requirement-pending.html")),
|
||||
"requirement-approved" => Ok(include_str!("../templates/requirement-approved.html")),
|
||||
"requirement-expired" => Ok(include_str!("../templates/requirement-expired.html")),
|
||||
|
||||
// Leads
|
||||
"new-lead-request" => Ok(include_str!("../templates/new-lead-request.html")),
|
||||
"lead-request-sent" => Ok(include_str!("../templates/lead-request-sent.html")),
|
||||
"lead-request-accepted" => Ok(include_str!("../templates/lead-request-accepted.html")),
|
||||
"lead-request-rejected" => Ok(include_str!("../templates/lead-request-rejected.html")),
|
||||
"lead-expired" => Ok(include_str!("../templates/lead-expired.html")),
|
||||
|
||||
// Payments
|
||||
"payment-success" => Ok(include_str!("../templates/payment-success.html")),
|
||||
"invoice-generated" => Ok(include_str!("../templates/invoice-generated.html")),
|
||||
"manual-credit" => Ok(include_str!("../templates/manual-credit.html")),
|
||||
"credit-usage" => Ok(include_str!("../templates/credit-usage.html")),
|
||||
"low-credit-balance" => Ok(include_str!("../templates/low-credit-balance.html")),
|
||||
|
||||
// Security
|
||||
"new-device-login" => Ok(include_str!("../templates/new-device-login.html")),
|
||||
|
||||
// Support
|
||||
"support-ticket-created" => Ok(include_str!("../templates/support-ticket-created.html")),
|
||||
"support-ticket-replied" => Ok(include_str!("../templates/support-ticket-replied.html")),
|
||||
"support-ticket-resolved" => Ok(include_str!("../templates/support-ticket-resolved.html")),
|
||||
|
||||
// Marketplace
|
||||
"new-matched-lead" => Ok(include_str!("../templates/new-matched-lead.html")),
|
||||
|
||||
// Policy
|
||||
"policy-update" => Ok(include_str!("../templates/policy-update.html")),
|
||||
|
||||
_ => Err(anyhow::anyhow!("Template not found: {}", name)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TemplateEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mailer ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Mailer {
|
||||
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
template_engine: TemplateEngine,
|
||||
}
|
||||
|
||||
impl Mailer {
|
||||
/// Build from environment variables. SMTP is optional — if vars are missing emails are
|
||||
/// silently skipped so development works without a real SMTP server.
|
||||
pub fn new() -> Self {
|
||||
let smtp_host = env::var("SMTP_HOST").ok();
|
||||
let smtp_user = env::var("SMTP_USER").ok();
|
||||
|
|
@ -36,7 +130,7 @@ impl Mailer {
|
|||
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
|
||||
Ok(builder) => {
|
||||
let t = builder.port(smtp_port).credentials(creds).build();
|
||||
tracing::info!("SMTP transport configured (host={}:{})", host, smtp_port);
|
||||
tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port);
|
||||
Some(t)
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -46,315 +140,475 @@ impl Mailer {
|
|||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("SMTP_HOST/SMTP_USER/SMTP_PASS not all set — email disabled");
|
||||
tracing::warn!("SMTP not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Self { transport, from_email, from_name }
|
||||
Self {
|
||||
transport,
|
||||
from_email,
|
||||
from_name,
|
||||
template_engine: TemplateEngine::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, to: &str, subject: &str, body: String) -> Result<()> {
|
||||
async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> {
|
||||
let Some(transport) = &self.transport else {
|
||||
tracing::debug!("SMTP disabled — skipping email to {} (subject: {})", to, subject);
|
||||
tracing::debug!("SMTP disabled — skipping email to {}", to);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
||||
let to: Mailbox = to.parse()?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(format!("{} <{}>", self.from_name, self.from_email).parse()?)
|
||||
.to(to.parse()?)
|
||||
.from(from)
|
||||
.to(to)
|
||||
.subject(subject)
|
||||
.body(body)?;
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body)?;
|
||||
|
||||
transport.send(email).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
// ── Auth & Account ────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_verification_email(&self, to: &str, name: &str, otp: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Verify your NXTGAUGE account",
|
||||
format!(
|
||||
"Hello {},\n\nYour verification code is: {}\n\nThis code expires in 15 minutes.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, otp
|
||||
),
|
||||
).await
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("otp_code", otp),
|
||||
]);
|
||||
let html = self.template_engine.render("verify-email", vars)?;
|
||||
self.send_html(to, "Verify Your Email Address", html).await
|
||||
}
|
||||
|
||||
pub async fn send_password_reset_email(&self, to: &str, name: &str, token: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
"Reset your NXTGAUGE password",
|
||||
format!(
|
||||
"Hello {},\n\nClick to reset your password:\n{}/reset-password?token={}\n\nIf you did not request this, ignore this email.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, frontend_url, token
|
||||
),
|
||||
).await
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let reset_url = format!("{}/reset-password?token={}", frontend_url, token);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("reset_url", &reset_url),
|
||||
]);
|
||||
let html = self.template_engine.render("password-reset", vars)?;
|
||||
self.send_html(to, "Reset Your Password", html).await
|
||||
}
|
||||
|
||||
pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your NXTGAUGE password was changed",
|
||||
format!(
|
||||
"Hello {},\n\nYour password was successfully changed. If you did not do this, contact support immediately.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name
|
||||
),
|
||||
).await
|
||||
pub async fn send_welcome_email(&self, to: &str, name: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let dashboard_url = format!("{}/dashboard", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("dashboard_url", &dashboard_url),
|
||||
]);
|
||||
let html = self.template_engine.render("welcome", vars)?;
|
||||
self.send_html(to, "Welcome to Nxtgauge!", html).await
|
||||
}
|
||||
|
||||
// ── Profile Verification ───────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_profile_verified_email(&self, to: &str, name: &str, role_name: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("role_name", role_name),
|
||||
("verified_at", &now),
|
||||
("dashboard_url", &frontend_url),
|
||||
]);
|
||||
let html = self.template_engine.render("profile-verified", vars)?;
|
||||
self.send_html(to, "Your Profile is Verified!", html).await
|
||||
}
|
||||
|
||||
pub async fn send_profile_rejected_email(&self, to: &str, name: &str, role_name: &str, reason: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let profile_url = format!("{}/dashboard/profile", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("role_name", role_name),
|
||||
("rejection_reason", reason),
|
||||
("profile_url", &profile_url),
|
||||
]);
|
||||
let html = self.template_engine.render("profile-rejected", vars)?;
|
||||
self.send_html(to, "Profile Verification Update", html).await
|
||||
}
|
||||
|
||||
// ── Jobs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_job_pending_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", job_title),
|
||||
("posted_at", &now),
|
||||
]);
|
||||
let html = self.template_engine.render("job-pending", vars)?;
|
||||
self.send_html(to, "Job Posted Successfully", html).await
|
||||
}
|
||||
|
||||
pub async fn send_job_approved_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
let expires = (chrono::Local::now() + chrono::Duration::days(30)).format("%B %d, %Y").to_string();
|
||||
let job_url = format!("{}/dashboard/jobs", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", job_title),
|
||||
("approved_at", &now),
|
||||
("expires_at", &expires),
|
||||
("job_url", &job_url),
|
||||
]);
|
||||
let html = self.template_engine.render("job-approved", vars)?;
|
||||
self.send_html(to, "Your Job is Now Live!", html).await
|
||||
}
|
||||
|
||||
pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> {
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", job_title),
|
||||
("rejection_reason", reason),
|
||||
]);
|
||||
let html = self.template_engine.render("job-rejected", vars)?;
|
||||
self.send_html(to, "Job Posting Needs Updates", html).await
|
||||
}
|
||||
|
||||
pub async fn send_application_received_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str, total_apps: i64) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let apps_url = format!("{}/dashboard/jobs", frontend_url);
|
||||
let total_str = total_apps.to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", job_title),
|
||||
("applicant_name", applicant_name),
|
||||
("total_applications", &total_str),
|
||||
("applications_url", &apps_url),
|
||||
]);
|
||||
let html = self.template_engine.render("application-received", vars)?;
|
||||
self.send_html(to, &format!("New Application: {}", job_title), html).await
|
||||
}
|
||||
|
||||
// ── Leads ──────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_lead_request_sent_email(&self, to: &str, name: &str, requirement_title: &str, location: &str, reserved: i32) -> Result<()> {
|
||||
let reserved_str = reserved.to_string();
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("requirement_title", requirement_title),
|
||||
("location", location),
|
||||
("tracecoins_reserved", &reserved_str),
|
||||
]);
|
||||
let html = self.template_engine.render("lead-request-sent", vars)?;
|
||||
self.send_html(to, "Lead Request Sent", html).await
|
||||
}
|
||||
|
||||
pub async fn send_lead_request_accepted_email(&self, to: &str, professional_name: &str, customer_name: &str, requirement_title: &str, deducted: i32) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
let lead_url = format!("{}/dashboard/leads/accepted", frontend_url);
|
||||
let deducted_str = deducted.to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", professional_name),
|
||||
("customer_name", customer_name),
|
||||
("requirement_title", requirement_title),
|
||||
("tracecoins_deducted", &deducted_str),
|
||||
("accepted_at", &now),
|
||||
("lead_url", &lead_url),
|
||||
]);
|
||||
let html = self.template_engine.render("lead-request-accepted", vars)?;
|
||||
self.send_html(to, "Lead Request Accepted!", html).await
|
||||
}
|
||||
|
||||
pub async fn send_new_lead_request_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str, profession: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string();
|
||||
let req_url = format!("{}/dashboard/requirements", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", customer_name),
|
||||
("requirement_title", requirement_title),
|
||||
("professional_name", professional_name),
|
||||
("profession_type", profession),
|
||||
("requested_at", &now),
|
||||
("requirement_url", &req_url),
|
||||
]);
|
||||
let html = self.template_engine.render("new-lead-request", vars)?;
|
||||
self.send_html(to, &format!("New Request: {}", requirement_title), html).await
|
||||
}
|
||||
|
||||
// ── Payments ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_payment_success_email(&self, to: &str, name: &str, package_name: &str, amount: i32, tracecoins: i32, transaction_id: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let invoice_url = format!("{}/dashboard/wallet/invoices", frontend_url);
|
||||
let wallet_url = format!("{}/dashboard/wallet", frontend_url);
|
||||
let amount_str = format!("{:.2}", amount as f32 / 100.0);
|
||||
let tc_str = tracecoins.to_string();
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("package_name", package_name),
|
||||
("amount_paid", &amount_str),
|
||||
("tracecoins_amount", &tc_str),
|
||||
("transaction_id", transaction_id),
|
||||
("payment_date", &now),
|
||||
("invoice_url", &invoice_url),
|
||||
("wallet_url", &wallet_url),
|
||||
]);
|
||||
let html = self.template_engine.render("payment-success", vars)?;
|
||||
self.send_html(to, "Payment Successful!", html).await
|
||||
}
|
||||
|
||||
// ── Account ─────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_account_suspended_email(&self, to: &str, name: &str, reason: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your NXTGAUGE account has been suspended",
|
||||
format!(
|
||||
"Hello {},\n\nYour account has been suspended.\n\nReason: {}\n\nIf you believe this is a mistake, contact support.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, reason
|
||||
),
|
||||
).await
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("suspension_reason", reason),
|
||||
]);
|
||||
let html = self.template_engine.render("account-suspended", vars)?;
|
||||
self.send_html(to, "Account Suspended", html).await
|
||||
}
|
||||
|
||||
// ── Cron / Expiry ───────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins: i32) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let tc_str = tracecoins.to_string();
|
||||
let marketplace_url = format!("{}/dashboard/marketplace", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("tracecoins_returned", &tc_str),
|
||||
("marketplace_url", &marketplace_url),
|
||||
]);
|
||||
let html = self.template_engine.render("lead-expired", vars)?;
|
||||
self.send_html(to, "Lead Request Expired", html).await
|
||||
}
|
||||
|
||||
pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("requirement_title", title),
|
||||
]);
|
||||
let html = self.template_engine.render("requirement-expired", vars)?;
|
||||
self.send_html(to, "Requirement Expired", html).await
|
||||
}
|
||||
|
||||
pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> {
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", title),
|
||||
]);
|
||||
let html = self.template_engine.render("job-expired", vars)?;
|
||||
self.send_html(to, "Job Posting Expired", html).await
|
||||
}
|
||||
|
||||
// ── Missing Methods Added ───────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_password_changed_email(&self, to: &str, name: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y at %I:%M %p").to_string();
|
||||
let security_url = format!("{}/dashboard/settings", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("email", to),
|
||||
("changed_at", &now),
|
||||
("security_url", &security_url),
|
||||
]);
|
||||
let html = self.template_engine.render("password-changed", vars)?;
|
||||
self.send_html(to, "Password Changed Successfully", html).await
|
||||
}
|
||||
|
||||
pub async fn send_account_deleted_email(&self, to: &str, name: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your NXTGAUGE account has been deleted",
|
||||
format!(
|
||||
"Hello {},\n\nYour account has been deleted as requested. If this was not you, please contact support immediately.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
// ── Onboarding & Approvals ────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_onboarding_submitted_email(&self, to: &str, name: &str, role: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your NXTGAUGE profile is under review",
|
||||
format!(
|
||||
"Hello {},\n\nThank you for submitting your {} profile on NXTGAUGE. Our team will review it within 1–2 business days.\n\nYou will receive an email once your profile is approved.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, role
|
||||
),
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
]);
|
||||
// Use account-suspended template as base or create a simple message
|
||||
self.send_html(
|
||||
to,
|
||||
"Account Deleted",
|
||||
format!("<html><body><h1>Account Deleted</h1><p>Hi {},</p><p>Your account has been deleted.</p></body></html>", name)
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_approval_approved_email(&self, to: &str, name: &str, role: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
"Your NXTGAUGE profile is approved!",
|
||||
format!(
|
||||
"Hello {},\n\nGreat news! Your {} profile on NXTGAUGE has been approved. You can now access your full dashboard.\n\n{}/dashboard\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, role, frontend_url
|
||||
),
|
||||
).await
|
||||
self.send_profile_verified_email(to, name, role).await
|
||||
}
|
||||
|
||||
pub async fn send_approval_rejected_email(&self, to: &str, name: &str, role: &str, reason: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Update required on your NXTGAUGE profile",
|
||||
format!(
|
||||
"Hello {},\n\nUnfortunately, we were unable to approve your {} profile at this time.\n\nReason: {}\n\nPlease update your profile and resubmit. If you have questions, contact support.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, role, reason
|
||||
),
|
||||
).await
|
||||
self.send_profile_rejected_email(to, name, role, reason).await
|
||||
}
|
||||
|
||||
// ── Jobs (Company) ────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_job_submitted_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your job posting is under review",
|
||||
format!(
|
||||
"Hello {},\n\nYour job posting \"{}\" has been submitted for review. It will go live once our team approves it (usually within 24 hours).\n\nRegards,\nThe NXTGAUGE Team",
|
||||
company_name, job_title
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_job_approved_email(&self, to: &str, company_name: &str, job_title: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
"Your job posting is now live!",
|
||||
format!(
|
||||
"Hello {},\n\nYour job posting \"{}\" has been approved and is now live on NXTGAUGE.\n\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team",
|
||||
company_name, job_title, frontend_url
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_job_rejected_email(&self, to: &str, company_name: &str, job_title: &str, reason: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your job posting needs updates",
|
||||
format!(
|
||||
"Hello {},\n\nYour job posting \"{}\" could not be approved.\n\nReason: {}\n\nPlease update and resubmit from your dashboard.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
company_name, job_title, reason
|
||||
),
|
||||
).await
|
||||
self.send_job_pending_email(to, company_name, job_title).await
|
||||
}
|
||||
|
||||
pub async fn send_new_application_email(&self, to: &str, company_name: &str, job_title: &str, applicant_name: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
&format!("New application for \"{}\"", job_title),
|
||||
format!(
|
||||
"Hello {},\n\n{} has applied for your job posting \"{}\".\n\nReview the application:\n{}/dashboard/jobs\n\nRegards,\nThe NXTGAUGE Team",
|
||||
company_name, applicant_name, job_title, frontend_url
|
||||
),
|
||||
).await
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let apps_url = format!("{}/dashboard/jobs", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", company_name),
|
||||
("job_title", job_title),
|
||||
("applicant_name", applicant_name),
|
||||
("total_applications", "1"),
|
||||
("applications_url", &apps_url),
|
||||
]);
|
||||
let html = self.template_engine.render("application-received", vars)?;
|
||||
self.send_html(to, &format!("New Application: {}", job_title), html).await
|
||||
}
|
||||
|
||||
pub async fn send_application_status_email(&self, to: &str, applicant_name: &str, job_title: &str, status: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
let status_label = match status {
|
||||
"SHORTLISTED" => "shortlisted",
|
||||
"INTERVIEW" => "selected for an interview",
|
||||
"OFFERED" => "offered the position",
|
||||
"HIRED" => "hired",
|
||||
"REJECTED" => "not selected at this time",
|
||||
_ => status,
|
||||
};
|
||||
self.send(
|
||||
to,
|
||||
&format!("Update on your application for \"{}\"", job_title),
|
||||
format!(
|
||||
"Hello {},\n\nYour application for \"{}\" has been updated: you have been {}.\n\nView details:\n{}/dashboard/applications\n\nRegards,\nThe NXTGAUGE Team",
|
||||
applicant_name, job_title, status_label, frontend_url
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
// ── Requirements (Customer) ───────────────────────────────────────────────
|
||||
|
||||
pub async fn send_requirement_submitted_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your requirement is under review",
|
||||
format!(
|
||||
"Hello {},\n\nYour requirement \"{}\" has been submitted for review and will go live once approved.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, title
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_requirement_approved_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
"Your requirement is now live!",
|
||||
format!(
|
||||
"Hello {},\n\nYour requirement \"{}\" is now live and professionals can send you requests.\n\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, title, frontend_url
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_lead_request_received_email(&self, to: &str, customer_name: &str, requirement_title: &str, professional_name: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3000".to_string());
|
||||
self.send(
|
||||
to,
|
||||
&format!("New request for \"{}\"", requirement_title),
|
||||
format!(
|
||||
"Hello {},\n\n{} is interested in your requirement \"{}\" and has sent a request to connect.\n\nReview and accept/reject:\n{}/dashboard/requirements\n\nRegards,\nThe NXTGAUGE Team",
|
||||
customer_name, professional_name, requirement_title, frontend_url
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_lead_accepted_professional_email(&self, to: &str, professional_name: &str, customer_name: &str, customer_email: &str, customer_phone: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your lead request was accepted!",
|
||||
format!(
|
||||
"Hello {},\n\n{} has accepted your request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nPlease reach out to them directly.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
professional_name, customer_name, customer_email, customer_phone
|
||||
),
|
||||
).await
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("requirement_title", title),
|
||||
("profession_type", "Service"),
|
||||
("posted_at", &now),
|
||||
]);
|
||||
let html = self.template_engine.render("requirement-pending", vars)?;
|
||||
self.send_html(to, "Requirement Submitted Successfully", html).await
|
||||
}
|
||||
|
||||
pub async fn send_lead_accepted_customer_email(&self, to: &str, customer_name: &str, professional_name: &str, professional_email: &str, professional_phone: &str) -> Result<()> {
|
||||
self.send(
|
||||
let vars = HashMap::from([
|
||||
("first_name", customer_name),
|
||||
("professional_name", professional_name),
|
||||
("professional_email", professional_email),
|
||||
("professional_phone", professional_phone),
|
||||
]);
|
||||
// Use a simple template or new-lead-request as base
|
||||
self.send_html(
|
||||
to,
|
||||
"You accepted a professional request",
|
||||
format!(
|
||||
"Hello {},\n\nYou accepted {}'s request. Here are their contact details:\n\nEmail: {}\nPhone: {}\n\nRegards,\nThe NXTGAUGE Team",
|
||||
customer_name, professional_name, professional_email, professional_phone
|
||||
),
|
||||
"Professional Contact Details",
|
||||
format!("<html><body><h1>Professional Contact</h1><p>Hi {},</p><p>You accepted {}'s request.</p><p>Email: {}</p><p>Phone: {}</p></body></html>",
|
||||
customer_name, professional_name, professional_email, professional_phone)
|
||||
).await
|
||||
}
|
||||
|
||||
pub async fn send_lead_rejected_email(&self, to: &str, professional_name: &str, requirement_title: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your lead request was not accepted",
|
||||
format!(
|
||||
"Hello {},\n\nYour request for the requirement \"{}\" was not accepted this time. Your Tracecoins have been returned to your wallet.\n\nKeep exploring the marketplace for more opportunities!\n\n{}/dashboard/marketplace\n\nRegards,\nThe NXTGAUGE Team",
|
||||
professional_name,
|
||||
requirement_title,
|
||||
env::var("FRONTEND_URL").unwrap_or_else(|_| "http://localhost:3000".to_string())
|
||||
),
|
||||
).await
|
||||
}
|
||||
|
||||
// ── Tracecoins ────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_manual_credit_email(&self, to: &str, name: &str, amount: i32, reason: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Tracecoins credited to your account",
|
||||
format!(
|
||||
"Hello {},\n\n{} Tracecoins have been credited to your NXTGAUGE wallet.\n\nReason: {}\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, amount, reason
|
||||
),
|
||||
).await
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let wallet_url = format!("{}/dashboard/wallet", frontend_url);
|
||||
let amount_str = amount.to_string();
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("tracecoins_amount", &amount_str),
|
||||
("credited_at", &now),
|
||||
("reason", reason),
|
||||
("wallet_url", &wallet_url),
|
||||
]);
|
||||
let html = self.template_engine.render("manual-credit", vars)?;
|
||||
self.send_html(to, "Tracecoins Credited to Your Account", html).await
|
||||
}
|
||||
|
||||
// ── Expiry (Cron) ─────────────────────────────────────────────────────────
|
||||
// ── Support Tickets ─────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_lead_expired_email(&self, to: &str, name: &str, tracecoins_returned: i32) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your lead request has expired",
|
||||
format!(
|
||||
"Hello {},\n\nYour lead request has expired because it wasn't accepted within 24 hours.\n\nWe have refunded your {} reserved Tracecoins back to your wallet.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, tracecoins_returned
|
||||
),
|
||||
).await
|
||||
pub async fn send_support_ticket_created_email(&self, to: &str, name: &str, ticket_id: &str, subject: &str, category: &str, priority: &str, response_time: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let ticket_url = format!("{}/dashboard/support/{}", frontend_url, ticket_id);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("ticket_id", ticket_id),
|
||||
("subject", subject),
|
||||
("category", category),
|
||||
("priority", priority),
|
||||
("response_time", response_time),
|
||||
("ticket_url", &ticket_url),
|
||||
]);
|
||||
let html = self.template_engine.render("support-ticket-created", vars)?;
|
||||
self.send_html(to, "Support Ticket Received", html).await
|
||||
}
|
||||
|
||||
pub async fn send_requirement_expired_email(&self, to: &str, name: &str, title: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your requirement has expired",
|
||||
format!(
|
||||
"Hello {},\n\nYour requirement \"{}\" has expired and is no longer visible to professionals.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
name, title
|
||||
),
|
||||
).await
|
||||
pub async fn send_support_ticket_replied_email(&self, to: &str, name: &str, subject: &str, message: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let ticket_url = format!("{}/dashboard/support", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("subject", subject),
|
||||
("latest_message", message),
|
||||
("support_agent_name", "Nxtgauge Support Team"),
|
||||
("ticket_url", &ticket_url),
|
||||
]);
|
||||
let html = self.template_engine.render("support-ticket-replied", vars)?;
|
||||
self.send_html(to, "New Response on Your Ticket", html).await
|
||||
}
|
||||
|
||||
pub async fn send_job_expired_email(&self, to: &str, company_name: &str, title: &str) -> Result<()> {
|
||||
self.send(
|
||||
to,
|
||||
"Your job posting has expired",
|
||||
format!(
|
||||
"Hello {},\n\nYour job posting \"{}\" has expired and is no longer accepting applications.\n\nRegards,\nThe NXTGAUGE Team",
|
||||
company_name, title
|
||||
),
|
||||
).await
|
||||
pub async fn send_support_ticket_resolved_email(&self, to: &str, name: &str, subject: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("subject", subject),
|
||||
("resolved_at", &now),
|
||||
("support_agent_name", "Nxtgauge Support Team"),
|
||||
]);
|
||||
let html = self.template_engine.render("support-ticket-resolved", vars)?;
|
||||
self.send_html(to, "Ticket Resolved", html).await
|
||||
}
|
||||
|
||||
// ── Documents Requested ─────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_documents_requested_email(&self, to: &str, name: &str, role_name: &str, document_request: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let profile_url = format!("{}/dashboard/profile", frontend_url);
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("role_name", role_name),
|
||||
("document_request", document_request),
|
||||
("profile_url", &profile_url),
|
||||
]);
|
||||
let html = self.template_engine.render("documents-requested", vars)?;
|
||||
self.send_html(to, "Additional Documents Required", html).await
|
||||
}
|
||||
|
||||
// ── Application Status ──────────────────────────────────────────────────────
|
||||
|
||||
pub async fn send_application_status_email(&self, to: &str, name: &str, job_title: &str, status: &str) -> Result<()> {
|
||||
let frontend_url = env::var("FRONTEND_URL").unwrap_or_else(|_| "https://nxtgauge.com".to_string());
|
||||
let apps_url = format!("{}/dashboard/applications", frontend_url);
|
||||
|
||||
let (status_class, status_color, next_steps) = match status {
|
||||
"SHORTLISTED" => ("status-pending", "#d97706", "The employer has shortlisted you. They may contact you soon for an interview."),
|
||||
"INTERVIEW" => ("status-pending", "#d97706", "Congratulations! You've been selected for an interview. Check your email for scheduling details."),
|
||||
"OFFERED" => ("status-approved", "#059669", "Great news! You've received a job offer. Please review the offer details in your dashboard."),
|
||||
"HIRED" => ("status-approved", "#059669", "Congratulations on your new job! The employer will contact you with next steps."),
|
||||
"REJECTED" => ("status-rejected", "#dc2626", "Unfortunately, you were not selected for this position. Don't give up - apply to other jobs!"),
|
||||
_ => ("status-pending", "#6b7280", "Your application status has been updated."),
|
||||
};
|
||||
|
||||
let now = chrono::Local::now().format("%B %d, %Y").to_string();
|
||||
|
||||
let vars = HashMap::from([
|
||||
("first_name", name),
|
||||
("job_title", job_title),
|
||||
("company_name", "The employer"),
|
||||
("status", status),
|
||||
("status_class", status_class),
|
||||
("status_color", status_color),
|
||||
("next_steps", next_steps),
|
||||
("note", ""),
|
||||
("updated_at", &now),
|
||||
("applications_url", &apps_url),
|
||||
]);
|
||||
let html = self.template_engine.render("application-status", vars)?;
|
||||
self.send_html(to, &format!("Application Update: {}", job_title), html).await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
38
crates/email/templates/account-suspended.html
Normal file
38
crates/email/templates/account-suspended.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!-- Account Suspended -->
|
||||
<h1 class="email-title">Account Suspended</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-rejected">Suspended</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your Nxtgauge account has been suspended due to a violation of our terms of
|
||||
service.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fee2e2; border-left-color: #dc2626"
|
||||
>
|
||||
<p class="info-box-title" style="color: #991b1b">Reason:</p>
|
||||
<p style="margin: 0; color: #7f1d1d">{{suspension_reason}}</p>
|
||||
</div>
|
||||
|
||||
<p><strong>What this means:</strong></p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px">
|
||||
<li>You cannot log in to your account</li>
|
||||
<li>Your listings are no longer visible</li>
|
||||
<li>Pending transactions may be cancelled</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
If you believe this was done in error, please contact our support team
|
||||
immediately.
|
||||
</p>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="mailto:support@nxtgauge.com" class="cta-button">Contact Support</a>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
33
crates/email/templates/application-received.html
Normal file
33
crates/email/templates/application-received.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!-- Application Received -->
|
||||
<h1 class="email-title">📨 New Application Received</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>You have received a new application for your job posting:</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Candidate</span>
|
||||
<span class="detail-value">{{applicant_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Applied On</span>
|
||||
<span class="detail-value">{{applied_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{applications_url}}" class="cta-button">View Application</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">📋 Quick Stats</p>
|
||||
<p style="margin: 0">
|
||||
Total applications for this job: <strong>{{total_applications}}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
52
crates/email/templates/application-status.html
Normal file
52
crates/email/templates/application-status.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<!-- Application Status Updated -->
|
||||
<h1 class="email-title">Application Status Update</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge {{status_class}}">{{status}}</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>There has been an update on your application for the position:</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Company</span>
|
||||
<span class="detail-value">{{company_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: {{status_color}};"
|
||||
>{{status}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Updated On</span>
|
||||
<span class="detail-value">{{updated_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when="{{note}}">
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #f3f4f6; border-left-color: #6b7280"
|
||||
>
|
||||
<p class="info-box-title" style="color: #374151">📝 Note from Employer:</p>
|
||||
<p style="margin: 0; color: #1f2937">{{note}}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{applications_url}}" class="cta-button">View Application</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">💡 What's Next?</p>
|
||||
<p style="margin: 0">{{next_steps}}</p>
|
||||
</div>
|
||||
|
||||
<p>Best of luck with your application!</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
329
crates/email/templates/base.html
Normal file
329
crates/email/templates/base.html
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{subject}}</title>
|
||||
<style>
|
||||
/* Reset styles */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p,
|
||||
a,
|
||||
li,
|
||||
blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background-color: #f5f5f5;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
|
||||
Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.email-header {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.email-content {
|
||||
padding: 40px 30px;
|
||||
color: #374151;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.email-title {
|
||||
color: #111827;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.email-subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 18px;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
/* Status Badge */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-expired {
|
||||
background-color: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* CTA Button */
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Info Box */
|
||||
.info-box {
|
||||
background-color: #fff7ed;
|
||||
border-left: 4px solid #f97316;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.info-box-title {
|
||||
color: #c2410c;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
/* Detail Cards */
|
||||
.detail-card {
|
||||
background-color: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Amount Display */
|
||||
.amount-display {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
color: #9a3412;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #c2410c;
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.amount-currency {
|
||||
color: #ea580c;
|
||||
font-size: 24px;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.email-footer {
|
||||
background-color: #f9fafb;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.social-links a {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 50%;
|
||||
margin: 0 8px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-address {
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 600px) {
|
||||
.email-content {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.email-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
width="100%"
|
||||
>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0">
|
||||
<div class="email-wrapper">
|
||||
<!-- Header -->
|
||||
<div class="email-header">
|
||||
<a href="https://nxtgauge.com" class="logo"
|
||||
>NXT<span>GAUGE</span></a
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Content -->
|
||||
<div class="email-content">{{content}}</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="email-footer">
|
||||
<div class="footer-links">
|
||||
<a href="https://nxtgauge.com/help">Help Center</a>
|
||||
<a href="https://nxtgauge.com/contact">Contact Us</a>
|
||||
<a href="https://nxtgauge.com/privacy">Privacy Policy</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-text">
|
||||
<p>
|
||||
You're receiving this email because you have an account on
|
||||
Nxtgauge.
|
||||
</p>
|
||||
<p>
|
||||
© 2026 Nxtgauge Technologies Pvt. Ltd. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-address">
|
||||
<p>Nxtgauge Technologies Pvt. Ltd.</p>
|
||||
<p>Bangalore, Karnataka, India</p>
|
||||
<p>GSTIN: 27AABCU9603R1ZX</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
66
crates/email/templates/credit-usage.html
Normal file
66
crates/email/templates/credit-usage.html
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<!-- Credit Usage Confirmation -->
|
||||
<h1 class="email-title">Tracecoins Used</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
This is a confirmation that Tracecoins have been deducted from your wallet.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="amount-display"
|
||||
style="background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)"
|
||||
>
|
||||
<div class="amount-label" style="color: #991b1b">Tracecoins Deducted</div>
|
||||
<div class="amount-value" style="color: #dc2626">
|
||||
<span class="amount-currency">-</span>{{amount_deducted}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Transaction Type</span>
|
||||
<span class="detail-value">{{transaction_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Description</span>
|
||||
<span class="detail-value">{{description}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Date & Time</span>
|
||||
<span class="detail-value">{{transaction_date}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Reference ID</span>
|
||||
<span class="detail-value" style="font-family: monospace; font-size: 12px"
|
||||
>{{reference_id}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="detail-card"
|
||||
style="background-color: #f0fdf4; border-color: #86efac"
|
||||
>
|
||||
<div class="detail-row" style="border-color: #86efac">
|
||||
<span class="detail-label" style="color: #166534">Remaining Balance</span>
|
||||
<span class="detail-value" style="color: #15803d; font-size: 18px"
|
||||
>{{remaining_balance}} TC</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{wallet_url}}" class="cta-button">View Wallet</a>
|
||||
<a
|
||||
href="{{ledger_url}}"
|
||||
style="margin-left: 10px; background: #6b7280"
|
||||
class="cta-button"
|
||||
>View Ledger</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
Questions? Contact our support team for assistance.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
52
crates/email/templates/documents-requested.html
Normal file
52
crates/email/templates/documents-requested.html
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<!-- Additional Documents Requested -->
|
||||
<h1 class="email-title">Additional Documents Required</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Our review team needs additional documents to complete the verification of
|
||||
your {{role_name}} profile.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fef3c7; border-left-color: #d97706"
|
||||
>
|
||||
<p class="info-box-title" style="color: #92400e">📋 Documents Requested:</p>
|
||||
<p style="margin: 0; color: #78350f; white-space: pre-line">
|
||||
{{document_request}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Profile Type</span>
|
||||
<span class="detail-value">{{role_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #d97706">Documents Requested</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><strong>How to upload:</strong></p>
|
||||
<ol style="margin: 10px 0; padding-left: 20px">
|
||||
<li>Go to your profile page</li>
|
||||
<li>Click on the "Documents" tab</li>
|
||||
<li>Upload the requested documents</li>
|
||||
<li>Click "Resubmit for Verification"</li>
|
||||
</ol>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{profile_url}}" class="cta-button">Upload Documents</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ Response Time</p>
|
||||
<p style="margin: 0">
|
||||
Please upload the documents within <strong>7 days</strong> to avoid delays
|
||||
in your verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Need help? Contact our support team.</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
54
crates/email/templates/invoice-generated.html
Normal file
54
crates/email/templates/invoice-generated.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!-- Invoice Generated -->
|
||||
<h1 class="email-title">Invoice Generated</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>An invoice has been generated for your recent Tracecoin purchase.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Invoice Number</span>
|
||||
<span class="detail-value" style="font-family: monospace"
|
||||
>{{invoice_number}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Package</span>
|
||||
<span class="detail-value">{{package_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tracecoins</span>
|
||||
<span class="detail-value">{{tracecoins_amount}} TC</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Subtotal</span>
|
||||
<span class="detail-value">₹{{subtotal}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">GST (18%)</span>
|
||||
<span class="detail-value">₹{{gst_amount}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Total</span>
|
||||
<span class="detail-value" style="font-weight: bold; color: #111827"
|
||||
>₹{{total}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Issued On</span>
|
||||
<span class="detail-value">{{issued_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{invoice_url}}" class="cta-button">Download Invoice</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">📄 Tax Information</p>
|
||||
<p style="margin: 0">
|
||||
This is a GST-compliant invoice for your records. You can download it
|
||||
anytime from your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
40
crates/email/templates/job-approved.html
Normal file
40
crates/email/templates/job-approved.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!-- Job Approved -->
|
||||
<h1 class="email-title">🎉 Your Job is Now Live!</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-approved">✓ Live</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Great news! Your job posting has been approved and is now live on Nxtgauge.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Approved On</span>
|
||||
<span class="detail-value">{{approved_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Expires On</span>
|
||||
<span class="detail-value">{{expires_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{job_url}}" class="cta-button">View Job Posting</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">💡 Pro Tip</p>
|
||||
<p style="margin: 0">
|
||||
You'll receive email notifications when candidates apply. Make sure to
|
||||
review applications promptly!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
33
crates/email/templates/job-expired.html
Normal file
33
crates/email/templates/job-expired.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!-- Job Expired -->
|
||||
<h1 class="email-title">Job Posting Expired</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-expired">Expired</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your job posting has expired after 30 days and is no longer accepting
|
||||
applications.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #6b7280">Expired</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">💡 Want to Repost?</p>
|
||||
<p style="margin: 0">
|
||||
You can create a new job posting anytime. Reposting helps reach fresh
|
||||
candidates!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
34
crates/email/templates/job-pending.html
Normal file
34
crates/email/templates/job-pending.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<!-- Job Posted - Pending Approval -->
|
||||
<h1 class="email-title">Job Posted Successfully</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-pending">⏳ Pending Approval</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Your job posting has been submitted and is now under review by our team.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Posted On</span>
|
||||
<span class="detail-value">{{posted_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #d97706">Pending Review</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ What happens next?</p>
|
||||
<p style="margin: 0">
|
||||
Our team typically reviews job postings within 24-48 hours. You'll receive
|
||||
an email once your job is approved and live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
41
crates/email/templates/job-rejected.html
Normal file
41
crates/email/templates/job-rejected.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!-- Job Rejected -->
|
||||
<h1 class="email-title">Job Posting Needs Updates</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-rejected">Needs Changes</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>We reviewed your job posting and couldn't approve it at this time.</p>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fee2e2; border-left-color: #dc2626"
|
||||
>
|
||||
<p class="info-box-title" style="color: #991b1b">Reason for Rejection:</p>
|
||||
<p style="margin: 0; color: #7f1d1d">{{rejection_reason}}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Job Title</span>
|
||||
<span class="detail-value">{{job_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #dc2626">Rejected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><strong>What you can do:</strong></p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px">
|
||||
<li>Review the feedback above</li>
|
||||
<li>Update your job posting accordingly</li>
|
||||
<li>Resubmit for approval</li>
|
||||
</ul>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{jobs_url}}" class="cta-button">Edit Job Posting</a>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
44
crates/email/templates/lead-expired.html
Normal file
44
crates/email/templates/lead-expired.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!-- Lead Request Expired -->
|
||||
<h1 class="email-title">Lead Request Expired</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-expired">Expired</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your lead request has expired because the customer didn't respond within 24
|
||||
hours.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #6b7280">Expired</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tracecoins Returned</span>
|
||||
<span class="detail-value" style="color: #059669"
|
||||
>{{tracecoins_returned}} TC</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #d1fae5; border-left-color: #059669"
|
||||
>
|
||||
<p class="info-box-title" style="color: #065f46">💰 Good News!</p>
|
||||
<p style="margin: 0; color: #064e3b">
|
||||
Your <strong>{{tracecoins_returned}} Tracecoins</strong> have been returned
|
||||
to your wallet. You can use them for other lead requests.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{marketplace_url}}" class="cta-button">Browse Marketplace</a>
|
||||
</div>
|
||||
|
||||
<p>Don't worry! There are many more opportunities waiting for you.</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
50
crates/email/templates/lead-request-accepted.html
Normal file
50
crates/email/templates/lead-request-accepted.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!-- Lead Request Accepted -->
|
||||
<h1 class="email-title">🎉 Lead Request Accepted!</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-approved">✓ Accepted</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Great news! The customer has accepted your lead request. You can now view
|
||||
their contact details.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Customer</span>
|
||||
<span class="detail-value">{{customer_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tracecoins Deducted</span>
|
||||
<span class="detail-value" style="color: #dc2626"
|
||||
>{{tracecoins_deducted}} TC</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Accepted On</span>
|
||||
<span class="detail-value">{{accepted_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{lead_url}}" class="cta-button">View Contact Details</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #d1fae5; border-left-color: #059669"
|
||||
>
|
||||
<p class="info-box-title" style="color: #065f46">💡 Next Steps</p>
|
||||
<p style="margin: 0; color: #064e3b">
|
||||
Contact the customer promptly to discuss the project details. Professional
|
||||
communication is key!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
48
crates/email/templates/lead-request-rejected.html
Normal file
48
crates/email/templates/lead-request-rejected.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!-- Lead Request Rejected -->
|
||||
<h1 class="email-title">Lead Request Update</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-rejected">Not Accepted</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
The customer has decided not to proceed with your request for this
|
||||
requirement.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Customer</span>
|
||||
<span class="detail-value">{{customer_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tracecoins Returned</span>
|
||||
<span class="detail-value" style="color: #059669"
|
||||
>{{tracecoins_returned}} TC</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #d1fae5; border-left-color: #059669"
|
||||
>
|
||||
<p class="info-box-title" style="color: #065f46">💰 Tracecoins Refunded</p>
|
||||
<p style="margin: 0; color: #064e3b">
|
||||
Your <strong>{{tracecoins_returned}} Tracecoins</strong> have been returned
|
||||
to your wallet and are available for other opportunities.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{marketplace_url}}" class="cta-button">Browse More Leads</a>
|
||||
</div>
|
||||
|
||||
<p>Don't be discouraged! There are many other opportunities waiting for you.</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
42
crates/email/templates/lead-request-sent.html
Normal file
42
crates/email/templates/lead-request-sent.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!-- Lead Request Sent -->
|
||||
<h1 class="email-title">Lead Request Sent</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Your lead request has been sent successfully. Here's what happens next:</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Location</span>
|
||||
<span class="detail-value">{{location}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Reserved</span>
|
||||
<span class="detail-value" style="color: #ea580c"
|
||||
>{{tracecoins_reserved}} TC</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Expires In</span>
|
||||
<span class="detail-value">24 hours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">💰 Tracecoin Policy</p>
|
||||
<p style="margin: 0">
|
||||
<strong>{{tracecoins_reserved}} Tracecoins</strong> have been reserved from
|
||||
your wallet. They will be:
|
||||
</p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px">
|
||||
<li><strong>Deducted</strong> if the customer accepts your request</li>
|
||||
<li><strong>Returned</strong> if rejected or expired</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>You'll receive an email when the customer responds.</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
46
crates/email/templates/low-credit-balance.html
Normal file
46
crates/email/templates/low-credit-balance.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<!-- Low Credit Balance Warning -->
|
||||
<h1 class="email-title">⚠️ Low Tracecoin Balance</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your Tracecoin balance is running low. Don't miss out on lead opportunities!
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="amount-display"
|
||||
style="background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)"
|
||||
>
|
||||
<div class="amount-label" style="color: #991b1b">Current Balance</div>
|
||||
<div class="amount-value" style="color: #dc2626">
|
||||
<span class="amount-currency"></span>{{current_balance}}
|
||||
</div>
|
||||
<div style="color: #7f1d1d; font-size: 14px; margin-top: 8px">
|
||||
Tracecoins remaining
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fef3c7; border-left-color: #d97706"
|
||||
>
|
||||
<p class="info-box-title" style="color: #92400e">💡 What you can do:</p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px; color: #78350f">
|
||||
<li>Most leads cost <strong>25 Tracecoins</strong> per request</li>
|
||||
<li>Purchase more Tracecoins to continue accessing leads</li>
|
||||
<li>New requirements are posted daily</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{buy_url}}" class="cta-button">Buy Tracecoins Now</a>
|
||||
</div>
|
||||
|
||||
<p
|
||||
style="margin-top: 20px; font-size: 14px; color: #6b7280; text-align: center"
|
||||
>
|
||||
<strong>Popular Package:</strong> {{popular_package}} -
|
||||
{{popular_package_price}}<br />
|
||||
Good for ~{{leads_count}} lead requests
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
49
crates/email/templates/manual-credit.html
Normal file
49
crates/email/templates/manual-credit.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!-- Manual Tracecoin Credit -->
|
||||
<h1 class="email-title">Tracecoins Credited to Your Account</h1>
|
||||
|
||||
<div class="amount-display">
|
||||
<div class="amount-label">Tracecoins Added</div>
|
||||
<div class="amount-value">
|
||||
<span class="amount-currency">+</span>{{tracecoins_amount}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Tracecoins have been manually credited to your wallet by our admin team.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Amount Credited</span>
|
||||
<span class="detail-value" style="color: #059669; font-weight: bold"
|
||||
>{{tracecoins_amount}} TC</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Date</span>
|
||||
<span class="detail-value">{{credited_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Reason</span>
|
||||
<span class="detail-value">{{reason}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #dbeafe; border-left-color: #2563eb"
|
||||
>
|
||||
<p class="info-box-title" style="color: #1e40af">
|
||||
ℹ️ Why did I receive this?
|
||||
</p>
|
||||
<p style="margin: 0; color: #1e3a8a">{{reason}}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{wallet_url}}" class="cta-button">View Wallet</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
If you have any questions about this credit, please contact our support team.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
64
crates/email/templates/new-device-login.html
Normal file
64
crates/email/templates/new-device-login.html
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<!-- Login from New Device -->
|
||||
<h1 class="email-title">🔐 New Login Detected</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #fef3c7;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 80px;
|
||||
font-size: 40px;
|
||||
"
|
||||
>
|
||||
⚠️
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
We detected a login to your Nxtgauge account from a new device or location.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Device</span>
|
||||
<span class="detail-value">{{device}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Location</span>
|
||||
<span class="detail-value">{{location}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">IP Address</span>
|
||||
<span class="detail-value">{{ip_address}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Time</span>
|
||||
<span class="detail-value">{{login_time}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fee2e2; border-left-color: #dc2626"
|
||||
>
|
||||
<p class="info-box-title" style="color: #991b1b">🚨 Wasn't you?</p>
|
||||
<p style="margin: 0; color: #7f1d1d">
|
||||
If you didn't log in, please
|
||||
<strong>secure your account immediately</strong> by changing your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{security_url}}" class="cta-button">Secure My Account</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
If this was you, you can safely ignore this email.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
38
crates/email/templates/new-lead-request.html
Normal file
38
crates/email/templates/new-lead-request.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!-- New Lead Request Received (Customer) -->
|
||||
<h1 class="email-title">📨 New Lead Request</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>You have received a new lead request for your requirement:</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Your Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Professional</span>
|
||||
<span class="detail-value">{{professional_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Profession</span>
|
||||
<span class="detail-value">{{profession_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Received On</span>
|
||||
<span class="detail-value">{{requested_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{requirement_url}}" class="cta-button">Review Request</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ Response Needed</p>
|
||||
<p style="margin: 0">
|
||||
This request will expire in <strong>24 hours</strong>. Please respond
|
||||
promptly to connect with the professional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
56
crates/email/templates/new-matched-lead.html
Normal file
56
crates/email/templates/new-matched-lead.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<!-- New Matched Lead Available -->
|
||||
<h1 class="email-title">🔔 New Lead Available!</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>A new requirement matching your expertise has been posted on Nxtgauge.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Service Type</span>
|
||||
<span class="detail-value">{{profession_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Location</span>
|
||||
<span class="detail-value">{{location}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Budget</span>
|
||||
<span class="detail-value">{{budget}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Posted</span>
|
||||
<span class="detail-value">{{posted_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fff7ed; border-left-color: #f97316"
|
||||
>
|
||||
<p class="info-box-title" style="color: #c2410c">⚡ Act Fast!</p>
|
||||
<p style="margin: 0; color: #7c2d12">
|
||||
This requirement will receive up to <strong>20 requests</strong> from
|
||||
professionals. Send your request early to increase your chances!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="amount-display" style="padding: 20px">
|
||||
<div class="amount-label">Tracecoins Required</div>
|
||||
<div class="amount-value" style="font-size: 36px">
|
||||
{{tracecoins_required}} TC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{requirement_url}}" class="cta-button">View & Request Lead</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
You have <strong>{{current_balance}} Tracecoins</strong> in your wallet.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
48
crates/email/templates/onboarding-submitted.html
Normal file
48
crates/email/templates/onboarding-submitted.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!-- Onboarding Submitted -->
|
||||
<h1 class="email-title">Profile Submitted for Review</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-pending">⏳ Under Review</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your {{role_name}} profile has been submitted for verification. Our team will
|
||||
review it shortly.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Profile Type</span>
|
||||
<span class="detail-value">{{role_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Submitted On</span>
|
||||
<span class="detail-value">{{submitted_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #d97706">Pending Review</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ What's Next?</p>
|
||||
<p style="margin: 0">
|
||||
Our team typically reviews profiles within <strong>24-48 hours</strong>.
|
||||
You'll receive an email once your profile is approved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #dbeafe; border-left-color: #2563eb"
|
||||
>
|
||||
<p class="info-box-title" style="color: #1e40af">💡 While You Wait</p>
|
||||
<p style="margin: 0; color: #1e3a8a">
|
||||
You can explore the platform and familiarize yourself with the dashboard.
|
||||
Full features will be unlocked once verified!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
54
crates/email/templates/password-changed.html
Normal file
54
crates/email/templates/password-changed.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!-- Password Changed -->
|
||||
<h1 class="email-title">Password Changed Successfully</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: #d1fae5;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 64px;
|
||||
font-size: 32px;
|
||||
"
|
||||
>
|
||||
🔒
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Your Nxtgauge account password has been successfully changed.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Account</span>
|
||||
<span class="detail-value">{{email}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Changed On</span>
|
||||
<span class="detail-value">{{changed_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #059669">✓ Successful</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fee2e2; border-left-color: #dc2626"
|
||||
>
|
||||
<p class="info-box-title" style="color: #991b1b">⚠️ Didn't do this?</p>
|
||||
<p style="margin: 0; color: #7f1d1d">
|
||||
If you didn't change your password, please
|
||||
<strong>contact support immediately</strong> to secure your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{security_url}}" class="cta-button">Account Security</a>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
37
crates/email/templates/password-reset.html
Normal file
37
crates/email/templates/password-reset.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!-- Password Reset -->
|
||||
<h1 class="email-title">Reset Your Password</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
We received a request to reset your password. Click the button below to set a
|
||||
new password:
|
||||
</p>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{reset_url}}" class="cta-button">Reset Password</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">🔒 Security Notice</p>
|
||||
<p style="margin: 0">
|
||||
This link will expire in <strong>1 hour</strong>. If you didn't request
|
||||
this, please ignore this email and your password will remain unchanged.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px">
|
||||
If the button doesn't work, copy and paste this link into your browser:
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
background: #f3f4f6;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
"
|
||||
>
|
||||
{{reset_url}}
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
54
crates/email/templates/payment-success.html
Normal file
54
crates/email/templates/payment-success.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!-- Payment Successful -->
|
||||
<h1 class="email-title">Payment Successful! 🎉</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your payment has been processed successfully. Tracecoins have been added to
|
||||
your wallet.
|
||||
</p>
|
||||
|
||||
<div class="amount-display">
|
||||
<div class="amount-label">Tracecoins Credited</div>
|
||||
<div class="amount-value">
|
||||
<span class="amount-currency">+</span>{{tracecoins_amount}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Package</span>
|
||||
<span class="detail-value">{{package_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Amount Paid</span>
|
||||
<span class="detail-value">₹{{amount_paid}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Transaction ID</span>
|
||||
<span class="detail-value" style="font-family: monospace; font-size: 12px"
|
||||
>{{transaction_id}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Date</span>
|
||||
<span class="detail-value">{{payment_date}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{invoice_url}}" class="cta-button">View Invoice</a>
|
||||
<a href="{{wallet_url}}" style="margin-left: 10px" class="cta-button"
|
||||
>Go to Wallet</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">📄 Tax Invoice</p>
|
||||
<p style="margin: 0">
|
||||
A GST invoice has been generated for this transaction. You can download it
|
||||
from your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Thank you for your purchase!</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
70
crates/email/templates/policy-update.html
Normal file
70
crates/email/templates/policy-update.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!-- Policy Update -->
|
||||
<h1 class="email-title">Important Policy Update</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #dbeafe;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 80px;
|
||||
font-size: 40px;
|
||||
"
|
||||
>
|
||||
📋
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
We've updated our <strong>{{policy_type}}</strong>. Please review the changes
|
||||
below.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #eff6ff; border-left-color: #3b82f6"
|
||||
>
|
||||
<p class="info-box-title" style="color: #1e40af">📝 Summary of Changes:</p>
|
||||
<div style="color: #1e3a8a; margin: 0">{{changes_summary}}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Policy</span>
|
||||
<span class="detail-value">{{policy_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Effective Date</span>
|
||||
<span class="detail-value">{{effective_date}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Last Updated</span>
|
||||
<span class="detail-value">{{updated_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fef3c7; border-left-color: #d97706"
|
||||
>
|
||||
<p class="info-box-title" style="color: #92400e">⚠️ Action Required</p>
|
||||
<p style="margin: 0; color: #78350f">
|
||||
By continuing to use Nxtgauge after <strong>{{effective_date}}</strong>, you
|
||||
agree to the updated {{policy_type}}. If you do not agree, please
|
||||
discontinue using our services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{policy_url}}" class="cta-button">Read Full {{policy_type}}</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
Questions about these changes? Contact our support team.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
36
crates/email/templates/profile-rejected.html
Normal file
36
crates/email/templates/profile-rejected.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!-- Profile Rejected -->
|
||||
<h1 class="email-title">Profile Verification Update</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-rejected">✗ Rejected</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
We reviewed your {{role_name}} profile submission. Unfortunately, we couldn't
|
||||
approve it at this time.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #fee2e2; border-left-color: #dc2626"
|
||||
>
|
||||
<p class="info-box-title" style="color: #991b1b">Reason for Rejection:</p>
|
||||
<p style="margin: 0; color: #7f1d1d">{{rejection_reason}}</p>
|
||||
</div>
|
||||
|
||||
<p><strong>What you can do:</strong></p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px">
|
||||
<li>Review the feedback above</li>
|
||||
<li>Update your profile with the required changes</li>
|
||||
<li>Resubmit for verification</li>
|
||||
</ul>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{profile_url}}" class="cta-button">Update Profile</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px">
|
||||
Need help? Contact our support team for assistance.
|
||||
</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
34
crates/email/templates/profile-verified.html
Normal file
34
crates/email/templates/profile-verified.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<!-- Profile Verified -->
|
||||
<h1 class="email-title">🎉 Your Profile is Verified!</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-approved">✓ Approved</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Great news! Your {{role_name}} profile has been verified by our team. You now
|
||||
have full access to all Nxtgauge features.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Verification Date</span>
|
||||
<span class="detail-value">{{verified_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Account Type</span>
|
||||
<span class="detail-value">{{role_name}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #059669">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{dashboard_url}}" class="cta-button">Go to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px">Welcome to the Nxtgauge community!</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
45
crates/email/templates/requirement-approved.html
Normal file
45
crates/email/templates/requirement-approved.html
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<!-- Requirement Approved -->
|
||||
<h1 class="email-title">🎉 Your Requirement is Now Live!</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-approved">✓ Live</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Great news! Your service requirement has been approved and is now live on
|
||||
Nxtgauge.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Service Type</span>
|
||||
<span class="detail-value">{{profession_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Approved On</span>
|
||||
<span class="detail-value">{{approved_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Expires On</span>
|
||||
<span class="detail-value">{{expires_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{requirement_url}}" class="cta-button">View Requirement</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">💡 What happens now?</p>
|
||||
<p style="margin: 0">
|
||||
Professionals can now see your requirement and send you requests. You'll
|
||||
receive an email each time someone is interested!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
33
crates/email/templates/requirement-expired.html
Normal file
33
crates/email/templates/requirement-expired.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!-- Requirement Expired -->
|
||||
<h1 class="email-title">Requirement Expired</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-expired">Expired</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your requirement has expired after 7 days and is no longer visible to
|
||||
professionals.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #6b7280">Expired</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">📝 Create a New Requirement</p>
|
||||
<p style="margin: 0">
|
||||
You can create a new requirement anytime. Make sure to provide clear details
|
||||
to attract the right professionals!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
41
crates/email/templates/requirement-pending.html
Normal file
41
crates/email/templates/requirement-pending.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!-- Requirement Posted - Pending Approval -->
|
||||
<h1 class="email-title">Requirement Submitted Successfully</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<span class="status-badge status-pending">⏳ Pending Approval</span>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Your service requirement has been submitted and is now under review by our
|
||||
team.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requirement</span>
|
||||
<span class="detail-value">{{requirement_title}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Service Type</span>
|
||||
<span class="detail-value">{{profession_type}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Posted On</span>
|
||||
<span class="detail-value">{{posted_at}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #d97706">Pending Review</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ What happens next?</p>
|
||||
<p style="margin: 0">
|
||||
Our team typically reviews requirements within <strong>24 hours</strong>.
|
||||
Once approved, professionals can send you requests.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
50
crates/email/templates/support-ticket-created.html
Normal file
50
crates/email/templates/support-ticket-created.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!-- Support Ticket Created -->
|
||||
<h1 class="email-title">Support Ticket Received</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
We've received your support request and our team will get back to you shortly.
|
||||
</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Ticket ID</span>
|
||||
<span class="detail-value" style="font-family: monospace"
|
||||
>#{{ticket_id}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Subject</span>
|
||||
<span class="detail-value">{{subject}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Category</span>
|
||||
<span class="detail-value">{{category}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Priority</span>
|
||||
<span class="detail-value">{{priority}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Created</span>
|
||||
<span class="detail-value">{{created_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏱️ Response Time</p>
|
||||
<p style="margin: 0">
|
||||
We typically respond to {{priority}} priority tickets within
|
||||
<strong>{{response_time}}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{ticket_url}}" class="cta-button">View Ticket</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
You can reply to this ticket at any time by visiting your dashboard.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Support Team</strong></p>
|
||||
50
crates/email/templates/support-ticket-replied.html
Normal file
50
crates/email/templates/support-ticket-replied.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!-- Support Ticket Replied -->
|
||||
<h1 class="email-title">New Response on Your Ticket</h1>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Our support team has responded to your ticket.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Ticket ID</span>
|
||||
<span class="detail-value" style="font-family: monospace"
|
||||
>#{{ticket_id}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Subject</span>
|
||||
<span class="detail-value">{{subject}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #d97706"
|
||||
>Awaiting Your Response</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #eff6ff; border-left-color: #3b82f6"
|
||||
>
|
||||
<p class="info-box-title" style="color: #1e40af">💬 Latest Message:</p>
|
||||
<p style="margin: 0; color: #1e3a8a; font-style: italic">
|
||||
"{{latest_message}}"
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; color: #3b82f6; font-size: 12px">
|
||||
— {{support_agent_name}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{ticket_url}}" class="cta-button">Reply to Ticket</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
Please reply if you need further assistance or if the issue is resolved.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best regards,<br /><strong>{{support_agent_name}}</strong><br />Nxtgauge
|
||||
Support Team
|
||||
</p>
|
||||
72
crates/email/templates/support-ticket-resolved.html
Normal file
72
crates/email/templates/support-ticket-resolved.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<!-- Support Ticket Resolved -->
|
||||
<h1 class="email-title">✅ Ticket Resolved</h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #d1fae5;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 80px;
|
||||
font-size: 40px;
|
||||
"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>Great news! Your support ticket has been resolved.</p>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Ticket ID</span>
|
||||
<span class="detail-value" style="font-family: monospace"
|
||||
>#{{ticket_id}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Subject</span>
|
||||
<span class="detail-value">{{subject}}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" style="color: #059669">✓ Resolved</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Resolved On</span>
|
||||
<span class="detail-value">{{resolved_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="info-box"
|
||||
style="background-color: #d1fae5; border-left-color: #059669"
|
||||
>
|
||||
<p class="info-box-title" style="color: #065f46">💡 How did we do?</p>
|
||||
<p style="margin: 0; color: #064e3b">
|
||||
Your feedback helps us improve. If you have a moment, please let us know
|
||||
about your support experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{feedback_url}}" style="margin-right: 10px" class="cta-button"
|
||||
>Rate Experience</a
|
||||
>
|
||||
<a href="{{help_center_url}}" style="background: #6b7280" class="cta-button"
|
||||
>Help Center</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #6b7280">
|
||||
If you're still experiencing issues, you can reopen this ticket within 7 days.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Best regards,<br /><strong>{{support_agent_name}}</strong><br />Nxtgauge
|
||||
Support Team
|
||||
</p>
|
||||
36
crates/email/templates/verify-email.html
Normal file
36
crates/email/templates/verify-email.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!-- Email Verification OTP -->
|
||||
<h1 class="email-title">Verify Your Email Address</h1>
|
||||
<p>Hi {{first_name}},</p>
|
||||
<p>
|
||||
Please use the following One-Time Password (OTP) to verify your email address:
|
||||
</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0">
|
||||
<div
|
||||
style="
|
||||
display: inline-block;
|
||||
background: #f3f4f6;
|
||||
padding: 20px 40px;
|
||||
border-radius: 8px;
|
||||
letter-spacing: 8px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
font-family: monospace;
|
||||
"
|
||||
>
|
||||
{{otp_code}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">⏰ This code expires in 10 minutes</p>
|
||||
<p style="margin: 0">
|
||||
For security reasons, this OTP will expire after 10 minutes. If you didn't
|
||||
request this, please ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px">
|
||||
Best regards,<br /><strong>The Nxtgauge Team</strong>
|
||||
</p>
|
||||
24
crates/email/templates/welcome.html
Normal file
24
crates/email/templates/welcome.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Welcome / Registration Confirmation -->
|
||||
<h1 class="email-title">Welcome to Nxtgauge, {{first_name}}! 🎉</h1>
|
||||
<p>Thank you for joining Nxtgauge. We're excited to have you on board!</p>
|
||||
|
||||
<div class="info-box">
|
||||
<p class="info-box-title">What's Next?</p>
|
||||
<p>
|
||||
Complete your profile and submit it for verification to unlock all features:
|
||||
</p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px">
|
||||
<li>Fill in your profile details</li>
|
||||
<li>Upload required documents</li>
|
||||
<li>Submit for verification</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{dashboard_url}}" class="cta-button">Complete Your Profile</a>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px">
|
||||
If you have any questions, our support team is here to help.
|
||||
</p>
|
||||
<p>Best regards,<br /><strong>The Nxtgauge Team</strong></p>
|
||||
|
|
@ -34,6 +34,7 @@ services:
|
|||
# ── Gateway ───────────────────────────────────────────────────────────────
|
||||
|
||||
gateway:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-gateway:high-performance-latest
|
||||
ports:
|
||||
- "9100:9100"
|
||||
|
|
@ -43,8 +44,8 @@ services:
|
|||
REDIS_URL: redis://redis:6379
|
||||
JWT_SECRET: local_dev_jwt_secret
|
||||
RUST_LOG: info
|
||||
FRONTEND_URL: http://localhost:3000
|
||||
ADMIN_URL: http://localhost:4000
|
||||
FRONTEND_URL: http://localhost:9201
|
||||
ADMIN_URL: http://localhost:9202
|
||||
USERS_SERVICE_URL: http://users:9101
|
||||
COMPANIES_SERVICE_URL: http://companies:9102
|
||||
JOB_SEEKERS_SERVICE_URL: http://job-seekers:9104
|
||||
|
|
@ -102,6 +103,7 @@ services:
|
|||
# ── Core Services ─────────────────────────────────────────────────────────
|
||||
|
||||
users:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-users:high-performance-latest
|
||||
environment:
|
||||
PORT: "9101"
|
||||
|
|
@ -115,6 +117,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
companies:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-companies:high-performance-latest
|
||||
environment:
|
||||
PORT: "9102"
|
||||
|
|
@ -128,6 +131,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
job-seekers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-job-seekers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9104"
|
||||
|
|
@ -141,6 +145,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
customers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-customers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9105"
|
||||
|
|
@ -154,6 +159,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
employees:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-employees:high-performance-latest
|
||||
environment:
|
||||
PORT: "9106"
|
||||
|
|
@ -169,6 +175,7 @@ services:
|
|||
# ── 9 Profession Services ─────────────────────────────────────────────────
|
||||
|
||||
photographers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-photographers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9107"
|
||||
|
|
@ -182,6 +189,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
tutors:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-tutors:high-performance-latest
|
||||
environment:
|
||||
PORT: "9108"
|
||||
|
|
@ -195,6 +203,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
makeup-artists:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-makeup-artists:high-performance-latest
|
||||
environment:
|
||||
PORT: "9109"
|
||||
|
|
@ -208,6 +217,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
developers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-developers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9110"
|
||||
|
|
@ -221,6 +231,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
video-editors:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-video-editors:high-performance-latest
|
||||
environment:
|
||||
PORT: "9111"
|
||||
|
|
@ -234,6 +245,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
graphic-designers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-graphic-designers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9112"
|
||||
|
|
@ -247,6 +259,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
social-media-managers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-social-media-managers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9113"
|
||||
|
|
@ -260,6 +273,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
fitness-trainers:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-fitness-trainers:high-performance-latest
|
||||
environment:
|
||||
PORT: "9114"
|
||||
|
|
@ -273,6 +287,7 @@ services:
|
|||
condition: service_healthy
|
||||
|
||||
catering-services:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-catering-services:high-performance-latest
|
||||
environment:
|
||||
PORT: "9115"
|
||||
|
|
@ -288,6 +303,7 @@ services:
|
|||
# ── Payments ──────────────────────────────────────────────────────────────
|
||||
|
||||
payments:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-payments:high-performance-latest
|
||||
environment:
|
||||
PORT: "9116"
|
||||
|
|
@ -303,6 +319,7 @@ services:
|
|||
# ── UGC ───────────────────────────────────────────────────────────────────
|
||||
|
||||
ugc-content-creators:
|
||||
platform: linux/amd64
|
||||
image: ghcr.io/traceworks2023/nxtgauge-rust-ugc-content-creators:high-performance-latest
|
||||
environment:
|
||||
PORT: "9117"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export let options = {
|
|||
},
|
||||
};
|
||||
|
||||
const BASE_URL = 'http://localhost:8000';
|
||||
const BASE_URL = 'http://localhost:9100';
|
||||
|
||||
export default function () {
|
||||
// Health check
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export let options = {
|
|||
maxRedirects: 5,
|
||||
};
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8000';
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:9100';
|
||||
const ADMIN_TOKEN = __ENV.ADMIN_TOKEN || ''; // optional
|
||||
|
||||
// Custom metrics
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue