Implemented public part and creating reservations.

This commit is contained in:
2024-02-01 09:18:00 +01:00
parent 0d4b4a0b3d
commit 6c7fd2e46f
16 changed files with 930 additions and 103 deletions
+50
View File
@@ -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)?)
}
}}
+191 -34
View File
@@ -1,14 +1,13 @@
//use chrono::{NaiveDate, NaiveTime, Weekday};
//use rust_decimal::Decimal;
#![allow(unused_variables)]
use std::fmt::Display;
use chrono::{NaiveTime, Weekday};
use std::str::FromStr;
use chrono::{Local, NaiveDate, NaiveTime, Weekday};
use lazy_static::lazy_static;
use regex::Regex;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
//use uuid::Uuid;
use uuid::Uuid;
use validator::{Validate, ValidationError};
use crate::error::AppError;
@@ -329,6 +328,193 @@ impl ResProperty {
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
pub struct PublicFormData {
pub property: ResProperty,
pub hours: Vec<DayHour>,
pub reservations: Vec<Reservation>
}
fn empty_slots() -> Vec<String> {
vec![]
}
fn validate_date(date: &NaiveDate) -> Result<(), ValidationError> {
if date < &Local::now().date_naive() {
Err(ValidationError::new("date_in_past"))
} else {
Ok(())
}
}
#[derive(Clone, Serialize, Deserialize, Debug, Validate)]
pub struct CrReservation {
#[validate(custom(function = "validate_date", message = "Date can't be in past"))]
date: NaiveDate,
#[validate(length(min = 1,message = "Select at last one time slot"))]
#[serde(default = "empty_slots")]
slots: Vec<String>,
#[validate(length(min = 1,message = "Enter your full name"))]
full_name: String,
#[validate(email(message = "Enter valid email address"))]
email: String,
#[validate(length(min = 1,message = "Enter your phone number"))]
phone: String,
note: String
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct TmCheck {
pub from: NaiveTime,
pub to: NaiveTime
}
impl FromStr for TmCheck {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let times = s.split("-").collect::<Vec<_>>();
if times.len() != 2 {
return Err(AppError::HourParseError);
}
Ok(TmCheck{
from: NaiveTime::from_str(times.get(0).unwrap_or(&""))?,
to: NaiveTime::from_str(times.get(1).unwrap_or(&""))?
})
}
}
impl CrReservation {
pub fn date(&self) -> NaiveDate {
self.date
}
pub fn slots(&self) -> &Vec<String> {
&self.slots
}
pub fn full_name(&self) -> &str {
&self.full_name
}
pub fn email(&self) -> &str {
&self.email
}
pub fn phone(&self) -> &str {
&self.phone
}
pub fn note(&self) -> &str {
&self.note
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Customer {
id: i32,
pub full_name: String,
pub email: String,
pub phone: String,
pub discount: i32
}
impl Customer {
pub fn id(&self) -> i32 {
self.id
}
pub fn new(id: i32, full_name: String, email: String, phone: String, discount: i32) -> Self {
Self { id, full_name, email, phone, discount }
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "reservation_state"))]
pub enum ReservationState {
#[default]
New,
Approved,
Canceled,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Reservation {
id: i32,
pub from: NaiveTime,
pub to: NaiveTime,
pub property: i32,
pub summary: i32,
}
pub struct Reservations(Vec<Reservation>);
// Transform slots to reservations
impl Reservations {
pub fn new() -> Self {
Reservations(vec![])
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn add_slot(&mut self, check: &TmCheck, property: i32) {
let mut added = false;
self.0.iter_mut().for_each(|r| {
if r.property == property && r.to == check.from {
r.to = check.to;
added = true;
}
});
if !added {
self.0.push(Reservation {
from: check.from,
to: check.to,
property,
id: 0,
summary: 0,
});
}
}
pub fn reservations(&self) -> &Vec<Reservation> {
&self.0
}
}
impl Reservation {
pub fn new(from: NaiveTime, to: NaiveTime, property: i32) -> Self {
Self { id: 0, from, to, property, summary: 0 }
}
pub fn id(&self) -> i32 {
self.id
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct ReservationSum {
id: i32,
pub uuid: Uuid,
pub date: NaiveDate,
pub customer: i32,
pub price: Decimal,
pub state: ReservationState,
pub date_create: NaiveDate,
pub edited_by: Option<i32>,
pub note: Option<String>,
}
impl ReservationSum {
pub fn id(&self) -> i32 {
self.id
}
}
/*
pub enum MessageType {
NewReservation,
@@ -344,33 +530,4 @@ pub struct Message {
text: String,
}
pub struct Customer {
id: u128,
full_name: String,
email: String,
phone: String,
discount: u8
}
pub enum ReservationState {
New,
Approved,
Canceled,
}
pub struct Reservation {
id: u128,
from: NaiveTime,
to: NaiveTime,
property: Property,
}
pub struct ReservationSum {
id: u128,
uuid: Uuid,
date: NaiveDate,
items: Vec<Reservation>,
customer: Customer,
price: Decimal,
state: ReservationState,
}*/
*/
+2
View File
@@ -5,6 +5,8 @@ pub mod user;
pub mod auth_middleware;
pub mod opening_hours;
pub mod property;
pub mod reservation;
pub mod customer;
#[macro_export]
macro_rules! perm_check {
+23
View File
@@ -1,10 +1,28 @@
use std::collections::HashMap;
use cfg_if::cfg_if;
use chrono::Weekday;
use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, DayHour, WeekHours};
use crate::components::data_form::ForValidation;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::error::AppError;
pub async fn hours_for_day(day: Weekday) -> Result<Vec<DayHour>, AppError> {
use crate::backend::get_pool;
use crate::backend::data::OpeningHour;
let pool = get_pool().await?;
let hours = sqlx::query_as::<_, OpeningHour>("SELECT * FROM opening_hour WHERE day = $1")
.bind(day.num_days_from_monday() as i32)
.fetch_all(&pool)
.await?;
Ok(hours.into_iter().map(|h| { DayHour::new(h.from, h.to, h.discount)}).collect())
}
}}
#[server]
pub async fn get_hours() -> Result<HashMap<Weekday, Vec<DayHour>>, ServerFnError> {
use crate::backend::get_pool;
@@ -29,6 +47,11 @@ pub async fn get_hours() -> Result<HashMap<Weekday, Vec<DayHour>>, ServerFnError
Ok(ret)
}
#[server]
pub async fn get_hours_for_day(day: Weekday) -> Result<Vec<DayHour>, ServerFnError> {
Ok(hours_for_day(day).await?)
}
#[server]
pub async fn update_hours(hours: WeekHours) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check;
+34 -4
View File
@@ -1,14 +1,44 @@
use cfg_if::cfg_if;
use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, ResProperty};
use crate::components::data_form::ForValidation;
#[server]
pub async fn get_properties() -> Result<ApiResponse<Vec<ResProperty>>, ServerFnError> {
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::backend::get_pool;
let pool = get_pool().await?;
let props = sqlx::query_as::<_, ResProperty>("SELECT * FROM property").fetch_all(&pool).await?;
pub async fn get_props(filter: Option<String>) -> Result<Vec<ResProperty>, ServerFnError> {
let pool = get_pool().await?;
let props = if let Some(f) = filter {
sqlx::query_as::<_, ResProperty>(&format!("SELECT * FROM property WHERE {} ORDER BY id", f)).fetch_all(&pool).await?
} else {
sqlx::query_as::<_, ResProperty>("SELECT * FROM property ORDER BY id").fetch_all(&pool).await?
};
Ok(props)
}
pub async fn get_prop_by_id(id: i32) -> Result<ResProperty, ServerFnError> {
let pool = get_pool().await?;
let prop = sqlx::query_as::<_, ResProperty>("SELECT * FROM property WHERE id = $1")
.bind(id)
.fetch_one(&pool)
.await?;
Ok(prop)
}
}}
#[server]
pub async fn get_properties() -> Result<ApiResponse<Vec<ResProperty>>, ServerFnError> {
let props = get_props(None).await?;
Ok(ApiResponse::Data(props))
}
#[server]
pub async fn get_active_properties() -> Result<ApiResponse<Vec<ResProperty>>, ServerFnError> {
let props = get_props(Some("active = true".to_string())).await?;
Ok(ApiResponse::Data(props))
}
+151
View File
@@ -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
}
}
+13 -4
View File
@@ -1,12 +1,14 @@
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use chrono::ParseError;
use leptos::ServerFnError;
#[derive(Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum AppError {
HourParseError,
ServerError(String),
FatalError(String)
FatalError(String),
SlotParseError
}
impl AppError {
@@ -14,7 +16,8 @@ impl AppError {
match self {
AppError::HourParseError => {"Hour parse error".to_string()},
AppError::ServerError(e) => {format!("Server error: {}", e)},
AppError::FatalError(e) => {format!("Fatal error: {}", e)}
AppError::FatalError(e) => {format!("Fatal error: {}", e)},
AppError::SlotParseError => {"Book slot parse error".to_string()}
}
}
}
@@ -38,4 +41,10 @@ impl From<sqlx::Error> for AppError {
fn from(value: sqlx::Error) -> Self {
AppError::FatalError(value.to_string())
}
}
}
impl From<ParseError> for AppError {
fn from(_value: ParseError) -> Self {
AppError::HourParseError
}
}
+12
View File
@@ -44,6 +44,18 @@ lazy_static! {
("Price", "Cena"),
("Edit hours", "Upravit hodiny"),
("Hours", "Hodiny"),
("Who is booking", "Kdo rezervuje"),
("Reservation", "Rezervace"),
("Booking", "Rezervace"),
("Enter full name", "Zdejte celé jméno"),
("Enter e-mail address", "Zadejte e-mailovou adresu"),
("Phone number", "Telefonní číslo"),
("Enter phone number", "Zadejte telefonní číslo"),
("Book", "Rezervovat"),
("Total price of booking", "Celková cena rezervace"),
("Date", "Datum"),
("Note", "Poznámka"),
("Enter note", "Zadejte poznámku"),
])),
("sk", HashMap::from( [
("Dashboard", "Prehlad"),
+19
View File
@@ -1,3 +1,5 @@
use chrono::NaiveDate;
use leptos::use_context;
use crate::locales::catalogues::get_dictionary;
mod catalogues;
@@ -32,4 +34,21 @@ pub fn trl(phrase: &str) -> impl Fn() -> String {
let out = translated.to_string();
move || { out.clone() }
}
// ToDo better date formatting
pub fn loc_date(date: NaiveDate) -> impl Fn() -> String {
let mut dt = date.format("%Y-%m-%d").to_string();
let locs = use_context::<Locales>().unwrap_or(Locales(vec![])).0;
for loc in locs {
if let Some(key) = loc {
if let Some(k) = key.split("-").collect::<Vec<_>>().get(0) {
if *k != "en" {
dt = date.format("%d. %m. %Y").to_string();
}
}
}
}
move || { dt.clone() }
}
+1 -1
View File
@@ -1,4 +1,4 @@
use log::{debug, error};
use log::error;
use rezervator::backend::company::check_company;
use rezervator::backend::user::create_admin;
+1
View File
@@ -14,4 +14,5 @@ mod hours_edit;
mod properties;
mod property_edit;
mod property_delete;
mod res_dialogs;
+254 -1
View File
@@ -1,8 +1,261 @@
use chrono::{Duration, Local, NaiveDate, NaiveTime, Timelike};
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]
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! {
<div>"public"</div>
<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! {
<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!("{}", 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>
}
}
+118
View File
@@ -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>}
}
}}
}
}