diff --git a/imphnen-dimentorin/src/sessions/application/session_query_service.rs b/imphnen-dimentorin/src/sessions/application/session_query_service.rs index 95808df..5a1b3ae 100644 --- a/imphnen-dimentorin/src/sessions/application/session_query_service.rs +++ b/imphnen-dimentorin/src/sessions/application/session_query_service.rs @@ -1,12 +1,14 @@ use crate::sessions::domain::{ - AvailabilitySlot, MentorAvailability, SessionDetail, SessionList, SessionListItem, - SessionRepository, + AdminSessionListItem, AvailabilitySlot, MentorAvailability, SessionDetail, + SessionList, SessionListItem, SessionRepository, }; use chrono::{Duration, Utc}; use imphnen_entities::seaorm::auth::mentors::{ Column as MentorColumn, Entity as MentorsEntity, }; +use imphnen_entities::seaorm::auth::users::{Column as UserColumn, Entity as UsersEntity}; use imphnen_utils::AppError; +use paginator_utils::PaginatorResponse; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use std::sync::Arc; use uuid::Uuid; @@ -17,6 +19,89 @@ pub struct SessionQueryService { } impl SessionQueryService { + pub async fn get_admin_sessions( + &self, + page: u64, + per_page: u64, + status_filter: Option, + ) -> Result, AppError> { + use imphnen_entities::seaorm::auth::sessions::{ + Column as SessionColumn, Entity as SessionsEntity, + }; + use sea_orm::{Order, PaginatorTrait, QueryOrder}; + + let mut query = SessionsEntity::find().order_by(SessionColumn::CreatedAt, Order::Desc); + if let Some(status) = status_filter.as_deref() { + query = query.filter(SessionColumn::Status.eq(status)); + } + let paginator = query.paginate(self.db.as_ref(), per_page); + let total = paginator.num_items().await?; + let models = paginator.fetch_page(page.saturating_sub(1)).await?; + + // Resolve mentor/mentee names in one pass. + let mut user_ids = std::collections::HashSet::new(); + for m in &models { + user_ids.insert(m.mentor_id); + user_ids.insert(m.mentee_id); + } + let users = if user_ids.is_empty() { + Vec::new() + } else { + UsersEntity::find() + .filter(UserColumn::Id.is_in(user_ids.iter().copied().collect::>())) + .all(self.db.as_ref()) + .await? + }; + let name_by_id: std::collections::HashMap, String)> = users + .into_iter() + .map(|u| { + let fullname = u + .first_name + .clone() + .zip(u.last_name.clone()) + .map(|(f, l)| format!("{} {}", f, l)) + .or_else(|| u.first_name.clone()) + .or_else(|| Some(u.username.clone())); + (u.id, (fullname, u.email)) + }) + .collect(); + + let data = models + .into_iter() + .map(|m| { + let (mentor_fullname, _) = + name_by_id.get(&m.mentor_id).cloned().unwrap_or_default(); + let (mentee_fullname, mentee_email) = + name_by_id.get(&m.mentee_id).cloned().unwrap_or_default(); + AdminSessionListItem { + id: m.id.to_string(), + mentor_id: m.mentor_id.to_string(), + mentor_fullname, + mentee_id: m.mentee_id.to_string(), + mentee_fullname, + mentee_email: Some(mentee_email), + topic: m.topic, + scheduled_at: m.scheduled_at.to_rfc3339(), + duration_minutes: m.duration_minutes, + meeting_link: m.meeting_link, + session_type: m.session_type, + status: m.status, + rating: m.rating, + feedback: m.feedback, + feedback_submitted_at: m + .feedback_submitted_at + .map(|dt| dt.to_rfc3339()), + created_at: m.created_at.to_rfc3339(), + } + }) + .collect(); + + Ok(PaginatorResponse { + data, + meta: paginator_utils::PaginatorResponseMeta::new(page as u32, per_page as u32, total as u32), + }) + } + pub async fn get_mentor_sessions( &self, mentor_id: String, diff --git a/imphnen-dimentorin/src/sessions/application/session_service.rs b/imphnen-dimentorin/src/sessions/application/session_service.rs index d38aadd..8b5c8bb 100644 --- a/imphnen-dimentorin/src/sessions/application/session_service.rs +++ b/imphnen-dimentorin/src/sessions/application/session_service.rs @@ -1,12 +1,14 @@ use super::session_booking_service::SessionBookingService; use super::session_query_service::SessionQueryService; use crate::sessions::domain::{ - BookSessionCommand, BookedSession, MentorAvailability, MentorStats, - SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, SessionList, - SessionRepository, SessionService, UpdateSessionStatusCommand, UpdatedSessionStatus, + AdminSessionListItem, BookSessionCommand, BookedSession, MentorAvailability, + MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, + SessionList, SessionRepository, SessionService, UpdateSessionStatusCommand, + UpdatedSessionStatus, }; use async_trait::async_trait; use imphnen_utils::AppError; +use paginator_utils::PaginatorResponse; use std::sync::Arc; pub struct SessionServiceImpl { @@ -39,6 +41,17 @@ impl SessionService for SessionServiceImpl { self.booking.book_session(mentor_id, user_id, cmd).await } + async fn get_admin_sessions( + &self, + page: u64, + per_page: u64, + status_filter: Option, + ) -> Result, AppError> { + self.query + .get_admin_sessions(page, per_page, status_filter) + .await + } + async fn get_mentor_sessions( &self, mentor_id: String, diff --git a/imphnen-dimentorin/src/sessions/domain/mod.rs b/imphnen-dimentorin/src/sessions/domain/mod.rs index 341e5ff..d03b153 100644 --- a/imphnen-dimentorin/src/sessions/domain/mod.rs +++ b/imphnen-dimentorin/src/sessions/domain/mod.rs @@ -7,7 +7,8 @@ pub use repository::SessionRepository; pub use service::SessionService; pub use session::SessionEntity; pub use session_types::{ - AvailabilitySlot, BookSessionCommand, BookedSession, MentorAvailability, - MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, - SessionList, SessionListItem, UpdateSessionStatusCommand, UpdatedSessionStatus, + AdminSessionListItem, AvailabilitySlot, BookSessionCommand, BookedSession, + MentorAvailability, MentorStats, SessionDetail, SessionFeedbackCommand, + SessionFeedbackResult, SessionList, SessionListItem, UpdateSessionStatusCommand, + UpdatedSessionStatus, }; diff --git a/imphnen-dimentorin/src/sessions/domain/service.rs b/imphnen-dimentorin/src/sessions/domain/service.rs index ec9cf9b..134303b 100644 --- a/imphnen-dimentorin/src/sessions/domain/service.rs +++ b/imphnen-dimentorin/src/sessions/domain/service.rs @@ -1,10 +1,11 @@ use super::session_types::{ - BookSessionCommand, BookedSession, MentorAvailability, MentorStats, - SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, SessionList, - UpdateSessionStatusCommand, UpdatedSessionStatus, + AdminSessionListItem, BookSessionCommand, BookedSession, MentorAvailability, + MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, + SessionList, UpdateSessionStatusCommand, UpdatedSessionStatus, }; use async_trait::async_trait; use imphnen_utils::AppError; +use paginator_utils::PaginatorResponse; #[async_trait] pub trait SessionService: Send + Sync { @@ -15,6 +16,15 @@ pub trait SessionService: Send + Sync { cmd: BookSessionCommand, ) -> Result; + /// Admin: list ALL sessions across every mentor/mentee, paginated, + /// with resolved names + feedback. Guards on Administrator permission. + async fn get_admin_sessions( + &self, + page: u64, + per_page: u64, + status_filter: Option, + ) -> Result, AppError>; + async fn get_mentor_sessions( &self, mentor_id: String, diff --git a/imphnen-dimentorin/src/sessions/domain/session_types.rs b/imphnen-dimentorin/src/sessions/domain/session_types.rs index 74170c6..ffa6bb0 100644 --- a/imphnen-dimentorin/src/sessions/domain/session_types.rs +++ b/imphnen-dimentorin/src/sessions/domain/session_types.rs @@ -39,6 +39,29 @@ pub struct SessionList { pub total: usize, } +/// Admin backoffice view of a session: enriches the list item with +/// mentor/mentee names, meeting link, and feedback so one endpoint +/// powers Session Management, Feedback & Review, and the dashboard. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct AdminSessionListItem { + pub id: String, + pub mentor_id: String, + pub mentor_fullname: Option, + pub mentee_id: String, + pub mentee_fullname: Option, + pub mentee_email: Option, + pub topic: String, + pub scheduled_at: String, + pub duration_minutes: i32, + pub meeting_link: Option, + pub session_type: String, + pub status: String, + pub rating: Option, + pub feedback: Option, + pub feedback_submitted_at: Option, + pub created_at: String, +} + pub struct SessionDetail { pub id: String, pub mentor_id: String, diff --git a/imphnen-dimentorin/src/sessions/infrastructure/http/dto/mod.rs b/imphnen-dimentorin/src/sessions/infrastructure/http/dto/mod.rs index 8ee13fc..9c607a6 100644 --- a/imphnen-dimentorin/src/sessions/infrastructure/http/dto/mod.rs +++ b/imphnen-dimentorin/src/sessions/infrastructure/http/dto/mod.rs @@ -5,8 +5,8 @@ pub use request::{ BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto, }; pub use response::{ - AvailabilitySlotDto, BookSessionResponseDto, MentorAvailabilityDto, - MentorStatsDto, SessionDetailDto, SessionFeedbackResponseDto, - SessionListItemDto, SessionListResponseDto, + AdminSessionListItemDto, AvailabilitySlotDto, BookSessionResponseDto, + MentorAvailabilityDto, MentorStatsDto, SessionDetailDto, + SessionFeedbackResponseDto, SessionListItemDto, SessionListResponseDto, UpdateSessionStatusResponseDto, }; diff --git a/imphnen-dimentorin/src/sessions/infrastructure/http/dto/response.rs b/imphnen-dimentorin/src/sessions/infrastructure/http/dto/response.rs index e5f12a0..aec33ad 100644 --- a/imphnen-dimentorin/src/sessions/infrastructure/http/dto/response.rs +++ b/imphnen-dimentorin/src/sessions/infrastructure/http/dto/response.rs @@ -1,6 +1,7 @@ use crate::sessions::domain::{ - AvailabilitySlot, BookedSession, MentorAvailability, MentorStats, SessionDetail, - SessionFeedbackResult, SessionList, SessionListItem, UpdatedSessionStatus, + AdminSessionListItem, AvailabilitySlot, BookedSession, MentorAvailability, + MentorStats, SessionDetail, SessionFeedbackResult, SessionList, SessionListItem, + UpdatedSessionStatus, }; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -229,3 +230,46 @@ impl From for SessionFeedbackResponseDto { } } } + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct AdminSessionListItemDto { + pub id: String, + pub mentor_id: String, + pub mentor_fullname: Option, + pub mentee_id: String, + pub mentee_fullname: Option, + pub mentee_email: Option, + pub topic: String, + pub scheduled_at: String, + pub duration_minutes: i32, + pub meeting_link: Option, + pub session_type: String, + pub status: String, + pub rating: Option, + pub feedback: Option, + pub feedback_submitted_at: Option, + pub created_at: String, +} + +impl From for AdminSessionListItemDto { + fn from(s: AdminSessionListItem) -> Self { + Self { + id: s.id, + mentor_id: s.mentor_id, + mentor_fullname: s.mentor_fullname, + 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, + meeting_link: s.meeting_link, + session_type: s.session_type, + status: s.status, + rating: s.rating, + feedback: s.feedback, + feedback_submitted_at: s.feedback_submitted_at, + created_at: s.created_at, + } + } +} diff --git a/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/mod.rs b/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/mod.rs index 662ea59..7412ed2 100644 --- a/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/mod.rs +++ b/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/mod.rs @@ -5,5 +5,6 @@ pub use mutation_handlers::{ post_book_session, post_submit_feedback, put_update_session_status, }; pub use query_handlers::{ - get_mentor_availability, get_mentor_sessions, get_mentor_stats, get_my_sessions, + get_admin_sessions, get_mentor_availability, get_mentor_sessions, get_mentor_stats, + get_my_sessions, }; diff --git a/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/query_handlers.rs b/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/query_handlers.rs index b12487f..22a7c2b 100644 --- a/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/query_handlers.rs +++ b/imphnen-dimentorin/src/sessions/infrastructure/http/handlers/query_handlers.rs @@ -1,4 +1,4 @@ -use super::super::dto::{MentorAvailabilityDto, MentorStatsDto, SessionListResponseDto}; +use super::super::dto::{AdminSessionListItemDto, MentorAvailabilityDto, MentorStatsDto, SessionListResponseDto}; use crate::sessions::domain::SessionService; use axum::{ extract::{Extension, Path, Query}, @@ -8,6 +8,7 @@ use axum::{ use imphnen_libs::decode_access_token; use imphnen_utils::AppError; use imphnen_utils::{ApiSuccess, extract_email}; +use paginator_axum::PaginationQuery; use serde::Deserialize; use std::sync::Arc; @@ -118,3 +119,41 @@ pub async fn get_my_sessions( ); Ok(ApiSuccess(resp)) } + +#[utoipa::path( + get, + path = "/v1/dimentorin/admin/sessions", + tag = "sessions", + security(("Bearer" = [])), + params( + ("page" = Option, Query, description = "Page number"), + ("per_page" = Option, Query, description = "Items per page"), + ("status" = Option, Query, description = "Filter by status"), + ), + responses( + (status = 200, description = "All sessions (admin) retrieved successfully", body = AdminSessionListItemDto), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden"), + ) +)] +pub async fn get_admin_sessions( + headers: HeaderMap, + Extension(state): Extension, + Extension(service): Extension>, + PaginationQuery(params): PaginationQuery, + Query(filter): Query, +) -> Result { + imphnen_iam::require_permissions!( + headers, + state, + [imphnen_entities::PermissionsEnum::Administrator], + { + let page = u64::from(params.page.max(1)); + let per_page = u64::from(params.per_page.clamp(1, 100)); + let resp = service + .get_admin_sessions(page, per_page, filter.status) + .await?; + Ok(imphnen_utils::ApiPaginated(resp)) + } + ) +} diff --git a/imphnen-dimentorin/src/sessions/infrastructure/http/routes.rs b/imphnen-dimentorin/src/sessions/infrastructure/http/routes.rs index db03f13..124461e 100644 --- a/imphnen-dimentorin/src/sessions/infrastructure/http/routes.rs +++ b/imphnen-dimentorin/src/sessions/infrastructure/http/routes.rs @@ -1,7 +1,6 @@ use super::handlers::{ - get_mentor_availability, get_mentor_sessions, get_mentor_stats, - get_my_sessions, post_book_session, post_submit_feedback, - put_update_session_status, + get_admin_sessions, get_mentor_availability, get_mentor_sessions, get_mentor_stats, + get_my_sessions, post_book_session, post_submit_feedback, put_update_session_status, }; use crate::sessions::application::SessionServiceImpl; use crate::sessions::domain::SessionService; @@ -42,6 +41,7 @@ pub fn sessions_protected_routes( ) .route("/sessions/{id}/feedback/create", post(post_submit_feedback)) .route("/sessions/me", get(get_my_sessions)) + .route("/admin/sessions", get(get_admin_sessions)) .layer(Extension(service)) .layer(Extension((*state).clone())) }