Files
ticketsystem/frontend/src/pages/ticket.rs
2026-05-16 19:49:08 +02:00

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, K8, D1)" }</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>
}
}
}