Implemented admin overview.

main
Josef Rokos 1 year ago
parent 6c7fd2e46f
commit e69db7f40b

@ -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<Reservation>);
// 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<ResWithProperty>
}
/*
pub enum MessageType {
NewReservation,

@ -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<ReservationSum, Error> {
@ -41,6 +42,70 @@ cfg_if! { if #[cfg(feature = "ssr")] {
Ok(reservations?)
}
}
async fn reservations_in_range(from: &NaiveDate, to: &NaiveDate, state: Option<ReservationState>) -> Result<Vec<ResSumWithItems>, 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<Vec<ResSumWithItems>, 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<ApiResponse<Vec<ResSumWithItems>>, 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<ApiResponse<Vec<ResSumWithItems>>, 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<ApiResponse<()>, 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<ApiResponse<()>, 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(()))
}

@ -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"),

@ -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") }
}
}

@ -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::<Request>();
//log!("{:?}", pok);
let app_opener = DialogOpener::new();
let cancel_opener = DialogOpener::new();
view! {
<ModalDialog opener={dialog} title="Titulek">
<ModalBody>
<div class="row">
<div class="col mb-3">
<label for="nameWithTitle" class="form-label">"Name"</label>
<input
type="text"
id="nameWithTitle"
class="form-control"
placeholder="Enter Name"
/>
</div>
<h1>{trl("Overview")}</h1>
<div class="row mb-5">
<div class="col-md">
<NewReservations app_opener=app_opener cancel_opener=cancel_opener/>
</div>
<div class="row g-2">
<div class="col mb-0">
<label for="emailWithTitle" class="form-label">"Email"</label>
<input
type="text"
id="emailWithTitle"
class="form-control"
placeholder="xxxx@xxx.xx"
/>
</div>
<div class="col mb-0">
<label for="dobWithTitle" class="form-label">"DOB"</label>
<input
type="text"
id="dobWithTitle"
class="form-control"
placeholder="DD / MM / YY"
/>
</div>
<div class="col-md">
<NextReservations app_opener=app_opener cancel_opener=cancel_opener/>
</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" on:click=move |_| dialog.hide()>
"Close"
</button>
<button type="button" class="btn btn-primary">"Save changes"</button>
</ModalFooter>
</ModalDialog>
<h1>"Welcome to Leptos!"</h1>
<button on:click=on_click>"Click Me: " {count}</button>
<button on:click=move |_| dialog.show()>"Dialog"</button>
<p>{trl("testik!")}</p>
</div>
}
}

@ -15,4 +15,6 @@ mod properties;
mod property_edit;
mod property_delete;
mod res_dialogs;
mod today_reservations;
mod new_reservations;

@ -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<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
let approve = create_server_action::<Approve>();
view! {
<QuestionDialog opener=opener action=approve title="Approve booking">
<input type="hidden" prop:value=move || reservation.get().summary.uuid.to_string() name="uuid"/>
<div>
{trl("Approve booking on ")}
{move || loc_date(reservation.get().summary.date)}
{trl(" for ")}
{move || reservation.get().customer.full_name}"?"
</div>
</QuestionDialog>
}
}
#[component]
fn cancel_dialog(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
let cancel = create_server_action::<Cancel>();
view! {
<QuestionDialog opener=opener action=cancel title="Cancel booking">
<input type="hidden" prop:value=move || reservation.get().summary.uuid.to_string() name="uuid"/>
<div>
{trl("Cancel booking on ")}
{move || loc_date(reservation.get().summary.date)}
{trl(" for ")}
{move || reservation.get().customer.full_name}"?"
</div>
</QuestionDialog>
}
}
#[component]
fn create_dialog(opener: DialogOpener) -> impl IntoView {
view! {
<ModalDialog opener=opener title="Create booking">
<ModalBody>
<Public/>
</ModalBody>
</ModalDialog>
}
}
#[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! {
<ApproveDialog opener=app_opener reservation=reservation.read_only() />
<CancelDialog opener=cancel_opener reservation=reservation.read_only() />
<CreateDialog opener=create />
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-basket"></i>" "{trl("New bookings")}</h5>
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{move || {
res.get().map(|r| match r {
Err(e) => {
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}}
Ok(r) => { match r {
ApiResponse::Data(r) => {
view! {
<div>
<For each=move || r.clone()
key=|res| res.summary.id()
let:data>
{move || {
let menu = MenuOpener::new();
let data = data.clone();
let app_data = data.clone();
let cancel_data = data.clone();
view! {
<b>{show_day(&data.summary.date.weekday())}" - "{loc_date(data.summary.date)}</b><br/>
<For each=move || data.reservations.clone()
key=|item| item.reservation.id()
let:item>
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
</For>
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email}</a>", "{data.customer.phone}<br/>
{move || {
let note = data.summary.note.clone();
let show = note.is_some() && !note.clone().unwrap().is_empty();
view! {
<Show when=move || show>
{trl("Note: ")}{note.clone()}<br/>
</Show>
}
}}
{trl("Price: ")}{data.summary.price.to_string()}<br/>
<table width="100%">
<tr>
<td width="100%"></td>
<td>
<div class="dropdown">
<button type="button" class="btn p-0 dropdown-toggle hide-arrow"
on:click=move |_| menu.toggle()>
<i class="bx bx-dots-vertical-rounded"></i>
</button>
<div class={move || if menu.visible() {"dropdown-menu show"} else {"dropdown-menu"} }
style="position: absolute; insert: 0px 0px auto; margin: 0px; transform: translate3d(-160px, 0px, 0px);"
on:mouseleave=move |_| menu.toggle()>
<a class="dropdown-item" href="javascript:void(0);" on:click=move |_| {
reservation.set(app_data.clone());
app_opener.show();
}>
<i class="bx bx-edit-alt me-1"></i> {trl("Approve")}</a>
<a class="dropdown-item text-danger" href="javascript:void(0);" on:click=move |_| {
reservation.set(cancel_data.clone());
cancel_opener.show();
}>
<i class="bx bx-trash me-1"></i> {trl("Cancel")}</a>
</div>
</div>
</td>
</tr>
</table>
<hr/>
}
}}
</For>
</div>
}
}
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}}}
}
})
}}
</Transition>
<a href="#" class="card-link" on:click=move |_| create.show()>
<i class="bx bx-plus-circle fs-4 lh-0"></i>
</a>
</div>
</div>
}
}

@ -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();

@ -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! {
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-basket"></i>" "{trl("Next approved bookings")}</h5>
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{move || {
res.get().map(|r| match r {
Err(e) => {
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}}
Ok(r) => { match r {
ApiResponse::Data(r) => {
view! {
<div>
<For each=move || r.clone()
key=|res| res.summary.id()
let:data>
{move || {
let data = data.clone();
view! {
<b>{show_day(&data.summary.date.weekday())}" - "{loc_date(data.summary.date)}</b><br/>
<For each=move || data.reservations.clone()
key=|item| item.reservation.id()
let:item>
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
</For>
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email}</a>", "{data.customer.phone}<br/>
{move || {
let note = data.summary.note.clone();
let show = note.is_some() && !note.clone().unwrap().is_empty();
view! {
<Show when=move || show>
{trl("Note: ")}{note.clone()}<br/>
</Show>
}
}}
{trl("Price: ")}{data.summary.price.to_string()}<br/>
<hr/>
}
}}
</For>
</div>
}
}
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}}}
}
})
}}
</Transition>
</div>
</div>
}
}
Loading…
Cancel
Save