Implemented public part and creating reservations.
This commit is contained in:
Generated
+42
-44
@@ -723,12 +723,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271"
|
||||
|
||||
[[package]]
|
||||
name = "common_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3f6d59c71e7dc3af60f0af9db32364d96a16e9310f3f5db2b55ed642162dd35"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.13.3"
|
||||
@@ -1692,9 +1686,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
@@ -1740,9 +1734,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98f0fe11faa66358ff8c2ee48881c54f8f216ecddabfc5b69cdc2e90c8e337b"
|
||||
checksum = "269ba4ba91ffa73d9559c975e0be17bd4eb34c6b6abd7fdd5704106132d89d2a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"leptos_config",
|
||||
@@ -1779,9 +1773,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_actix"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f50f8c459143ef36c6dce5786e33e48d46dfb6829af87c985d65fb3b0b402aa"
|
||||
checksum = "89db4657bdcd28193e9d8cd640ec5d76b55abdf4b16cd5066f1b03f8aea49758"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
@@ -1799,9 +1793,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_config"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0f0e1a9a583d943b19c740c82a3ec69224c979af90f40738d93ec59ee1475bb"
|
||||
checksum = "e72d8689d54737991831e9b279bb4fba36d27a93aa975c75cd4241d9a4a425ec"
|
||||
dependencies = [
|
||||
"config",
|
||||
"regex",
|
||||
@@ -1812,9 +1806,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_dom"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "111391d1ccbc3355344f90f0893f4137db13a7f98d53fede0a3613c522ebaf19"
|
||||
checksum = "ad314950d41acb1bfdb8b5924811b2983484a8d6f69a20b834a173a682657ed4"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"cfg-if",
|
||||
@@ -1823,7 +1817,7 @@ dependencies = [
|
||||
"getrandom",
|
||||
"html-escape",
|
||||
"indexmap 2.0.0",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.12.0",
|
||||
"js-sys",
|
||||
"leptos_reactive",
|
||||
"once_cell",
|
||||
@@ -1842,9 +1836,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_hot_reload"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6902fabee84955a85a6cdebf8ddfbfb134091087b172e32ebb26e571d4640ca"
|
||||
checksum = "3f62dcab17728877f2d2f16d2c8a6701c4c5fbdfb4964792924acb0b50529659"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -1860,9 +1854,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_integration_utils"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb816f3c809227b090b538994368a756d494c829b07c1bd312d07263552b8c87"
|
||||
checksum = "fddda3a3b768dad90f80fb56ac6e250bc5c60755f8e3df225913aba4364ed7ee"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"leptos",
|
||||
@@ -1874,15 +1868,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_macro"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e68201041cc5af68f7eb35015336827a36c543d87dcf2403117d7244db1f14a0"
|
||||
checksum = "57955d66f624265222444a5c565fea38efa5b0152a1dfc7c060a78e5fb62a852"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"cfg-if",
|
||||
"convert_case 0.6.0",
|
||||
"html-escape",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.12.0",
|
||||
"leptos_hot_reload",
|
||||
"prettyplease",
|
||||
"proc-macro-error",
|
||||
@@ -1897,9 +1891,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_meta"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b64d2b4bd0ab25a4897179ee603f2fa8178da6c9f97ef3efd4fa46580fd7efc1"
|
||||
checksum = "1bc25c0f7f14ed5daf42b8d0d273ed790b0449e8ba8cff1c2fa800dc90a75acb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indexmap 2.0.0",
|
||||
@@ -1911,9 +1905,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_reactive"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282e84ae3e3eb30ab1eb1c881bfeea8a3cb6d6c683dc99f26f2f69ee240b148d"
|
||||
checksum = "b4f54a525a0edfc8c2bf3ee92aae411800b8b10892c9cd819f8e8a6d4f0d62f3"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"cfg-if",
|
||||
@@ -1938,15 +1932,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_router"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5ca4422fdfba8af03d347d346f9364a4393ad36e227a018567192395285cf65"
|
||||
checksum = "b31087173c60e25c329a1c6786756dd9ee97750b378622df4d780db160a09040"
|
||||
dependencies = [
|
||||
"cached",
|
||||
"cfg-if",
|
||||
"common_macros",
|
||||
"gloo-net",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.12.0",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"leptos",
|
||||
@@ -1970,9 +1963,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "leptos_server"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67f3810352bab860bcfa85f1760de4bd6e82cd72b14a97779d9168d37661bbf"
|
||||
checksum = "2fd1517c2024bc47d764e96053e55b927f8a2159e735a0cc47232542b493df9d"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"lazy_static",
|
||||
@@ -2961,9 +2954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0186f969a1f9572af27159b8273252abf9a6a38934130fe6f3ae0e439d48cf14"
|
||||
checksum = "6c265de965fe48e09ad8899d0ab1ffebdfa1a9914e4de5ff107b07bd94cf7541"
|
||||
dependencies = [
|
||||
"ciborium",
|
||||
"const_format",
|
||||
@@ -2986,9 +2979,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dbc70e4f185ff2b5c11f02a91baf830f33e456e0571d0680d1d76999ed242ed"
|
||||
checksum = "f77000541a62ceeec01eef3ee0f86c155c33dac5fae750ad04a40852c6d5469a"
|
||||
dependencies = [
|
||||
"const_format",
|
||||
"proc-macro-error",
|
||||
@@ -3001,9 +2994,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "server_fn_macro_default"
|
||||
version = "0.5.2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8aaf8cf1f5dde82d3f37548732a4852f65d5279b4ae40add5a2a3c9e559f662"
|
||||
checksum = "8a3353f22e2bcc451074d4feaa37317d9d17dff11d4311928384734ea17ab9ca"
|
||||
dependencies = [
|
||||
"server_fn_macro",
|
||||
"syn 2.0.28",
|
||||
@@ -3218,6 +3211,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
@@ -3301,6 +3295,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -3343,6 +3338,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -3367,6 +3363,7 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3647,18 +3644,18 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.16.0"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6605aaa56cce0947127ffa0675a8a1b181f87773364390174de60a86ab9085f1"
|
||||
checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e"
|
||||
dependencies = [
|
||||
"typed-builder-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder-macro"
|
||||
version = "0.16.0"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a6a6884f6a890a012adcc20ce498f30ebdc70fb1ea242c333cc5f435b0b3871"
|
||||
checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3765,6 +3762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+6
-6
@@ -12,18 +12,18 @@ actix-web = { version = "4.4.0", optional = true, features = ["macros"] }
|
||||
actix-session = { version = "0.8.0", optional = true, features = ["cookie-session"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.5.2" }
|
||||
leptos_meta = { version = "0.5.2" }
|
||||
leptos_actix = { version = "0.5.2", optional = true }
|
||||
leptos_router = { version = "0.5.2" }
|
||||
leptos = { version = "0.5.7" }
|
||||
leptos_meta = { version = "0.5.7" }
|
||||
leptos_actix = { version = "0.5.7", optional = true }
|
||||
leptos_router = { version = "0.5.7" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
wasm-bindgen = "=0.2.87"
|
||||
web-sys = { version = "0.3.61", features = ["Navigator"] }
|
||||
lazy_static = "1.4.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"]}
|
||||
sqlx = { version = "0.7.1", optional = true, features = ["runtime-tokio-rustls", "postgres", "chrono", "rust_decimal"] }
|
||||
sqlx = { version = "0.7.1", optional = true, features = ["runtime-tokio-rustls", "postgres", "chrono", "rust_decimal", "uuid"] }
|
||||
rust_decimal = "1.31.0"
|
||||
uuid = {version = "1.4.1", features = ["v4"]}
|
||||
uuid = {version = "1.4.1", features = ["v4", "serde"]}
|
||||
validator = {version = "0.16.1", features = ["derive"]}
|
||||
pwhash = "1.0.0"
|
||||
futures-util = "0.3.28"
|
||||
|
||||
@@ -14,7 +14,8 @@ CREATE TABLE "user" (
|
||||
full_name VARCHAR,
|
||||
email VARCHAR,
|
||||
admin bool NOT NULL default false,
|
||||
get_emails bool NOT NULL default false
|
||||
get_emails bool NOT NULL default false,
|
||||
active bool NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
CREATE TYPE slot_type AS ENUM ('Quarter', 'Half', 'Hour', 'Day');
|
||||
@@ -47,29 +48,32 @@ CREATE TABLE opening_hour (
|
||||
);
|
||||
|
||||
CREATE TABLE customer (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id SERIAL PRIMARY KEY,
|
||||
full_name VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
phone VARCHAR,
|
||||
discount INTEGER
|
||||
phone VARCHAR NOT NULL,
|
||||
discount INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TYPE reservation_state AS ENUM ('New', 'Approved', 'Canceled');
|
||||
|
||||
CREATE TABLE reservation_sum (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id SERIAL PRIMARY KEY,
|
||||
uuid uuid NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
customer BIGINT REFERENCES customer(id) NOT NULL,
|
||||
customer INTEGER REFERENCES customer(id) NOT NULL,
|
||||
price NUMERIC(9, 2) NOT NULL,
|
||||
state reservation_state
|
||||
state reservation_state NOT NULL DEFAULT 'New',
|
||||
date_create DATE NOT NULL,
|
||||
edited_by INTEGER REFERENCES "user"(id) ON DELETE SET NULL,
|
||||
note VARCHAR
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE reservation (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
id SERIAL PRIMARY KEY,
|
||||
"from" TIME NOT NULL,
|
||||
"to" TIME NOT NULL,
|
||||
property INTEGER REFERENCES property(id) NOT NULL,
|
||||
summary BIGINT REFERENCES reservation_sum(id) NOT NULL
|
||||
summary INTEGER REFERENCES reservation_sum(id) NOT NULL
|
||||
);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}*/
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
use log::{debug, error};
|
||||
use log::error;
|
||||
use rezervator::backend::company::check_company;
|
||||
use rezervator::backend::user::create_admin;
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ mod hours_edit;
|
||||
mod properties;
|
||||
mod property_edit;
|
||||
mod property_delete;
|
||||
mod res_dialogs;
|
||||
|
||||
|
||||
+254
-1
@@ -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>}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user