feat(dimentorin): admin sessions endpoint untuk backoffice (GET /admin/sessions, paginated + nama + feedback)
This commit is contained in:
@@ -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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user