From aa45cb48baf5a9fbc4496300ce089c0bd18767b3 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Wed, 14 Feb 2024 16:46:31 +0100 Subject: [PATCH] Implemented booking overview. --- Cargo.lock | 111 +++++++++++++++---- Cargo.toml | 5 + src/app.rs | 6 ++ src/backend/data.rs | 49 +++++++-- src/backend/reservation.rs | 190 +++++++++++++++++++++++++++++---- src/components/admin_portal.rs | 4 +- src/pages/all_reservations.rs | 154 ++++++++++++++++++++++++++ src/pages/mod.rs | 1 + 8 files changed, 469 insertions(+), 51 deletions(-) create mode 100644 src/pages/all_reservations.rs diff --git a/Cargo.lock b/Cargo.lock index b8a1c34..7b2a869 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ "actix-utils", "ahash 0.8.3", "base64 0.21.7", - "bitflags 2.4.0", + "bitflags 2.4.2", "brotli", "bytes", "bytestring", @@ -488,9 +488,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" dependencies = [ "serde", ] @@ -704,6 +704,32 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "charts-rs" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078698cfc3b1c699a0285167d63362de2e57af09a30d6329a465c921eb75be95" +dependencies = [ + "charts-rs-derive", + "fontdue", + "once_cell", + "regex", + "serde", + "serde_json", + "snafu", + "substring", +] + +[[package]] +name = "charts-rs-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273f0cf07255ec493cfb6559d83620a856bac2ca8aa4f09df400eb09f8d5d7b2" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "chrono" version = "0.4.31" @@ -942,12 +968,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -1277,6 +1300,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontdue" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9099a2f86b8e674b75d03ff154b3fe4c5208ed249ced8d69cc313a9fa40bb488" +dependencies = [ + "hashbrown 0.14.0", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -2449,9 +2482,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2465,7 +2498,7 @@ version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -2830,9 +2863,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -2842,9 +2875,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2873,7 +2906,9 @@ dependencies = [ "actix-files", "actix-session", "actix-web", + "base64 0.21.7", "cfg-if", + "charts-rs", "chrono", "console_error_panic_hook", "env_logger", @@ -3017,7 +3052,7 @@ version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -3187,9 +3222,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -3400,6 +3435,27 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "snafu" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d342c51730e54029130d7dc9fd735d28c4cd360f1368c01981d4f03ff207f096" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080c44971436b1af15d6f61ddd8b543995cf63ab8e677d46b00cc06f4ef267a0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "socket2" version = "0.4.9" @@ -3577,7 +3633,7 @@ checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.0", + "bitflags 2.4.2", "byteorder", "bytes", "chrono", @@ -3622,7 +3678,7 @@ checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.0", + "bitflags 2.4.2", "byteorder", "chrono", "crc", @@ -3710,6 +3766,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + [[package]] name = "subtle" version = "2.4.1" @@ -3959,6 +4024,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + [[package]] name = "typed-builder" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index 34973e0..bcc3ebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,9 @@ getopts = "0.2.21" leptos-use = "0.10.1" lettre = {version = "0.11", features = ["tokio1-native-tls", "smtp-transport", "file-transport"], optional = true} leptos-captcha = "0.2.0" +charts-rs = { version = "0.3.3", optional = true} +#image = { version = "0.24.8", optional = true } +base64 = "0.21.7" [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -46,6 +49,8 @@ ssr = [ "dep:actix-session", "dep:sqlx", "dep:lettre", + "dep:charts-rs", + #"dep:image", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/src/app.rs b/src/app.rs index 092a64b..d05676e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,7 @@ use leptos_router::*; use crate::components::admin_portal::AdminPortal; use crate::components::header::Header; use crate::components::user_menu::MenuOpener; +use crate::pages::all_reservations::Bookings; use crate::pages::login::Login; use crate::pages::mail_settings::MailSettings; use crate::pages::public::Public; @@ -91,6 +92,11 @@ pub fn App() -> impl IntoView { }/> + + + + }/> diff --git a/src/backend/data.rs b/src/backend/data.rs index e578990..8d905ef 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -1,6 +1,6 @@ #![allow(unused_variables)] -use std::fmt::Display; +use std::fmt::{Display, Formatter}; use std::str::FromStr; use chrono::{Local, NaiveDate, NaiveTime, Weekday}; use lazy_static::lazy_static; @@ -310,15 +310,20 @@ fn def_true() -> bool { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct ResProperty { + #[cfg_attr(feature = "ssr", sqlx(default))] id: i32, #[validate(length(min = 1,message = "Name cannot be empty"))] pub name: String, pub description: String, + #[cfg_attr(feature = "ssr", sqlx(default))] pub price: Decimal, + #[cfg_attr(feature = "ssr", sqlx(default))] pub slot: SlotType, #[serde(default = "def_true")] + #[cfg_attr(feature = "ssr", sqlx(default))] pub allow_multi: bool, #[serde(default = "def_true")] + #[cfg_attr(feature = "ssr", sqlx(default))] pub active: bool } @@ -409,10 +414,12 @@ impl CrReservation { #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct Customer { + #[cfg_attr(feature = "ssr", sqlx(default))] id: i32, pub full_name: String, pub email: String, pub phone: String, + #[cfg_attr(feature = "ssr", sqlx(default))] pub discount: i32 } @@ -437,30 +444,36 @@ pub enum ReservationState { Canceled, } +impl Display for ReservationState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + ReservationState::New => {"New"} + ReservationState::Approved => {"Approved"} + ReservationState::Canceled => {"Canceled"} + }) + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct Reservation { + #[cfg_attr(feature = "ssr", sqlx(default))] id: i32, pub from: NaiveTime, pub to: NaiveTime, + #[cfg_attr(feature = "ssr", sqlx(default))] pub property: i32, + #[cfg_attr(feature = "ssr", sqlx(default))] pub summary: i32, } -#[derive(Clone, Serialize, Deserialize, Debug, Default)] -#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] -pub struct ResPropertyView { - pub name: String, - pub description: String -} - #[derive(Clone, Serialize, Deserialize, Debug, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] pub struct ResWithProperty { #[cfg_attr(feature = "ssr", sqlx(flatten))] pub reservation: Reservation, #[cfg_attr(feature = "ssr", sqlx(flatten))] - pub property: ResPropertyView + pub property: ResProperty } pub struct Reservations(Vec); @@ -538,6 +551,17 @@ pub struct ResSumWithItems { pub reservations: Vec } +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] +pub struct ResAllView { + #[cfg_attr(feature = "ssr", sqlx(flatten))] + pub reservation: ResWithProperty, + #[cfg_attr(feature = "ssr", sqlx(flatten))] + pub summary: ReservationSum, + #[cfg_attr(feature = "ssr", sqlx(flatten))] + pub customer: Customer, +} + #[derive(Clone, Serialize, Deserialize, Debug, Default)] #[cfg_attr(feature = "ssr", derive(sqlx::Type))] #[cfg_attr(feature = "ssr", sqlx(type_name = "message_type"))] @@ -576,3 +600,10 @@ impl Message { self.id } } + +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] +pub struct ChartData { + pub count: i64, + pub period: f64 +} diff --git a/src/backend/reservation.rs b/src/backend/reservation.rs index fee2f29..00d2b87 100644 --- a/src/backend/reservation.rs +++ b/src/backend/reservation.rs @@ -12,9 +12,9 @@ cfg_if! { if #[cfg(feature = "ssr")] { use uuid::Uuid; use std::ops::DerefMut; use std::str::FromStr; - use futures_util::future::join_all; + use std::collections::HashMap; use log::warn; - use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType}; + use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType, ChartData, ResAllView}; use crate::backend::get_pool; use crate::backend::get_mailing; use crate::backend::mail::MailMessage; @@ -23,6 +23,7 @@ cfg_if! { if #[cfg(feature = "ssr")] { use sqlx::PgPool; use crate::backend::user::admin_email; use crate::backend::user::emails_for_notify; + use rust_decimal::prelude::ToPrimitive; async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result { let reservation = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1") @@ -88,46 +89,64 @@ cfg_if! { if #[cfg(feature = "ssr")] { async fn reservations_in_range(from: &NaiveDate, to: &NaiveDate, state: Option) -> Result, ServerFnError> { let pool = get_pool().await?; - let sums = if let Some(s) = state { - query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE date >= $1 AND date <= $2 AND state = $3 ORDER BY date") + let view = if let Some(s) = state { + query_as::<_, ResAllView>( + "SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \ + FROM reservation r \ + JOIN property p on p.id = r.property \ + JOIN reservation_sum s on s.id = r.summary \ + JOIN customer c on c.id = s.customer \ + WHERE s.date >= $1 AND s.date <= $2 AND s.state = $3 \ + ORDER BY s.id, s.date") .bind(from) .bind(to) .bind(s) .fetch_all(&pool) .await } else { - query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE date >= $1 AND date <= $2 ORDER BY date") + query_as::<_, ResAllView>( + "SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \ + FROM reservation r \ + JOIN property p on p.id = r.property \ + JOIN reservation_sum s on s.id = r.summary \ + JOIN customer c on c.id = s.customer \ + WHERE s.date >= $1 AND s.date <= $2 \ + ORDER BY s.id, s.date") .bind(from) .bind(to) .fetch_all(&pool) .await }; - let sums = if let Err(ref e) = sums { + let view = if let Err(ref e) = view { if matches!(e, Error::RowNotFound) { vec![] } else { - sums? + view? } } else { - sums? + view? }; - if sums.is_empty() { + if view.is_empty() { return Ok(vec![]) } - let res: Result, ServerFnError> = join_all(sums.into_iter().map(|s| async { - let reservations = items_for_reservation(s.id(), &pool).await?; - let customer = customer_for_reservation(s.customer, &pool).await?; - Ok(ResSumWithItems { - summary: s, - customer, - reservations - }) - })).await.into_iter().collect(); - - Ok(res?) + let mut ret = view.into_iter().fold(HashMap::new(), |mut m, v| { + m.entry(v.summary.id()) + .and_modify(|i: &mut (ReservationSum, Customer, Vec)| i.2.push(v.reservation.clone())) + .or_insert((v.summary, v.customer, vec![v.reservation])); + m + }).into_iter().map(|i| { + ResSumWithItems { + summary: i.1.0, + customer: i.1.1, + reservations: i.1.2 + } + }).collect::>(); + + ret.sort_by(|a, b| a.summary.date.cmp(&b.summary.date) ); + Ok(ret) } async fn set_state(uuid: Uuid, state: ReservationState) -> Result<(), ServerFnError> { @@ -185,6 +204,29 @@ cfg_if! { if #[cfg(feature = "ssr")] { async fn notify_cancel(uuid: Uuid) -> Result<(), AppError> { send_notify(uuid, get_message(MessageType::ReservationCanceled).await?).await } + + async fn month_chart_data(year: i32) -> Result, AppError> { + let pool = get_pool().await?; + let data = query_as::<_, ChartData>( + "SELECT count(id) as count, date_part('month', date) as period \ + FROM reservation_sum \ + WHERE date_part('year', date) = $1 GROUP BY period ORDER BY period") + .bind(year) + .fetch_all(&pool) + .await?; + + Ok(data) + } + + async fn year_chart_data() -> Result, AppError> { + let pool = get_pool().await?; + let data = query_as::<_, ChartData>( + "SELECT count(id) as count, date_part('year', date) as period FROM reservation_sum GROUP BY period ORDER BY period") + .fetch_all(&pool) + .await?; + + Ok(data) + } }} #[server] @@ -327,6 +369,25 @@ pub async fn get_next_reservations() -> Result> Some(ReservationState::Approved)).await?)) } +#[cfg(feature = "ssr")] +fn num_days(month: u32, year: i32) -> i64 { + if month == 12 { + NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() + } else { + NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() + }.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()) + .num_days() +} + +#[server] +pub async fn get_reservations_for_month(month: u32, year: i32) -> Result>, ServerFnError> { + let data = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).unwrap(), + &NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default()).unwrap(), + None).await?; + + Ok(ApiResponse::Data(data)) +} + #[server] pub async fn approve(uuid: String) -> Result, ServerFnError> { use crate::perm_check; @@ -357,4 +418,93 @@ pub async fn cancel(uuid: String) -> Result, ServerFnError> { } Ok(ApiResponse::Data(())) +} + +#[cfg(feature = "ssr")] +fn chart(data: &Vec, title: &str, month: bool) -> String { + use charts_rs::{BarChart, THEME_ANT}; + use base64::Engine; + use base64::prelude::BASE64_STANDARD; + use std::ops::Add; + use chrono::Month; + + let mut chart = BarChart::new_with_theme( + vec![("Bookings", data.iter().map(|d| d.count.to_f32().unwrap_or_default()).collect()).into()], + if month { + data.iter().map(|d| Month::try_from(d.period.to_u8().unwrap_or_default()).unwrap_or(Month::January).name().to_string()).collect() + } else { + data.iter().map(|d| d.period.to_string()).collect() + }, + THEME_ANT); + chart.title_text = title.to_string(); + chart.legend_show = Some(false); + chart.series_label_font_size = 8.0; + chart.x_axis_font_size = 8.0; + chart.title_font_size = 11.0; + chart.font_family = "Arial".to_string(); + chart.y_axis_configs[0].axis_font_size = 8.0; + + + chart.width = 300.0; + chart.height = 150.0; + + "data:image/svg+xml;base64,".to_string().add(&BASE64_STANDARD.encode(&chart.svg().unwrap_or_default())) +} + +#[server] +pub async fn month_chart(year: i32) -> Result, ServerFnError> { + use crate::perm_check; + perm_check!(is_logged_in); + + let data = month_chart_data(year).await?; + Ok(ApiResponse::Data(chart(&data, "Month bookings", true))) +} + +#[server] +pub async fn year_chart() -> Result, ServerFnError> { + use crate::perm_check; + perm_check!(is_logged_in); + + let data = year_chart_data().await?; + Ok(ApiResponse::Data(chart(&data, "Year bookings", false))) +} + +#[server] +pub async fn years() -> Result, ServerFnError> { + let pool = get_pool().await?; + + let data: Result, Error> = query_as("SELECT DISTINCT date_part('year', date) as year FROM reservation_sum ORDER BY year DESC") + .fetch_all(&pool) + .await; + + Ok(data?.into_iter().map(|d| d.0.to_i32().unwrap_or_default()).collect()) +} + +#[server] +pub async fn months(year: i32) -> Result, ServerFnError> { + let pool = get_pool().await?; + + let data: Result, Error> = query_as("SELECT DISTINCT date_part('month', date) as month \ + FROM reservation_sum \ + WHERE date_part('year', date) = $1 \ + ORDER BY month DESC") + .bind(year) + .fetch_all(&pool) + .await; + + Ok(data?.into_iter().map(|d| d.0.to_u32().unwrap_or_default()).collect()) +} + +#[server] +pub async fn reservations_in_month(year: i32, month: u32) -> Result>, ServerFnError> { + use crate::perm_check; + perm_check!(is_logged_in); + + let ret = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).ok_or(ServerFnError::new("Cannot parse date"))?, + &NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default()) + .ok_or(ServerFnError::new("Cannot parse date"))?, + None) + .await?; + + Ok(ApiResponse::Data(ret)) } \ No newline at end of file diff --git a/src/components/admin_portal.rs b/src/components/admin_portal.rs index 0a70b78..f709194 100644 --- a/src/components/admin_portal.rs +++ b/src/components/admin_portal.rs @@ -69,13 +69,13 @@ pub fn AdminPortal(children: Children) -> impl IntoView { + + + + + +
+
+
+ "Loading"
}> + { + chart.get().map(move |c| match c { + Ok(c) => { match c { + ApiResponse::Data(c) => {view! { }} + ApiResponse::Error(_) => {view! { }} } + } + Err(_) => { view! { } } + }) + } + +
+
+
+
+ "Loading"
}> + { + year_chart.get().map(move |c| match c { + Ok(c) => { match c { + ApiResponse::Data(c) => {view! { }} + ApiResponse::Error(_) => {view! { }} } + } + Err(_) => { view! { } } + }) + } + +
+ + + +
+
+
+ "Loading"
}> + + + + + + + + + + + { + reservations.get().map(|r| match r { + Ok(r) => { match r { + ApiResponse::Data(r) => { + view! { + + + + + + + + + + + + } + } + ApiResponse::Error(e) => { + view! { + } + } + } + } + Err(e) => { + view! { + } + } + }) + } +
{trl("Date")}{trl("Customer")}{trl("Price")}{trl("State")}{trl("Actions")}
{loc_date(data.summary.date)}{data.customer.full_name}{data.summary.price.to_string()}{data.summary.state.to_string()}
{trl("Something went wrong")}
{e.to_string()}
{trl("Something went wrong")}
{e.to_string()}
+ +
+
+ + } +} \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 49b9f2e..5e612e3 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -19,4 +19,5 @@ mod today_reservations; mod new_reservations; pub mod mail_settings; mod mail_view; +pub mod all_reservations;