333 lines
11 KiB
Rust
333 lines
11 KiB
Rust
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<FilteredUser>,
|
|
State(data): State<Arc<AppState>>,
|
|
Json(body): Json<TicketCreateScheme>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
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<i32>,
|
|
State(data): State<Arc<AppState>>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
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<Arc<AppState>>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
// 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<TicketResponse> = 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<i32>,
|
|
State(data): State<Arc<AppState>>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
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<i32>,
|
|
State(data): State<Arc<AppState>>,
|
|
Json(body): Json<TicketUpdateScheme>,
|
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
// 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))
|
|
}
|