frontend/pages/
ticket.rs

1use std::collections::HashSet;
2
3use chrono_tz::Europe::Berlin;
4use gloo_net::http::Request;
5use serde::{Deserialize, Serialize};
6use wasm_bindgen::JsCast;
7use wasm_bindgen_futures::spawn_local;
8use web_sys::HtmlSelectElement;
9use yew::prelude::*;
10use yew_router::prelude::*;
11
12/// A hardcoded list of valid room numbers for ticket submissions.
13///
14/// This array contains `i16` values representing valid rooms. Negative numbers typically
15/// denote rooms prefixed with 'K' (e.g., -1 for K1), and numbers greater than or equal to 1000
16/// denote rooms prefixed with 'D' (e.g., 1001 for D1).
17///
18/// This list is used for client-side validation of room input during ticket creation.
19const VALID_ROOMS: &[i16] = &[
20    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,
21    32, 34, 36, 37, 39, 41, 49, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
22    118, 120, 121, 122, 123, 124, 126, 127, 128, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,
23    143, 144, 145, 146, 147, 148, 150, 152, 153, 154, 155, 156, 157, 158, 159, 160, 201, 202, 203,
24    204, 205, 206, 207, 208, 209, 210, 211, 212, 214, 1001, 1002, 1003, -1, -2, -3, -4, -5, -6, -7,
25    -8,
26];
27
28/// Data transfer object (DTO) for creating a new ticket.
29///
30/// This struct defines the payload sent to the backend when a user submits a new ticket.
31/// It includes all necessary information for initial ticket creation.
32///
33/// # Fields
34/// - `category`: The category of the ticket (e.g., "Internet", "Hardware").
35/// - `betreff`: The subject or brief summary of the ticket.
36/// - `description`: A detailed description of the issue or request.
37/// - `room`: The room number where the issue is located.
38#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
39pub struct TicketCreateScheme {
40    pub category: String,
41    pub betreff: String,
42    pub description: String,
43    pub room: i16,
44}
45
46/// Data transfer object (DTO) for updating a ticket's status.
47///
48/// This struct is used when sending a request to the backend to change the status
49/// of an existing ticket.
50///
51/// # Fields
52/// - `status`: The new status of the ticket (e.g., "ToDo", "InProgress", "Completed", "Archived").
53#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
54pub struct TicketUpdateScheme {
55    pub status: String,
56}
57
58/// Represents a complete ticket entity retrieved from the backend.
59///
60/// This struct holds all details of a ticket, including its metadata, content,
61/// and associated user information.
62///
63/// # Fields
64/// - `id`: The unique identifier of the ticket.
65/// - `category`: The category of the ticket.
66/// - `betreff`: The subject of the ticket.
67/// - `description`: The detailed description of the ticket.
68/// - `room`: The room number associated with the ticket.
69/// - `status`: The current status of the ticket.
70/// - `date`: The creation date and time of the ticket in UTC.
71/// - `user_id`: The ID of the user who created the ticket.
72/// - `user_first_name`: The first name of the user who created the ticket.
73/// - `user_last_name`: The last name of the user who created the ticket.
74#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
75pub struct Ticket {
76    pub id: i32,
77    pub category: String,
78    pub betreff: String,
79    pub description: String,
80    pub room: i16,
81    pub status: String,
82    pub date: chrono::DateTime<chrono::Utc>,
83    pub user_id: i16,
84    pub user_first_name: String,
85    pub user_last_name: String,
86}
87
88/// Properties for components that display a single ticket.
89///
90/// This struct is used to pass the ID of a specific ticket to components
91/// that need to fetch or display its details.
92///
93/// # Fields
94/// - `id`: The unique identifier of the ticket to be displayed.
95#[derive(Properties, PartialEq)]
96pub struct TicketProps {
97    pub id: i32,
98}
99
100/// Represents the essential information of the currently authenticated user.
101///
102/// This struct is used to hold a subset of user data relevant for client-side
103/// logic, such as determining if the user is an administrator or filtering
104/// tickets by their ID.
105///
106/// # Fields
107/// - `id`: An `Option<i16>` holding the unique ID of the active user. `None` if not authenticated or ID is unavailable.
108/// - `is_admin`: A boolean indicating if the active user has administrator privileges.
109#[derive(Clone, Debug, PartialEq)]
110pub struct ActiveUser {
111    pub id: Option<i16>,
112    pub is_admin: bool,
113}
114
115/// Represents a standardized error response from the API.
116///
117/// This struct is used to deserialize error messages returned by the backend API,
118/// providing a consistent way to handle and display error information to the user.
119///
120/// # Fields
121/// - `message`: A descriptive error message.
122/// - `_status`: (Ignored) The HTTP status code or a status string from the API.
123#[derive(Deserialize, Debug)]
124struct ApiError {
125    message: String,
126    _status: String,
127}
128
129/// A component that provides a form for users to submit new tickets.
130///
131/// This component allows users to input details such as category, subject (`betreff`),
132/// description, and room number for a new support ticket. It includes client-side
133/// validation for the room number and interacts with the backend API to create the ticket.
134///
135/// # State
136/// Uses `use_state` hooks to manage:
137/// - `category`, `betreff`, `description`: Values of the form input fields.
138/// - `room`, `room_input`: The parsed and raw room numbers, respectively.
139/// - `status`: To display messages about submission success or errors.
140///
141/// # Room Input Handling
142/// The `room_change` callback handles parsing the room input:
143/// - Supports numerical room numbers (e.g., "101").
144/// - Supports 'K' prefixed rooms (e.g., "K1" parses to -1).
145/// - Supports 'D' prefixed rooms (e.g., "D1" parses to 1001).
146/// - Validates the parsed room against the `VALID_ROOMS` constant.
147///
148/// # Form Submission
149/// - Prevents default form submission behavior.
150/// - Performs validation for room number and presence in `VALID_ROOMS`.
151/// - Constructs a `TicketCreateScheme` payload.
152/// - Sends a POST request to `/api/tickets/create`.
153/// - Updates the `status` state based on the API response.
154///
155/// # Example
156/// ```rust
157/// html! {
158///     <SubmitTicket />
159/// }
160/// ```
161#[component(SubmitTicket)]
162pub fn submit_ticket_component() -> Html {
163    let category = use_state(|| "".to_string());
164    let betreff = use_state(|| "".to_string());
165    let description = use_state(|| "".to_string());
166    let room = use_state(|| None::<i16>);
167    let room_input = use_state(|| "".to_string());
168    let status = use_state(|| None::<String>);
169
170    let valid_rooms: HashSet<i16> = VALID_ROOMS.iter().copied().collect();
171
172    let onsubmit = {
173        let category = category.clone();
174        let betreff = betreff.clone();
175        let description = description.clone();
176        let room = room.clone();
177        let status = status.clone();
178        let valid_rooms = valid_rooms.clone();
179
180        Callback::from(move |e: SubmitEvent| {
181            e.prevent_default();
182            if room.is_none() {
183                status.set(Some("Ungültiger Raum".into()));
184                return;
185            }
186            let category = (*category).clone();
187            let betreff = (*betreff).clone();
188            let description = (*description).clone();
189            let room = room.unwrap();
190            if !valid_rooms.contains(&room) {
191                status.set(Some("Raum nicht erlaubt".into()));
192                return;
193            }
194            let status = status.clone();
195
196            spawn_local(async move {
197                let payload = TicketCreateScheme {
198                    category,
199                    betreff,
200                    description,
201                    room,
202                };
203
204                let request = Request::post("/api/tickets/create")
205                    .header("Content-Type", "application/json")
206                    .credentials(web_sys::RequestCredentials::Include)
207                    .json(&payload)
208                    .expect("Failed to build request");
209
210                match request.send().await {
211                    Ok(response) if response.status() == 200 => status.set(Some("Erfolg".into())),
212                    Ok(response) => {
213                        let text = response.text().await.unwrap_or_else(|_| "Unbekannt".into());
214                        status.set(Some(text));
215                    },
216                    Err(err) => status.set(Some(format!("Netzwerkfehler: {}", err))),
217                }
218            });
219        })
220    };
221
222    let category_change = {
223        let category = category.clone();
224        Callback::from(move |e: Event| {
225            let input: web_sys::HtmlSelectElement = e.target_unchecked_into();
226            category.set(input.value());
227        })
228    };
229
230    let betreff_change = {
231        let betreff = betreff.clone();
232        Callback::from(move |e: InputEvent| {
233            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
234            betreff.set(input.value());
235        })
236    };
237
238    let description_change = {
239        let description = description.clone();
240        Callback::from(move |e: InputEvent| {
241            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
242            description.set(input.value());
243        })
244    };
245
246    let room_change = {
247        let room = room.clone();
248        let room_input = room_input.clone();
249        Callback::from(move |e: InputEvent| {
250            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
251            let raw = input.value();
252            room_input.set(raw.clone());
253
254            let parsed_value: Option<i16> = if raw.trim().is_empty() {
255                None
256            } else {
257                let raw_trim = raw.trim();
258                if raw_trim.len() > 1 && (raw_trim.starts_with('K') || raw_trim.starts_with('k')) {
259                    match raw_trim[1..].parse::<i16>() {
260                        Ok(n) => Some(-n),
261                        Err(_) => None,
262                    }
263                } else if raw_trim.len() > 1
264                    && (raw_trim.starts_with('D') || raw_trim.starts_with('d'))
265                {
266                    match raw_trim[1..].parse::<i16>() {
267                        Ok(n) => Some(1000 + n),
268                        Err(_) => None,
269                    }
270                } else {
271                    match raw_trim.parse::<i16>() {
272                        Ok(n) => Some(n),
273                        Err(_) => None,
274                    }
275                }
276            };
277
278            if let Some(v) = parsed_value {
279                if !valid_rooms.contains(&v) {
280                    room.set(None);
281                } else {
282                    room.set(Some(v));
283                }
284            } else {
285                room.set(None);
286            }
287        })
288    };
289
290    let room_valid = (*room).is_some();
291
292    html! {
293        <form class="rundbg" {onsubmit}>
294            <label>{ "Betreff:" }
295                <input type="text" value={(*betreff).clone()} oninput={betreff_change}/>
296            </label>
297            <br/>
298            <label>{ "Beschreibung:" }
299                <input type="text" value={(*description).clone()} oninput={description_change}/>
300            </label>
301            <br/>
302            <label>{ "Kategorie:" }
303                <select value={(*category).clone()} onchange={category_change}>
304                    <option value="Whiteboard Beamer">{ "Whiteboard Beamer" }</option>
305                    <option value="Internet">{ "Internet" }</option>
306                    <option value="iPad Koffer">{ "iPad Koffer" }</option>
307                    <option value="Apple TV">{ "Apple TV" }</option>
308                    <option value="Docu Cam">{ "Dokumenten Kamera" }</option>
309                    <option value="Sonstiges">{ "Sonstiges" }</option>
310                </select>
311            </label>
312            <br/>
313            <label>{ "Raum:" }
314                <input type="text" value={(*room_input).clone()} oninput={room_change}/>
315            </label>
316            {
317                if !room_valid {
318                    html! {
319                        <p class="warning" >{ "Ungültiger/nicht erlaubter Raum (z. B. 101, K12, D5)" }</p>
320                    }
321                } else {
322                    html! {}
323                }
324            }
325            <br/>
326            <button type="submit">{ "Absenden" }</button>
327
328            <Link<crate::Route> to={crate::Route::AllTickets}>{ "Tickets ansehen" }</Link<crate::Route>>
329
330            {
331                if let Some(s) = &*status {
332                    html!{ <p>{ s }</p> }
333                } else {
334                    html!{}
335                }
336            }
337
338        </form>
339    }
340}
341
342/// A component for displaying, updating, and deleting a single ticket by its ID.
343///
344/// This component fetches a specific ticket's details from the backend based on the
345/// `id` provided in its `TicketProps`. It allows administrators to update the ticket's
346/// status and delete the ticket.
347///
348/// # Props
349/// - `id`: The `i32` ID of the ticket to fetch and display.
350///
351/// # State
352/// Uses `use_state` hooks to manage:
353/// - `ticket`: The fetched [`Ticket`] data, if available.
354/// - `error`: Any error message encountered during API calls.
355/// - `loading`: A boolean indicating if data is currently being fetched.
356/// - `status`: The selected status for updating the ticket.
357/// - `deleting`: A boolean indicating if a delete operation is in progress.
358/// - `delete_error`: Any error message from a delete operation.
359///
360/// # Functionality
361/// - **Data Fetching**: On component mount or `id` change, fetches ticket data from
362///   `/api/tickets/:id`.
363/// - **Status Update**: Provides a dropdown to change the ticket's status. Submitting the
364///   form sends a PATCH request to `/api/tickets/:id` with a `TicketUpdateScheme` payload.
365/// - **Ticket Deletion**: Includes a "Delete" button that, after user confirmation,
366///   sends a DELETE request to `/api/tickets/:id`. On success, clears the ticket from display.
367/// - **Error Handling**: Displays error messages for network issues, API errors, or parsing failures.
368/// - **Date Formatting**: Displays the ticket creation date formatted to `Europe/Berlin` timezone.
369///
370/// # Example
371/// ```rust
372/// html! {
373///     <TicketByID id={123} />
374/// }
375/// ```
376#[component(TicketByID)]
377pub fn ticket_by_id_component(props: &TicketProps) -> Html {
378    let ticket = use_state(|| None::<Ticket>);
379    let error = use_state(|| None::<String>);
380    let loading = use_state(|| false);
381    let id = props.id;
382    let status = use_state(|| "".to_string());
383
384    {
385        let ticket = ticket.clone();
386        let error = error.clone();
387        let loading = loading.clone();
388
389        use_effect_with(id, move |id_ref| {
390            loading.set(true);
391            let ticket = ticket.clone();
392            let error = error.clone();
393            let id = *id_ref;
394            spawn_local(async move {
395                let url = format!("/api/tickets/{}", id);
396                match Request::get(&url).send().await {
397                    Ok(response) => {
398                        let status = response.status();
399                        if status == 200 {
400                            match response.json::<Ticket>().await {
401                                Ok(t) => ticket.set(Some(t)),
402                                Err(err) => error.set(Some(format!("Parse error: {}", err))),
403                            }
404                        } else {
405                            match response.json::<ApiError>().await {
406                                Ok(ae) => error.set(Some(ae.message)),
407                                Err(_) => {
408                                    if let Ok(text) = response.text().await {
409                                        error.set(Some(text));
410                                    } else {
411                                        error.set(Some(format!("Server error: {}", status)));
412                                    }
413                                }
414                            }
415                        }
416                    }
417                    Err(err) => error.set(Some(format!("Network error: {}", err))),
418                }
419                loading.set(false);
420            });
421            || ()
422        });
423    }
424    let onsubmit = {
425        let status = status.clone();
426        let id = id.clone();
427        let error = error.clone();
428
429        Callback::from(move |e: SubmitEvent| {
430            e.prevent_default();
431            let form: web_sys::HtmlFormElement = e.target_unchecked_into();
432            let select_element = form
433                .query_selector("select[name=\"status\"]")
434                .ok()
435                .and_then(|opt| opt)
436                .and_then(|el| el.dyn_into::<HtmlSelectElement>().ok());
437            let new_status = select_element
438                .map(|s| s.value())
439                .unwrap_or_else(|| (*status).clone());
440            status.set(new_status.clone());
441
442            let id = id.clone();
443            let error = error.clone();
444
445            spawn_local(async move {
446                let value = TicketUpdateScheme { status: new_status };
447
448                let url = format!("/api/tickets/{}", id);
449                let request = Request::patch(&url)
450                    .json(&value)
451                    .expect("Failed to construct request");
452
453                match request.send().await {
454                    Ok(response) if response.status() == 200 => error.set(Some("Erfolg".into())),
455                    Ok(response) => {
456                        let text = response.text().await.unwrap_or_else(|_| "Unbekannt".into());
457                        error.set(Some(text));
458                    },
459                    Err(err) => error.set(Some(format!("Netzwerkfehler: {}", err))),
460                }
461            });
462        })
463    };
464
465    let deleting = use_state(|| false);
466    let delete_error = use_state(|| None::<String>);
467
468    let ondelete = {
469        let deleting = deleting.clone();
470        let delete_error = delete_error.clone();
471        let ticket_state = ticket.clone();
472        let id = id;
473
474        Callback::from(move |e: MouseEvent| {
475            e.prevent_default();
476            if !web_sys::window()
477                .and_then(|w| w.confirm_with_message("Sicher löschen?").ok())
478                .unwrap_or(false)
479            {
480                return;
481            }
482
483            deleting.set(true);
484            delete_error.set(None);
485
486            let deleting = deleting.clone();
487            let delete_error = delete_error.clone();
488            let ticket_state = ticket_state.clone();
489
490            spawn_local(async move {
491                let url = format!("/api/tickets/{}", id);
492                let request =
493                    Request::delete(&url).credentials(web_sys::RequestCredentials::Include);
494
495                match request.send().await {
496                    Ok(resp) if resp.status() == 200 || resp.status() == 204 => {
497                        ticket_state.set(None);
498                    }
499                    Ok(resp) => {
500                        let txt = resp.text().await.unwrap_or_else(|_| "Unbekannt".into());
501                        delete_error.set(Some(txt));
502                    }
503                    Err(err) => delete_error.set(Some(format!("Netzwerkfehler: {}", err))),
504                }
505                deleting.set(false);
506            });
507        })
508    };
509
510    if *loading {
511        html! {<p>{ "Wird geladen" }</p>}
512    } else if let Some(e) = &*error {
513        html! { <p>{ format!("Fehler: {}", e) }</p> }
514    } else if let Some(t) = &*ticket {
515        html! {
516            <div class="rundbg">
517                <h1>{ format!("Ticket #{}", t.id) }</h1>
518                <p><strong>{ "Kategorie: " }</strong>{ &t.category }</p>
519                <p><strong>{ "Betreff: " }</strong>{ &t.betreff }</p>
520                <p><strong>{ "Beschreibung: " }</strong>{ &t.description }</p>
521                <p><strong>{ "Raum: " }</strong>{ t.room }</p>
522                <p><strong>{ "Datum: " }</strong>{
523                    t.date.with_timezone(&Berlin).format("%d.%m.%Y %H:%M:%S").to_string()
524                }</p>
525                <p><strong>{ "Status: "}</strong>{ match t.status.as_str() {
526                    "ToDo" => "Zu tun",
527                    "InProgress" => "In Bearbeitung",
528                    "Completed" => "Erledigt",
529                    "Archived" => "Archiviert",
530                    _ => "Ungültiger Status"
531                }}</p>
532                <p><strong>{ "Name: "}</strong>{ format!{"{} {}", t.user_first_name, t.user_last_name } }</p>
533
534                <form {onsubmit}>
535                    <label>{ "Status ändern" }
536                        <select name="status">
537                            <option value="ToDo">{ "Zu tun" }</option>
538                            <option value="InProgress">{ "In Bearbeitung" }</option>
539                            <option value="Completed">{ "Erledigt" }</option>
540                            <option value="Archived">{ "Archiviert" }</option>
541                        </select>
542                    </label>
543                    <button type="submit">{ "Aktualisieren" }</button>
544                </form>
545
546                <button onclick={ondelete} disabled={*deleting}>
547                    {if *deleting {"Löschen..."} else {"Löschen"}}
548                </button>
549
550                <Link<crate::Route> to={crate::Route::AllTickets}>{ "Zurück zur Ticketübersicht" }</Link<crate::Route>>
551                if let Some(err) = &*delete_error {
552                    <p style="color:red">{ err.clone() }</p>
553                }
554            </div>
555        }
556    } else {
557        html! { <p>{ "Kein Ticket gefunden." }</p> }
558    }
559}
560
561/// A component for fetching and displaying a list of all tickets.
562///
563/// This component retrieves all tickets from the backend and presents them as a list.
564/// It dynamically filters the displayed tickets based on the current user's role:
565/// administrators see all tickets, while regular users only see tickets they created.
566///
567/// # State
568/// Uses `use_state` hooks to manage:
569/// - `tickets`: A vector of `Ticket` structs to store the fetched tickets.
570/// - `error`: Any error message encountered during API calls.
571/// - `loading`: A boolean indicating if data is currently being fetched.
572/// - `user`: An `ActiveUser` struct holding the current user's ID and admin status.
573///
574/// # Functionality
575/// - **Fetch Tickets**: On component mount, fetches all tickets from `/api/tickets`.
576/// - **Fetch Current User**: Concurrently fetches the current user's details from
577///   `/api/users/current` to determine their `user_id` and `is_admin` status.
578/// - **Conditional Display**:
579///   - If `loading` is true, displays "Loading...".
580///   - If an `error` occurs, displays the error message.
581///   - Otherwise, renders a list of tickets.
582/// - **Filtering**:
583///   - Administrators (`user.is_admin = true`) see all tickets.
584///   - Regular users (`user.is_admin = false`) only see tickets where `t.user_id == user.id`.
585/// - **Navigation**: Each ticket in the list is a link to [`crate::Route::TicketById`]
586///   for viewing individual ticket details.
587///
588/// # Example
589/// ```rust
590/// html! {
591///     <AllTickets />
592/// }
593/// ```
594#[component(AllTickets)]
595pub fn all_tickets_component() -> Html {
596    let tickets = use_state(|| Vec::<Ticket>::new());
597    let error = use_state(|| None::<String>);
598    let loading = use_state(|| false);
599    let user = use_state(|| ActiveUser {
600        id: None,
601        is_admin: false,
602    });
603
604    {
605        let tickets = tickets.clone();
606        let error = error.clone();
607        let loading = loading.clone();
608
609        use_effect_with((), move |_| {
610            loading.set(true);
611            spawn_local(async move {
612                let url = format!("/api/tickets");
613                match Request::get(&url).send().await {
614                    Ok(response) if response.status() == 200 => {
615                        match response.json::<Vec<Ticket>>().await {
616                            Ok(t) => tickets.set(t),
617                            Err(e) => error.set(Some(format!("parse error: {}", e))),
618                        }
619                    }
620                    Ok(response) => {
621                        if let Ok(text) = response.text().await {
622                            error.set(Some(text));
623                        } else {
624                            error.set(Some(format!("status {}", response.status())));
625                        }
626                    }
627                    Err(err) => error.set(Some(format!("Network error: {}", err))),
628                }
629                loading.set(false);
630            });
631            || ()
632        });
633    }
634
635    {
636        let user = user.clone();
637        use_effect_with((), move |_| {
638            let user = user.clone();
639            spawn_local(async move {
640                if let Ok(response) = Request::get("/api/users/current")
641                    .credentials(web_sys::RequestCredentials::Include)
642                    .send()
643                    .await
644                {
645                    if response.status() == 200 {
646                        if let Ok(json) = response.json::<serde_json::Value>().await {
647                            let id = json
648                                .get("data")
649                                .and_then(|d| d.get("id"))
650                                .and_then(|v| v.as_i64())
651                                .and_then(|n| i16::try_from(n).ok());
652                            let is_admin = json
653                                .get("data")
654                                .and_then(|d| d.get("is_admin"))
655                                .and_then(|v| v.as_bool())
656                                .unwrap_or(false);
657                            user.set(ActiveUser { id, is_admin });
658                        }
659                    }
660                }
661            });
662            || ()
663        });
664    }
665
666    if *loading {
667        html! {<p>{ "Wird geladen" }</p>}
668    } else if let Some(e) = &*error {
669        html! { <p>{ format!("Fehler: {}", e) }</p> }
670    } else {
671        html! {
672            <ul class= "postits">
673            { 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| {
674                let status_class = match t.status.as_str() {
675                    "ToDo" => "To-Do",
676                    "InProgress" => "InProgress",
677                    "Completed" => "Completed",
678                    "Archived" => "Archived",
679                    _ => "To-Do"
680                };
681                html! {
682                    <div class={classes!{status_class, "listbox"}}>
683                        <li key={t.id.to_string()}>
684                            <Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{}", t.betreff,) }</h3></Link<crate::Route>>
685                        </li>
686
687                    </div>
688                }})}
689            </ul>
690        }
691    }
692}
693
694/// A component for fetching and displaying a list of archived tickets.
695///
696/// This component retrieves all tickets from the backend and presents them as a list,
697/// filtered to only show those with "Archived" status.
698///
699/// # State
700/// Uses `use_state` hooks to manage:
701/// - `tickets`: A vector of `Ticket` structs to store the fetched tickets.
702/// - `error`: Any error message encountered during API calls.
703/// - `loading`: A boolean indicating if data is currently being fetched.
704/// - `user`: An `ActiveUser` struct holding the current user's ID and admin status.
705///
706/// # Functionality
707/// - **Fetch Tickets**: On component mount, fetches all tickets from `/api/tickets`.
708/// - **Fetch Current User**: Concurrently fetches the current user's details from
709///   `/api/users/current` to determine their `user_id` and `is_admin` status.
710/// - **Conditional Display**:
711///   - If `loading` is true, displays "Loading...".
712///   - If an `error` occurs, displays the error message.
713///   - Otherwise, renders a list of tickets.
714/// - **Filtering**:
715///   - Only tickets with `t.status == "Archived"` are displayed.
716///   - If the user is an admin, all archived tickets are shown.
717///   - If the user is not an admin, only their own archived tickets are shown.
718/// - **Navigation**: Each ticket in the list is a link to [`crate::Route::TicketById`]
719///   for viewing individual ticket details.
720///
721/// # Example
722/// ```rust
723/// html! {
724///     <ArchivedTickets />
725/// }
726/// ```
727#[component(ArchivedTickets)]
728pub fn archived_tickets_component() -> Html {
729    let tickets = use_state(|| Vec::<Ticket>::new());
730    let error = use_state(|| None::<String>);
731    let loading = use_state(|| false);
732    let user = use_state(|| ActiveUser {
733        id: None,
734        is_admin: false,
735    });
736
737    {
738        let tickets = tickets.clone();
739        let error = error.clone();
740        let loading = loading.clone();
741
742        use_effect_with((), move |_| {
743            loading.set(true);
744            spawn_local(async move {
745                let url = format!("/api/tickets");
746                match Request::get(&url).send().await {
747                    Ok(response) if response.status() == 200 => {
748                        match response.json::<Vec<Ticket>>().await {
749                            Ok(t) => tickets.set(t),
750                            Err(e) => error.set(Some(format!("Parsefehler: {}", e))),
751                        }
752                    }
753                    Ok(response) => {
754                        if let Ok(text) = response.text().await {
755                            error.set(Some(text));
756                        } else {
757                            error.set(Some(format!("Status {}", response.status())));
758                        }
759                    }
760                    Err(err) => error.set(Some(format!("Netzwerkfehler: {}", err))),
761                }
762                loading.set(false);
763            });
764            || ()
765        });
766    }
767
768    {
769        let user = user.clone();
770        use_effect_with((), move |_| {
771            let user = user.clone();
772            spawn_local(async move {
773                if let Ok(response) = Request::get("/api/users/current")
774                    .credentials(web_sys::RequestCredentials::Include)
775                    .send()
776                    .await
777                {
778                    if response.status() == 200 {
779                        if let Ok(json) = response.json::<serde_json::Value>().await {
780                            let id = json
781                                .get("data")
782                                .and_then(|d| d.get("id"))
783                                .and_then(|v| v.as_i64())
784                                .and_then(|n| i16::try_from(n).ok());
785                            let is_admin = json
786                                .get("data")
787                                .and_then(|d| d.get("is_admin"))
788                                .and_then(|v| v.as_bool())
789                                .unwrap_or(false);
790                            user.set(ActiveUser { id, is_admin });
791                        }
792                    }
793                }
794            });
795            || ()
796        });
797    }
798
799    if *loading {
800        html! {<p>{ "Wird geladen" }</p>}
801    } else if let Some(e) = &*error {
802        html! { <p>{ format!("Fehler: {}", e) }</p> }
803    } else {
804        html! {
805            <ul class="postits">
806            { 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! {
807                    <div class="listbox">
808                        <li key={t.id.to_string()}>
809                            <Link<crate::Route> to={crate::Route::TicketById{id: t.id}}><h3>{ format!("{} - #{}", t.betreff, t.id) }</h3></Link<crate::Route>>
810                            <p>{ &t.description }</p>
811                            <p>{ match t.status.as_str() {
812                                "ToDo" => "Zu tun",
813                                "InProgress" => "In Bearbeitung",
814                                "Completed" => "Erledigt",
815                                "Archived" => "Archiviert",
816                                _ => "Ungültiger Status"
817                            }}</p>
818                        </li>
819
820                    </div>
821                })}
822            </ul>
823        }
824    }
825}