use std::sync::Arc; use axum::{ Extension, Json, extract::{Path, State}, http::StatusCode, response::IntoResponse, }; use serde_json::json; use sqlx::{Row, query}; use crate::{ AppState, models::{FilteredUser, TicketCreateScheme, TicketResponse, TicketUpdateScheme}, }; /// Creates a new support ticket. /// /// Associates the ticket with the authenticated [`FilteredUser`] and sets the current timestamp. /// Converts the [`TicketCreateScheme`] request into a database record. /// Tickets are automatically created with "open" status. /// /// # Arguments /// - `Extension(user)`: Authenticated [`FilteredUser`] (extracted from JWT token via middleware) /// - `State(data)`: Application state containing [`AppState`] for database access /// - `Json(body)`: [`TicketCreateScheme`] containing ticket details (category, subject, description, room) /// /// # Returns /// - `200 OK` on successful creation /// - `500 Internal Server Error` if database insertion fails /// /// # Database Fields Set Automatically /// - `user_id`: From authenticated user /// - `status`: Defaults to "open" /// - `date`: Current UTC timestamp pub async fn create_ticket( Extension(user): Extension, State(data): State>, Json(body): Json, ) -> Result)> { let query = query( r#"INSERT INTO tickets (category, description, betreff, room, user_id) VALUES ($1, $2, $3, $4, $5)"#, ) .bind(body.category.to_string()) .bind(body.description.to_string()) .bind(body.betreff.to_string()) .bind(body.room) .bind(user.id) .execute(&data.db) .await; if let Err(err) = query { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"status": "error", "message": format!("{:?}", err),})), )); } let response_status = serde_json::json!({"status": "success"}); Ok(Json(response_status)) } /// Deletes a ticket by ID. /// /// Only admins can delete tickets (enforced by middleware). Removes the [`TicketResponse`] and associated data from the database. /// /// # Arguments /// - `Path(id)`: Ticket ID to delete, extracted from URL path /// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Returns /// - `204 No Content` on successful deletion /// - `404 Not Found` if ticket doesn't exist /// - `500 Internal Server Error` if database error occurs pub async fn delete_ticket( Path(id): Path, State(data): State>, ) -> Result)> { let query = sqlx::query(r#"DELETE FROM tickets WHERE id = $1"#) .bind(id) .execute(&data.db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"status": "error", "message": format!("{:?}", e)})), ) })?; if query.rows_affected() == 0 { let error_response = serde_json::json!({ "status": "error", "message": format!("Ticket with ID {} not found", id) }); return Err((StatusCode::NOT_FOUND, Json(error_response))); } Ok(StatusCode::NO_CONTENT) } /// Retrieves all non-archived tickets. /// /// Returns a list of all active [`TicketResponse`] objects with user information denormalized for easier rendering. /// Tickets are ordered by creation date (newest first). Joins with [`User`](crate::models::User) table to include creator information. /// /// # Arguments /// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Filtering /// - Excludes tickets with status "Archived" /// - Uses LEFT JOIN to include creator information from [`User`](crate::models::User) /// /// # Returns /// - `200 OK` with array of [`TicketResponse`] objects /// - `500 Internal Server Error` if database query fails /// /// # Example Response /// ```json /// [ /// { /// "id": 1, /// "category": "maintenance", /// "betreff": "Broken light", /// "description": "Ceiling light not working", /// "room": 101, /// "status": "open", /// "date": "2024-01-15T10:30:00Z", /// "user_id": 5, /// "user_first_name": "John", /// "user_last_name": "Doe" /// } /// ] /// ``` pub async fn get_tickets( State(data): State>, ) -> Result)> { // Query tickets with denormalized user info, excluding archived tickets let tickets = sqlx::query( r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name FROM tickets t LEFT JOIN users u ON t.user_id = u.id ORDER BY t.date DESC"#, ) .fetch_all(&data.db) .await .map_err(|e| { let error_response = serde_json::json!({ "status": "error", "message": format!("Database error: {}", e), }); (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)) })?; // Transform raw database rows into TicketResponse structs let ticket_response: Vec = tickets .iter() .map(|row| TicketResponse { id: row.get("id"), category: row.get("category"), betreff: row.get("betreff"), description: row.get("description"), room: row.get("room"), status: row.get("status"), date: row.get("date"), user_id: row.get("user_id"), user_first_name: row.get("first_name"), user_last_name: row.get("last_name"), }) .collect(); let json_response = serde_json::json!(ticket_response); Ok(Json(json_response)) } /// Retrieves a specific ticket by ID. /// /// Includes full ticket details and denormalized user information (creator name). /// Returns a [`TicketResponse`] with all metadata by joining with [`User`](crate::models::User) table. /// /// # Arguments /// - `Path(id)`: Ticket ID to retrieve, extracted from URL path /// - `State(data)`: Application state containing [`AppState`] for database access /// /// # Returns /// - `200 OK` with [`TicketResponse`] object /// - `404 Not Found` if ticket doesn't exist /// - `500 Internal Server Error` if database error occurs /// /// # Example Response /// ```json /// { /// "id": 1, /// "category": "maintenance", /// "betreff": "Broken light in room 101", /// "description": "The ceiling light is not working", /// "room": 101, /// "status": "open", /// "date": "2024-01-15T10:30:00Z", /// "user_id": 5, /// "user_first_name": "John", /// "user_last_name": "Doe" /// } /// ``` pub async fn get_ticket_by_id( Path(id): Path, State(data): State>, ) -> Result)> { let query = sqlx::query( r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name FROM tickets t LEFT JOIN users u ON t.user_id = u.id WHERE t.id = $1"#, ) .bind(id) .fetch_one(&data.db) .await; match query { Ok(row) => { let ticket_response = TicketResponse { id: row.get("id"), category: row.get("category"), betreff: row.get("betreff"), description: row.get("description"), room: row.get("room"), status: row.get("status"), date: row.get("date"), user_id: row.get("user_id"), user_first_name: row.get("first_name"), user_last_name: row.get("last_name"), }; let response = serde_json::json!(ticket_response); Ok(Json(response)) } Err(sqlx::Error::RowNotFound) => { let error_response = serde_json::json!({ "status": "fail", "message": format!("Ticket with ID {} not found", id) }); Err((StatusCode::NOT_FOUND, Json(error_response))) } Err(e) => { Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"status": "error", "message": format!("{:?}", e)})), )) } } } /// Updates a ticket's status. /// /// Only admins can update ticket status (enforced by middleware). Applies [`TicketUpdateScheme`] to modify the [`TicketResponse`]. /// This is typically used to transition tickets through their lifecycle (open → in_progress → resolved → archived). /// /// # Arguments /// - `Path(id)`: Ticket ID to update, extracted from URL path /// - `State(data)`: Application state containing [`AppState`] for database access /// - `Json(body)`: [`TicketUpdateScheme`] update payload containing new status /// /// # Returns /// - `200 OK` with updated [`TicketResponse`] /// - `500 Internal Server Error` if ticket not found or database error /// /// # Typical Status Flow /// - `open`: Initial state, waiting for action /// - `in_progress`: Currently being worked on /// - `resolved`: Issue fixed /// - `archived`: Closed/hidden from normal view pub async fn edit_ticket( Path(id): Path, State(data): State>, Json(body): Json, ) -> Result)> { // Update the ticket status let update_result = sqlx::query(r#"UPDATE tickets SET status = $1 WHERE id = $2"#) .bind(body.status.to_owned()) .bind(id) .execute(&data.db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"status": "error", "message": format!("{:?}", e)})), ) })?; if update_result.rows_affected() == 0 { let error_response = serde_json::json!({ "status": "error", "message": format!("Ticket with ID {} not found", id) }); return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))); } // Fetch and return the updated ticket let updated_ticket = sqlx::query( r#"SELECT t.id, t.category, t.betreff, t.description, t.room, t.status, t.date, t.user_id, u.first_name, u.last_name FROM tickets t LEFT JOIN users u ON t.user_id = u.id WHERE t.id = $1"#, ) .bind(id) .fetch_one(&data.db) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"status": "error", "message": format!("{:?}", e)})), ) })?; let ticket_response = TicketResponse { id: updated_ticket.get("id"), category: updated_ticket.get("category"), betreff: updated_ticket.get("betreff"), description: updated_ticket.get("description"), room: updated_ticket.get("room"), status: updated_ticket.get("status"), date: updated_ticket.get("date"), user_id: updated_ticket.get("user_id"), user_first_name: updated_ticket.get("first_name"), user_last_name: updated_ticket.get("last_name"), }; let response = serde_json::json!({ "ticket": ticket_response, "status": "success" }); Ok(Json(response)) }