diff --git a/Cargo.lock b/Cargo.lock index 8a7facb..9a81c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 8cdf2ba..52bc1c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/migrations/01_init_db.sql b/migrations/01_init_db.sql index 0c67c13..9bb3bb4 100644 --- a/migrations/01_init_db.sql +++ b/migrations/01_init_db.sql @@ -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 ); diff --git a/src/backend/customer.rs b/src/backend/customer.rs new file mode 100644 index 0000000..e1c8517 --- /dev/null +++ b/src/backend/customer.rs @@ -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 { + 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 { + 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 { + 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)?) + } +}} diff --git a/src/backend/data.rs b/src/backend/data.rs index 49bf721..44e270b 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -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, + pub reservations: Vec } -pub struct Message { - id: u16, - msg_type: MessageType, - subject: String, - text: String, +fn empty_slots() -> Vec { + 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, + #[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 { + let times = s.split("-").collect::>(); + 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 { + &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); + +// 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 { + &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, - 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, + pub note: Option, +} + +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, +} + +*/ diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 8922e8e..13b5ce8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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 { diff --git a/src/backend/opening_hours.rs b/src/backend/opening_hours.rs index 3bfde4f..1c6d070 100644 --- a/src/backend/opening_hours.rs +++ b/src/backend/opening_hours.rs @@ -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, 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>, ServerFnError> { use crate::backend::get_pool; @@ -29,6 +47,11 @@ pub async fn get_hours() -> Result>, ServerFnError Ok(ret) } +#[server] +pub async fn get_hours_for_day(day: Weekday) -> Result, ServerFnError> { + Ok(hours_for_day(day).await?) +} + #[server] pub async fn update_hours(hours: WeekHours) -> Result, ServerFnError> { use crate::perm_check; diff --git a/src/backend/property.rs b/src/backend/property.rs index 0304685..f602bb1 100644 --- a/src/backend/property.rs +++ b/src/backend/property.rs @@ -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) -> Result, 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 { + 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>, 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>, ServerFnError> { + let props = get_props(Some("active = true".to_string())).await?; Ok(ApiResponse::Data(props)) } diff --git a/src/backend/reservation.rs b/src/backend/reservation.rs new file mode 100644 index 0000000..c344989 --- /dev/null +++ b/src/backend/reservation.rs @@ -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 { + 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, 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>, 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::>())) +} + +pub fn is_reserved(reservations: &Vec, 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, 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::>(); + map.entry(i32::from_str(slot_str.get(1).unwrap_or(&"")).unwrap_or(0)) + .and_modify(|slot: &mut Vec>| 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 + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 470393d..affcc3c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 for AppError { fn from(value: sqlx::Error) -> Self { AppError::FatalError(value.to_string()) } -} \ No newline at end of file +} + +impl From for AppError { + fn from(_value: ParseError) -> Self { + AppError::HourParseError + } +} diff --git a/src/locales/catalogues.rs b/src/locales/catalogues.rs index a570697..0cf9999 100644 --- a/src/locales/catalogues.rs +++ b/src/locales/catalogues.rs @@ -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"), diff --git a/src/locales/mod.rs b/src/locales/mod.rs index d0eacc3..dc72bb6 100644 --- a/src/locales/mod.rs +++ b/src/locales/mod.rs @@ -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::().unwrap_or(Locales(vec![])).0; + for loc in locs { + if let Some(key) = loc { + if let Some(k) = key.split("-").collect::>().get(0) { + if *k != "en" { + dt = date.format("%d. %m. %Y").to_string(); + } + } + } + } + + move || { dt.clone() } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b0ced0c..db8bd4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use log::{debug, error}; +use log::error; use rezervator::backend::company::check_company; use rezervator::backend::user::create_admin; diff --git a/src/pages/mod.rs b/src/pages/mod.rs index d631262..6111658 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -14,4 +14,5 @@ mod hours_edit; mod properties; mod property_edit; mod property_delete; +mod res_dialogs; diff --git a/src/pages/public.rs b/src/pages/public.rs index 124807d..cecb558 100644 --- a/src/pages/public.rs +++ b/src/pages/public.rs @@ -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, + reservations: Vec, + property: ResProperty, + slots: RwSignal>, + price: RwSignal) -> impl IntoView { + let checks = hours.into_iter().map(|h| { + match property.slot { + SlotType::Quarter => { + let mut ret: Vec = 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 = 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 = 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 = vec![]; + ret.push(TmCheck { + from: NaiveTime::from_hms_opt(0,0,0).unwrap(), + to: NaiveTime::from_hms_opt(23, 59, 59).unwrap() + }); + ret + } + } + }).collect::>().into_iter().flatten().collect::>(); + + let prop_id = property.id(); + view! { + + + + + } +} #[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![]); + let price = create_rw_signal(Decimal::from(0)); + let cr_reservation = create_server_action::(); + 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! { -
"public"
+ + +
+ +
+
+
+
+
" "{trl("Booking")}
+
+
+ + +
+
+ {trl("Loading...")}

}> + {move || { + form_data.get().map(|u| match u { + Err(e) => { + view! {
{e.to_string()}
}} + Ok(u) => { + match u { + ApiResponse::Data(p) => { + view! { +
+ +
+
+
{data.property.name.clone()}
+
+ +
+
+
+
+ + + +
+ } + }, + ApiResponse::Error(s) => { + view! {
{trl(&s)}
} + } + }} + }) + }} +
+
{trl("Total price of booking")}
+
{move || format!("{} Kč", price.get())}
+
+
+
+
+
+
+
" "{trl("Who is booking")}
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+
+
+
} } \ No newline at end of file diff --git a/src/pages/res_dialogs.rs b/src/pages/res_dialogs.rs new file mode 100644 index 0000000..bba022a --- /dev/null +++ b/src/pages/res_dialogs.rs @@ -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! { + + + + + + + + + } +} + +#[component] +pub fn res_saved( + opener: DialogOpener, + save_result: RwSignal, ServerFnError>>>, + day: RwSignal, + price: WriteSignal, + slots: WriteSignal>) -> impl IntoView { + view! {{move ||{ + if let Some(r) = save_result.get() { + match r { + Ok(ar) => { + match ar { + ApiResponse::Data(d) => { + view! { +
+ + +

+ {trl("Your reservation has been successfully saved.")} +

+

+ {trl("We look forward to seeing you on")}" "{loc_date(d)} +

+
+ + + +
+
+ } + }, + ApiResponse::Error(err) => { + view! { +
+ + +
+ {trl("Reservation cannot be saved.")}
{trl(&err)} +
+
+ + + +
+
+ } + } + } + } + Err(err) => { + view! { +
+ + +
+ {trl("Error while saving reservation.")}
{trl(&err.to_string())} +
+
+ + + +
+
+ } + } + } + } else { + view! {
} + } + }} + } +}