You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
12 KiB
Rust

use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData, ResSumWithItems};
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, query};
use sqlx::query_as;
use sqlx::Error;
use uuid::Uuid;
use std::ops::DerefMut;
use std::str::FromStr;
use futures_util::future::join_all;
use log::warn;
use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType};
use crate::backend::get_pool;
use crate::backend::get_mailing;
use crate::backend::mail::MailMessage;
use crate::backend::mail::get_message;
use crate::error::AppError;
use sqlx::PgPool;
use crate::backend::user::admin_email;
use crate::backend::user::emails_for_notify;
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 AND reservation_sum.state <> 'Canceled'")
.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?)
}
}
async fn reservation_by_uuid(uuid: Uuid) -> Result<ResSumWithItems, ServerFnError> {
let pool = get_pool().await?;
let summary = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1")
.bind(uuid)
.fetch_one(&pool)
.await?;
let sum_id = summary.id();
let cust_id = summary.customer;
Ok(ResSumWithItems{
summary,
customer: customer_for_reservation(cust_id, &pool).await?,
reservations: items_for_reservation(sum_id, &pool).await?
})
}
async fn items_for_reservation(id: i32, pool: &PgPool) -> Result<Vec<ResWithProperty>, ServerFnError> {
Ok(query_as::<_, ResWithProperty>(
"SELECT r.id, r.from, r.to, r.property, r.summary, p.name, p,description \
FROM reservation as r \
JOIN property as p ON r.property = p.id WHERE r.summary = $1")
.bind(id)
.fetch_all(pool)
.await?)
}
async fn customer_for_reservation(id: i32, pool: &PgPool) -> Result<Customer, ServerFnError> {
Ok(query_as::<_, Customer>("SELECT * FROM customer WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await?)
}
async fn reservations_in_range(from: &NaiveDate, to: &NaiveDate, state: Option<ReservationState>) -> Result<Vec<ResSumWithItems>, ServerFnError> {
let pool = get_pool().await?;
let sums = if let Some(s) = state {
query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE date >= $1 AND date <= $2 AND state = $3 ORDER BY date")
.bind(from)
.bind(to)
.bind(s)
.fetch_all(&pool)
.await
} else {
query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE date >= $1 AND date <= $2 ORDER BY date")
.bind(from)
.bind(to)
.fetch_all(&pool)
.await
};
let sums = if let Err(ref e) = sums {
if matches!(e, Error::RowNotFound) {
vec![]
} else {
sums?
}
} else {
sums?
};
if sums.is_empty() {
return Ok(vec![])
}
let res: Result<Vec<ResSumWithItems>, ServerFnError> = join_all(sums.into_iter().map(|s| async {
let reservations = items_for_reservation(s.id(), &pool).await?;
let customer = customer_for_reservation(s.customer, &pool).await?;
Ok(ResSumWithItems {
summary: s,
customer,
reservations
})
})).await.into_iter().collect();
Ok(res?)
}
async fn set_state(uuid: Uuid, state: ReservationState) -> Result<(), ServerFnError> {
let pool = get_pool().await?;
query("UPDATE reservation_sum SET state = $1 WHERE uuid = $2")
.bind(state)
.bind(uuid)
.execute(&pool)
.await?;
Ok(())
}
async fn notify_new_all(admin_mail: String, reservation: &ResSumWithItems) -> Result<(), AppError> {
let mailing = get_mailing().await?;
let msg = get_message(MessageType::NewReservation).await?;
for m in emails_for_notify().await? {
mailing.send_mail(MailMessage::new(admin_mail.clone(), m, msg.clone(), reservation)).await?;
}
Ok(())
}
async fn notify_new(uuid: Uuid) -> Result<(), AppError> {
let mailing = get_mailing().await?;
let msg = get_message(MessageType::NewReservationCust).await?;
let reservation = reservation_by_uuid(uuid).await?;
let admin_mail = admin_email().await;
if admin_mail.is_none() {
return Err(AppError::MailSendError("No admin mail".to_string()))
}
mailing.send_mail(MailMessage::new(admin_mail.clone().unwrap(), reservation.customer.email.clone(), msg, &reservation)).await?;
notify_new_all(admin_mail.unwrap(), &reservation).await
}
async fn send_notify(uuid: Uuid, msg: Message) -> Result<(), AppError> {
let mailing = get_mailing().await?;
let reservation = reservation_by_uuid(uuid).await?;
let admin_mail = admin_email().await;
if admin_mail.is_none() {
return Err(AppError::MailSendError("No admin mail".to_string()))
}
mailing.send_mail(MailMessage::new(admin_mail.unwrap(), reservation.customer.email.clone(), msg, &reservation)).await
}
async fn notify_approve(uuid: Uuid) -> Result<(), AppError> {
send_notify(uuid, get_message(MessageType::ReservationApp).await?).await
}
async fn notify_cancel(uuid: Uuid) -> Result<(), AppError> {
send_notify(uuid, get_message(MessageType::ReservationCanceled).await?).await
}
}}
#[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?;
if let Err(e) = notify_new(res_uuid).await {
warn!("Notification not send: {}", e);
}
Ok(ApiResponse::Data(reservation.date()))
}
impl ForValidation for CreateReservation {
fn entity(&self) -> &dyn Validate {
&self.reservation
}
}
#[server]
pub async fn get_new_reservations() -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
use crate::perm_check;
use chrono::{Days, Local};
use crate::backend::data::ReservationState;
perm_check!(is_logged_in);
Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(),
&Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(),
Some(ReservationState::New)).await?))
}
#[server]
pub async fn get_next_reservations() -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
use crate::perm_check;
use chrono::{Days, Local};
use crate::backend::data::ReservationState;
perm_check!(is_logged_in);
Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(),
&Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(),
Some(ReservationState::Approved)).await?))
}
#[server]
pub async fn approve(uuid: String) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check;
use crate::backend::data::ReservationState;
perm_check!(is_logged_in);
let uuid = Uuid::parse_str(&uuid)?;
set_state(uuid, ReservationState::Approved).await?;
if let Err(e) = notify_approve(uuid).await {
warn!("Approve notification not send: {}", e);
}
Ok(ApiResponse::Data(()))
}
#[server]
pub async fn cancel(uuid: String) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check;
use crate::backend::data::ReservationState;
perm_check!(is_logged_in);
let uuid = Uuid::parse_str(&uuid)?;
set_state(uuid, ReservationState::Canceled).await?;
if let Err(e) = notify_cancel(uuid).await {
warn!("Cancel notification not send: {}", e);
}
Ok(ApiResponse::Data(()))
}