From 2c0c979c91fb083ed67bf48755edb9e321448831 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 6 Apr 2026 19:08:00 +0200 Subject: [PATCH] feat: add admin APIs for jobs, applications, and all 9 professions - Companies service: add GET /api/admin/jobs and GET /api/admin/applications - Gateway: route /api/admin/applications to companies; add routing for all 9 profession admin endpoints - For each profession service (photographers, makeup_artists, tutors, developers, video_editors, graphic_designers, social_media_managers, fitness_trainers, catering_services): - Create admin.rs with list and detail endpoints that join with users - Update main.rs to mount admin router under /api/admin/ - Admin endpoints enable cross-platform visibility of all professionals by internal staff --- apps/catering_services/src/admin.rs | 79 ++++++++++++++++++++++++ apps/catering_services/src/main.rs | 2 + apps/developers/src/admin.rs | 79 ++++++++++++++++++++++++ apps/developers/src/main.rs | 2 + apps/fitness_trainers/src/admin.rs | 79 ++++++++++++++++++++++++ apps/fitness_trainers/src/main.rs | 2 + apps/gateway/src/main.rs | 19 +++--- apps/graphic_designers/src/admin.rs | 79 ++++++++++++++++++++++++ apps/graphic_designers/src/main.rs | 2 + apps/makeup_artists/src/main.rs | 2 + apps/photographers/src/admin.rs | 81 +++++++++++++++++++++++++ apps/photographers/src/main.rs | 2 + apps/social_media_managers/src/admin.rs | 79 ++++++++++++++++++++++++ apps/social_media_managers/src/main.rs | 2 + apps/tutors/src/admin.rs | 81 +++++++++++++++++++++++++ apps/tutors/src/main.rs | 2 + apps/video_editors/src/admin.rs | 79 ++++++++++++++++++++++++ apps/video_editors/src/main.rs | 2 + 18 files changed, 664 insertions(+), 9 deletions(-) create mode 100644 apps/catering_services/src/admin.rs create mode 100644 apps/developers/src/admin.rs create mode 100644 apps/fitness_trainers/src/admin.rs create mode 100644 apps/graphic_designers/src/admin.rs create mode 100644 apps/photographers/src/admin.rs create mode 100644 apps/social_media_managers/src/admin.rs create mode 100644 apps/tutors/src/admin.rs create mode 100644 apps/video_editors/src/admin.rs diff --git a/apps/catering_services/src/admin.rs b/apps/catering_services/src/admin.rs new file mode 100644 index 0000000..989972a --- /dev/null +++ b/apps/catering_services/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminCateringServiceList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminCateringServiceDetail = AdminCateringServiceList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_catering_services)) + .route("/{id}", get(get_catering_service)) +} + +async fn list_catering_services( + State(state): State, +) -> Result { + let services = sqlx::query_as!( + AdminCateringServiceList, + r#" + SELECT + c.id, c.user_id, c.bio, c.experience_years, c.custom_data, + c.created_at, c.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM catering_service_profiles c + JOIN users u ON c.user_id = u.id + ORDER BY c.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(services)) +} + +async fn get_catering_service( + State(state): State, + Path(id): Path, +) -> Result { + let service = sqlx::query_as!( + AdminCateringServiceDetail, + r#" + SELECT + c.id, c.user_id, c.bio, c.experience_years, c.custom_data, + c.created_at, c.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM catering_service_profiles c + JOIN users u ON c.user_id = u.id + WHERE c.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match service { + Some(c) => Ok(Json(c)), + None => Err((StatusCode::NOT_FOUND, "Catering service not found".to_string())), + } +} diff --git a/apps/catering_services/src/main.rs b/apps/catering_services/src/main.rs index bfca3f5..a0ce3b9 100644 --- a/apps/catering_services/src/main.rs +++ b/apps/catering_services/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/catering-services", handlers::router()) + .nest("/api/admin/catering-services", admin::router()) .route("/health", get(|| async { "Catering Services OK" })) .with_state(state); diff --git a/apps/developers/src/admin.rs b/apps/developers/src/admin.rs new file mode 100644 index 0000000..0b1032c --- /dev/null +++ b/apps/developers/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminDeveloperList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminDeveloperDetail = AdminDeveloperList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_developers)) + .route("/{id}", get(get_developer)) +} + +async fn list_developers( + State(state): State, +) -> Result { + let developers = sqlx::query_as!( + AdminDeveloperList, + r#" + SELECT + d.id, d.user_id, d.bio, d.experience_years, d.custom_data, + d.created_at, d.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM developer_profiles d + JOIN users u ON d.user_id = u.id + ORDER BY d.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(developers)) +} + +async fn get_developer( + State(state): State, + Path(id): Path, +) -> Result { + let developer = sqlx::query_as!( + AdminDeveloperDetail, + r#" + SELECT + d.id, d.user_id, d.bio, d.experience_years, d.custom_data, + d.created_at, d.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM developer_profiles d + JOIN users u ON d.user_id = u.id + WHERE d.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match developer { + Some(d) => Ok(Json(d)), + None => Err((StatusCode::NOT_FOUND, "Developer not found".to_string())), + } +} diff --git a/apps/developers/src/main.rs b/apps/developers/src/main.rs index 71f98e9..5d46e98 100644 --- a/apps/developers/src/main.rs +++ b/apps/developers/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/developers", handlers::router()) + .nest("/api/admin/developers", admin::router()) .route("/health", get(|| async { "Developers OK" })) .with_state(state); diff --git a/apps/fitness_trainers/src/admin.rs b/apps/fitness_trainers/src/admin.rs new file mode 100644 index 0000000..e0fbf0b --- /dev/null +++ b/apps/fitness_trainers/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminFitnessTrainerList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminFitnessTrainerDetail = AdminFitnessTrainerList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_fitness_trainers)) + .route("/{id}", get(get_fitness_trainer)) +} + +async fn list_fitness_trainers( + State(state): State, +) -> Result { + let trainers = sqlx::query_as!( + AdminFitnessTrainerList, + r#" + SELECT + f.id, f.user_id, f.bio, f.experience_years, f.custom_data, + f.created_at, f.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM fitness_trainer_profiles f + JOIN users u ON f.user_id = u.id + ORDER BY f.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(trainers)) +} + +async fn get_fitness_trainer( + State(state): State, + Path(id): Path, +) -> Result { + let trainer = sqlx::query_as!( + AdminFitnessTrainerDetail, + r#" + SELECT + f.id, f.user_id, f.bio, f.experience_years, f.custom_data, + f.created_at, f.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM fitness_trainer_profiles f + JOIN users u ON f.user_id = u.id + WHERE f.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match trainer { + Some(f) => Ok(Json(f)), + None => Err((StatusCode::NOT_FOUND, "Fitness trainer not found".to_string())), + } +} diff --git a/apps/fitness_trainers/src/main.rs b/apps/fitness_trainers/src/main.rs index 63ca2c9..7e97266 100644 --- a/apps/fitness_trainers/src/main.rs +++ b/apps/fitness_trainers/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/fitness-trainers", handlers::router()) + .nest("/api/admin/fitness-trainers", admin::router()) .route("/health", get(|| async { "Fitness Trainers OK" })) .with_state(state); diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index 15f6924..994db92 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -116,6 +116,7 @@ impl Services { || path.starts_with("/api/pricing") || path.starts_with("/api/admin/companies") || path.starts_with("/api/admin/jobs") + || path.starts_with("/api/admin/applications") { Some(self.companies_url.clone()) } @@ -131,31 +132,31 @@ impl Services { Some(self.customers_url.clone()) } // ── 9 Separate Profession Services ──────────────────────────────── - else if path.starts_with("/api/photographers") { + else if path.starts_with("/api/photographers") || path.starts_with("/api/admin/photographers") { Some(self.photographers_url.clone()) } - else if path.starts_with("/api/makeup-artists") { + else if path.starts_with("/api/makeup-artists") || path.starts_with("/api/admin/makeup-artists") { Some(self.makeup_artists_url.clone()) } - else if path.starts_with("/api/tutors") { + else if path.starts_with("/api/tutors") || path.starts_with("/api/admin/tutors") { Some(self.tutors_url.clone()) } - else if path.starts_with("/api/developers") { + else if path.starts_with("/api/developers") || path.starts_with("/api/admin/developers") { Some(self.developers_url.clone()) } - else if path.starts_with("/api/video-editors") { + else if path.starts_with("/api/video-editors") || path.starts_with("/api/admin/video-editors") { Some(self.video_editors_url.clone()) } - else if path.starts_with("/api/graphic-designers") { + else if path.starts_with("/api/graphic-designers") || path.starts_with("/api/admin/graphic-designers") { Some(self.graphic_designers_url.clone()) } - else if path.starts_with("/api/social-media-managers") { + else if path.starts_with("/api/social-media-managers") || path.starts_with("/api/admin/social-media-managers") { Some(self.social_media_managers_url.clone()) } - else if path.starts_with("/api/fitness-trainers") { + else if path.starts_with("/api/fitness-trainers") || path.starts_with("/api/admin/fitness-trainers") { Some(self.fitness_trainers_url.clone()) } - else if path.starts_with("/api/catering-services") { + else if path.starts_with("/api/catering-services") || path.starts_with("/api/admin/catering-services") { Some(self.catering_services_url.clone()) } else if path.starts_with("/api/ugc-content-creators") { diff --git a/apps/graphic_designers/src/admin.rs b/apps/graphic_designers/src/admin.rs new file mode 100644 index 0000000..4609aeb --- /dev/null +++ b/apps/graphic_designers/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminGraphicDesignerList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminGraphicDesignerDetail = AdminGraphicDesignerList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_graphic_designers)) + .route("/{id}", get(get_graphic_designer)) +} + +async fn list_graphic_designers( + State(state): State, +) -> Result { + let designers = sqlx::query_as!( + AdminGraphicDesignerList, + r#" + SELECT + g.id, g.user_id, g.bio, g.experience_years, g.custom_data, + g.created_at, g.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM graphic_designer_profiles g + JOIN users u ON g.user_id = u.id + ORDER BY g.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(designers)) +} + +async fn get_graphic_designer( + State(state): State, + Path(id): Path, +) -> Result { + let designer = sqlx::query_as!( + AdminGraphicDesignerDetail, + r#" + SELECT + g.id, g.user_id, g.bio, g.experience_years, g.custom_data, + g.created_at, g.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM graphic_designer_profiles g + JOIN users u ON g.user_id = u.id + WHERE g.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match designer { + Some(g) => Ok(Json(g)), + None => Err((StatusCode::NOT_FOUND, "Graphic designer not found".to_string())), + } +} diff --git a/apps/graphic_designers/src/main.rs b/apps/graphic_designers/src/main.rs index 64b8606..bd64f0a 100644 --- a/apps/graphic_designers/src/main.rs +++ b/apps/graphic_designers/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/graphic-designers", handlers::router()) + .nest("/api/admin/graphic-designers", admin::router()) .route("/health", get(|| async { "Graphic Designers OK" })) .with_state(state); diff --git a/apps/makeup_artists/src/main.rs b/apps/makeup_artists/src/main.rs index ba092d8..28bac1b 100644 --- a/apps/makeup_artists/src/main.rs +++ b/apps/makeup_artists/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/makeup-artists", handlers::router()) + .nest("/api/admin/makeup-artists", admin::router()) .route("/health", get(|| async { "Makeup Artists OK" })) .with_state(state); diff --git a/apps/photographers/src/admin.rs b/apps/photographers/src/admin.rs new file mode 100644 index 0000000..ce64254 --- /dev/null +++ b/apps/photographers/src/admin.rs @@ -0,0 +1,81 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminPhotographerList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub location: Option, + pub years_experience: Option, + pub avg_rating: Option, + pub is_verified: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminPhotographerDetail = AdminPhotographerList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_photographers)) + .route("/{id}", get(get_photographer)) +} + +async fn list_photographers( + State(state): State, +) -> Result { + let photographers = sqlx::query_as!( + AdminPhotographerList, + r#" + SELECT + p.id, p.user_id, p.bio, p.location, p.years_experience, p.avg_rating, p.is_verified, + p.created_at, p.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM photographers p + JOIN users u ON p.user_id = u.id + ORDER BY p.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(photographers)) +} + +async fn get_photographer( + State(state): State, + Path(id): Path, +) -> Result { + let photographer = sqlx::query_as!( + AdminPhotographerDetail, + r#" + SELECT + p.id, p.user_id, p.bio, p.location, p.years_experience, p.avg_rating, p.is_verified, + p.created_at, p.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM photographers p + JOIN users u ON p.user_id = u.id + WHERE p.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match photographer { + Some(p) => Ok(Json(p)), + None => Err((StatusCode::NOT_FOUND, "Photographer not found".to_string())), + } +} diff --git a/apps/photographers/src/main.rs b/apps/photographers/src/main.rs index bca0baf..3515a74 100644 --- a/apps/photographers/src/main.rs +++ b/apps/photographers/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/photographers", handlers::router()) + .nest("/api/admin/photographers", admin::router()) .route("/health", get(|| async { "Photographers OK" })) .with_state(state); diff --git a/apps/social_media_managers/src/admin.rs b/apps/social_media_managers/src/admin.rs new file mode 100644 index 0000000..c779a58 --- /dev/null +++ b/apps/social_media_managers/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminSocialMediaManagerList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminSocialMediaManagerDetail = AdminSocialMediaManagerList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_social_media_managers)) + .route("/{id}", get(get_social_media_manager)) +} + +async fn list_social_media_managers( + State(state): State, +) -> Result { + let managers = sqlx::query_as!( + AdminSocialMediaManagerList, + r#" + SELECT + s.id, s.user_id, s.bio, s.experience_years, s.custom_data, + s.created_at, s.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM social_media_manager_profiles s + JOIN users u ON s.user_id = u.id + ORDER BY s.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(managers)) +} + +async fn get_social_media_manager( + State(state): State, + Path(id): Path, +) -> Result { + let manager = sqlx::query_as!( + AdminSocialMediaManagerDetail, + r#" + SELECT + s.id, s.user_id, s.bio, s.experience_years, s.custom_data, + s.created_at, s.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM social_media_manager_profiles s + JOIN users u ON s.user_id = u.id + WHERE s.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match manager { + Some(s) => Ok(Json(s)), + None => Err((StatusCode::NOT_FOUND, "Social media manager not found".to_string())), + } +} diff --git a/apps/social_media_managers/src/main.rs b/apps/social_media_managers/src/main.rs index 63d8cb1..b35a739 100644 --- a/apps/social_media_managers/src/main.rs +++ b/apps/social_media_managers/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/social-media-managers", handlers::router()) + .nest("/api/admin/social-media-managers", admin::router()) .route("/health", get(|| async { "Social Media Managers OK" })) .with_state(state); diff --git a/apps/tutors/src/admin.rs b/apps/tutors/src/admin.rs new file mode 100644 index 0000000..5b13ea4 --- /dev/null +++ b/apps/tutors/src/admin.rs @@ -0,0 +1,81 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminTutorList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub subjects_taught: Option>, + pub education_level: Option, + pub certifications: Option, + pub years_of_experience: Option, + pub hourly_rate: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminTutorDetail = AdminTutorList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_tutors)) + .route("/{id}", get(get_tutor)) +} + +async fn list_tutors( + State(state): State, +) -> Result { + let tutors = sqlx::query_as!( + AdminTutorList, + r#" + SELECT + t.id, t.user_id, t.subjects_taught, t.education_level, t.certifications, t.years_of_experience, t.hourly_rate, + t.created_at, t.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM tutor_profiles t + JOIN users u ON t.user_id = u.id + ORDER BY t.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(tutors)) +} + +async fn get_tutor( + State(state): State, + Path(id): Path, +) -> Result { + let tutor = sqlx::query_as!( + AdminTutorDetail, + r#" + SELECT + t.id, t.user_id, t.subjects_taught, t.education_level, t.certifications, t.years_of_experience, t.hourly_rate, + t.created_at, t.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM tutor_profiles t + JOIN users u ON t.user_id = u.id + WHERE t.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match tutor { + Some(t) => Ok(Json(t)), + None => Err((StatusCode::NOT_FOUND, "Tutor not found".to_string())), + } +} diff --git a/apps/tutors/src/main.rs b/apps/tutors/src/main.rs index 920a39b..fa1b9d9 100644 --- a/apps/tutors/src/main.rs +++ b/apps/tutors/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/tutors", handlers::router()) + .nest("/api/admin/tutors", admin::router()) .route("/health", get(|| async { "Tutors OK" })) .with_state(state); diff --git a/apps/video_editors/src/admin.rs b/apps/video_editors/src/admin.rs new file mode 100644 index 0000000..5361d24 --- /dev/null +++ b/apps/video_editors/src/admin.rs @@ -0,0 +1,79 @@ +use crate::AppState; +use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Serialize)] +pub struct AdminVideoEditorList { + pub id: Uuid, + pub user_id: Uuid, + pub first_name: String, + pub last_name: String, + pub email: String, + pub phone: Option, + pub status: String, + pub bio: Option, + pub experience_years: Option, + pub custom_data: serde_json::Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +type AdminVideoEditorDetail = AdminVideoEditorList; + +pub fn router() -> Router { + Router::new() + .route("/", get(list_video_editors)) + .route("/{id}", get(get_video_editor)) +} + +async fn list_video_editors( + State(state): State, +) -> Result { + let editors = sqlx::query_as!( + AdminVideoEditorList, + r#" + SELECT + v.id, v.user_id, v.bio, v.experience_years, v.custom_data, + v.created_at, v.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM video_editor_profiles v + JOIN users u ON v.user_id = u.id + ORDER BY v.created_at DESC + LIMIT 100 + "# + ) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + Ok(Json(editors)) +} + +async fn get_video_editor( + State(state): State, + Path(id): Path, +) -> Result { + let editor = sqlx::query_as!( + AdminVideoEditorDetail, + r#" + SELECT + v.id, v.user_id, v.bio, v.experience_years, v.custom_data, + v.created_at, v.updated_at, + u.first_name, u.last_name, u.email, u.phone, u.status + FROM video_editor_profiles v + JOIN users u ON v.user_id = u.id + WHERE v.id = $1 + "#, + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; + + match editor { + Some(v) => Ok(Json(v)), + None => Err((StatusCode::NOT_FOUND, "Video editor not found".to_string())), + } +} diff --git a/apps/video_editors/src/main.rs b/apps/video_editors/src/main.rs index e409605..dc77bb0 100644 --- a/apps/video_editors/src/main.rs +++ b/apps/video_editors/src/main.rs @@ -1,4 +1,5 @@ mod handlers; +mod admin; use axum::{routing::get, Router}; use std::net::SocketAddr; @@ -33,6 +34,7 @@ async fn main() { let app = Router::new() .nest("/api/video-editors", handlers::router()) + .nest("/api/admin/video-editors", admin::router()) .route("/health", get(|| async { "Video Editors OK" })) .with_state(state);