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::{
|
use crate::sessions::domain::{
|
||||||
AvailabilitySlot, MentorAvailability, SessionDetail, SessionList, SessionListItem,
|
AdminSessionListItem, AvailabilitySlot, MentorAvailability, SessionDetail,
|
||||||
SessionRepository,
|
SessionList, SessionListItem, SessionRepository,
|
||||||
};
|
};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use imphnen_entities::seaorm::auth::mentors::{
|
use imphnen_entities::seaorm::auth::mentors::{
|
||||||
Column as MentorColumn, Entity as MentorsEntity,
|
Column as MentorColumn, Entity as MentorsEntity,
|
||||||
};
|
};
|
||||||
|
use imphnen_entities::seaorm::auth::users::{Column as UserColumn, Entity as UsersEntity};
|
||||||
use imphnen_utils::AppError;
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -17,6 +19,89 @@ pub struct SessionQueryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub async fn get_mentor_sessions(
|
||||||
&self,
|
&self,
|
||||||
mentor_id: String,
|
mentor_id: String,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use super::session_booking_service::SessionBookingService;
|
use super::session_booking_service::SessionBookingService;
|
||||||
use super::session_query_service::SessionQueryService;
|
use super::session_query_service::SessionQueryService;
|
||||||
use crate::sessions::domain::{
|
use crate::sessions::domain::{
|
||||||
BookSessionCommand, BookedSession, MentorAvailability, MentorStats,
|
AdminSessionListItem, BookSessionCommand, BookedSession, MentorAvailability,
|
||||||
SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, SessionList,
|
MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult,
|
||||||
SessionRepository, SessionService, UpdateSessionStatusCommand, UpdatedSessionStatus,
|
SessionList, SessionRepository, SessionService, UpdateSessionStatusCommand,
|
||||||
|
UpdatedSessionStatus,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use imphnen_utils::AppError;
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct SessionServiceImpl {
|
pub struct SessionServiceImpl {
|
||||||
@@ -39,6 +41,17 @@ impl SessionService for SessionServiceImpl {
|
|||||||
self.booking.book_session(mentor_id, user_id, cmd).await
|
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(
|
async fn get_mentor_sessions(
|
||||||
&self,
|
&self,
|
||||||
mentor_id: String,
|
mentor_id: String,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ pub use repository::SessionRepository;
|
|||||||
pub use service::SessionService;
|
pub use service::SessionService;
|
||||||
pub use session::SessionEntity;
|
pub use session::SessionEntity;
|
||||||
pub use session_types::{
|
pub use session_types::{
|
||||||
AvailabilitySlot, BookSessionCommand, BookedSession, MentorAvailability,
|
AdminSessionListItem, AvailabilitySlot, BookSessionCommand, BookedSession,
|
||||||
MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult,
|
MentorAvailability, MentorStats, SessionDetail, SessionFeedbackCommand,
|
||||||
SessionList, SessionListItem, UpdateSessionStatusCommand, UpdatedSessionStatus,
|
SessionFeedbackResult, SessionList, SessionListItem, UpdateSessionStatusCommand,
|
||||||
|
UpdatedSessionStatus,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use super::session_types::{
|
use super::session_types::{
|
||||||
BookSessionCommand, BookedSession, MentorAvailability, MentorStats,
|
AdminSessionListItem, BookSessionCommand, BookedSession, MentorAvailability,
|
||||||
SessionDetail, SessionFeedbackCommand, SessionFeedbackResult, SessionList,
|
MentorStats, SessionDetail, SessionFeedbackCommand, SessionFeedbackResult,
|
||||||
UpdateSessionStatusCommand, UpdatedSessionStatus,
|
SessionList, UpdateSessionStatusCommand, UpdatedSessionStatus,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use imphnen_utils::AppError;
|
use imphnen_utils::AppError;
|
||||||
|
use paginator_utils::PaginatorResponse;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SessionService: Send + Sync {
|
pub trait SessionService: Send + Sync {
|
||||||
@@ -15,6 +16,15 @@ pub trait SessionService: Send + Sync {
|
|||||||
cmd: BookSessionCommand,
|
cmd: BookSessionCommand,
|
||||||
) -> Result<BookedSession, AppError>;
|
) -> 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(
|
async fn get_mentor_sessions(
|
||||||
&self,
|
&self,
|
||||||
mentor_id: String,
|
mentor_id: String,
|
||||||
|
|||||||
@@ -39,6 +39,29 @@ pub struct SessionList {
|
|||||||
pub total: usize,
|
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 struct SessionDetail {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub mentor_id: String,
|
pub mentor_id: String,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ pub use request::{
|
|||||||
BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto,
|
BookSessionRequestDto, SessionFeedbackRequestDto, UpdateSessionStatusRequestDto,
|
||||||
};
|
};
|
||||||
pub use response::{
|
pub use response::{
|
||||||
AvailabilitySlotDto, BookSessionResponseDto, MentorAvailabilityDto,
|
AdminSessionListItemDto, AvailabilitySlotDto, BookSessionResponseDto,
|
||||||
MentorStatsDto, SessionDetailDto, SessionFeedbackResponseDto,
|
MentorAvailabilityDto, MentorStatsDto, SessionDetailDto,
|
||||||
SessionListItemDto, SessionListResponseDto,
|
SessionFeedbackResponseDto, SessionListItemDto, SessionListResponseDto,
|
||||||
UpdateSessionStatusResponseDto,
|
UpdateSessionStatusResponseDto,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::sessions::domain::{
|
use crate::sessions::domain::{
|
||||||
AvailabilitySlot, BookedSession, MentorAvailability, MentorStats, SessionDetail,
|
AdminSessionListItem, AvailabilitySlot, BookedSession, MentorAvailability,
|
||||||
SessionFeedbackResult, SessionList, SessionListItem, UpdatedSessionStatus,
|
MentorStats, SessionDetail, SessionFeedbackResult, SessionList, SessionListItem,
|
||||||
|
UpdatedSessionStatus,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
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,
|
post_book_session, post_submit_feedback, put_update_session_status,
|
||||||
};
|
};
|
||||||
pub use query_handlers::{
|
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 crate::sessions::domain::SessionService;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
@@ -8,6 +8,7 @@ use axum::{
|
|||||||
use imphnen_libs::decode_access_token;
|
use imphnen_libs::decode_access_token;
|
||||||
use imphnen_utils::AppError;
|
use imphnen_utils::AppError;
|
||||||
use imphnen_utils::{ApiSuccess, extract_email};
|
use imphnen_utils::{ApiSuccess, extract_email};
|
||||||
|
use paginator_axum::PaginationQuery;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -118,3 +119,41 @@ pub async fn get_my_sessions(
|
|||||||
);
|
);
|
||||||
Ok(ApiSuccess(resp))
|
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::{
|
use super::handlers::{
|
||||||
get_mentor_availability, get_mentor_sessions, get_mentor_stats,
|
get_admin_sessions, get_mentor_availability, get_mentor_sessions, get_mentor_stats,
|
||||||
get_my_sessions, post_book_session, post_submit_feedback,
|
get_my_sessions, post_book_session, post_submit_feedback, put_update_session_status,
|
||||||
put_update_session_status,
|
|
||||||
};
|
};
|
||||||
use crate::sessions::application::SessionServiceImpl;
|
use crate::sessions::application::SessionServiceImpl;
|
||||||
use crate::sessions::domain::SessionService;
|
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/{id}/feedback/create", post(post_submit_feedback))
|
||||||
.route("/sessions/me", get(get_my_sessions))
|
.route("/sessions/me", get(get_my_sessions))
|
||||||
|
.route("/admin/sessions", get(get_admin_sessions))
|
||||||
.layer(Extension(service))
|
.layer(Extension(service))
|
||||||
.layer(Extension((*state).clone()))
|
.layer(Extension((*state).clone()))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user