Implemented public part and creating reservations.

main
Josef Rokos 1 year ago
parent 0d4b4a0b3d
commit 6c7fd2e46f

86
Cargo.lock generated

@ -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]]

@ -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)?)
}
}}

@ -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,48 +328,206 @@ impl ResProperty {
}
}
/*
pub enum MessageType {
NewReservation,
NewReservationCust,
ReservationApp,
ReservationCanceled,
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
pub struct PublicFormData {
pub property: ResProperty,
pub hours: Vec<DayHour>,
pub reservations: Vec<Reservation>
}
pub struct Message {
id: u16,
msg_type: MessageType,
subject: String,
text: String,
fn empty_slots() -> Vec<String> {
vec![]
}
pub struct Customer {
id: u128,
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,
discount: u8
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: u128,
from: NaiveTime,
to: NaiveTime,
property: Property,
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: u128,
uuid: Uuid,
date: NaiveDate,
items: Vec<Reservation>,
customer: Customer,
price: Decimal,
state: ReservationState,
}*/
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,
NewReservationCust,
ReservationApp,
ReservationCanceled,
}
pub struct Message {
id: u16,
msg_type: MessageType,
subject: String,
text: String,
}
*/

@ -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;

@ -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;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::backend::get_pool;
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> {
use crate::backend::get_pool;
let props = get_props(None).await?;
let pool = get_pool().await?;
let props = sqlx::query_as::<_, ResProperty>("SELECT * FROM property").fetch_all(&pool).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
}
}

@ -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,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;

@ -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…
Cancel
Save