use gloo_net::http::Request; use serde::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; // #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] // pub struct User { // pub id: i16, // pub last_name: String, // pub first_name: String, // pub username: String, // pub is_admin: bool, // pub pwd: String, // } /// Data transfer object (DTO) for creating a new user. /// /// This struct defines the payload sent to the backend when an administrator /// registers a new user. It includes all necessary information for user creation. /// /// # Fields /// - `first_name`: The first name of the user. /// - `last_name`: The last name of the user. /// - `username`: The unique username for the user's login. /// - `is_admin`: A boolean indicating whether the new user should have administrator privileges. /// - `pwd`: The password for the user's account. This will be hashed on the backend. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct UserCreateScheme { pub first_name: String, pub last_name: String, pub username: String, pub is_admin: bool, pub pwd: String, } /// Data transfer object (DTO) for user login credentials. /// /// This struct defines the payload sent to the backend when a user attempts to log in. /// /// # Fields /// - `username`: The username provided by the user. /// - `pwd`: The password provided by the user. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct LoginScheme { pub username: String, pub pwd: String, } /// Data transfer object (DTO) for updating an existing user's information. /// /// This struct is used to send updated user details, including an optional new password, /// to the backend for an existing user. /// /// # Fields /// - `id`: The unique identifier of the user to be updated. /// - `first_name`: The updated first name of the user. /// - `last_name`: The updated last name of the user. /// - `username`: The updated username for the user. /// - `make_admin`: A boolean indicating whether to grant/revoke administrator privileges. /// - `new_pwd`: An optional new password for the user. If empty, the password remains unchanged. #[derive(Deserialize, Serialize, Debug)] pub struct UserUpdateScheme { pub id: i16, pub first_name: String, pub last_name: String, pub username: String, pub make_admin: bool, pub new_pwd: String, } /// Represents a user entity with sensitive information filtered out. /// /// This struct is used for displaying user information in contexts where /// the password hash or other sensitive details are not needed or should not be exposed. /// It's typically used for listing users or displaying user profiles. /// /// # Fields /// - `id`: The unique identifier of the user. /// - `first_name`: The first name of the user. /// - `last_name`: The last name of the user. /// - `username`: The unique username of the user. /// - `is_admin`: A boolean indicating if the user has administrator privileges. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct FilteredUser { pub id: i16, pub first_name: String, pub last_name: String, pub username: String, pub is_admin: bool, } /// Properties for components that display or interact with a single user. /// /// This struct is used to pass the ID of a specific user to components /// that need to fetch, display, or modify their details. /// /// # Fields /// - `id`: The unique identifier of the user to be displayed or acted upon. #[derive(Properties, PartialEq)] pub struct UserProps { pub id: i16, } #[derive(Deserialize, Debug)] struct ApiError { message: String, _status: String, } /// A component providing a form for registering new users. /// /// This component allows an administrator to create new user accounts by providing /// their first name, last name, username, password, and specifying if they should /// be an administrator. The component handles form input, validation (basic), /// and interaction with the backend `/api/register` endpoint. /// /// # State /// Uses `use_state` hooks to manage: /// - `first_name`, `last_name`, `username`, `pwd`: Values of the form input fields. /// - `is_admin`: Boolean state for the admin checkbox. /// - `status`: To display messages about registration success or errors. /// /// # Form Submission /// - Prevents default form submission behavior. /// - Constructs a `UserCreateScheme` payload from the form data. /// - Sends a POST request to `/api/register`. /// - Updates the `status` state based on the API response. /// /// # Example /// ```rust /// html! { /// /// } /// ``` #[component(Register)] pub fn register_component() -> Html { let first_name = use_state(|| "".to_string()); let last_name = use_state(|| "".to_string()); let username = use_state(|| "".to_string()); let is_admin = use_state(|| false); let pwd = use_state(|| "".to_string()); let status = use_state(|| None::); let onsubmit = { let first_name = first_name.clone(); let last_name = last_name.clone(); let username = username.clone(); let is_admin = is_admin.clone(); let pwd = pwd.clone(); let status = status.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); let first_name = (*first_name).clone(); let last_name = (*last_name).clone(); let username = (*username).clone(); let is_admin = *is_admin; let pwd = (*pwd).clone(); let status = status.clone(); spawn_local(async move { let payload = UserCreateScheme { first_name, last_name, username, is_admin, pwd, }; let request = Request::post("/api/register") .json(&payload) .expect("Error building 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 fn_change = { let first_name = first_name.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); first_name.set(input.value()); }) }; let ln_change = { let last_name = last_name.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); last_name.set(input.value()); }) }; let un_change = { let username = username.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); username.set(input.value()); }) }; let admin_change = { let is_admin = is_admin.clone(); Callback::from(move |e: Event| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); is_admin.set(input.checked()); }) }; let pwd_change = { let pwd = pwd.clone(); Callback::from(move |e: InputEvent| { let input: web_sys::HtmlInputElement = e.target_unchecked_into(); pwd.set(input.value()); }) }; html! {
} } /// A component providing a form for user login. /// /// This component handles user authentication by collecting a username and password, /// then sending these credentials to the backend `/api/login` endpoint. It manages /// UI states for loading, errors, and success, and redirects to the home page on successful login. /// /// # State /// Uses `use_state` hooks to manage: /// - `username`, `pwd`: Values of the login form input fields. /// - `loading`: A boolean to indicate if the login process is ongoing. /// - `error`: A string to display any login error messages. /// - `success`: A boolean to indicate if login was successful. /// /// # Form Submission /// - Prevents default form submission behavior. /// - Constructs a `LoginScheme` payload from the input fields. /// - Sends a POST request to `/api/login`. /// - On successful response (HTTP 200), sets `success` to true and redirects to `crate::Route::Home`. /// - On error, updates the `error` state with the appropriate message. /// /// # Example /// ```rust /// html! { /// /// } /// ``` #[component(Login)] pub fn login_component() -> Html { let username = use_state(|| "".to_string()); let pwd = use_state(|| "".to_string()); let loading = use_state(|| false); let error = use_state(|| String::new()); let success = use_state(|| false); let navigator = use_navigator().unwrap(); let onsubmit = { let username = username.clone(); let pwd = pwd.clone(); let loading = loading.clone(); let error = error.clone(); let success = success.clone(); let navigator = navigator.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); let username = (*username).clone(); let pwd = (*pwd).clone(); let loading = loading.clone(); let error = error.clone(); let success = success.clone(); let navigator = navigator.clone(); loading.set(true); error.set(String::new()); success.set(false); spawn_local(async move { let payload = LoginScheme { username, pwd }; let response = Request::post("/api/login") .header("Content-Type", "application/json") .credentials(web_sys::RequestCredentials::Include) .json(&payload) .unwrap() .send() .await; loading.set(false); match response { Ok(r) if r.status() == 200 => { success.set(true); navigator.push(&crate::Route::Home); } Ok(r) => { let text = r.text().await.unwrap_or_else(|_| "unknown".into()); error.set(format!("HTTP {}: {}", r.status(), text)); } Err(err) => error.set(format!("Network error: {}", err)), } }); }) }; html! {

{ "Anmeldung" }

if !error.is_empty() {

{(*error).clone()}

}
} } /// A component for fetching and displaying a list of all registered users. /// /// This component retrieves a list of `FilteredUser` entities from the backend /// via the `/api/users` endpoint and displays them. Each user entry includes a /// link to view their individual details through `UserByID`. /// /// # State /// Uses `use_state` hooks to manage: /// - `users`: A vector of `FilteredUser` structs to store the fetched user data. /// - `error`: Any error message encountered during API calls. /// - `loading`: A boolean indicating if user data is currently being fetched. /// /// # Functionality /// - **Data Fetching**: On component mount, fetches all users from `/api/users`. /// - **Conditional Display**: /// - If `loading` is true, displays "Loading...". /// - If an `error` occurs, displays the error message. /// - Otherwise, renders an unordered list of users. /// - **Navigation**: Each user in the list is a link to [`crate::Route::UserByID`] /// for viewing individual user details. /// /// # Example /// ```rust /// html! { /// /// } /// ``` #[component(AllUsers)] pub fn all_users_component() -> Html { let users = use_state(|| Vec::::new()); let error = use_state(|| None::); let loading = use_state(|| false); { let users = users.clone(); let error = error.clone(); let loading = loading.clone(); use_effect_with((), move |_| { loading.set(true); spawn_local(async move { let url = format!("/api/users"); match Request::get(&url).send().await { Ok(response) if response.status() == 200 => { match response.json::>().await { Ok(u) => users.set(u), Err(err) => error.set(Some(format!("Parse error: {}", err))), } } 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); }); || () }); } if *loading { html! {

{ "Loading" }

} } else if let Some(e) = &*error { html! {

{ format!("Error: {}", e) }

} } else { html! {
    { for users.iter().map(|t| html! {
  • to={crate::Route::UserByID{id: t.id}}>

    { format!("{} {}- #{}", t.first_name, t.last_name, t.id) }

    >
  • })}
} } } /// A component for displaying, updating, and deleting a single user by their ID. /// /// This component fetches a specific user's details from the backend based on the /// `id` provided in its `UserProps`. It allows administrators to view, modify /// (first name, last name, username, admin status, and password), and delete user accounts. /// /// # Props /// - `id`: The `i16` ID of the user to fetch and display. /// /// # State /// Uses `use_state` hooks to manage: /// - `user`: The fetched [`FilteredUser`] data, if available. /// - `error`: Any error message encountered during initial user data fetching. /// - `loading`: A boolean indicating if initial user data is being fetched. /// - `first_name`, `last_name`, `username`, `make_admin`, `new_pwd`: /// States for the update form fields, pre-filled with current user data. /// - `saving`, `save_error`, `save_success`: States for the user update operation. /// - `deleting`, `delete_error`: States for the user deletion operation. /// /// # Functionality /// - **Initial Data Fetching**: On component mount or `id` change, fetches user data from /// `/api/users/:id`. /// - **Form Initialization**: Populates the update form fields with the fetched user's /// current details once loaded. /// - **User Update**: The update form sends a PATCH request to `/api/users/:id` with a /// `UserUpdateScheme` payload. On success, updates the displayed user data and shows a /// success message. /// - **User Deletion**: Includes a "Delete" button that, after user confirmation, sends /// a DELETE request to `/api/users/:id`. On success, clears the user from display. /// - **Error Handling**: Displays error messages for various API and network issues. /// /// # Example /// ```rust /// html! { /// /// } /// ``` #[component(UserByID)] pub fn user_by_id_component(props: &UserProps) -> Html { let user = use_state(|| None::); let error = use_state(|| None::); let loading = use_state(|| false); let id = props.id; { let user = user.clone(); let error = error.clone(); let loading = loading.clone(); use_effect_with(id, move |id_ref| { loading.set(true); let user = user.clone(); let error = error.clone(); let id = *id_ref; spawn_local(async move { let url = format!("/api/users/{}", id); match Request::get(&url).send().await { Ok(response) => { let status = response.status(); if status == 200 { match response.json::().await { Ok(u) => user.set(Some(u)), Err(err) => error.set(Some(format!("Parse error: {}", err))), } } else { match response.json::().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 first_name = use_state(|| "".to_string()); let last_name = use_state(|| "".to_string()); let username = use_state(|| "".to_string()); let make_admin = use_state(|| false); let new_pwd = use_state(|| String::new()); let saving = use_state(|| false); let save_error = use_state(|| None::); let save_success = use_state(|| false); { let user = user.clone(); let first_name = first_name.clone(); let last_name = last_name.clone(); let username = username.clone(); let make_admin = make_admin.clone(); use_effect_with(user, move |user_ref| { if let Some(u) = &**user_ref { first_name.set(u.first_name.clone()); last_name.set(u.last_name.clone()); username.set(u.username.clone()); make_admin.set(u.is_admin); } || () }); } let onsubmit = { let first_name = first_name.clone(); let last_name = last_name.clone(); let username = username.clone(); let make_admin = make_admin.clone(); let new_pwd = new_pwd.clone(); let saving = saving.clone(); let save_error = save_error.clone(); let save_success = save_success.clone(); let user_state = user.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); let first_name = (*first_name).clone(); let last_name = (*last_name).clone(); let username = (*username).clone(); let make_admin = *make_admin; let new_pwd = (*new_pwd).clone(); saving.set(true); save_error.set(None); save_success.set(false); let saving = saving.clone(); let save_error = save_error.clone(); let save_success = save_success.clone(); let user_state = user_state.clone(); let id = id; spawn_local(async move { let payload = UserUpdateScheme { id: id, first_name, last_name, username, make_admin, new_pwd, }; let url = format!("/api/users/{}", id); let request = Request::patch(&url) .header("Content-Type", "application/json") .credentials(web_sys::RequestCredentials::Include) .json(&payload) .expect("Failed to construct Request"); match request.send().await { Ok(resp) if resp.status() == 200 => { if let Ok(updated) = resp.json::().await { user_state.set(Some(updated)); } save_success.set(true); } Ok(resp) => { let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into()); save_error.set(Some(format!("HTTP {}: {}", resp.status(), txt))); } Err(err) => save_error.set(Some(format!("Network error: {}", err))), } saving.set(false); }); }) }; let deleting = use_state(|| false); let delete_error = use_state(|| None::); let ondelete = { let deleting = deleting.clone(); let delete_error = delete_error.clone(); let user_state = user.clone(); // or ticket let id = id; Callback::from(move |e: MouseEvent| { e.prevent_default(); // confirm if !web_sys::window() .and_then(|w| { w.confirm_with_message("Are you sure you want to delete this item?") .ok() }) .unwrap_or(false) { return; } deleting.set(true); delete_error.set(None); let deleting = deleting.clone(); let delete_error = delete_error.clone(); let user_state = user_state.clone(); spawn_local(async move { let url = format!("/api/users/{}", id); // or /api/tickets/{} let req = Request::delete(&url).credentials(web_sys::RequestCredentials::Include); match req.send().await { Ok(resp) if resp.status() == 200 || resp.status() == 204 => { // remove local state or navigate away user_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! {

{ "Loading" }

} } else if let Some(e) = &*error { html! {

{ format!("Error: {}", e) }

} } else if let Some(u) = &*user { html! {

{ "Vorname: " }{ &u.first_name }

{ "Nachname: " }{ &u.last_name }

{ "Benutzername: " }{ &u.username }

{ "Ist Admin: " }{ u.is_admin }

{ format!("User #{}", u.id) }

if *save_success {

{ "Updated successfully" }

} if let Some(err) = &*save_error {

{ err.clone() }

}
to={crate::Route::AllUsers}>{ "Zurück zur Benutzerübersicht" }> if let Some(err) = &*delete_error {

{ err.clone() }

}
} } else { html! {

{ "No ticket found." }

} } }