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_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!("{} 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