use std::collections::HashSet; use chrono_tz::Europe::Berlin; use gloo_net::http::Request; use serde::{Deserialize, Serialize}; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use web_sys::HtmlSelectElement; use yew::prelude::*; use yew_router::prelude::*; /// A hardcoded list of valid room numbers for ticket submissions. /// /// This array contains `i16` values representing valid rooms. Negative numbers typically /// denote rooms prefixed with 'K' (e.g., -1 for K1), and numbers greater than or equal to 1000 /// denote rooms prefixed with 'D' (e.g., 1001 for D1). /// /// This list is used for client-side validation of room input during ticket creation. const VALID_ROOMS: &[i16] = &[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 32, 34, 36, 37, 39, 41, 49, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 118, 120, 121, 122, 123, 124, 126, 127, 128, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 150, 152, 153, 154, 155, 156, 157, 158, 159, 160, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 214, 1001, 1002, 1003, -1, -2, -3, -4, -5, -6, -7, -8, ]; /// Data transfer object (DTO) for creating a new ticket. /// /// This struct defines the payload sent to the backend when a user submits a new ticket. /// It includes all necessary information for initial ticket creation. /// /// # Fields /// - `category`: The category of the ticket (e.g., "Internet", "Hardware"). /// - `betreff`: The subject or brief summary of the ticket. /// - `description`: A detailed description of the issue or request. /// - `room`: The room number where the issue is located. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TicketCreateScheme { pub category: String, pub betreff: String, pub description: String, pub room: i16, } /// Data transfer object (DTO) for updating a ticket's status. /// /// This struct is used when sending a request to the backend to change the status /// of an existing ticket. /// /// # Fields /// - `status`: The new status of the ticket (e.g., "ToDo", "InProgress", "Completed", "Archived"). #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TicketUpdateScheme { pub status: String, } /// Represents a complete ticket entity retrieved from the backend. /// /// This struct holds all details of a ticket, including its metadata, content, /// and associated user information. /// /// # Fields /// - `id`: The unique identifier of the ticket. /// - `category`: The category of the ticket. /// - `betreff`: The subject of the ticket. /// - `description`: The detailed description of the ticket. /// - `room`: The room number associated with the ticket. /// - `status`: The current status of the ticket. /// - `date`: The creation date and time of the ticket in UTC. /// - `user_id`: The ID of the user who created the ticket. /// - `user_first_name`: The first name of the user who created the ticket. /// - `user_last_name`: The last name of the user who created the ticket. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Ticket { pub id: i32, pub category: String, pub betreff: String, pub description: String, pub room: i16, pub status: String, pub date: chrono::DateTime, pub user_id: i16, pub user_first_name: String, pub user_last_name: String, } /// Properties for components that display a single ticket. /// /// This struct is used to pass the ID of a specific ticket to components /// that need to fetch or display its details. /// /// # Fields /// - `id`: The unique identifier of the ticket to be displayed. #[derive(Properties, PartialEq)] pub struct TicketProps { pub id: i32, } /// Represents the essential information of the currently authenticated user. /// /// This struct is used to hold a subset of user data relevant for client-side /// logic, such as determining if the user is an administrator or filtering /// tickets by their ID. /// /// # Fields /// - `id`: An `Option` holding the unique ID of the active user. `None` if not authenticated or ID is unavailable. /// - `is_admin`: A boolean indicating if the active user has administrator privileges. #[derive(Clone, Debug, PartialEq)] pub struct ActiveUser { pub id: Option, pub is_admin: bool, } /// Represents a standardized error response from the API. /// /// This struct is used to deserialize error messages returned by the backend API, /// providing a consistent way to handle and display error information to the user. /// /// # Fields /// - `message`: A descriptive error message. /// - `_status`: (Ignored) The HTTP status code or a status string from the API. #[derive(Deserialize, Debug)] struct ApiError { message: String, _status: String, } /// A component that provides a form for users to submit new tickets. /// /// This component allows users to input details such as category, subject (`betreff`), /// description, and room number for a new support ticket. It includes client-side /// validation for the room number and interacts with the backend API to create the ticket. /// /// # State /// Uses `use_state` hooks to manage: /// - `category`, `betreff`, `description`: Values of the form input fields. /// - `room`, `room_input`: The parsed and raw room numbers, respectively. /// - `status`: To display messages about submission success or errors. /// /// # Room Input Handling /// The `room_change` callback handles parsing the room input: /// - Supports numerical room numbers (e.g., "101"). /// - Supports 'K' prefixed rooms (e.g., "K1" parses to -1). /// - Supports 'D' prefixed rooms (e.g., "D1" parses to 1001). /// - Validates the parsed room against the `VALID_ROOMS` constant. /// /// # Form Submission /// - Prevents default form submission behavior. /// - Performs validation for room number and presence in `VALID_ROOMS`. /// - Constructs a `TicketCreateScheme` payload. /// - Sends a POST request to `/api/tickets/create`. /// - Updates the `status` state based on the API response. /// /// # Example /// ```rust /// html! { /// /// } /// ``` #[component(SubmitTicket)] pub fn submit_ticket_component() -> Html { let category = use_state(|| "".to_string()); let betreff = use_state(|| "".to_string()); let description = use_state(|| "".to_string()); let room = use_state(|| None::); let room_input = use_state(|| "".to_string()); let status = use_state(|| None::); let valid_rooms: HashSet = VALID_ROOMS.iter().copied().collect(); let onsubmit = { let category = category.clone(); let betreff = betreff.clone(); let description = description.clone(); let room = room.clone(); let status = status.clone(); let valid_rooms = valid_rooms.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); if room.is_none() { status.set(Some("Ungültiger Raum".into())); return; } let category = (*category).clone(); let betreff = (*betreff).clone(); let description = (*description).clone(); let room = room.unwrap(); if !valid_rooms.contains(&room) { status.set(Some("Raum nicht erlaubt".into())); return; } let status = status.clone(); spawn_local(async move { let payload = TicketCreateScheme { category, betreff, description, room, }; let request = Request::post("/api/tickets/create") .header("Content-Type", "application/json") .credentials(web_sys::RequestCredentials::Include) .json(&payload) .expect("Failed to build request"); match request.send().await { Ok(response) if response.status() == 200 => { status.set(Some("Erfolgreich".into())) } Ok(response) => status.set(Some(format!("Fehler: {}", response.status()))), Err(err) => status.set(Some(format!("Netzwerkfehler: {}", err))), } }); }) }; let category_change = { let category = category.clone(); Callback::from(move |e: Event| { let input: web_sys::HtmlSelectElement = e.target_unchecked_into(); category.set(input.value()); }) }; let betreff_change = { let betreff = betreff.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); betreff.set(input.value()); }) }; let description_change = { let description = description.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); description.set(input.value()); }) }; let room_change = { let room = room.clone(); let room_input = room_input.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); let raw = input.value(); room_input.set(raw.clone()); let parsed_value: Option = if raw.trim().is_empty() { None } else { let raw_trim = raw.trim(); if raw_trim.len() > 1 && (raw_trim.starts_with('K') || raw_trim.starts_with('k')) { match raw_trim[1..].parse::() { Ok(n) => Some(-n), Err(_) => None, } } else if raw_trim.len() > 1 && (raw_trim.starts_with('D') || raw_trim.starts_with('d')) { match raw_trim[1..].parse::() { Ok(n) => Some(1000 + n), Err(_) => None, } } else { raw_trim.parse::().ok() } }; if let Some(v) = parsed_value { if !valid_rooms.contains(&v) { room.set(None); } else { room.set(Some(v)); } } else { room.set(None); } }) }; let room_valid = (*room).is_some(); html! {