Styles better
This commit is contained in:
@@ -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! {
|
||||
<div class="form-container">
|
||||
<div class="form-container home">
|
||||
<div class="page-header">
|
||||
<h1>{ "Welcome" }</h1>
|
||||
</div>
|
||||
|
||||
@@ -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<Utc>,
|
||||
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<TicketPartial>,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct UserTotalProps {
|
||||
users: Vec<UserPartial>,
|
||||
tickets: Vec<TicketPartial>,
|
||||
}
|
||||
|
||||
/// Converts a `chrono::DateTime<Utc>` 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! {
|
||||
<div>
|
||||
<h2>{ "Offene Tickets" }</h2>
|
||||
<h4 class="ticket_count">{ count }</h4>
|
||||
<div class="open-tickets">
|
||||
<h2 class="left">{ "Offene Tickets" }</h2>
|
||||
<h4 class="ticket_count center">{ count }</h4>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -325,11 +340,13 @@ pub fn ticket_count_component() -> Html {
|
||||
#[component(SubmitStats)]
|
||||
pub fn submit_stats_component() -> Html {
|
||||
let tickets = use_state(|| Vec::<TicketPartial>::new());
|
||||
let users = use_state(|| Vec::<UserPartial>::new());
|
||||
let error = use_state(|| None::<String>);
|
||||
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::<Vec<UserPartial>>().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 {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<RoomTotalTickets tickets={(*tickets).clone()}/>
|
||||
<div class="diagnostics-row">
|
||||
<div class="diagnostics-column">
|
||||
<h3>{ "Tickets pro Raum" }</h3>
|
||||
<RoomTotalTickets tickets={(*tickets).clone()}/>
|
||||
</div>
|
||||
<div class="diagnostics-column">
|
||||
<h3>{ "Tickets pro Benutzer" }</h3>
|
||||
<UserTotal users={(*users).clone()} tickets={(*tickets).clone()}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -456,19 +500,72 @@ fn room_total_component(props: &RoomTotalsProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class="diagnostics-section">
|
||||
<h3>{ "Tickets pro Raum" }</h3>
|
||||
<div class="room-chart">
|
||||
<div class="chart">
|
||||
{ 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! {
|
||||
<div class="room-bar-item">
|
||||
<div class="room-header">
|
||||
<span class="room-label">{ label }</span>
|
||||
<span class="room-count">{ count }</span>
|
||||
<div class="bar-item">
|
||||
<div class="diagnostics-header">
|
||||
<span class="diagnostics-label">{ label }</span>
|
||||
<span class="diagnostics-count">{ count }</span>
|
||||
</div>
|
||||
<div class="room-bar-container">
|
||||
<div class="room-bar" style={format!("width: {}%;", bar_width_percent)}>
|
||||
<div class="bar-container">
|
||||
<div class="bar" style={format!("width: {}%;", bar_width_percent)}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component(UserTotal)]
|
||||
fn user_total_component(props: &UserTotalProps) -> Html {
|
||||
let name_map: HashMap<i16, (String, String)> = props
|
||||
.users
|
||||
.iter()
|
||||
.map(|u| (u.id, (u.first_name.clone(), u.last_name.clone())))
|
||||
.collect();
|
||||
|
||||
let mut counts: HashMap<i16, usize> = 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! {
|
||||
<div class="diagnostics-section">
|
||||
<div class="chart">
|
||||
{ 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! {
|
||||
<div class="bar-item">
|
||||
<div class="diagnostics-header">
|
||||
<span class="diagnostics-label">{ label }</span>
|
||||
<span class="diagnostics-count">{ count }</span>
|
||||
</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar" style={format!("width: {}%;", bar_width_percent)}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,4 +75,6 @@
|
||||
border-radius: 10px;
|
||||
height: 50px;
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user