There is a popup that tells the user simple ways to fix things and also increments a counter if it worked
853 lines
33 KiB
Rust
853 lines
33 KiB
Rust
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<chrono::Utc>,
|
|
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<i16>` 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<i16>,
|
|
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! {
|
|
/// <SubmitTicket />
|
|
/// }
|
|
/// ```
|
|
#[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::<i16>);
|
|
let room_input = use_state(|| "".to_string());
|
|
let status = use_state(|| None::<String>);
|
|
|
|
let valid_rooms: HashSet<i16> = VALID_ROOMS.iter().copied().collect();
|
|
|
|
{
|
|
let message = "Bevor sie zum Support weitergeleitet werden prüfen sie ob:
|
|
- Ob das Problem durch Neustarten gelößt wird
|
|
- Ob sie die richtigen Anmeldedaten genutzt habem
|
|
- Alle notwendigen Kabel eingesteckt sind
|
|
|
|
Wenn es funktioniert hat klicken sie bitte \"Abbrechen\""
|
|
.to_string();
|
|
use_effect(move || {
|
|
if let Some(win) = web_sys::window() {
|
|
let _ = if win.confirm_with_message(&message).unwrap() {
|
|
} else {
|
|
spawn_local(async move {
|
|
let _ = Request::post("/api/count")
|
|
.credentials(web_sys::RequestCredentials::Include)
|
|
.send()
|
|
.await;
|
|
});
|
|
let _ = win.location().set_href("/");
|
|
};
|
|
}
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
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("Invalid room".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("Room not allowed".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("Success".into())),
|
|
Ok(response) => status.set(Some(format!("Error: {}", response.status()))),
|
|
Err(err) => status.set(Some(format!("Network error: {}", 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<i16> = 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::<i16>() {
|
|
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::<i16>() {
|
|
Ok(n) => Some(1000 + n),
|
|
Err(_) => None,
|
|
}
|
|
} else {
|
|
match raw_trim.parse::<i16>() {
|
|
Ok(n) => Some(n),
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
};
|
|
|
|
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! {
|
|
<div class="form-container">
|
|
<div class="page-header">
|
|
<h1>{ "Create Ticket" }</h1>
|
|
</div>
|
|
<form {onsubmit}>
|
|
<label>{ "Betreff:" }
|
|
<input type="text" value={(*betreff).clone()} oninput={betreff_change}/>
|
|
</label>
|
|
<label>{ "Beschreibung:" }
|
|
<textarea value={(*description).clone()} oninput={description_change}/>
|
|
</label>
|
|
<label>{ "Kategorie:" }
|
|
<select value={(*category).clone()} onchange={category_change}>
|
|
<option value="Whiteboard Beamer">{ "Whiteboard Beamer" }</option>
|
|
<option value="Internet">{ "Internet" }</option>
|
|
<option value="iPad Koffer">{ "iPad Koffer" }</option>
|
|
<option value="Apple TV">{ "Apple TV" }</option>
|
|
<option value="Docu Cam">{ "Dokumenten Kamera" }</option>
|
|
<option value="Sonstiges">{ "Sonstiges" }</option>
|
|
</select>
|
|
</label>
|
|
<label>{ "Raum:" }
|
|
<input type="text" value={(*room_input).clone()} oninput={room_change}/>
|
|
</label>
|
|
{
|
|
if !room_valid {
|
|
html! {
|
|
<p class="alert error">{ "Ungültiger oder nicht erlaubter Raum (z. B. 101, K12, D5)" }</p>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
<button type="submit">{ "Send" }</button>
|
|
|
|
<Link<crate::Route> to={crate::Route::AllTickets}>{ "View All Tickets" }</Link<crate::Route>>
|
|
|
|
{
|
|
if let Some(s) = &*status {
|
|
html!{ <p class="alert success">{ s }</p> }
|
|
} else {
|
|
html!{}
|
|
}
|
|
}
|
|
|
|
</form>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// A component for displaying, updating, and deleting a single ticket by its ID.
|
|
///
|
|
/// This component fetches a specific ticket's details from the backend based on the
|
|
/// `id` provided in its `TicketProps`. It allows administrators to update the ticket's
|
|
/// status and delete the ticket.
|
|
///
|
|
/// # Props
|
|
/// - `id`: The `i32` ID of the ticket to fetch and display.
|
|
///
|
|
/// # State
|
|
/// Uses `use_state` hooks to manage:
|
|
/// - `ticket`: The fetched [`Ticket`] data, if available.
|
|
/// - `error`: Any error message encountered during API calls.
|
|
/// - `loading`: A boolean indicating if data is currently being fetched.
|
|
/// - `status`: The selected status for updating the ticket.
|
|
/// - `deleting`: A boolean indicating if a delete operation is in progress.
|
|
/// - `delete_error`: Any error message from a delete operation.
|
|
///
|
|
/// # Functionality
|
|
/// - **Data Fetching**: On component mount or `id` change, fetches ticket data from
|
|
/// `/api/tickets/:id`.
|
|
/// - **Status Update**: Provides a dropdown to change the ticket's status. Submitting the
|
|
/// form sends a PATCH request to `/api/tickets/:id` with a `TicketUpdateScheme` payload.
|
|
/// - **Ticket Deletion**: Includes a "Delete" button that, after user confirmation,
|
|
/// sends a DELETE request to `/api/tickets/:id`. On success, clears the ticket from display.
|
|
/// - **Error Handling**: Displays error messages for network issues, API errors, or parsing failures.
|
|
/// - **Date Formatting**: Displays the ticket creation date formatted to `Europe/Berlin` timezone.
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// html! {
|
|
/// <TicketByID id={123} />
|
|
/// }
|
|
/// ```
|
|
#[component(TicketByID)]
|
|
pub fn ticket_by_id_component(props: &TicketProps) -> Html {
|
|
let ticket = use_state(|| None::<Ticket>);
|
|
let error = use_state(|| None::<String>);
|
|
let loading = use_state(|| false);
|
|
let id = props.id;
|
|
let status = use_state(|| "".to_string());
|
|
|
|
{
|
|
let ticket = ticket.clone();
|
|
let error = error.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with(id, move |id_ref| {
|
|
loading.set(true);
|
|
let ticket = ticket.clone();
|
|
let error = error.clone();
|
|
let id = *id_ref;
|
|
spawn_local(async move {
|
|
let url = format!("/api/tickets/{}", id);
|
|
match Request::get(&url).send().await {
|
|
Ok(response) => {
|
|
let status = response.status();
|
|
if status == 200 {
|
|
match response.json::<Ticket>().await {
|
|
Ok(t) => ticket.set(Some(t)),
|
|
Err(err) => error.set(Some(format!("Parse error: {}", err))),
|
|
}
|
|
} else {
|
|
match response.json::<ApiError>().await {
|
|
Ok(ae) => error.set(Some(ae.message)),
|
|
Err(_) => {
|
|
if let Ok(text) = response.text().await {
|
|
error.set(Some(text));
|
|
} else {
|
|
error.set(Some(format!("Server error: {}", status)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
loading.set(false);
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
let onsubmit = {
|
|
let status = status.clone();
|
|
let id = id.clone();
|
|
let error = error.clone();
|
|
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
let form: web_sys::HtmlFormElement = e.target_unchecked_into();
|
|
let select_element = form
|
|
.query_selector("select[name=\"status\"]")
|
|
.ok()
|
|
.and_then(|opt| opt)
|
|
.and_then(|el| el.dyn_into::<HtmlSelectElement>().ok());
|
|
let new_status = select_element
|
|
.map(|s| s.value())
|
|
.unwrap_or_else(|| (*status).clone());
|
|
status.set(new_status.clone());
|
|
|
|
let id = id.clone();
|
|
let error = error.clone();
|
|
|
|
spawn_local(async move {
|
|
let value = TicketUpdateScheme { status: new_status };
|
|
|
|
let url = format!("/api/tickets/{}", id);
|
|
let request = Request::patch(&url)
|
|
.json(&value)
|
|
.expect("Failed to construct request");
|
|
|
|
match request.send().await {
|
|
Ok(response) if response.status() == 200 => error.set(Some("Success".into())),
|
|
Ok(response) => error.set(Some(format!("Error: {}", response.status()))),
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
let deleting = use_state(|| false);
|
|
let delete_error = use_state(|| None::<String>);
|
|
|
|
let ondelete = {
|
|
let deleting = deleting.clone();
|
|
let delete_error = delete_error.clone();
|
|
let ticket_state = ticket.clone();
|
|
let id = id;
|
|
|
|
Callback::from(move |e: MouseEvent| {
|
|
e.prevent_default();
|
|
if !web_sys::window()
|
|
.and_then(|w| w.confirm_with_message("Sicher löschen?").ok())
|
|
.unwrap_or(false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
deleting.set(true);
|
|
delete_error.set(None);
|
|
|
|
let deleting = deleting.clone();
|
|
let delete_error = delete_error.clone();
|
|
let ticket_state = ticket_state.clone();
|
|
|
|
spawn_local(async move {
|
|
let url = format!("/api/tickets/{}", id);
|
|
let request =
|
|
Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
|
|
|
|
match request.send().await {
|
|
Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
|
|
// remove local state or navigate away
|
|
ticket_state.set(None); // clears the shown item
|
|
}
|
|
Ok(resp) => {
|
|
let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into());
|
|
delete_error.set(Some(format!("HTTP {}: {}", resp.status(), txt)));
|
|
}
|
|
Err(err) => delete_error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
deleting.set(false);
|
|
});
|
|
})
|
|
};
|
|
|
|
if *loading {
|
|
html! {<p>{ "Loading" }</p>}
|
|
} else if let Some(e) = &*error {
|
|
html! { <p>{ format!("Error: {}", e) }</p> }
|
|
} else if let Some(t) = &*ticket {
|
|
html! {
|
|
<div>
|
|
<h1>{ format!("Ticket #{}", t.id) }</h1>
|
|
<p><strong>{ "Kategorie: " }</strong>{ &t.category }</p>
|
|
<p><strong>{ "Betreff: " }</strong>{ &t.betreff }</p>
|
|
<p><strong>{ "Beschreibung: " }</strong>{ &t.description }</p>
|
|
<p><strong>{ "Raum: " }</strong>{ t.room }</p>
|
|
<p><strong>{ "Datum: " }</strong>{
|
|
t.date.with_timezone(&Berlin).format("%d.%m.%Y %H:%M:%S").to_string()
|
|
}</p>
|
|
<p><strong>{ "Status: "}</strong>{ match t.status.as_str() {
|
|
"ToDo" => "Zu tun",
|
|
"InProgress" => "In Bearbeitung",
|
|
"Completed" => "Erledigt",
|
|
"Archived" => "Archiviert",
|
|
_ => "Ungültiger Status"
|
|
}}</p>
|
|
<p><strong>{ "Name: "}</strong>{ format!{"{} {}", t.user_first_name, t.user_last_name } }</p>
|
|
|
|
<form {onsubmit}>
|
|
<label>{ "Status ändern" }
|
|
<select name="status">
|
|
<option value="ToDo">{ "Zu tun" }</option>
|
|
<option value="InProgress">{ "In Bearbeitung" }</option>
|
|
<option value="Completed">{ "Erledigt" }</option>
|
|
<option value="Archived">{ "Archiviert" }</option>
|
|
</select>
|
|
</label>
|
|
<button type="submit">{ "Aktualisieren" }</button>
|
|
</form>
|
|
|
|
<button onclick={ondelete} disabled={*deleting}>
|
|
{if *deleting {"Löschen..."} else {"Löschen"}}
|
|
</button>
|
|
|
|
<Link<crate::Route> to={crate::Route::AllTickets}>{ "Zurück zur Ticketübersicht" }</Link<crate::Route>>
|
|
if let Some(err) = &*delete_error {
|
|
<p style="color:red">{ err.clone() }</p>
|
|
}
|
|
</div>
|
|
}
|
|
} else {
|
|
html! { <p>{ "No ticket found." }</p> }
|
|
}
|
|
}
|
|
|
|
/// A component for fetching and displaying a list of all tickets.
|
|
///
|
|
/// This component retrieves all tickets from the backend and presents them as a list.
|
|
/// It dynamically filters the displayed tickets based on the current user's role:
|
|
/// administrators see all tickets, while regular users only see tickets they created.
|
|
///
|
|
/// # State
|
|
/// Uses `use_state` hooks to manage:
|
|
/// - `tickets`: A vector of `Ticket` structs to store the fetched tickets.
|
|
/// - `error`: Any error message encountered during API calls.
|
|
/// - `loading`: A boolean indicating if data is currently being fetched.
|
|
/// - `user`: An `ActiveUser` struct holding the current user's ID and admin status.
|
|
///
|
|
/// # Functionality
|
|
/// - **Fetch Tickets**: On component mount, fetches all tickets from `/api/tickets`.
|
|
/// - **Fetch Current User**: Concurrently fetches the current user's details from
|
|
/// `/api/users/current` to determine their `user_id` and `is_admin` status.
|
|
/// - **Conditional Display**:
|
|
/// - If `loading` is true, displays "Loading...".
|
|
/// - If an `error` occurs, displays the error message.
|
|
/// - Otherwise, renders a list of tickets.
|
|
/// - **Filtering**:
|
|
/// - Administrators (`user.is_admin = true`) see all tickets.
|
|
/// - Regular users (`user.is_admin = false`) only see tickets where `t.user_id == user.id`.
|
|
/// - **Navigation**: Each ticket in the list is a link to [`crate::Route::TicketById`]
|
|
/// for viewing individual ticket details.
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// html! {
|
|
/// <AllTickets />
|
|
/// }
|
|
/// ```
|
|
#[component(AllTickets)]
|
|
pub fn all_tickets_component() -> Html {
|
|
let tickets = use_state(|| Vec::<Ticket>::new());
|
|
let error = use_state(|| None::<String>);
|
|
let loading = use_state(|| false);
|
|
let user = use_state(|| ActiveUser {
|
|
id: None,
|
|
is_admin: false,
|
|
});
|
|
|
|
{
|
|
let tickets = tickets.clone();
|
|
let error = error.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with((), move |_| {
|
|
loading.set(true);
|
|
spawn_local(async move {
|
|
let url = format!("/api/tickets");
|
|
match Request::get(&url).send().await {
|
|
Ok(response) if response.status() == 200 => {
|
|
match response.json::<Vec<Ticket>>().await {
|
|
Ok(t) => tickets.set(t),
|
|
Err(e) => error.set(Some(format!("parse error: {}", e))),
|
|
}
|
|
}
|
|
Ok(response) => {
|
|
if let Ok(text) = response.text().await {
|
|
error.set(Some(text));
|
|
} else {
|
|
error.set(Some(format!("status {}", response.status())));
|
|
}
|
|
}
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
loading.set(false);
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
{
|
|
let user = user.clone();
|
|
use_effect_with((), move |_| {
|
|
let user = user.clone();
|
|
spawn_local(async move {
|
|
if let Ok(response) = Request::get("/api/users/current")
|
|
.credentials(web_sys::RequestCredentials::Include)
|
|
.send()
|
|
.await
|
|
{
|
|
if response.status() == 200 {
|
|
if let Ok(json) = response.json::<serde_json::Value>().await {
|
|
let id = json
|
|
.get("data")
|
|
.and_then(|d| d.get("id"))
|
|
.and_then(|v| v.as_i64())
|
|
.and_then(|n| i16::try_from(n).ok());
|
|
let is_admin = json
|
|
.get("data")
|
|
.and_then(|d| d.get("is_admin"))
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
user.set(ActiveUser { id, is_admin });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
if *loading {
|
|
html! {<p>{ "Loading" }</p>}
|
|
} else if let Some(e) = &*error {
|
|
html! { <p>{ format!("Error: {}", e) }</p> }
|
|
} else {
|
|
html! {
|
|
<div>
|
|
<div class="page-header">
|
|
<h1>{ "All Tickets" }</h1>
|
|
</div>
|
|
<ul class="ticket-list">
|
|
{ for tickets.iter().filter(|t| t.status != "Archived" && (if user.is_admin { true } else if let Some(uid) = user.id { t.user_id == uid } else { false })).map(|t| {
|
|
let status_class = match t.status.as_str() {
|
|
"ToDo" => "To-Do",
|
|
"InProgress" => "InProgress",
|
|
"Completed" => "Completed",
|
|
"Archived" => "Archived",
|
|
_ => "To-Do"
|
|
};
|
|
html! {
|
|
<li key={t.id.to_string()} class={status_class}>
|
|
<Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{}", t.betreff) }</h3></Link<crate::Route>>
|
|
<p>{ &t.description }</p>
|
|
</li>
|
|
}
|
|
})}
|
|
</ul>
|
|
<div class="ticket-list-actions">
|
|
<Link<crate::Route> to={crate::Route::Ticket}>{ "Zurück zur Startseite" }</Link<crate::Route>>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A component for fetching and displaying a list of archived tickets.
|
|
///
|
|
/// This component retrieves all tickets from the backend and presents them as a list,
|
|
/// filtered to only show those with "Archived" status.
|
|
///
|
|
/// # State
|
|
/// Uses `use_state` hooks to manage:
|
|
/// - `tickets`: A vector of `Ticket` structs to store the fetched tickets.
|
|
/// - `error`: Any error message encountered during API calls.
|
|
/// - `loading`: A boolean indicating if data is currently being fetched.
|
|
/// - `user`: An `ActiveUser` struct holding the current user's ID and admin status.
|
|
///
|
|
/// # Functionality
|
|
/// - **Fetch Tickets**: On component mount, fetches all tickets from `/api/tickets`.
|
|
/// - **Fetch Current User**: Concurrently fetches the current user's details from
|
|
/// `/api/users/current` to determine their `user_id` and `is_admin` status.
|
|
/// - **Conditional Display**:
|
|
/// - If `loading` is true, displays "Loading...".
|
|
/// - If an `error` occurs, displays the error message.
|
|
/// - Otherwise, renders a list of tickets.
|
|
/// - **Filtering**:
|
|
/// - Only tickets with `t.status == "Archived"` are displayed.
|
|
/// - If the user is an admin, all archived tickets are shown.
|
|
/// - If the user is not an admin, only their own archived tickets are shown.
|
|
/// - **Navigation**: Each ticket in the list is a link to [`crate::Route::TicketById`]
|
|
/// for viewing individual ticket details.
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// html! {
|
|
/// <ArchivedTickets />
|
|
/// }
|
|
/// ```
|
|
#[component(ArchivedTickets)]
|
|
pub fn archived_tickets_component() -> Html {
|
|
let tickets = use_state(|| Vec::<Ticket>::new());
|
|
let error = use_state(|| None::<String>);
|
|
let loading = use_state(|| false);
|
|
let user = use_state(|| ActiveUser {
|
|
id: None,
|
|
is_admin: false,
|
|
});
|
|
|
|
{
|
|
let tickets = tickets.clone();
|
|
let error = error.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with((), move |_| {
|
|
loading.set(true);
|
|
spawn_local(async move {
|
|
let url = format!("/api/tickets");
|
|
match Request::get(&url).send().await {
|
|
Ok(response) if response.status() == 200 => {
|
|
match response.json::<Vec<Ticket>>().await {
|
|
Ok(t) => tickets.set(t),
|
|
Err(e) => error.set(Some(format!("parse error: {}", e))),
|
|
}
|
|
}
|
|
Ok(response) => {
|
|
if let Ok(text) = response.text().await {
|
|
error.set(Some(text));
|
|
} else {
|
|
error.set(Some(format!("status {}", response.status())));
|
|
}
|
|
}
|
|
Err(err) => error.set(Some(format!("Network error: {}", err))),
|
|
}
|
|
loading.set(false);
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
{
|
|
let user = user.clone();
|
|
use_effect_with((), move |_| {
|
|
let user = user.clone();
|
|
spawn_local(async move {
|
|
if let Ok(response) = Request::get("/api/users/current")
|
|
.credentials(web_sys::RequestCredentials::Include)
|
|
.send()
|
|
.await
|
|
{
|
|
if response.status() == 200 {
|
|
if let Ok(json) = response.json::<serde_json::Value>().await {
|
|
let id = json
|
|
.get("data")
|
|
.and_then(|d| d.get("id"))
|
|
.and_then(|v| v.as_i64())
|
|
.and_then(|n| i16::try_from(n).ok());
|
|
let is_admin = json
|
|
.get("data")
|
|
.and_then(|d| d.get("is_admin"))
|
|
.and_then(|v| v.as_bool())
|
|
.unwrap_or(false);
|
|
user.set(ActiveUser { id, is_admin });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
if *loading {
|
|
html! {<p>{ "Loading" }</p>}
|
|
} else if let Some(e) = &*error {
|
|
html! { <p>{ format!("Error: {}", e) }</p> }
|
|
} else {
|
|
html! {
|
|
<div>
|
|
<div class="page-header">
|
|
<h1>{ "Archivierte Tickets" }</h1>
|
|
</div>
|
|
<ul class="ticket-list">
|
|
{ for tickets.iter().filter(|t| t.status == "Archived" && (user.is_admin || if let Some(uid) = user.id { t.user_id == uid } else { false })).map(|t| html! {
|
|
<div>
|
|
|
|
<li key={t.id.to_string()}>
|
|
<Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{}", t.betreff) }</h3></Link<crate::Route>>
|
|
<p>{ &t.description }</p>
|
|
</li>
|
|
|
|
</div>
|
|
})}
|
|
</ul>
|
|
</div>
|
|
}
|
|
}
|
|
}
|