a
This commit is contained in:
@@ -3,3 +3,10 @@ pub mod v1;
|
||||
// Explicitly export only what's needed from v1
|
||||
pub use v1::dimentorin_router;
|
||||
pub use v1::mentors::mentors_router;
|
||||
pub use v1::sessions::{
|
||||
sessions_router, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
AvailabilitySlotDto,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod mentors;
|
||||
pub mod sessions;
|
||||
|
||||
/// Creates the main Dimentorin router with all version 1 endpoints
|
||||
/// Routes:
|
||||
/// - /mentors -> mentors::mentors_router()
|
||||
/// - /sessions -> sessions::sessions_router()
|
||||
/// - /users/me/sessions -> sessions::get_my_sessions()
|
||||
pub fn dimentorin_router() -> Router {
|
||||
Router::new()
|
||||
.nest("/mentors", mentors::mentors_router())
|
||||
.merge(sessions::sessions_router())
|
||||
}
|
||||
|
||||
// Explicitly re-export key items for easier consumption
|
||||
@@ -15,3 +19,8 @@ pub use mentors::mentors_router;
|
||||
pub use mentors::MentorsService;
|
||||
pub use mentors::MentorsRepository;
|
||||
pub use mentors::MentorSchema;
|
||||
|
||||
pub use sessions::sessions_router;
|
||||
pub use sessions::SessionsService;
|
||||
pub use sessions::SessionsRepository;
|
||||
pub use sessions::SessionSchema;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod sessions_controller;
|
||||
pub mod sessions_dto;
|
||||
pub mod sessions_repository;
|
||||
pub mod sessions_schema;
|
||||
pub mod sessions_service;
|
||||
|
||||
pub use sessions_controller::*;
|
||||
pub use sessions_dto::*;
|
||||
pub use sessions_repository::*;
|
||||
pub use sessions_schema::*;
|
||||
pub use sessions_service::*;
|
||||
@@ -0,0 +1,297 @@
|
||||
use super::{
|
||||
BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListResponseDto,
|
||||
SessionsService, UpdateSessionStatusRequestDto, UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
http::HeaderMap,
|
||||
response::Response,
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::extract_email;
|
||||
use serde::Deserialize;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
post_book_session,
|
||||
get_mentor_sessions,
|
||||
get_mentor_availability,
|
||||
put_update_session_status,
|
||||
post_submit_feedback,
|
||||
get_my_sessions,
|
||||
),
|
||||
components(schemas(
|
||||
BookSessionRequestDto,
|
||||
BookSessionResponseDto,
|
||||
SessionListResponseDto,
|
||||
super::SessionListItemDto,
|
||||
MentorAvailabilityDto,
|
||||
super::AvailabilitySlotDto,
|
||||
UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
SessionFeedbackRequestDto,
|
||||
SessionFeedbackResponseDto,
|
||||
)),
|
||||
tags(
|
||||
(name = "sessions", description = "Mentoring Sessions Management API")
|
||||
)
|
||||
)]
|
||||
pub struct SessionsApiDoc;
|
||||
|
||||
// ============================================
|
||||
// Book Session
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/mentors/{id}/sessions/book",
|
||||
tag = "sessions",
|
||||
summary = "Book a mentoring session",
|
||||
description = "Book a mentoring session with a specific mentor. Requires authentication.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
request_body = BookSessionRequestDto,
|
||||
responses(
|
||||
(status = 201, description = "Session booked successfully", body = BookSessionResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_book_session(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Json(dto): Json<BookSessionRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::book_session(&state, mentor_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor's Sessions
|
||||
// ============================================
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SessionStatusFilter {
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/sessions",
|
||||
tag = "sessions",
|
||||
summary = "List mentor's sessions",
|
||||
description = "Get all sessions for a specific mentor. Only accessible by the mentor themselves or admin.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::get_mentor_sessions(&state, mentor_id, user_email, filter.status).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/mentors/{id}/availability",
|
||||
tag = "sessions",
|
||||
summary = "Get mentor availability",
|
||||
description = "Get available time slots for booking with a mentor. Public endpoint.",
|
||||
params(
|
||||
("id" = String, Path, description = "Mentor ID"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Availability retrieved successfully", body = MentorAvailabilityDto),
|
||||
(status = 404, description = "Mentor not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_mentor_availability(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(mentor_id): Path<String>,
|
||||
) -> Response {
|
||||
SessionsService::get_mentor_availability(&state, mentor_id).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/sessions/{id}/status",
|
||||
tag = "sessions",
|
||||
summary = "Update session status",
|
||||
description = "Update the status of a session (confirm, complete, cancel). Only accessible by the mentor.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = UpdateSessionStatusRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Status updated successfully", body = UpdateSessionStatusResponseDto),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn put_update_session_status(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(dto): Json<UpdateSessionStatusRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::update_session_status(&state, session_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sessions/{id}/feedback",
|
||||
tag = "sessions",
|
||||
summary = "Submit session feedback",
|
||||
description = "Submit feedback and rating for a completed session. Only accessible by the mentee.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("id" = String, Path, description = "Session ID"),
|
||||
),
|
||||
request_body = SessionFeedbackRequestDto,
|
||||
responses(
|
||||
(status = 200, description = "Feedback submitted successfully", body = SessionFeedbackResponseDto),
|
||||
(status = 400, description = "Invalid request or session not completed"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 403, description = "Forbidden"),
|
||||
(status = 404, description = "Session not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn post_submit_feedback(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(dto): Json<SessionFeedbackRequestDto>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::submit_feedback(&state, session_id, user_email, dto).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get User's Sessions
|
||||
// ============================================
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/users/me/sessions",
|
||||
tag = "sessions",
|
||||
summary = "Get my sessions",
|
||||
description = "Get all sessions for the authenticated user (as mentee). Requires authentication.",
|
||||
security(("Bearer" = [])),
|
||||
params(
|
||||
("status" = Option<String>, Query, description = "Filter by status (pending, confirmed, completed, cancelled, no_show)"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Sessions retrieved successfully", body = SessionListResponseDto),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_my_sessions(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Query(filter): Query<SessionStatusFilter>,
|
||||
) -> Response {
|
||||
let user_email = match extract_email(&headers) {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return imphnen_utils::common_response(
|
||||
axum::http::StatusCode::UNAUTHORIZED,
|
||||
"Token tidak valid",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SessionsService::get_user_sessions(&state, user_email, filter.status).await
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Router
|
||||
// ============================================
|
||||
|
||||
pub fn sessions_router() -> Router {
|
||||
Router::new()
|
||||
// Book session (under mentors path)
|
||||
.route("/mentors/:id/sessions/book", post(post_book_session))
|
||||
// Get mentor's sessions
|
||||
.route("/mentors/:id/sessions", get(get_mentor_sessions))
|
||||
// Get mentor availability (public - no auth)
|
||||
.route("/mentors/:id/availability", get(get_mentor_availability))
|
||||
// Update session status
|
||||
.route("/sessions/:id/status", put(put_update_session_status))
|
||||
// Submit feedback
|
||||
.route("/sessions/:id/feedback", post(post_submit_feedback))
|
||||
// Get my sessions
|
||||
.route("/users/me/sessions", get(get_my_sessions))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
// ============================================
|
||||
// Book Session (POST /v1/mentors/{id}/sessions/book)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct BookSessionRequestDto {
|
||||
#[validate(length(min = 3, max = 200, message = "Topic must be 3-200 characters"))]
|
||||
pub topic: String,
|
||||
|
||||
#[validate(length(max = 1000, message = "Description must be max 1000 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
#[validate(length(min = 1, message = "Scheduled time is required"))]
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
|
||||
#[validate(range(min = 15, max = 240, message = "Duration must be 15-240 minutes"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_minutes: Option<i32>,
|
||||
|
||||
#[validate(length(max = 50, message = "Session type must be max 50 characters"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_type: Option<String>, // "video_call", "phone_call", "chat"
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BookSessionResponseDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Sessions (GET /v1/mentors/{id}/sessions & /v1/users/me/sessions)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListItemDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionListResponseDto {
|
||||
pub sessions: Vec<SessionListItemDto>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Detail
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionDetailDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_id: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mentor Availability (GET /v1/mentors/{id}/availability)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AvailabilitySlotDto {
|
||||
pub date: String, // YYYY-MM-DD
|
||||
pub time: String, // HH:MM
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MentorAvailabilityDto {
|
||||
pub mentor_id: String,
|
||||
pub availability_commitment: String,
|
||||
pub preferred_formats: Vec<String>,
|
||||
pub slots: Vec<AvailabilitySlotDto>,
|
||||
pub booked_dates: Vec<String>, // Dates with existing sessions
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status (PUT /v1/sessions/{id}/status)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateSessionStatusRequestDto {
|
||||
#[validate(length(min = 1, max = 50, message = "Status must be 1-50 characters"))]
|
||||
pub status: String, // "confirmed", "completed", "cancelled", "no_show"
|
||||
|
||||
#[validate(url(message = "Meeting link must be a valid URL"))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meeting_link: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateSessionStatusResponseDto {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub meeting_link: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback (POST /v1/sessions/{id}/feedback)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct SessionFeedbackRequestDto {
|
||||
#[validate(length(min = 10, max = 2000, message = "Feedback must be 10-2000 characters"))]
|
||||
pub feedback: String,
|
||||
|
||||
#[validate(range(min = 1, max = 5, message = "Rating must be 1-5"))]
|
||||
pub rating: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SessionFeedbackResponseDto {
|
||||
pub id: String,
|
||||
pub feedback: String,
|
||||
pub rating: i32,
|
||||
pub submitted_at: String,
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Query DTOs (internal use)
|
||||
// ============================================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionDetailQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>,
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub mentor_fullname: Option<String>,
|
||||
pub mentee_fullname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionListQueryDto {
|
||||
pub id: String,
|
||||
pub mentor_id: String,
|
||||
pub mentee_id: String,
|
||||
pub topic: String,
|
||||
pub scheduled_at: String,
|
||||
pub duration_minutes: i32,
|
||||
pub session_type: String,
|
||||
pub status: String,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: String,
|
||||
pub mentee_fullname: Option<String>,
|
||||
pub mentee_email: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
use super::{SessionDetailQueryDto, SessionListQueryDto, SessionSchema};
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::get_id;
|
||||
use serde::Deserialize;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
pub struct SessionsRepository<'a> {
|
||||
pub state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> SessionsRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Create Session
|
||||
// ============================================
|
||||
pub async fn create_session(&self, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let created: Option<SessionSchema> = db
|
||||
.create("sessions")
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create session: {}", e))?;
|
||||
|
||||
created.ok_or_else(|| "Session creation returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session by ID
|
||||
// ============================================
|
||||
pub async fn query_session_by_id(&self, id: &Thing) -> Result<Option<SessionSchema>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let session: Option<SessionSchema> = db
|
||||
.select(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch session: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Session Detail with User Info
|
||||
// ============================================
|
||||
pub async fn query_session_detail(&self, id: &Thing) -> Result<Option<SessionDetailQueryDto>, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let query = r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
description,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
meeting_link,
|
||||
session_type,
|
||||
status,
|
||||
feedback,
|
||||
rating,
|
||||
feedback_submitted_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
(SELECT fullname FROM $parent.mentor_id.user_id)[0].fullname AS mentor_fullname,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname
|
||||
FROM type::thing($table, $id)
|
||||
"#;
|
||||
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("table", "sessions"))
|
||||
.bind(("id", id.id.to_string()))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query session detail: {}", e))?;
|
||||
|
||||
let session: Option<SessionDetailQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse session detail: {}", e))?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn query_mentor_sessions(
|
||||
&self,
|
||||
mentor_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query mentor sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse mentor sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// List User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn query_user_sessions(
|
||||
&self,
|
||||
user_id: &Thing,
|
||||
status_filter: Option<String>,
|
||||
) -> Result<Vec<SessionListQueryDto>, String> {
|
||||
let query = if let Some(_status) = status_filter.as_ref() {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id AND status = $status
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
} else {
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic,
|
||||
scheduled_at,
|
||||
duration_minutes,
|
||||
session_type,
|
||||
status,
|
||||
rating,
|
||||
created_at,
|
||||
(SELECT fullname FROM $parent.mentee_id)[0].fullname AS mentee_fullname,
|
||||
(SELECT email FROM $parent.mentee_id)[0].email AS mentee_email
|
||||
FROM sessions
|
||||
WHERE mentee_id = $user_id
|
||||
ORDER BY scheduled_at DESC
|
||||
"#
|
||||
};
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let user_id_clone = user_id.clone();
|
||||
let mut result = if let Some(status_val) = status_filter {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.bind(("status", status_val))
|
||||
.await
|
||||
} else {
|
||||
db.query(query)
|
||||
.bind(("user_id", user_id_clone))
|
||||
.await
|
||||
}
|
||||
.map_err(|e| format!("Failed to query user sessions: {}", e))?;
|
||||
|
||||
let sessions: Vec<SessionListQueryDto> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse user sessions: {}", e))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Booked Dates for Mentor
|
||||
// ============================================
|
||||
pub async fn query_booked_dates(&self, mentor_id: &Thing) -> Result<Vec<String>, String> {
|
||||
let query = r#"
|
||||
SELECT scheduled_at FROM sessions
|
||||
WHERE mentor_id = $mentor_id
|
||||
AND status IN ['pending', 'confirmed']
|
||||
ORDER BY scheduled_at ASC
|
||||
"#;
|
||||
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let mentor_id_clone = mentor_id.clone();
|
||||
let mut result = db
|
||||
.query(query)
|
||||
.bind(("mentor_id", mentor_id_clone))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query booked dates: {}", e))?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DateOnly {
|
||||
scheduled_at: String,
|
||||
}
|
||||
|
||||
let dates: Vec<DateOnly> = result
|
||||
.take(0)
|
||||
.map_err(|e| format!("Failed to parse booked dates: {}", e))?;
|
||||
|
||||
Ok(dates.into_iter().map(|d| d.scheduled_at).collect())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session
|
||||
// ============================================
|
||||
pub async fn update_session(&self, id: &Thing, schema: SessionSchema) -> Result<SessionSchema, String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let updated: Option<SessionSchema> = db
|
||||
.update(record_key)
|
||||
.content(schema)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update session: {}", e))?;
|
||||
|
||||
updated.ok_or_else(|| "Session update returned None".to_string())
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
// Delete Session (soft delete)
|
||||
// ============================================
|
||||
pub async fn delete_session(&self, id: &Thing) -> Result<(), String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let record_key = get_id(id).map_err(|e| e.to_string())?;
|
||||
let _: Option<SessionSchema> = db
|
||||
.delete(record_key)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete session: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use super::{BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto};
|
||||
use imphnen_libs::ResourceEnum;
|
||||
use imphnen_utils::{get_iso_date, make_thing};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{sql::Thing, Uuid};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SessionSchema {
|
||||
pub id: Thing,
|
||||
pub mentor_id: Thing,
|
||||
pub mentee_id: Thing,
|
||||
pub topic: String,
|
||||
pub description: Option<String>,
|
||||
pub scheduled_at: String, // ISO 8601 datetime
|
||||
pub duration_minutes: i32,
|
||||
pub meeting_link: Option<String>,
|
||||
pub session_type: String, // "video_call", "phone_call", "chat"
|
||||
pub status: String, // "pending", "confirmed", "completed", "cancelled", "no_show"
|
||||
pub feedback: Option<String>,
|
||||
pub rating: Option<i32>, // 1-5
|
||||
pub feedback_submitted_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
impl Default for SessionSchema {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: make_thing(
|
||||
ResourceEnum::Sessions.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentor_id: make_thing(
|
||||
ResourceEnum::Mentors.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
mentee_id: make_thing(
|
||||
ResourceEnum::Users.to_string().as_str(),
|
||||
&Uuid::new_v4().to_string(),
|
||||
),
|
||||
topic: String::new(),
|
||||
description: None,
|
||||
scheduled_at: get_iso_date(),
|
||||
duration_minutes: 60,
|
||||
meeting_link: None,
|
||||
session_type: "video_call".to_string(),
|
||||
status: "pending".to_string(),
|
||||
feedback: None,
|
||||
rating: None,
|
||||
feedback_submitted_at: None,
|
||||
created_at: get_iso_date(),
|
||||
updated_at: get_iso_date(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionSchema {
|
||||
pub fn from_book_request(
|
||||
mentor_id: Thing,
|
||||
mentee_id: Thing,
|
||||
request: BookSessionRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
mentor_id,
|
||||
mentee_id,
|
||||
topic: request.topic,
|
||||
description: request.description,
|
||||
scheduled_at: request.scheduled_at,
|
||||
duration_minutes: request.duration_minutes.unwrap_or(60),
|
||||
session_type: request.session_type.unwrap_or_else(|| "video_call".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, request: UpdateSessionStatusRequestDto) {
|
||||
self.status = request.status;
|
||||
if let Some(link) = request.meeting_link {
|
||||
self.meeting_link = Some(link);
|
||||
}
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
|
||||
pub fn add_feedback(&mut self, request: SessionFeedbackRequestDto) {
|
||||
self.feedback = Some(request.feedback);
|
||||
self.rating = Some(request.rating);
|
||||
self.feedback_submitted_at = Some(get_iso_date());
|
||||
self.updated_at = get_iso_date();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use super::{
|
||||
AvailabilitySlotDto, BookSessionRequestDto, BookSessionResponseDto, MentorAvailabilityDto,
|
||||
SessionFeedbackRequestDto, SessionFeedbackResponseDto, SessionListItemDto,
|
||||
SessionListResponseDto, SessionSchema, SessionsRepository, UpdateSessionStatusRequestDto,
|
||||
UpdateSessionStatusResponseDto,
|
||||
};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use chrono::{Duration, Utc};
|
||||
use imphnen_entities::ResponseSuccessDto;
|
||||
use imphnen_libs::AppState;
|
||||
use imphnen_utils::{common_response, extract_id, get_iso_date, make_thing, success_response, validate_request};
|
||||
|
||||
pub struct SessionsService;
|
||||
|
||||
impl SessionsService {
|
||||
// ============================================
|
||||
// Book Session
|
||||
// ============================================
|
||||
pub async fn book_session(
|
||||
state: &AppState,
|
||||
mentor_id: String,
|
||||
user_id: String,
|
||||
dto: BookSessionRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
let mentee_thing = make_thing("users", &user_id);
|
||||
|
||||
let schema = SessionSchema::from_book_request(mentor_thing.clone(), mentee_thing.clone(), dto);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.create_session(schema).await {
|
||||
Ok(created) => {
|
||||
let response = BookSessionResponseDto {
|
||||
id: extract_id(&created.id),
|
||||
mentor_id: extract_id(&created.mentor_id),
|
||||
mentee_id: extract_id(&created.mentee_id),
|
||||
topic: created.topic,
|
||||
description: created.description,
|
||||
scheduled_at: created.scheduled_at,
|
||||
duration_minutes: created.duration_minutes,
|
||||
session_type: created.session_type,
|
||||
status: created.status,
|
||||
created_at: created.created_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor's Sessions
|
||||
// ============================================
|
||||
pub async fn get_mentor_sessions(
|
||||
state: &AppState,
|
||||
mentor_id: String,
|
||||
_user_email: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_mentor_sessions(&mentor_thing, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SessionListResponseDto {
|
||||
sessions: session_items,
|
||||
total: 0, // TODO: implement proper pagination
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get User's Sessions (as mentee)
|
||||
// ============================================
|
||||
pub async fn get_user_sessions(
|
||||
state: &AppState,
|
||||
user_id: String,
|
||||
status_filter: Option<String>,
|
||||
) -> Response {
|
||||
let user_thing = make_thing("users", &user_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_user_sessions(&user_thing, status_filter).await {
|
||||
Ok(sessions) => {
|
||||
let session_items: Vec<SessionListItemDto> = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionListItemDto {
|
||||
id: s.id,
|
||||
mentor_id: s.mentor_id,
|
||||
mentee_id: s.mentee_id,
|
||||
mentee_fullname: s.mentee_fullname,
|
||||
mentee_email: s.mentee_email,
|
||||
topic: s.topic,
|
||||
scheduled_at: s.scheduled_at,
|
||||
duration_minutes: s.duration_minutes,
|
||||
session_type: s.session_type,
|
||||
status: s.status,
|
||||
rating: s.rating,
|
||||
created_at: s.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SessionListResponseDto {
|
||||
sessions: session_items,
|
||||
total: 0, // TODO: implement proper pagination
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Get Mentor Availability
|
||||
// ============================================
|
||||
pub async fn get_mentor_availability(state: &AppState, mentor_id: String) -> Response {
|
||||
let mentor_thing = make_thing("mentors", &mentor_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_booked_dates(&mentor_thing).await {
|
||||
Ok(booked_dates) => {
|
||||
// Generate sample availability slots (next 7 days)
|
||||
let mut slots = Vec::new();
|
||||
let today = Utc::now().date_naive();
|
||||
|
||||
for i in 0..7 {
|
||||
let date = today + Duration::days(i);
|
||||
let date_str = date.format("%Y-%m-%d").to_string();
|
||||
|
||||
// Generate time slots (9 AM to 5 PM, every hour)
|
||||
for hour in 9..17 {
|
||||
let time_str = format!("{:02}:00", hour);
|
||||
let datetime_str = format!("{}T{}:00Z", date_str, time_str);
|
||||
|
||||
// Check if this slot is booked
|
||||
let is_booked = booked_dates.iter().any(|d| d.starts_with(&datetime_str[..13]));
|
||||
|
||||
slots.push(AvailabilitySlotDto {
|
||||
date: date_str.clone(),
|
||||
time: time_str,
|
||||
available: !is_booked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let response = MentorAvailabilityDto {
|
||||
mentor_id,
|
||||
availability_commitment: "Available weekdays 9 AM - 5 PM".to_string(),
|
||||
preferred_formats: vec!["video_call".to_string(), "phone_call".to_string()],
|
||||
slots,
|
||||
booked_dates,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Update Session Status
|
||||
// ============================================
|
||||
pub async fn update_session_status(
|
||||
state: &AppState,
|
||||
session_id: String,
|
||||
_user_id: String,
|
||||
dto: UpdateSessionStatusRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
Ok(Some(mut session)) => {
|
||||
session.update_status(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
Ok(updated) => {
|
||||
let response = UpdateSessionStatusResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
status: updated.status,
|
||||
meeting_link: updated.meeting_link,
|
||||
updated_at: updated.updated_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Submit Feedback
|
||||
// ============================================
|
||||
pub async fn submit_feedback(
|
||||
state: &AppState,
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
dto: SessionFeedbackRequestDto,
|
||||
) -> Response {
|
||||
if let Err((status, message)) = validate_request(&dto) {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
|
||||
let session_thing = make_thing("sessions", &session_id);
|
||||
|
||||
let repo = SessionsRepository::new(state);
|
||||
match repo.query_session_by_id(&session_thing).await {
|
||||
Ok(Some(mut session)) => {
|
||||
// Authorization: Only mentee can submit feedback
|
||||
let mentee_id = extract_id(&session.mentee_id);
|
||||
if mentee_id != user_id {
|
||||
return common_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"Unauthorized: Only the mentee can submit feedback",
|
||||
);
|
||||
}
|
||||
|
||||
// Validate session is completed
|
||||
if session.status != "completed" {
|
||||
return common_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Feedback can only be submitted for completed sessions",
|
||||
);
|
||||
}
|
||||
|
||||
session.add_feedback(dto.clone());
|
||||
match repo.update_session(&session_thing, session).await {
|
||||
Ok(updated) => {
|
||||
let response = SessionFeedbackResponseDto {
|
||||
id: extract_id(&updated.id),
|
||||
feedback: dto.feedback,
|
||||
rating: dto.rating,
|
||||
submitted_at: updated.feedback_submitted_at.unwrap_or_else(get_iso_date),
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: response })
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
Ok(None) => common_response(StatusCode::NOT_FOUND, "Session not found"),
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user