From e69db7f40bd7c2313f98e0dab5032daa10106274 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Mon, 5 Feb 2024 12:00:38 +0100 Subject: [PATCH] Implemented admin overview. --- src/backend/data.rs | 23 +++++ src/backend/reservation.rs | 119 ++++++++++++++++++++++++- src/locales/catalogues.rs | 22 +++++ src/locales/mod.rs | 14 ++- src/pages/home_page.rs | 69 +++------------ src/pages/mod.rs | 2 + src/pages/new_reservations.rs | 150 ++++++++++++++++++++++++++++++++ src/pages/opening_hours.rs | 14 +-- src/pages/today_reservations.rs | 63 ++++++++++++++ 9 files changed, 402 insertions(+), 74 deletions(-) create mode 100644 src/pages/new_reservations.rs create mode 100644 src/pages/today_reservations.rs diff --git a/src/backend/data.rs b/src/backend/data.rs index 44e270b..2404db4 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -447,6 +447,22 @@ pub struct Reservation { 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 struct Reservations(Vec); // Transform slots to reservations @@ -515,6 +531,13 @@ impl ReservationSum { } } +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +pub struct ResSumWithItems { + pub summary: ReservationSum, + pub customer: Customer, + pub reservations: Vec +} + /* pub enum MessageType { NewReservation, diff --git a/src/backend/reservation.rs b/src/backend/reservation.rs index c344989..1e624f2 100644 --- a/src/backend/reservation.rs +++ b/src/backend/reservation.rs @@ -1,18 +1,19 @@ use leptos::*; use validator::Validate; -use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData}; +use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData, ResSumWithItems}; use crate::components::data_form::ForValidation; use cfg_if::cfg_if; use chrono::{NaiveDate, NaiveTime}; cfg_if! { if #[cfg(feature = "ssr")] { - use sqlx::{Postgres, Transaction}; + use sqlx::{Postgres, Transaction, query}; use sqlx::query_as; use sqlx::Error; use uuid::Uuid; use std::ops::DerefMut; use std::str::FromStr; - use crate::backend::data::ReservationSum; + use futures_util::future::join_all; + use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer}; use crate::backend::get_pool; async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result { @@ -41,6 +42,70 @@ cfg_if! { if #[cfg(feature = "ssr")] { Ok(reservations?) } } + + 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") + .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") + .bind(from) + .bind(to) + .fetch_all(&pool) + .await + }; + + let sums = if let Err(ref e) = sums { + if matches!(e, Error::RowNotFound) { + vec![] + } else { + sums? + } + } else { + sums? + }; + + if sums.is_empty() { + return Ok(vec![]) + } + + let res: Result, Error> = join_all(sums.into_iter().map(|s| async { + let reservations = query_as::<_, ResWithProperty>( + "SELECT r.id, r.from, r.to, r.property, r.summary, p.name, p,description \ + FROM reservation as r \ + JOIN property as p ON r.property = p.id WHERE r.summary = $1") + .bind(s.id()) + .fetch_all(&pool) + .await?; + let customer = query_as::<_, Customer>("SELECT * FROM customer WHERE id = $1") + .bind(s.customer) + .fetch_one(&pool) + .await?; + Ok(ResSumWithItems { + summary: s, + customer, + reservations + }) + })).await.into_iter().collect(); + + Ok(res?) + } + + async fn set_state(uuid: Uuid, state: ReservationState) -> Result<(), ServerFnError> { + let pool = get_pool().await?; + query("UPDATE reservation_sum SET state = $1 WHERE uuid = $2") + .bind(state) + .bind(uuid) + .execute(&pool) + .await?; + + Ok(()) + } }} #[server] @@ -148,4 +213,52 @@ impl ForValidation for CreateReservation { fn entity(&self) -> &dyn Validate { &self.reservation } +} + +#[server] +pub async fn get_new_reservations() -> Result>, ServerFnError> { + use crate::perm_check; + use chrono::{Days, Local}; + use crate::backend::data::ReservationState; + + perm_check!(is_logged_in); + + Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(), + &Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(), + Some(ReservationState::New)).await?)) +} + +#[server] +pub async fn get_next_reservations() -> Result>, ServerFnError> { + use crate::perm_check; + use chrono::{Days, Local}; + use crate::backend::data::ReservationState; + + perm_check!(is_logged_in); + + Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(), + &Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(), + Some(ReservationState::Approved)).await?)) +} + +#[server] +pub async fn approve(uuid: String) -> Result, ServerFnError> { + use crate::perm_check; + use crate::backend::data::ReservationState; + + perm_check!(is_logged_in); + set_state(Uuid::parse_str(&uuid)?, ReservationState::Approved).await?; + + Ok(ApiResponse::Data(())) +} + +#[server] +pub async fn cancel(uuid: String) -> Result, ServerFnError> { + use crate::perm_check; + use crate::backend::data::ReservationState; + + perm_check!(is_logged_in); + set_state(Uuid::parse_str(&uuid)?, ReservationState::Canceled).await?; + + Ok(ApiResponse::Data(())) } \ No newline at end of file diff --git a/src/locales/catalogues.rs b/src/locales/catalogues.rs index 0cf9999..e51d0b0 100644 --- a/src/locales/catalogues.rs +++ b/src/locales/catalogues.rs @@ -56,6 +56,28 @@ lazy_static! { ("Date", "Datum"), ("Note", "Poznámka"), ("Enter note", "Zadejte poznámku"), + ("New bookings", "Nové rezervace"), + ("Next approved bookings", "Potvrzené rezervace"), + ("Approve", "Potvrdit"), + ("Cancel booking", "Zrušit rezervaci"), + ("Approve booking", "Potvrdit rezervaci"), + ("Reservation saved", "Rezervace byla uložena"), + ("Cancel", "Zrušit"), + ("Approve booking on ", "Potvrdit rezervaci na "), + (" for ", " pro "), + ("Cancel booking on ", "Zrušit rezervaci na "), + ("Customer: ", "Zákazník: "), + ("Price: ", "Cena: "), + ("Note: ", "Poznámka: "), + ("Overview", "Přehled"), + ("Can't create reservation", "Rezervaci nelze vytvořit"), + ("Enter your full name", "Zadejte své jméno"), + ("Enter valid email address", "Zadejte platný e-mail"), + ("Select at last one time slot", "Zvolte čas rezervace"), + ("Enter your phone number", "Zadejte telefonní číslo"), + ("Your reservation has been successfully saved.", "Vaše rezervace byla úspěšně uložena."), + ("We look forward to seeing you on", "Těšíme se na vaši návštěvu"), + ("Create booking", "Vytvořit rezervaci"), ])), ("sk", HashMap::from( [ ("Dashboard", "Prehlad"), diff --git a/src/locales/mod.rs b/src/locales/mod.rs index dc72bb6..df41e81 100644 --- a/src/locales/mod.rs +++ b/src/locales/mod.rs @@ -1,4 +1,4 @@ -use chrono::NaiveDate; +use chrono::{NaiveDate, Weekday}; use leptos::use_context; use crate::locales::catalogues::get_dictionary; @@ -51,4 +51,16 @@ pub fn loc_date(date: NaiveDate) -> impl Fn() -> String { } move || { dt.clone() } +} + +pub fn show_day(day: &Weekday) -> impl Fn() -> String { + match day { + Weekday::Mon => { trl("Monday") } + Weekday::Tue => { trl("Tuesday") } + Weekday::Wed => { trl("Wednesday") } + Weekday::Thu => { trl("Thursday") } + Weekday::Fri => { trl("Friday") } + Weekday::Sat => { trl("Saturday") } + Weekday::Sun => { trl("Sunday") } + } } \ No newline at end of file diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 74a0157..d1305b8 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,69 +1,24 @@ use leptos::*; -use crate::components::modal_box::{DialogOpener, ModalDialog, ModalBody, ModalFooter}; +use crate::components::modal_box::DialogOpener; use crate::locales::trl; +use crate::pages::new_reservations::NewReservations; +use crate::pages::today_reservations::NextReservations; /// Renders the home page of your application. #[component] pub fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let (count, set_count) = create_signal(0); - let on_click = move |_| set_count.update(|count| *count += 1); - - let dialog = DialogOpener::new(); - - //let (dialog, set_dialog) = create_signal(false); - //let on_dialog = move |_| dialog.set_visible.update(|dialog| {*dialog = true}); - - //let pok = use_context::(); - //log!("{:?}", pok); - + let app_opener = DialogOpener::new(); + let cancel_opener = DialogOpener::new(); view! { - - -
-
- - -
+

{trl("Overview")}

+
+
+
-
-
- - -
-
- - -
+
+
- - - - - - - -

"Welcome to Leptos!"

- - -

{trl("testik!")}

+
} } \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 6111658..9dbae3b 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -15,4 +15,6 @@ mod properties; mod property_edit; mod property_delete; mod res_dialogs; +mod today_reservations; +mod new_reservations; diff --git a/src/pages/new_reservations.rs b/src/pages/new_reservations.rs new file mode 100644 index 0000000..14a1404 --- /dev/null +++ b/src/pages/new_reservations.rs @@ -0,0 +1,150 @@ +use leptos::*; +use crate::backend::data::{ApiResponse, ResSumWithItems}; +use crate::backend::reservation::{Approve, Cancel, get_new_reservations}; +use crate::components::data_form::QuestionDialog; +use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog}; +use crate::components::user_menu::MenuOpener; +use crate::locales::{loc_date, show_day, trl}; +use crate::pages::public::Public; +use chrono::Datelike; + +#[component] +fn approve_dialog(reservation: ReadSignal, opener: DialogOpener) -> impl IntoView { + let approve = create_server_action::(); + + view! { + + +
+ {trl("Approve booking on ")} + {move || loc_date(reservation.get().summary.date)} + {trl(" for ")} + {move || reservation.get().customer.full_name}"?" +
+
+ } +} + +#[component] +fn cancel_dialog(reservation: ReadSignal, opener: DialogOpener) -> impl IntoView { + let cancel = create_server_action::(); + + view! { + + +
+ {trl("Cancel booking on ")} + {move || loc_date(reservation.get().summary.date)} + {trl(" for ")} + {move || reservation.get().customer.full_name}"?" +
+
+ } +} + +#[component] +fn create_dialog(opener: DialogOpener) -> impl IntoView { + view! { + + + + + + } +} + +#[component] +pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -> impl IntoView { + let create = DialogOpener::new(); + let res = create_blocking_resource(move || app_opener.visible() || cancel_opener.visible() || create.visible(), + move |_| get_new_reservations()); + let reservation = create_rw_signal(ResSumWithItems::default()); + + view! { + + + +
+
+
" "{trl("New bookings")}
+ {trl("Loading...")}

}> + {move || { + res.get().map(|r| match r { + Err(e) => { + view! {
{trl("Something went wrong")}
{e.to_string()}
}} + Ok(r) => { match r { + ApiResponse::Data(r) => { + view! { +
+ + {move || { + let menu = MenuOpener::new(); + let data = data.clone(); + let app_data = data.clone(); + let cancel_data = data.clone(); + view! { + {show_day(&data.summary.date.weekday())}" - "{loc_date(data.summary.date)}
+ + {item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}
+
+ {trl("Customer: ")}{data.customer.full_name}", "{data.customer.email}", "{data.customer.phone}
+ {move || { + let note = data.summary.note.clone(); + let show = note.is_some() && !note.clone().unwrap().is_empty(); + view! { + + {trl("Note: ")}{note.clone()}
+
+ } + }} + {trl("Price: ")}{data.summary.price.to_string()}
+ + + + + +
+ +
+
+ } + }} +
+
+ } + } + ApiResponse::Error(e) => {view! {
{trl(&e)}
}}} + } + }) + }} +
+ + + +
+
+ } +} \ No newline at end of file diff --git a/src/pages/opening_hours.rs b/src/pages/opening_hours.rs index d3f23ae..247bf23 100644 --- a/src/pages/opening_hours.rs +++ b/src/pages/opening_hours.rs @@ -3,7 +3,7 @@ use leptos::*; use crate::backend::data::{DayHours, WeekHours}; use crate::backend::opening_hours::get_hours; use crate::components::modal_box::DialogOpener; -use crate::locales::trl; +use crate::locales::{show_day, trl}; use crate::pages::hours_edit::EditHours; fn show_time(tm: &str) -> impl Fn() -> String { @@ -14,18 +14,6 @@ fn show_time(tm: &str) -> impl Fn() -> String { } } -fn show_day(day: &Weekday) -> impl Fn() -> String { - match day { - Weekday::Mon => { trl("Monday") } - Weekday::Tue => { trl("Tuesday") } - Weekday::Wed => { trl("Wednesday") } - Weekday::Thu => { trl("Thursday") } - Weekday::Fri => { trl("Friday") } - Weekday::Sat => { trl("Saturday") } - Weekday::Sun => { trl("Sunday") } - } -} - #[component] pub fn OpeningHours() -> impl IntoView { let editor = DialogOpener::new(); diff --git a/src/pages/today_reservations.rs b/src/pages/today_reservations.rs new file mode 100644 index 0000000..3f810b6 --- /dev/null +++ b/src/pages/today_reservations.rs @@ -0,0 +1,63 @@ +use leptos::*; +use crate::backend::data::ApiResponse; +use crate::backend::reservation::get_next_reservations; +use crate::components::modal_box::DialogOpener; +use crate::locales::{loc_date, show_day, trl}; +use chrono::Datelike; + +#[component] +pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -> impl IntoView { + let res = create_blocking_resource( move || app_opener.visible() || cancel_opener.visible(), move |_| get_next_reservations()); + + view! { +
+
+
" "{trl("Next approved bookings")}
+ {trl("Loading...")}

}> + {move || { + res.get().map(|r| match r { + Err(e) => { + view! {
{trl("Something went wrong")}
{e.to_string()}
}} + Ok(r) => { match r { + ApiResponse::Data(r) => { + view! { +
+ + {move || { + let data = data.clone(); + view! { + {show_day(&data.summary.date.weekday())}" - "{loc_date(data.summary.date)}
+ + {item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}
+
+ {trl("Customer: ")}{data.customer.full_name}", "{data.customer.email}", "{data.customer.phone}
+ {move || { + let note = data.summary.note.clone(); + let show = note.is_some() && !note.clone().unwrap().is_empty(); + view! { + + {trl("Note: ")}{note.clone()}
+
+ } + }} + {trl("Price: ")}{data.summary.price.to_string()}
+
+ } + }} +
+
+ } + } + ApiResponse::Error(e) => {view! {
{trl(&e)}
}}} + } + }) + }} +
+
+
+ } +} \ No newline at end of file