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 { 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 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 { 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, 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 { 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) -> Result, 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, 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>, 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?; 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>, 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>, 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, 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, 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(())) }