Implemented public part and creating reservations.
parent
0d4b4a0b3d
commit
6c7fd2e46f
@ -0,0 +1,50 @@
|
|||||||
|
use cfg_if::cfg_if;
|
||||||
|
|
||||||
|
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||||
|
use sqlx::{Postgres, Transaction};
|
||||||
|
use sqlx::{query_as, query};
|
||||||
|
use sqlx::Error;
|
||||||
|
use crate::backend::data::Customer;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
|
pub async fn find_customer_by_email(email: &str, tx: &mut Transaction<'_, Postgres>) -> Option<Customer> {
|
||||||
|
let customer = query_as::<_, Customer>("SELECT * FROM customer WHERE email = $1")
|
||||||
|
.bind(email)
|
||||||
|
.fetch_one(tx.deref_mut())
|
||||||
|
.await.unwrap_or_default();
|
||||||
|
|
||||||
|
if customer.email == email {
|
||||||
|
Some(customer)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_customer_data(customer: &Customer, full_name: &str, phone: &str, tx: &mut Transaction<'_, Postgres>) -> Result<Customer, Error> {
|
||||||
|
if &customer.full_name != full_name || &customer.phone != phone {
|
||||||
|
query("UPDATE CUSTOMER SET full_name=$1, phone=$2 WHERE id=$3")
|
||||||
|
.bind(full_name)
|
||||||
|
.bind(phone)
|
||||||
|
.bind(customer.id())
|
||||||
|
.execute(tx.deref_mut())
|
||||||
|
.await?;
|
||||||
|
Ok(Customer::new(customer.id(),
|
||||||
|
full_name.to_string(),
|
||||||
|
customer.email.clone(),
|
||||||
|
phone.to_string(),
|
||||||
|
customer.discount))
|
||||||
|
} else {
|
||||||
|
Ok(customer.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_customer(full_name: &str, email: &str, phone: &str, tx: &mut Transaction<'_, Postgres>) -> Result<Customer, Error> {
|
||||||
|
query("INSERT INTO customer(full_name, email, phone) VALUES($1, $2, $3)")
|
||||||
|
.bind(full_name)
|
||||||
|
.bind(email)
|
||||||
|
.bind(phone)
|
||||||
|
.execute(tx.deref_mut())
|
||||||
|
.await?;
|
||||||
|
Ok(find_customer_by_email(email, tx).await.ok_or(Error::RowNotFound)?)
|
||||||
|
}
|
||||||
|
}}
|
@ -0,0 +1,151 @@
|
|||||||
|
use leptos::*;
|
||||||
|
use validator::Validate;
|
||||||
|
use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData};
|
||||||
|
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::query_as;
|
||||||
|
use sqlx::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use crate::backend::data::ReservationSum;
|
||||||
|
use crate::backend::get_pool;
|
||||||
|
|
||||||
|
async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result<ReservationSum, Error> {
|
||||||
|
let reservation = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1")
|
||||||
|
.bind(uuid)
|
||||||
|
.fetch_one(tx.deref_mut())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(reservation)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reservations_for_day(day: &NaiveDate) -> Result<Vec<Reservation>, ServerFnError> {
|
||||||
|
let pool = get_pool().await?;
|
||||||
|
let reservations = query_as::<_, Reservation>("SELECT * FROM reservation JOIN reservation_sum on reservation.summary=reservation_sum.id WHERE reservation_sum.date=$1")
|
||||||
|
.bind(day)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = reservations {
|
||||||
|
if matches!(e, Error::RowNotFound) {
|
||||||
|
Ok(vec![])
|
||||||
|
} else {
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(reservations?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_public_form_data(day: NaiveDate) -> Result<ApiResponse<Vec<PublicFormData>>, ServerFnError> {
|
||||||
|
use crate::backend::opening_hours::hours_for_day;
|
||||||
|
use crate::backend::property::get_props;
|
||||||
|
use chrono::Datelike;
|
||||||
|
|
||||||
|
let hours = hours_for_day(day.weekday()).await?;
|
||||||
|
let props = get_props(Some("active = true".to_string())).await?;
|
||||||
|
let reservations = reservations_for_day(&day).await?;
|
||||||
|
|
||||||
|
Ok(ApiResponse::Data(props.into_iter().map(|p| PublicFormData {
|
||||||
|
property: p,
|
||||||
|
hours: hours.clone(),
|
||||||
|
reservations: reservations.clone()
|
||||||
|
}).collect::<Vec<_>>()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_reserved(reservations: &Vec<Reservation>, time: &NaiveTime, property: i32) -> bool {
|
||||||
|
for r in reservations {
|
||||||
|
if r.property == property && &r.from <= time && time < &r.to {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn create_reservation(reservation: CrReservation) -> Result<ApiResponse<NaiveDate>, ServerFnError> {
|
||||||
|
use crate::backend::get_pool;
|
||||||
|
use crate::backend::customer::find_customer_by_email;
|
||||||
|
use crate::backend::customer::sync_customer_data;
|
||||||
|
use crate::backend::customer::create_customer;
|
||||||
|
use crate::backend::property::get_prop_by_id;
|
||||||
|
use crate::backend::data::{TmCheck, ReservationState, Reservations};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use chrono::Local;
|
||||||
|
use sqlx::query;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
|
||||||
|
let slots = reservation.slots().iter().fold(HashMap::new(), |mut map, s| {
|
||||||
|
let slot_str = s.split("|").collect::<Vec<_>>();
|
||||||
|
map.entry(i32::from_str(slot_str.get(1).unwrap_or(&"")).unwrap_or(0))
|
||||||
|
.and_modify(|slot: &mut Vec<Result<TmCheck, AppError>>| slot.push(TmCheck::from_str(slot_str.get(0).unwrap_or(&""))))
|
||||||
|
.or_insert(vec![TmCheck::from_str(slot_str.get(0).unwrap_or(&""))]);
|
||||||
|
map
|
||||||
|
});
|
||||||
|
|
||||||
|
let res_for_day = reservations_for_day(&reservation.date()).await?;
|
||||||
|
let mut reservations = Reservations::new();
|
||||||
|
let mut price = Decimal::from(0);
|
||||||
|
for sl in slots {
|
||||||
|
let mut checks = sl.1.clone();
|
||||||
|
checks.sort();
|
||||||
|
let property = get_prop_by_id(sl.0).await?;
|
||||||
|
for c in checks {
|
||||||
|
reservations.add_slot(&c.clone()?, sl.0);
|
||||||
|
price = price + property.price;
|
||||||
|
if is_reserved(&res_for_day, &c?.from, sl.0) {
|
||||||
|
return Ok(ApiResponse::Error("Slot and time already booked".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = get_pool().await?;
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
let customer = if let Some(c) = find_customer_by_email(reservation.email(), &mut tx).await {
|
||||||
|
sync_customer_data(&c, reservation.full_name(), reservation.phone(), &mut tx).await?
|
||||||
|
} else {
|
||||||
|
create_customer(reservation.full_name(), reservation.email(), reservation.phone(), &mut tx).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let res_uuid = Uuid::new_v4();
|
||||||
|
query("INSERT INTO reservation_sum(uuid, date, customer, price, state, note, date_create) VALUES($1, $2, $3, $4, $5, $6, $7)")
|
||||||
|
.bind(res_uuid)
|
||||||
|
.bind(reservation.date())
|
||||||
|
.bind(customer.id())
|
||||||
|
.bind(price)
|
||||||
|
.bind(ReservationState::New)
|
||||||
|
.bind(reservation.note())
|
||||||
|
.bind(Local::now().date_naive())
|
||||||
|
.execute(tx.deref_mut())
|
||||||
|
.await?;
|
||||||
|
let sum = find_sum_by_uuid(&res_uuid, &mut tx).await?;
|
||||||
|
|
||||||
|
for r in reservations.reservations() {
|
||||||
|
query(r#"INSERT INTO reservation("from", "to", property, summary) VALUES($1, $2, $3, $4)"#)
|
||||||
|
.bind(r.from)
|
||||||
|
.bind(r.to)
|
||||||
|
.bind(r.property)
|
||||||
|
.bind(sum.id())
|
||||||
|
.execute(tx.deref_mut())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(ApiResponse::Data(reservation.date()))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForValidation for CreateReservation {
|
||||||
|
fn entity(&self) -> &dyn Validate {
|
||||||
|
&self.reservation
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,261 @@
|
|||||||
|
use chrono::{Duration, Local, NaiveDate, NaiveTime, Timelike};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use crate::backend::data::{ApiResponse, DayHour, Reservation, ResProperty, SlotType, TmCheck};
|
||||||
|
use crate::backend::reservation::{CreateReservation, get_public_form_data, is_reserved};
|
||||||
|
use crate::components::data_form::ForValidation;
|
||||||
|
use crate::components::modal_box::DialogOpener;
|
||||||
|
use crate::locales::trl;
|
||||||
|
use crate::pages::res_dialogs::{ResError, ResSaved};
|
||||||
|
use crate::validator::Validator;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn time_selector(
|
||||||
|
hours: Vec<DayHour>,
|
||||||
|
reservations: Vec<Reservation>,
|
||||||
|
property: ResProperty,
|
||||||
|
slots: RwSignal<Vec<String>>,
|
||||||
|
price: RwSignal<Decimal>) -> impl IntoView {
|
||||||
|
let checks = hours.into_iter().map(|h| {
|
||||||
|
match property.slot {
|
||||||
|
SlotType::Quarter => {
|
||||||
|
let mut ret: Vec<TmCheck> = vec![];
|
||||||
|
logging::log!("quarter");
|
||||||
|
for n in 0..(h.to() - h.from()).num_minutes() * 4 / 60 {
|
||||||
|
ret.push(TmCheck {
|
||||||
|
from: h.from() + Duration::minutes(n * 15),
|
||||||
|
to: h.from() + Duration::minutes((n + 1) * 15)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
SlotType::Half => {
|
||||||
|
let mut ret: Vec<TmCheck> = vec![];
|
||||||
|
logging::log!("half");
|
||||||
|
for n in 0..(h.to() - h.from()).num_minutes() * 2 / 60 {
|
||||||
|
ret.push(TmCheck {
|
||||||
|
from: h.from() + Duration::minutes(n * 30),
|
||||||
|
to: h.from() + Duration::minutes((n + 1) * 30)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
SlotType::Hour => {
|
||||||
|
let mut ret: Vec<TmCheck> = vec![];
|
||||||
|
for n in 0..(h.to() - h.from()).num_minutes() / 60 {
|
||||||
|
ret.push(TmCheck {
|
||||||
|
from: h.from() + Duration::minutes(n * 60),
|
||||||
|
to: h.from() + Duration::minutes((n + 1) * 60)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
SlotType::Day => {
|
||||||
|
let mut ret: Vec<TmCheck> = vec![];
|
||||||
|
ret.push(TmCheck {
|
||||||
|
from: NaiveTime::from_hms_opt(0,0,0).unwrap(),
|
||||||
|
to: NaiveTime::from_hms_opt(23, 59, 59).unwrap()
|
||||||
|
});
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>().into_iter().flatten().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let prop_id = property.id();
|
||||||
|
view! {
|
||||||
|
<For each=move || checks.clone() key=|c| c.from.minute() let:data>
|
||||||
|
<input type="checkbox"
|
||||||
|
class="btn-check"
|
||||||
|
id={data.from.to_string() + &property.name.clone()}
|
||||||
|
autocomplete="off"
|
||||||
|
disabled={is_reserved(&reservations, &data.from, prop_id)}
|
||||||
|
on:change= move |ev| {
|
||||||
|
let mut sl = slots.get();
|
||||||
|
if event_target_checked(&ev) {
|
||||||
|
sl.push(data.from.to_string() + "-" + &data.to.to_string() + "|" + &prop_id.to_string());
|
||||||
|
price.set(price.get() + property.price);
|
||||||
|
slots.set(sl);
|
||||||
|
} else {
|
||||||
|
slots.set(sl.into_iter().filter(|s| { s.clone() != (data.from.to_string() + "-" + &data.to.to_string() + "|" + &prop_id.to_string())}).collect());
|
||||||
|
price.set(price.get() - property.price);
|
||||||
|
}
|
||||||
|
}/>
|
||||||
|
<label class="btn btn-outline-primary" for={data.from.to_string() + &property.name.clone()}>{data.from.format("%H:%M").to_string()}</label>
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Public() -> impl IntoView {
|
pub fn Public() -> impl IntoView {
|
||||||
|
let day = create_rw_signal(NaiveDate::default());
|
||||||
|
let form_data = create_blocking_resource(move || day.get(), move |d| get_public_form_data(d));
|
||||||
|
let slots = create_rw_signal::<Vec<String>>(vec![]);
|
||||||
|
let price = create_rw_signal(Decimal::from(0));
|
||||||
|
let cr_reservation = create_server_action::<CreateReservation>();
|
||||||
|
let validator = Validator::new();
|
||||||
|
let invalid_dlg = DialogOpener::new();
|
||||||
|
let result_dlg = DialogOpener::new();
|
||||||
|
let result = cr_reservation.value();
|
||||||
|
|
||||||
|
create_effect(move |_| {
|
||||||
|
day.set(Local::now().date_naive());
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ResError opener=invalid_dlg validator=validator/>
|
||||||
|
<ResSaved opener=result_dlg save_result=result day=day price=price.write_only() slots=slots.write_only()/>
|
||||||
|
<div class="card-body">
|
||||||
|
<ActionForm
|
||||||
|
on:submit=move |ev| {
|
||||||
|
let act = CreateReservation::from_event(&ev);
|
||||||
|
if !act.is_err() {
|
||||||
|
validator.check(act.unwrap().entity(), &ev);
|
||||||
|
}
|
||||||
|
if !validator.is_valid() {
|
||||||
|
invalid_dlg.show();
|
||||||
|
} else {
|
||||||
|
result_dlg.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action=cr_reservation>
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bx bx-basket"></i>" "{trl("Booking")}</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="date" class="form-label">{trl("Date")}</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="date"
|
||||||
|
class="form-control"
|
||||||
|
prop:value={move || day.get().format("%Y-%m-%d").to_string()}
|
||||||
|
on:input=move |ev| {
|
||||||
|
price.set(Decimal::from(0));
|
||||||
|
slots.set(vec![]);
|
||||||
|
day.set(NaiveDate::parse_from_str(&event_target_value(&ev), "%Y-%m-%d").unwrap());
|
||||||
|
}
|
||||||
|
name="reservation[date]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Transition fallback=|| view! {<p>{trl("Loading...")}</p> }>
|
||||||
|
{move || {
|
||||||
|
form_data.get().map(|u| match u {
|
||||||
|
Err(e) => {
|
||||||
|
view! {<div>{e.to_string()}</div>}}
|
||||||
|
Ok(u) => {
|
||||||
|
match u {
|
||||||
|
ApiResponse::Data(p) => {
|
||||||
view! {
|
view! {
|
||||||
<div>"public"</div>
|
<div>
|
||||||
|
<For each=move || p.clone()
|
||||||
|
key=|prop| prop.property.id()
|
||||||
|
let:data>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<div class="form-label">{data.property.name.clone()}</div>
|
||||||
|
<div>
|
||||||
|
<TimeSelector
|
||||||
|
hours={data.hours.clone()}
|
||||||
|
reservations={data.reservations.clone()}
|
||||||
|
property={data.property.clone()}
|
||||||
|
slots={slots}
|
||||||
|
price={price}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</For>
|
||||||
|
<For each=move || slots.get()
|
||||||
|
key=|s| s.clone()
|
||||||
|
let:data>
|
||||||
|
<input type="hidden"
|
||||||
|
name="reservation[slots][]"
|
||||||
|
value={data}/>
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ApiResponse::Error(s) => {
|
||||||
|
view! {<div>{trl(&s)}</div>}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Transition>
|
||||||
|
<div class="form-label">{trl("Total price of booking")}</div>
|
||||||
|
<div>{move || format!("{} Kč", price.get())}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="bx bxs-contact"></i>" "{trl("Who is booking")}</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="full_name" class="form-label">{trl("Full name")}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="full_name"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={trl("Enter full name")}
|
||||||
|
//prop:value={move || opener.empty()}
|
||||||
|
name="reservation[full_name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="email" class="form-label">"Email"</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={trl("Enter e-mail address")}
|
||||||
|
//prop:value={move || opener.empty()}
|
||||||
|
name="reservation[email]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="phone" class="form-label">{trl("Phone number")}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="phone"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={trl("Enter phone number")}
|
||||||
|
//prop:value={move || opener.empty()}
|
||||||
|
name="reservation[phone]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="note" class="form-label">{trl("Note")}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="note"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={trl("Enter note")}
|
||||||
|
//prop:value={move || opener.empty()}
|
||||||
|
name="reservation[note]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{trl("Book")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ActionForm>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
use chrono::{Local, NaiveDate};
|
||||||
|
use leptos::*;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use crate::backend::data::ApiResponse;
|
||||||
|
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter};
|
||||||
|
use crate::components::validation_err::ValidationErr;
|
||||||
|
use crate::locales::{loc_date, trl};
|
||||||
|
use crate::validator::Validator;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn res_error(opener: DialogOpener, validator: Validator) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<ModalDialog opener=opener title="Can't create reservation">
|
||||||
|
<ModalBody>
|
||||||
|
<ValidationErr validator=validator/>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
|
||||||
|
on:click=move |_| {
|
||||||
|
validator.reset();
|
||||||
|
opener.hide();}>
|
||||||
|
{trl("Close")}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalDialog>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn res_saved(
|
||||||
|
opener: DialogOpener,
|
||||||
|
save_result: RwSignal<Option<Result<ApiResponse<NaiveDate>, ServerFnError>>>,
|
||||||
|
day: RwSignal<NaiveDate>,
|
||||||
|
price: WriteSignal<Decimal>,
|
||||||
|
slots: WriteSignal<Vec<String>>) -> impl IntoView {
|
||||||
|
view! {{move ||{
|
||||||
|
if let Some(r) = save_result.get() {
|
||||||
|
match r {
|
||||||
|
Ok(ar) => {
|
||||||
|
match ar {
|
||||||
|
ApiResponse::Data(d) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<ModalDialog opener=opener title="Reservation saved">
|
||||||
|
<ModalBody>
|
||||||
|
<p>
|
||||||
|
{trl("Your reservation has been successfully saved.")}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{trl("We look forward to seeing you on")}" "{loc_date(d)}
|
||||||
|
</p>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
|
||||||
|
on:click=move |_| {
|
||||||
|
opener.hide();
|
||||||
|
if day.get() == Local::now().date_naive() {
|
||||||
|
day.set(NaiveDate::parse_from_str("2024-01-01", "%Y-%m-%d").unwrap());
|
||||||
|
}
|
||||||
|
slots.set(vec![]);
|
||||||
|
price.set(Decimal::default());
|
||||||
|
day.set(Local::now().date_naive());}>
|
||||||
|
{trl("Ok")}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ApiResponse::Error(err) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<ModalDialog opener=opener title="Reservation not saved">
|
||||||
|
<ModalBody>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{trl("Reservation cannot be saved.")}<br/>{trl(&err)}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
|
||||||
|
on:click=move |_| {
|
||||||
|
opener.hide();}>
|
||||||
|
{trl("Close")}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<ModalDialog opener=opener title="Save error">
|
||||||
|
<ModalBody>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{trl("Error while saving reservation.")}<br/>{trl(&err.to_string())}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"
|
||||||
|
on:click=move |_| {
|
||||||
|
opener.hide();}>
|
||||||
|
{trl("Close")}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view! {<div></div>}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue