api/routes/modules/assignments/tickets/
get.rs1use crate::{
10 auth::AuthUser, response::ApiResponse,
11 routes::modules::assignments::tickets::common::{is_valid, TicketResponse, TicketWithUserResponse},
12};
13use axum::{
14 Extension,
15 extract::{Path, Query, State},
16 http::StatusCode,
17 response::{IntoResponse, Json},
18};
19use db::models::{tickets::{
20 Column as TicketColumn, Entity as TicketEntity, TicketStatus,
21}, user, user_module_role::{self, Role}};
22use db::models::user::{Entity as UserEntity};
23use migration::Expr;
24use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait};
25use serde::{Deserialize, Serialize};
26use util::state::AppState;
27
28pub async fn get_ticket(
90 State(app_state): State<AppState>,
91 Path((module_id, _, ticket_id)): Path<(i64, i64, i64)>,
92 Extension(AuthUser(claims)): Extension<AuthUser>,
93) -> impl IntoResponse {
94 let db = app_state.db();
95 let user_id = claims.sub;
96
97 if !is_valid(user_id, ticket_id, module_id, claims.admin, db).await {
98 return (
99 StatusCode::FORBIDDEN,
100 Json(ApiResponse::<()>::error("Forbidden")),
101 ).into_response();
102 }
103
104 match TicketEntity::find_by_id(ticket_id)
106 .find_also_related(UserEntity)
107 .one(db)
108 .await
109 {
110 Ok(Some((ticket, Some(user)))) => {
111 let response = TicketWithUserResponse {
112 ticket: ticket.into(),
113 user: user.into(),
114 };
115 (
116 StatusCode::OK,
117 Json(ApiResponse::success(response, "Ticket with user retrieved")),
118 ).into_response()
119 }
120 Ok(Some((_ticket, None))) => (
121 StatusCode::INTERNAL_SERVER_ERROR,
122 Json(ApiResponse::<()>::error("User not found")),
123 ).into_response(),
124 Ok(None) => (
125 StatusCode::NOT_FOUND,
126 Json(ApiResponse::<()>::error("Ticket not found")),
127 ).into_response(),
128 Err(_) => (
129 StatusCode::INTERNAL_SERVER_ERROR,
130 Json(ApiResponse::<()>::error("Failed to retrieve ticket")),
131 ).into_response(),
132 }
133}
134
135#[derive(Debug, Deserialize)]
137pub struct FilterReq {
138 pub page: Option<i32>,
140 pub per_page: Option<i32>,
142 pub query: Option<String>,
144 pub status: Option<String>,
146 pub sort: Option<String>,
148}
149
150#[derive(Serialize)]
152pub struct FilterResponse {
153 pub tickets: Vec<TicketResponse>,
154 pub page: i32,
155 pub per_page: i32,
156 pub total: i32,
157}
158
159impl FilterResponse {
160 fn new(tickets: Vec<TicketResponse>, page: i32, per_page: i32, total: i32) -> Self {
161 Self {
162 tickets,
163 page,
164 per_page,
165 total,
166 }
167 }
168}
169
170async fn is_student(module_id: i64, user_id: i64, db: &DatabaseConnection) -> bool {
172 user_module_role::Entity::find()
173 .filter(user_module_role::Column::UserId.eq(user_id))
174 .filter(user_module_role::Column::ModuleId.eq(module_id))
175 .filter(user_module_role::Column::Role.eq(Role::Student))
176 .join(JoinType::InnerJoin, user_module_role::Relation::User.def())
177 .filter(user::Column::Admin.eq(false))
178 .one(db)
179 .await
180 .map(|opt| opt.is_some())
181 .unwrap_or(false)
182}
183
184pub async fn get_tickets(
233 Path((module_id, assignment_id)): Path<(i64, i64)>,
234 Extension(AuthUser(claims)): Extension<AuthUser>,
235 State(app_state): State<AppState>,
236 Query(params): Query<FilterReq>,
237) -> impl IntoResponse {
238 let db = app_state.db();
239 let user_id = claims.sub;
240
241 let page = params.page.unwrap_or(1).max(1);
242 let per_page = params.per_page.unwrap_or(20).min(100);
243
244 if let Some(sort_field) = ¶ms.sort {
245 let valid_fields = ["created_at", "updated_at", "status"];
246 for field in sort_field.split(',') {
247 let field = field.trim().trim_start_matches('-');
248 if !valid_fields.contains(&field) {
249 return (
250 StatusCode::BAD_REQUEST,
251 Json(ApiResponse::<FilterResponse>::error("Invalid field used")),
252 )
253 .into_response();
254 }
255 }
256 }
257
258 let mut condition = Condition::all().add(TicketColumn::AssignmentId.eq(assignment_id));
259
260 if is_student(module_id, user_id, db).await {
261 condition = condition.add(TicketColumn::UserId.eq(user_id));
262 }
263
264 if let Some(ref query) = params.query {
265 let pattern = format!("%{}%", query.to_lowercase());
266 condition = condition.add(
267 Condition::any()
268 .add(Expr::cust("LOWER(title)").like(&pattern))
269 .add(Expr::cust("LOWER(description)").like(&pattern)),
270 );
271 }
272
273 if let Some(ref status) = params.status {
274 match status.parse::<TicketStatus>() {
275 Ok(status_enum) => {
276 condition = condition.add(TicketColumn::Status.eq(status_enum));
277 }
278 Err(_) => {
279 return (
280 StatusCode::BAD_REQUEST,
281 Json(ApiResponse::<FilterResponse>::error("Invalid status value")),
282 )
283 .into_response();
284 }
285 }
286 }
287
288 let mut query = TicketEntity::find().filter(condition);
289
290 if let Some(sort_param) = ¶ms.sort {
291 for sort in sort_param.split(',') {
292 let (field, asc) = if sort.starts_with('-') {
293 (&sort[1..], false)
294 } else {
295 (sort, true)
296 };
297
298 query = match field {
299 "created_at" => {
300 if asc {
301 query.order_by_asc(TicketColumn::CreatedAt)
302 } else {
303 query.order_by_desc(TicketColumn::CreatedAt)
304 }
305 }
306 "updated_at" => {
307 if asc {
308 query.order_by_asc(TicketColumn::UpdatedAt)
309 } else {
310 query.order_by_desc(TicketColumn::UpdatedAt)
311 }
312 }
313 "status" => {
314 if asc {
315 query.order_by_asc(TicketColumn::Status)
316 } else {
317 query.order_by_desc(TicketColumn::Status)
318 }
319 }
320 _ => query,
321 };
322 }
323 }
324
325 let paginator = query.clone().paginate(db, per_page as u64);
326 let total = match paginator.num_items().await {
327 Ok(n) => n as i32,
328 Err(e) => {
329 eprintln!("Error counting tickets: {:?}", e);
330 return (
331 StatusCode::INTERNAL_SERVER_ERROR,
332 Json(ApiResponse::<FilterResponse>::error(
333 "Error counting tickets",
334 )),
335 )
336 .into_response();
337 }
338 };
339
340 match paginator.fetch_page((page - 1) as u64).await {
341 Ok(results) => {
342 let tickets: Vec<TicketResponse> =
343 results.into_iter().map(TicketResponse::from).collect();
344
345 let response = FilterResponse::new(tickets, page, per_page, total);
346 (
347 StatusCode::OK,
348 Json(ApiResponse::success(
349 response,
350 "Tickets retrieved successfully",
351 )),
352 )
353 .into_response()
354 }
355 Err(err) => {
356 eprintln!("Error fetching tickets: {:?}", err);
357 (
358 StatusCode::INTERNAL_SERVER_ERROR,
359 Json(ApiResponse::<FilterResponse>::error(
360 "Failed to retrieve tickets",
361 )),
362 )
363 .into_response()
364 }
365 }
366}