frontend/pages/
user.rs

1use gloo_net::http::Request;
2use serde::{Deserialize, Serialize};
3use wasm_bindgen_futures::spawn_local;
4use yew::prelude::*;
5use yew_router::prelude::*;
6
7// #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8// pub struct User {
9//     pub id: i16,
10//     pub last_name: String,
11//     pub first_name: String,
12//     pub username: String,
13//     pub is_admin: bool,
14//     pub pwd: String,
15// }
16
17/// Data transfer object (DTO) for creating a new user.
18///
19/// This struct defines the payload sent to the backend when an administrator
20/// registers a new user. It includes all necessary information for user creation.
21///
22/// # Fields
23/// - `first_name`: The first name of the user.
24/// - `last_name`: The last name of the user.
25/// - `username`: The unique username for the user's login.
26/// - `is_admin`: A boolean indicating whether the new user should have administrator privileges.
27/// - `pwd`: The password for the user's account. This will be hashed on the backend.
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct UserCreateScheme {
30    pub first_name: String,
31    pub last_name: String,
32    pub username: String,
33    pub is_admin: bool,
34    pub pwd: String,
35}
36
37/// Data transfer object (DTO) for user login credentials.
38///
39/// This struct defines the payload sent to the backend when a user attempts to log in.
40///
41/// # Fields
42/// - `username`: The username provided by the user.
43/// - `pwd`: The password provided by the user.
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45pub struct LoginScheme {
46    pub username: String,
47    pub pwd: String,
48}
49
50/// Data transfer object (DTO) for updating an existing user's information.
51///
52/// This struct is used to send updated user details, including an optional new password,
53/// to the backend for an existing user.
54///
55/// # Fields
56/// - `id`: The unique identifier of the user to be updated.
57/// - `first_name`: The updated first name of the user.
58/// - `last_name`: The updated last name of the user.
59/// - `username`: The updated username for the user.
60/// - `make_admin`: A boolean indicating whether to grant/revoke administrator privileges.
61/// - `new_pwd`: An optional new password for the user. If empty, the password remains unchanged.
62#[derive(Deserialize, Serialize, Debug)]
63pub struct UserUpdateScheme {
64    pub id: i16,
65    pub first_name: String,
66    pub last_name: String,
67    pub username: String,
68    pub make_admin: bool,
69    pub new_pwd: String,
70}
71
72/// Represents a user entity with sensitive information filtered out.
73///
74/// This struct is used for displaying user information in contexts where
75/// the password hash or other sensitive details are not needed or should not be exposed.
76/// It's typically used for listing users or displaying user profiles.
77///
78/// # Fields
79/// - `id`: The unique identifier of the user.
80/// - `first_name`: The first name of the user.
81/// - `last_name`: The last name of the user.
82/// - `username`: The unique username of the user.
83/// - `is_admin`: A boolean indicating if the user has administrator privileges.
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct FilteredUser {
86    pub id: i16,
87    pub first_name: String,
88    pub last_name: String,
89    pub username: String,
90    pub is_admin: bool,
91}
92
93/// Properties for components that display or interact with a single user.
94///
95/// This struct is used to pass the ID of a specific user to components
96/// that need to fetch, display, or modify their details.
97///
98/// # Fields
99/// - `id`: The unique identifier of the user to be displayed or acted upon.
100#[derive(Properties, PartialEq)]
101pub struct UserProps {
102    pub id: i16,
103}
104
105/// Represents an error response from the API.
106///
107/// This struct is used to deserialize error messages received from the backend API.
108/// It typically contains a human-readable message and an internal status code.
109///
110/// # Fields
111/// - `message`: A `String` containing a description of the error.
112/// - `_status`: An internal status code or message, often ignored in frontend display.
113#[derive(Deserialize, Debug)]
114struct ApiError {
115    message: String,
116    _status: String,
117}
118
119/// A component providing a form for registering new users.
120///
121/// This component allows an administrator to create new user accounts by providing
122/// their first name, last name, username, password, and specifying if they should
123/// be an administrator. The component handles form input, validation (basic),
124/// and interaction with the backend `/api/register` endpoint.
125///
126/// # State
127/// Uses `use_state` hooks to manage:
128/// - `first_name`, `last_name`, `username`, `pwd`: Values of the form input fields.
129/// - `is_admin`: Boolean state for the admin checkbox.
130/// - `status`: To display messages about registration success or errors.
131///
132/// # Form Submission
133/// - Prevents default form submission behavior.
134/// - Constructs a `UserCreateScheme` payload from the form data.
135/// - Sends a POST request to `/api/register`.
136/// - Updates the `status` state based on the API response.
137///
138/// # Example
139/// ```rust
140/// html! {
141///     <Register />
142/// }
143/// ```
144#[component(Register)]
145pub fn register_component() -> Html {
146    let first_name = use_state(|| "".to_string());
147    let last_name = use_state(|| "".to_string());
148    let username = use_state(|| "".to_string());
149    let is_admin = use_state(|| false);
150    let pwd = use_state(|| "".to_string());
151    let status = use_state(|| None::<String>);
152
153    let onsubmit = {
154        let first_name = first_name.clone();
155        let last_name = last_name.clone();
156        let username = username.clone();
157        let is_admin = is_admin.clone();
158        let pwd = pwd.clone();
159        let status = status.clone();
160
161        Callback::from(move |e: SubmitEvent| {
162            e.prevent_default();
163            let first_name = (*first_name).clone();
164            let last_name = (*last_name).clone();
165            let username = (*username).clone();
166            let is_admin = *is_admin;
167            let pwd = (*pwd).clone();
168            let status = status.clone();
169
170            spawn_local(async move {
171                let payload = UserCreateScheme {
172                    first_name,
173                    last_name,
174                    username,
175                    is_admin,
176                    pwd,
177                };
178
179                let request = Request::post("/api/register")
180                    .json(&payload)
181                    .expect("Error building request");
182
183                match request.send().await {
184                    Ok(response) if response.status() == 200 => status.set(Some("Erfolg".into())),
185                    Ok(response) => {
186                        let text = response.text().await.unwrap_or_else(|_| "Unbekannt".into());
187                        status.set(Some(text));
188                    },
189                    Err(err) => status.set(Some(format!("Netzwerkfehler: {}", err))),
190                }
191            });
192        })
193    };
194
195    let fn_change = {
196        let first_name = first_name.clone();
197        Callback::from(move |e: InputEvent| {
198            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
199            first_name.set(input.value());
200        })
201    };
202
203    let ln_change = {
204        let last_name = last_name.clone();
205        Callback::from(move |e: InputEvent| {
206            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
207            last_name.set(input.value());
208        })
209    };
210
211    let un_change = {
212        let username = username.clone();
213        Callback::from(move |e: InputEvent| {
214            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
215            username.set(input.value());
216        })
217    };
218
219    let admin_change = {
220        let is_admin = is_admin.clone();
221        Callback::from(move |e: Event| {
222            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
223            is_admin.set(input.checked());
224        })
225    };
226
227    let pwd_change = {
228        let pwd = pwd.clone();
229        Callback::from(move |e: InputEvent| {
230            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
231            pwd.set(input.value());
232        })
233    };
234
235    html! {
236        <form {onsubmit} class="rundbg">
237            <label>{ "Vorname:" }
238                <input type="text" value={(*first_name).clone()} oninput={fn_change}/>
239            </label>
240            <label>{ "Nachname:" }
241                <input type="text" value={(*last_name).clone()} oninput={ln_change}/>
242            </label>
243            <label>{ "Benutzername:" }
244                <input type="text" value={(*username).clone()} oninput={un_change}/>
245            </label>
246            <label>{ "Admin:" }
247                <input type="checkbox" checked={*is_admin} onchange={admin_change}/>
248            </label>
249            <label>{ "Password:" }
250                <input type="password" value={(*pwd).clone()} oninput={pwd_change}/>
251            </label>
252            <button type="submit">{ "Bestätigen" }</button>
253        </form>
254    }
255}
256
257/// A component providing a form for user login.
258///
259/// This component handles user authentication by collecting a username and password,
260/// then sending these credentials to the backend `/api/login` endpoint. It manages
261/// UI states for loading, errors, and success, and redirects to the home page on successful login.
262///
263/// # State
264/// Uses `use_state` hooks to manage:
265/// - `username`, `pwd`: Values of the login form input fields.
266/// - `loading`: A boolean to indicate if the login process is ongoing.
267/// - `error`: A string to display any login error messages.
268/// - `success`: A boolean to indicate if login was successful.
269///
270/// # Form Submission
271/// - Prevents default form submission behavior.
272/// - Constructs a `LoginScheme` payload from the input fields.
273/// - Sends a POST request to `/api/login`.
274/// - On successful response (HTTP 200), sets `success` to true and redirects to `crate::Route::Home`.
275/// - On error, updates the `error` state with the appropriate message.
276///
277/// # Example
278/// ```rust
279/// html! {
280///     <Login />
281/// }
282/// ```
283#[component(Login)]
284pub fn login_component() -> Html {
285    let username = use_state(|| "".to_string());
286    let pwd = use_state(|| "".to_string());
287    let loading = use_state(|| false);
288    let error = use_state(|| String::new());
289    let success = use_state(|| false);
290    let navigator = use_navigator().unwrap();
291
292    let onsubmit = {
293        let username = username.clone();
294        let pwd = pwd.clone();
295        let loading = loading.clone();
296        let error = error.clone();
297        let success = success.clone();
298        let navigator = navigator.clone();
299
300        Callback::from(move |e: SubmitEvent| {
301            e.prevent_default();
302            let username = (*username).clone();
303            let pwd = (*pwd).clone();
304            let loading = loading.clone();
305            let error = error.clone();
306            let success = success.clone();
307            let navigator = navigator.clone();
308
309            loading.set(true);
310            error.set(String::new());
311            success.set(false);
312
313            spawn_local(async move {
314                let payload = LoginScheme { username, pwd };
315
316                let response = Request::post("/api/login")
317                    .header("Content-Type", "application/json")
318                    .credentials(web_sys::RequestCredentials::Include)
319                    .json(&payload)
320                    .unwrap()
321                    .send()
322                    .await;
323
324                loading.set(false);
325
326                match response {
327                    Ok(r) if r.status() == 200 => {
328                        success.set(true);
329                        navigator.push(&crate::Route::Home);
330                    }
331                    Ok(r) => {
332                        let text = r.text().await.unwrap_or_else(|_| "Unbekannt".into());
333                        error.set(text);
334                    }
335                    Err(err) => error.set(format!("Netzwerkfehler: {}", err)),
336                }
337            });
338        })
339    };
340
341    html! {
342        <div>
343        <h1 class="headline">{ "Anmeldung" }</h1>
344        <form {onsubmit} class="rundbg login">
345            <input
346                placeholder="Benutzername"
347                value={(*username).clone()}
348                oninput={Callback::from(move |e: InputEvent| {
349                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
350                    username.set(input.value());
351                    })}
352            />
353            <input
354                type="password"
355                placeholder="Passwort"
356                value={(*pwd).clone()}
357                oninput={Callback::from(move |e: InputEvent| {
358                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
359                    pwd.set(input.value());
360                })}
361            />
362            <button type="submit" disabled={*loading}>{ if *loading { "Anmeldung läuft..." } else { "Anmelden" } }</button>
363            if !error.is_empty() { <p style="color:red">{(*error).clone()}</p> }
364        </form>
365        </div>
366    }
367}
368
369/// A component for fetching and displaying a list of all registered users.
370///
371/// This component retrieves a list of `FilteredUser` entities from the backend
372/// via the `/api/users` endpoint and displays them. Each user entry includes a
373/// link to view their individual details through `UserByID`.
374///
375/// # State
376/// Uses `use_state` hooks to manage:
377/// - `users`: A vector of `FilteredUser` structs to store the fetched user data.
378/// - `error`: Any error message encountered during API calls.
379/// - `loading`: A boolean indicating if user data is currently being fetched.
380///
381/// # Functionality
382/// - **Data Fetching**: On component mount, fetches all users from `/api/users`.
383/// - **Conditional Display**:
384///   - If `loading` is true, displays "Loading...".
385///   - If an `error` occurs, displays the error message.
386///   - Otherwise, renders an unordered list of users.
387/// - **Navigation**: Each user in the list is a link to [`crate::Route::UserByID`]
388///   for viewing individual user details.
389///
390/// # Example
391/// ```rust
392/// html! {
393///     <AllUsers />
394/// }
395/// ```
396#[component(AllUsers)]
397pub fn all_users_component() -> Html {
398    let users = use_state(|| Vec::<FilteredUser>::new());
399    let error = use_state(|| None::<String>);
400    let loading = use_state(|| false);
401
402    {
403        let users = users.clone();
404        let error = error.clone();
405        let loading = loading.clone();
406
407        use_effect_with((), move |_| {
408            loading.set(true);
409            spawn_local(async move {
410                let url = format!("/api/users");
411                match Request::get(&url).send().await {
412                    Ok(response) if response.status() == 200 => {
413                        match response.json::<Vec<FilteredUser>>().await {
414                            Ok(u) => users.set(u),
415                            Err(err) => error.set(Some(format!("Parse error: {}", err))),
416                        }
417                    }
418                    Ok(response) => {
419                        if let Ok(text) = response.text().await {
420                            error.set(Some(text));
421                        } else {
422                            error.set(Some(format!("status {}", response.status())));
423                        }
424                    }
425                    Err(err) => error.set(Some(format!("Network error: {}", err))),
426                }
427                loading.set(false);
428            });
429            || ()
430        });
431    }
432
433    if *loading {
434        html! {<p>{ "Wird geladen" }</p>}
435    } else if let Some(e) = &*error {
436        html! { <p>{ format!("Fehler: {}", e) }</p> }
437    } else {
438        html! {
439            <ul class="postits">
440            { for users.iter().map(|t| html! {
441                    <li key={t.id.to_string()} class="listbox">
442                        <Link<crate::Route> to={crate::Route::UserByID{id: t.id}}><h3>{ format!("{} {}", t.first_name, t.last_name) }</h3></Link<crate::Route>>
443                    </li>
444                })}
445            </ul>
446        }
447    }
448}
449
450/// A component for displaying, updating, and deleting a single user by their ID.
451///
452/// This component fetches a specific user's details from the backend based on the
453/// `id` provided in its `UserProps`. It allows administrators to view, modify
454/// (first name, last name, username, admin status, and password), and delete user accounts.
455///
456/// # Props
457/// - `id`: The `i16` ID of the user to fetch and display.
458///
459/// # State
460/// Uses `use_state` hooks to manage:
461/// - `user`: The fetched [`FilteredUser`] data, if available.
462/// - `error`: Any error message encountered during initial user data fetching.
463/// - `loading`: A boolean indicating if initial user data is being fetched.
464/// - `first_name`, `last_name`, `username`, `make_admin`, `new_pwd`:
465///   States for the update form fields, pre-filled with current user data.
466/// - `saving`, `save_error`, `save_success`: States for the user update operation.
467/// - `deleting`, `delete_error`: States for the user deletion operation.
468///
469/// # Functionality
470/// - **Initial Data Fetching**: On component mount or `id` change, fetches user data from
471///   `/api/users/:id`.
472/// - **Form Initialization**: Populates the update form fields with the fetched user's
473///   current details once loaded.
474/// - **User Update**: The update form sends a PATCH request to `/api/users/:id` with a
475///   `UserUpdateScheme` payload. On success, updates the displayed user data and shows a
476///   success message.
477/// - **User Deletion**: Includes a "Delete" button that, after user confirmation, sends
478///   a DELETE request to `/api/users/:id`. On success, clears the user from display.
479/// - **Error Handling**: Displays error messages for various API and network issues.
480///
481/// # Example
482/// ```rust
483/// html! {
484///     <UserByID id={42} />
485/// }
486/// ```
487#[component(UserByID)]
488pub fn user_by_id_component(props: &UserProps) -> Html {
489    let user = use_state(|| None::<FilteredUser>);
490    let error = use_state(|| None::<String>);
491    let loading = use_state(|| false);
492    let id = props.id;
493
494    {
495        let user = user.clone();
496        let error = error.clone();
497        let loading = loading.clone();
498
499        use_effect_with(id, move |id_ref| {
500            loading.set(true);
501            let user = user.clone();
502            let error = error.clone();
503            let id = *id_ref;
504            spawn_local(async move {
505                let url = format!("/api/users/{}", id);
506
507                match Request::get(&url).send().await {
508                    Ok(response) => {
509                        let status = response.status();
510                        if status == 200 {
511                            match response.json::<FilteredUser>().await {
512                                Ok(u) => user.set(Some(u)),
513                                Err(err) => error.set(Some(format!("Parse error: {}", err))),
514                            }
515                        } else {
516                            match response.json::<ApiError>().await {
517                                Ok(ae) => error.set(Some(ae.message)),
518                                Err(_) => {
519                                    if let Ok(text) = response.text().await {
520                                        error.set(Some(text));
521                                    } else {
522                                        error.set(Some(format!("Server error: {}", status)));
523                                    }
524                                }
525                            }
526                        }
527                    }
528                    Err(err) => error.set(Some(format!("Network error: {}", err))),
529                }
530                loading.set(false);
531            });
532            || ()
533        });
534    }
535
536    let first_name = use_state(|| "".to_string());
537    let last_name = use_state(|| "".to_string());
538    let username = use_state(|| "".to_string());
539    let make_admin = use_state(|| false);
540    let new_pwd = use_state(|| String::new());
541    let saving = use_state(|| false);
542    let save_error = use_state(|| None::<String>);
543    let save_success = use_state(|| false);
544
545    {
546        let user = user.clone();
547        let first_name = first_name.clone();
548        let last_name = last_name.clone();
549        let username = username.clone();
550        let make_admin = make_admin.clone();
551
552        use_effect_with(user, move |user_ref| {
553            if let Some(u) = &**user_ref {
554                first_name.set(u.first_name.clone());
555                last_name.set(u.last_name.clone());
556                username.set(u.username.clone());
557                make_admin.set(u.is_admin);
558            }
559            || ()
560        });
561    }
562
563    let onsubmit = {
564        let first_name = first_name.clone();
565        let last_name = last_name.clone();
566        let username = username.clone();
567        let make_admin = make_admin.clone();
568        let new_pwd = new_pwd.clone();
569        let saving = saving.clone();
570        let save_error = save_error.clone();
571        let save_success = save_success.clone();
572        let user_state = user.clone();
573
574        Callback::from(move |e: SubmitEvent| {
575            e.prevent_default();
576            let first_name = (*first_name).clone();
577            let last_name = (*last_name).clone();
578            let username = (*username).clone();
579            let make_admin = *make_admin;
580            let new_pwd_state = new_pwd.clone();
581            let new_pwd = (*new_pwd).clone();
582
583            saving.set(true);
584            save_error.set(None);
585            save_success.set(false);
586
587            let saving = saving.clone();
588            let save_error = save_error.clone();
589            let save_success = save_success.clone();
590            let user_state = user_state.clone();
591            let id = id;
592
593            spawn_local(async move {
594                let payload = UserUpdateScheme {
595                    id: id,
596                    first_name,
597                    last_name,
598                    username,
599                    make_admin,
600                    new_pwd,
601                };
602
603                let url = format!("/api/users/{}", id);
604                let request = Request::patch(&url)
605                    .header("Content-Type", "application/json")
606                    .credentials(web_sys::RequestCredentials::Include)
607                    .json(&payload)
608                    .expect("Failed to construct Request");
609
610                match request.send().await {
611                    Ok(resp) if resp.status() == 200 => {
612                        if let Ok(updated) = resp.json::<FilteredUser>().await {
613                            user_state.set(Some(updated));
614                        }
615                        new_pwd_state.set(String::new());
616                        save_success.set(true);
617                    }
618                    Ok(resp) => {
619                        let txt = resp.text().await.unwrap_or_else(|_| "Unknown".into());
620                        save_error.set(Some(format!("HTTP {}: {}", resp.status(), txt)));
621                    }
622                    Err(err) => save_error.set(Some(format!("Network error: {}", err))),
623                }
624                saving.set(false);
625            });
626        })
627    };
628
629    let deleting = use_state(|| false);
630    let delete_error = use_state(|| None::<String>);
631
632    let ondelete = {
633        let deleting = deleting.clone();
634        let delete_error = delete_error.clone();
635        let user_state = user.clone(); // or ticket
636        let id = id;
637
638        Callback::from(move |e: MouseEvent| {
639            e.prevent_default();
640            // confirm
641            if !web_sys::window()
642                .and_then(|w| {
643                    w.confirm_with_message("Möchten Sie diesen Eintrag wirklich löschen?")
644                        .ok()
645                })
646                .unwrap_or(false)
647            {
648                return;
649            }
650
651            deleting.set(true);
652            delete_error.set(None);
653
654            let deleting = deleting.clone();
655            let delete_error = delete_error.clone();
656            let user_state = user_state.clone();
657
658            spawn_local(async move {
659                let url = format!("/api/users/{}", id);
660                let req = Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
661
662                match req.send().await {
663                    Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
664                        user_state.set(None);
665                    }
666                    Ok(resp) => {
667                        let txt = resp.text().await.unwrap_or_else(|_| "Unbekannt".into());
668                        delete_error.set(Some(txt));
669                    }
670                    Err(err) => delete_error.set(Some(format!("Netzwerkfehler: {}", err))),
671                }
672                deleting.set(false);
673            });
674        })
675    };
676
677    if *loading {
678        html! {<p>{ "Wird geladen" }</p>}
679    } else if let Some(e) = &*error {
680        html! { <p>{ format!("Fehler: {}", e) }</p> }
681    } else if let Some(u) = &*user {
682        html! {
683            <div class="rundbg">
684                <div>
685                    <p><strong>{ "Vorname: " }</strong>{ &u.first_name }</p>
686                    <p><strong>{ "Nachname: " }</strong>{ &u.last_name }</p>
687                    <p><strong>{ "Benutzername: " }</strong>{ &u.username }</p>
688                    <p><strong>{ "Ist Admin: " }</strong>{ u.is_admin }</p>
689                </div>
690
691                <h1>{ format!("User #{}", u.id) }</h1>
692                <form onsubmit={onsubmit}>
693                    <div>
694                        <label>{ "Vorname" }
695                            <input name="first_name"
696                                value={(*first_name).clone()}
697                                oninput={Callback::from(move |e: InputEvent| {
698                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
699                                    first_name.set(input.value());
700                                })}
701                            />
702                        </label>
703                    </div>
704                    <div>
705                        <label>{ "Nachname" }
706                            <input name="last_name"
707                                value={(*last_name).clone()}
708                                oninput={Callback::from(move |e: InputEvent| {
709                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
710                                    last_name.set(input.value());
711                                })}
712                            />
713                        </label>
714                    </div>
715                    <div>
716                        <label>{ "Benutzername" }
717                            <input name="username"
718                                value={(*username).clone()}
719                                oninput={Callback::from(move |e: InputEvent| {
720                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
721                                    username.set(input.value());
722                                })}
723                            />
724                        </label>
725                    </div>
726                    <div>
727                        <label>{ "Ist Admin" }
728                            <input type="checkbox" name="make_admin" checked={*make_admin} onchange={Callback::from(move |e: Event| {
729                                let input: web_sys::HtmlInputElement = e.target_unchecked_into();
730                                make_admin.set(input.checked());
731                            })}/>
732                        </label>
733                    </div>
734                    <div>
735                        <label>{ "Neues Passwort (leer = unchanged)" }
736                            <input name="new_pwd" type="password"
737                                value={(*new_pwd).clone()}
738                                oninput={Callback::from(move |e: InputEvent| {
739                                    let input: web_sys::HtmlInputElement = e.target_unchecked_into();
740                                    new_pwd.set(input.value());
741                                })}
742                            />
743                        </label>
744                    </div>
745
746                    <button type="submit" disabled={*saving}>{ if *saving { "Speichern..." } else { "Speichern" } }</button>
747                    if *save_success {
748                        <p style="color:green">{ "Erfolgreich aktualisiert" }</p>
749                    }
750                    if let Some(err) = &*save_error {
751                        <p style="color:red">{ err.clone() }</p>
752                    }
753                </form>
754
755                <button onclick={ondelete} disabled={*deleting}>
756                    {if *deleting {"Löschen..."} else {"Löschen"}}
757                </button>
758
759                <Link<crate::Route> to={crate::Route::AllUsers}>{ "Zurück zur Benutzerübersicht" }</Link<crate::Route>>
760                if let Some(err) = &*delete_error {
761                    <p style="color:red">{ err.clone() }</p>
762                }
763            </div>
764        }
765    } else {
766        html! { <p>{ "Kein Benutzer gefunden." }</p> }
767    }
768}