You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

344 lines
15 KiB
Rust

use chrono::{Duration, Local, NaiveDate, NaiveTime, Timelike};
use leptos::*;
use leptos_captcha::{Captcha, pow_dispatch};
use leptos_router::*;
use rust_decimal::Decimal;
use crate::backend::appearance::get_appearance;
use crate::backend::customer::get_remembered;
use crate::backend::data::{ApiResponse, ClosingTime, Customer, DayHour, Reservation, ResProperty, SlotType, TmCheck};
use crate::backend::reservation::{CreateReservation, get_public_form_data, is_reserved};
use crate::backend::user::get_pow;
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>,
day: ReadSignal<NaiveDate>,
closing_days: Option<ClosingTime>) -> impl IntoView {
let closed = if let Some(c) = closing_days {
day.get() >= c.from_date && day.get() <= c.to_date
} else {
false
};
let checks = if !closed {
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<_>>()
} else {
vec![]
};
let prop_id = property.id();
let closed = checks.is_empty();
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>
<Show when=move || closed>
<div class="fs-3">{trl("Closed")}</div>
</Show>
}
}
#[component]
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();
let is_pending = create_rw_signal(None);
let active_str = create_rw_signal("true".to_string());
let get_customer = create_blocking_resource(||(), move |_| get_remembered());
let appearance = create_blocking_resource(||(), move |_| get_appearance());
let customer = create_rw_signal(Customer::default());
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()
captcha_state=is_pending.read_only()/>
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{
appearance.get().map(|a| match a {
Ok(a) => {
let app = a.clone();
view! {
<div>
<Show when=move || a.clone().banner.is_some()>
<div class="header_banner">
<h1 class="header_banner">{app.title.clone().unwrap_or_default()}</h1>
</div>
</Show>
<div>
<div class="header_banner_text" inner_html={app.text.unwrap_or("".to_string())}>
</div>
</div>
</div>
}
},
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
<p>{e.to_string()}</p></div>
}}
})
}
</Transition>
<div class="card-body">
<ActionForm
on:submit=move |ev| {
cr_reservation.set_pending(true);
if let Ok(mut act) = CreateReservation::from_event(&ev) {
validator.check(act.entity(), &ev);
if !validator.is_valid() {
invalid_dlg.show();
} else {
ev.prevent_default();
pow_dispatch(get_pow, is_pending, move |pow| {
act.pow = pow.unwrap();
cr_reservation.dispatch(act);
});
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 || {
get_customer.get().map(|c| match c {
Err(_) => {},
Ok(c) => {
if let Some(c) = c {
customer.set(c);
}
}
});
form_data.get().map(|u| match u {
Err(e) => {
view! {<div>{e.to_string()}</div>}}
Ok(u) => {
match u {
ApiResponse::Data(p) => {
view! {
<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()}
day={day.read_only()}
closing_days={data.closing_days.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 || customer.get().full_name}
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 || customer.get().email}
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 || customer.get().phone}
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>
<input
type="checkbox"
id="remember"
class="form-check-input"
checked="true"
on:change=move |ev| active_str.set(if event_target_checked(&ev)
{ "true".to_string() } else { "false".to_string() }) />
<label for="remember" class="form-label">{trl("Remember for next time")}</label>
<input type="hidden" prop:value=active_str name="reservation[remember]"/>
<Captcha is_pending />
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
{trl("Book")}
</button>
</div>
</div>
</div>
</div>
</div>
</ActionForm>
</div>
}
}