feat(dimentorin): admin sessions endpoint untuk backoffice (GET /admin/sessions, paginated + nama + feedback)

This commit is contained in:
asepharyana
2026-08-06 01:03:42 +07:00
parent 7a5c560347
commit b43396aa44
10 changed files with 237 additions and 21 deletions
@@ -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<String>,
) -> Result<PaginatorResponse<AdminSessionListItem>, 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::<Vec<_>>()))
.all(self.db.as_ref())
.await?
};
let name_by_id: std::collections::HashMap<Uuid, (Option<String>, 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,
@@ -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<String>,
) -> Result<PaginatorResponse<AdminSessionListItem>, AppError> {
self.query
.get_admin_sessions(page, per_page, status_filter)
.await
}
async fn get_mentor_sessions(
&self,
mentor_id: String,
@@ -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,
};
@@ -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<BookedSession, AppError>;
/// 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<String>,
) -> Result<PaginatorResponse<AdminSessionListItem>, AppError>;
async fn get_mentor_sessions(
&self,
mentor_id: String,
@@ -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<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 meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub feedback: Option<String>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
}
pub struct SessionDetail {
pub id: String,
pub mentor_id: String,
@@ -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,
};
@@ -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<SessionFeedbackResult> for SessionFeedbackResponseDto {
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AdminSessionListItemDto {
pub id: String,
pub mentor_id: String,
pub mentor_fullname: Option<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 meeting_link: Option<String>,
pub session_type: String,
pub status: String,
pub rating: Option<i32>,
pub feedback: Option<String>,
pub feedback_submitted_at: Option<String>,
pub created_at: String,
}
impl From<AdminSessionListItem> 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,
}
}
}
@@ -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,
};
@@ -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<u64>, Query, description = "Page number"),
("per_page" = Option<u64>, Query, description = "Items per page"),
("status" = Option<String>, 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<imphnen_libs::AppState>,
Extension(service): Extension<Arc<dyn SessionService>>,
PaginationQuery(params): PaginationQuery,
Query(filter): Query<SessionStatusFilter>,
) -> Result<impl IntoResponse, AppError> {
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))
}
)
}
@@ -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()))
}