From b25e045b99a6d7d5d5cdb93d6d0d7923826be814 Mon Sep 17 00:00:00 2001 From: schn33fuchs Date: Wed, 13 May 2026 22:01:58 +0200 Subject: [PATCH] Styles better --- frontend/src/pages/basic_pages.rs | 3 +- frontend/src/pages/utilities.rs | 121 ++++++++++++++++-- .../src/styles/components/_diagnostics.scss | 68 ++++++++-- frontend/src/styles/components/_tickets.scss | 2 + 4 files changed, 173 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/basic_pages.rs b/frontend/src/pages/basic_pages.rs index d689e50..fe5850e 100644 --- a/frontend/src/pages/basic_pages.rs +++ b/frontend/src/pages/basic_pages.rs @@ -3,6 +3,7 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; +#[macro_export] macro_rules! dequote { ($str:expr) => { $str.trim_matches('"').to_string() @@ -56,7 +57,7 @@ pub fn home_component() -> Html { } html! { -
+
diff --git a/frontend/src/pages/utilities.rs b/frontend/src/pages/utilities.rs index 299f9a9..968f840 100644 --- a/frontend/src/pages/utilities.rs +++ b/frontend/src/pages/utilities.rs @@ -6,6 +6,7 @@ use serde::Deserialize; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; +use crate::dequote; use crate::pages::ticket::{ActiveUser, Ticket}; /// A partial representation of a ticket, containing only the fields necessary for statistical analysis. @@ -20,6 +21,14 @@ use crate::pages::ticket::{ActiveUser, Ticket}; struct TicketPartial { date: DateTime, room: i16, + user_id: i16, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +struct UserPartial { + id: i16, + first_name: String, + last_name: String, } /// Properties for components that display room-wise ticket totals. @@ -34,6 +43,12 @@ struct RoomTotalsProps { tickets: Vec, } +#[derive(Properties, PartialEq)] +struct UserTotalProps { + users: Vec, + tickets: Vec, +} + /// Converts a `chrono::DateTime` object's weekday to a 0-indexed integer. /// /// This function maps `chrono::Weekday` values (where Monday is 1, Sunday is 7) @@ -287,9 +302,9 @@ pub fn ticket_count_component() -> Html { }) .count(); html! { -
-

{ "Offene Tickets" }

-

{ count }

+
+

{ "Offene Tickets" }

+

{ count }

} } @@ -325,11 +340,13 @@ pub fn ticket_count_component() -> Html { #[component(SubmitStats)] pub fn submit_stats_component() -> Html { let tickets = use_state(|| Vec::::new()); + let users = use_state(|| Vec::::new()); let error = use_state(|| None::); let loading = use_state(|| false); { let tickets = tickets.clone(); + let users = users.clone(); let error = error.clone(); let loading = loading.clone(); @@ -353,6 +370,24 @@ pub fn submit_stats_component() -> Html { } Err(err) => error.set(Some(format!("Network error: {}", err))), } + + match Request::get("/api/users").send().await { + Ok(response) if response.status() == 200 => { + match response.json::>().await { + Ok(u) => users.set(u), + Err(e) => error.set(Some(format!("users parse error: {}", e))), + } + } + Ok(response) => { + if let Ok(text) = response.text().await { + error.set(Some(text)); + } else { + error.set(Some(format!("users status {}", response.status()))); + } + } + Err(err) => error.set(Some(format!("users network error: {}", err))), + } + loading.set(false); }); || () @@ -414,7 +449,16 @@ pub fn submit_stats_component() -> Html { })}
- +
+
+

{ "Tickets pro Raum" }

+ +
+
+

{ "Tickets pro Benutzer" }

+ +
+
} } @@ -456,19 +500,72 @@ fn room_total_component(props: &RoomTotalsProps) -> Html { html! {
-

{ "Tickets pro Raum" }

-
+
{ for totals_vec.into_iter().map(|(room, count)| { let label = parse_room(room); let bar_width_percent = (count as f64 / max_count as f64) * 100.0; html! { -
-
- { label } - { count } +
+
+ { label } + { count }
-
-
+
+
+
+
+
+ } + }) } +
+
+ } +} + +#[component(UserTotal)] +fn user_total_component(props: &UserTotalProps) -> Html { + let name_map: HashMap = props + .users + .iter() + .map(|u| (u.id, (u.first_name.clone(), u.last_name.clone()))) + .collect(); + + let mut counts: HashMap = HashMap::new(); + for t in &props.tickets { + *counts.entry(t.user_id).or_insert(0) += 1; + } + + let mut totals_vec: Vec<(i16, String, String, usize)> = name_map + .into_iter() + .map(|(id, (fname, lname))| { + let c = counts.get(&id).cloned().unwrap_or(0); + (id, fname, lname, c) + }) + .collect(); + + totals_vec.sort_by(|a, b| b.3.cmp(&a.3)); + + let max_count = totals_vec + .iter() + .map(|(_, _, _, c)| *c) + .max() + .unwrap_or(1) + .max(1); + + html! { +
+
+ { for totals_vec.into_iter().map(|(_id, fname, lname, count)| { + let label = format!("{} {}", dequote!(fname), dequote!(lname)); + let bar_width_percent = (count as f64 / max_count as f64) * 100.0; + html! { +
+
+ { label } + { count } +
+
+
diff --git a/frontend/src/styles/components/_diagnostics.scss b/frontend/src/styles/components/_diagnostics.scss index a3d7ff7..42775c0 100644 --- a/frontend/src/styles/components/_diagnostics.scss +++ b/frontend/src/styles/components/_diagnostics.scss @@ -18,7 +18,7 @@ } h3 { - margin: 0 0 $spacing-md 0; + margin: 0; color: $color-primary; font-size: 1.1rem; font-weight: 600; @@ -32,6 +32,7 @@ border: 1px solid #e5e7eb; border-radius: $border-radius; background-color: $color-container; + margin: 16px 0px; @media (prefers-color-scheme: dark) { background-color: $color-container-dark; @@ -100,7 +101,7 @@ } } -.room-chart { +.chart { display: flex; flex-direction: column; gap: 12px; @@ -108,6 +109,7 @@ border: 1px solid #e5e7eb; border-radius: $border-radius; background-color: $color-container; + width: 100%; @media (prefers-color-scheme: dark) { background-color: $color-container-dark; @@ -115,17 +117,17 @@ } } -.room-bar-item { +.bar-item { display: flex; flex-direction: column; gap: 4px; - .room-header { + .diagnostics-header { display: flex; justify-content: space-between; align-items: center; - .room-label { + .diagnostics-label { font-weight: 600; font-size: 14px; color: $color-text; @@ -136,14 +138,14 @@ } } - .room-count { + .diagnostics-count { font-size: 12px; color: $color-muted; font-weight: 500; } } - .room-bar-container { + .bar-container { width: 100%; height: 24px; background-color: #e5e7eb; @@ -154,10 +156,60 @@ background-color: #555; } - .room-bar { + .bar { height: 100%; background-color: $color-primary; transition: width 0.3s ease; } } } + +.diagnostics-row { + display: flex; + gap: 24px; + align-items: flex-start; + flex-wrap: wrap; +} + +.diagnostics-column { + display: flex; + flex-direction: column; + gap: 12px; + width: 48%; + min-width: 280px; +} + +.open-tickets{ + display: flex; + align-items: center; + position: relative; + width: 100%; + padding: 0 1rem; + box-sizing: border-box; + min-height: 60px; + + .left { + display: flex; + align-items: center; + flex-shrink: 0; + + h2 { + margin: 0; + white-space: nowrap; + } + } + + .center { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } +} + +.home { + .ticket_count { + font-size: xxx-large; + height: 115px; + } +} diff --git a/frontend/src/styles/components/_tickets.scss b/frontend/src/styles/components/_tickets.scss index 1bd7500..e71c13e 100644 --- a/frontend/src/styles/components/_tickets.scss +++ b/frontend/src/styles/components/_tickets.scss @@ -75,4 +75,6 @@ border-radius: 10px; height: 50px; width: 250px; + margin: 0; + font-size: xx-large; }