1use axum::{
10 Extension, Json,
11 extract::{Query, State},
12 http::StatusCode,
13 response::IntoResponse,
14};
15use db::models::{
16 assignment, module, tickets, user,
17 user_module_role::{self},
18};
19use migration::Expr;
20use sea_orm::{
21 ColumnTrait, Condition, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait
22};
23use serde::{Deserialize, Serialize};
24use util::state::AppState;
25use crate::{auth::AuthUser, response::ApiResponse};
26
27#[derive(Debug, Deserialize)]
29pub struct FilterReq {
30 pub page: Option<i32>,
32 pub per_page: Option<i32>,
34 pub query: Option<String>,
36 pub role: Option<String>,
38 pub year: Option<i32>,
40 pub status: Option<String>,
42 pub sort: Option<String>,
44}
45
46#[derive(Serialize)]
48pub struct TicketsResponse {
49 pub id: i64,
50 pub title: String,
51 pub status: String,
52 pub created_at: String,
53 pub updated_at: String,
54 pub module: ModuleResponse,
55 pub assignment: AssignmentResponse,
56 pub user: UserResponse,
57}
58
59#[derive(Serialize)]
61pub struct UserResponse {
62 pub id: i64,
63 pub username: String,
64}
65
66#[derive(Serialize)]
68pub struct AssignmentResponse {
69 pub id: i64,
70 pub name: String,
71}
72
73#[derive(Serialize)]
75pub struct ModuleResponse {
76 pub id: i64,
77 pub code: String,
78}
79
80#[derive(Serialize)]
82pub struct FilterResponse {
83 pub tickets: Vec<TicketsResponse>,
84 pub page: i32,
85 pub per_page: i32,
86 pub total: i32,
87}
88
89impl FilterResponse {
90 fn new(tickets: Vec<TicketsResponse>, page: i32, per_page: i32, total: i32) -> Self {
91 Self { tickets, page, per_page, total }
92 }
93}
94
95pub async fn get_my_tickets(
142 State(state): State<AppState>,
143 Extension(AuthUser(claims)): Extension<AuthUser>,
144 Query(params): Query<FilterReq>,
145) -> impl IntoResponse {
146 let db = state.db();
147 let user_id = claims.sub;
148
149 let page = params.page.unwrap_or(1).max(1);
150 let per_page = params.per_page.unwrap_or(20).min(100);
151
152 let requested_role = params.role.clone().unwrap_or_else(|| "student".to_string());
153
154 let memberships = user_module_role::Entity::find()
155 .filter(user_module_role::Column::UserId.eq(user_id))
156 .filter(user_module_role::Column::Role.eq(requested_role.clone()))
157 .all(db)
158 .await
159 .unwrap_or_default();
160
161 if memberships.is_empty() {
162 let response = FilterResponse::new(vec![], page, per_page, 0);
163 return (
164 StatusCode::OK,
165 Json(ApiResponse::success(response, "Tickets retrieved successfully")),
166 )
167 .into_response();
168 }
169
170 let module_ids: Vec<i64> = memberships.iter().map(|m| m.module_id).collect();
171
172 let assignments = assignment::Entity::find()
173 .filter(assignment::Column::ModuleId.is_in(module_ids.clone()))
174 .all(db)
175 .await
176 .unwrap_or_default();
177
178 if assignments.is_empty() {
179 let response = FilterResponse::new(vec![], page, per_page, 0);
180 return (
181 StatusCode::OK,
182 Json(ApiResponse::success(response, "Tickets retrieved successfully")),
183 )
184 .into_response();
185 }
186
187 let assignment_ids: Vec<i64> = assignments.iter().map(|a| a.id).collect();
188
189 let mut condition = Condition::all()
190 .add(tickets::Column::AssignmentId.is_in(assignment_ids.clone()));
191
192 if requested_role == "student" {
193 condition = condition.add(tickets::Column::UserId.eq(user_id));
194 } else {
195 condition = condition.add(tickets::Column::UserId.ne(user_id));
196 }
197
198 if let Some(year) = params.year {
199 condition = condition.add(Expr::col((module::Entity, module::Column::Year)).eq(year));
200 }
201
202 if let Some(ref q) = params.query {
203 let pattern = format!("%{}%", q.to_lowercase());
204 condition = condition.add(
205 Condition::any()
206 .add(Expr::cust("LOWER(tickets.title)").like(&pattern))
207 .add(Expr::cust("LOWER(module.code)").like(&pattern))
208 .add(Expr::cust("LOWER(assignment.name)").like(&pattern)),
209 );
210 if requested_role != "student" {
211 condition = condition.add(Expr::cust("LOWER(user.username)").like(&pattern));
212 }
213 }
214
215 if let Some(ref s) = params.status {
216 match s.parse::<tickets::TicketStatus>() {
217 Ok(st) => condition = condition.add(tickets::Column::Status.eq(st)),
218 Err(_) => {
219 return (
220 StatusCode::BAD_REQUEST,
221 Json(ApiResponse::<FilterResponse>::error("Invalid status value")),
222 )
223 .into_response();
224 }
225 }
226 }
227
228 let mut query = tickets::Entity::find()
229 .join(JoinType::InnerJoin, tickets::Relation::Assignment.def())
230 .join(JoinType::InnerJoin, assignment::Relation::Module.def())
231 .filter(condition);
232
233 if requested_role != "student" {
234 query = query.join(JoinType::InnerJoin, tickets::Relation::User.def());
235 }
236
237 if let Some(sort_param) = ¶ms.sort {
238 for sort in sort_param.split(',') {
239 let (field, asc) = if sort.starts_with('-') { (&sort[1..], false) } else { (sort, true) };
240 query = match field {
241 "created_at" => if asc { query.order_by_asc(tickets::Column::CreatedAt) } else { query.order_by_desc(tickets::Column::CreatedAt) },
242 _ => query,
243 };
244 }
245 } else {
246 query = query
247 .order_by_desc(tickets::Column::CreatedAt)
248 .order_by_asc(tickets::Column::Id);
249 }
250
251 let paginator = query.clone().paginate(db, per_page as u64);
252 let total = match paginator.num_items().await {
253 Ok(n) => n as i32,
254 Err(_) => {
255 return (
256 StatusCode::INTERNAL_SERVER_ERROR,
257 Json(ApiResponse::<FilterResponse>::error("Error counting tickets")),
258 )
259 .into_response();
260 }
261 };
262
263 match paginator.fetch_page((page - 1) as u64).await {
264 Ok(results) => {
265 let mut tickets_vec = Vec::new();
266 for t in results {
267 let a = assignment::Entity::find_by_id(t.assignment_id)
268 .one(db)
269 .await
270 .unwrap_or(None);
271 if a.is_none() { continue; }
272 let a = a.unwrap();
273
274 let m = module::Entity::find_by_id(a.module_id)
275 .one(db)
276 .await
277 .unwrap_or(None);
278 if m.is_none() { continue; }
279 let m = m.unwrap();
280
281 let u = user::Entity::find_by_id(t.user_id)
282 .one(db)
283 .await
284 .unwrap_or(None);
285
286 tickets_vec.push(TicketsResponse {
287 id: t.id,
288 title: t.title,
289 status: t.status.to_string(),
290 created_at: t.created_at.to_string(),
291 updated_at: t.updated_at.to_string(),
292 module: ModuleResponse { id: m.id, code: m.code },
293 assignment: AssignmentResponse { id: a.id, name: a.name },
294 user: UserResponse {
295 id: t.user_id,
296 username: u.map(|uu| uu.username).unwrap_or_default(),
297 },
298 });
299 }
300
301 let response = FilterResponse::new(tickets_vec, page, per_page, total);
302 (
303 StatusCode::OK,
304 Json(ApiResponse::success(response, "Tickets retrieved successfully")),
305 )
306 .into_response()
307 }
308 Err(_) => (
309 StatusCode::INTERNAL_SERVER_ERROR,
310 Json(ApiResponse::<FilterResponse>::error("Failed to retrieve tickets")),
311 )
312 .into_response(),
313 }
314}