Implemented public part and creating reservations.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user