Implemented booking overview.
This commit is contained in:
+40
-9
@@ -1,6 +1,6 @@
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use chrono::{Local, NaiveDate, NaiveTime, Weekday};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -310,15 +310,20 @@ fn def_true() -> bool {
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ResProperty {
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
id: i32,
|
||||
#[validate(length(min = 1,message = "Name cannot be empty"))]
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub price: Decimal,
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub slot: SlotType,
|
||||
#[serde(default = "def_true")]
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub allow_multi: bool,
|
||||
#[serde(default = "def_true")]
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub active: bool
|
||||
}
|
||||
|
||||
@@ -409,10 +414,12 @@ impl CrReservation {
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Customer {
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
id: i32,
|
||||
pub full_name: String,
|
||||
pub email: String,
|
||||
pub phone: String,
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub discount: i32
|
||||
}
|
||||
|
||||
@@ -437,30 +444,36 @@ pub enum ReservationState {
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl Display for ReservationState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
ReservationState::New => {"New"}
|
||||
ReservationState::Approved => {"Approved"}
|
||||
ReservationState::Canceled => {"Canceled"}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct Reservation {
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
id: i32,
|
||||
pub from: NaiveTime,
|
||||
pub to: NaiveTime,
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub property: i32,
|
||||
#[cfg_attr(feature = "ssr", sqlx(default))]
|
||||
pub summary: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ResPropertyView {
|
||||
pub name: String,
|
||||
pub description: String
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ResWithProperty {
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub reservation: Reservation,
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub property: ResPropertyView
|
||||
pub property: ResProperty
|
||||
}
|
||||
|
||||
pub struct Reservations(Vec<Reservation>);
|
||||
@@ -538,6 +551,17 @@ pub struct ResSumWithItems {
|
||||
pub reservations: Vec<ResWithProperty>
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ResAllView {
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub reservation: ResWithProperty,
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub summary: ReservationSum,
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub customer: Customer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
|
||||
#[cfg_attr(feature = "ssr", sqlx(type_name = "message_type"))]
|
||||
@@ -576,3 +600,10 @@ impl Message {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
||||
pub struct ChartData {
|
||||
pub count: i64,
|
||||
pub period: f64
|
||||
}
|
||||
|
||||
+169
-19
@@ -12,9 +12,9 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use uuid::Uuid;
|
||||
use std::ops::DerefMut;
|
||||
use std::str::FromStr;
|
||||
use futures_util::future::join_all;
|
||||
use std::collections::HashMap;
|
||||
use log::warn;
|
||||
use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType};
|
||||
use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType, ChartData, ResAllView};
|
||||
use crate::backend::get_pool;
|
||||
use crate::backend::get_mailing;
|
||||
use crate::backend::mail::MailMessage;
|
||||
@@ -23,6 +23,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use sqlx::PgPool;
|
||||
use crate::backend::user::admin_email;
|
||||
use crate::backend::user::emails_for_notify;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
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")
|
||||
@@ -88,46 +89,64 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
|
||||
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")
|
||||
let view = if let Some(s) = state {
|
||||
query_as::<_, ResAllView>(
|
||||
"SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \
|
||||
FROM reservation r \
|
||||
JOIN property p on p.id = r.property \
|
||||
JOIN reservation_sum s on s.id = r.summary \
|
||||
JOIN customer c on c.id = s.customer \
|
||||
WHERE s.date >= $1 AND s.date <= $2 AND s.state = $3 \
|
||||
ORDER BY s.id, s.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")
|
||||
query_as::<_, ResAllView>(
|
||||
"SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \
|
||||
FROM reservation r \
|
||||
JOIN property p on p.id = r.property \
|
||||
JOIN reservation_sum s on s.id = r.summary \
|
||||
JOIN customer c on c.id = s.customer \
|
||||
WHERE s.date >= $1 AND s.date <= $2 \
|
||||
ORDER BY s.id, s.date")
|
||||
.bind(from)
|
||||
.bind(to)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
};
|
||||
|
||||
let sums = if let Err(ref e) = sums {
|
||||
let view = if let Err(ref e) = view {
|
||||
if matches!(e, Error::RowNotFound) {
|
||||
vec![]
|
||||
} else {
|
||||
sums?
|
||||
view?
|
||||
}
|
||||
} else {
|
||||
sums?
|
||||
view?
|
||||
};
|
||||
|
||||
if sums.is_empty() {
|
||||
if view.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();
|
||||
let mut ret = view.into_iter().fold(HashMap::new(), |mut m, v| {
|
||||
m.entry(v.summary.id())
|
||||
.and_modify(|i: &mut (ReservationSum, Customer, Vec<ResWithProperty>)| i.2.push(v.reservation.clone()))
|
||||
.or_insert((v.summary, v.customer, vec![v.reservation]));
|
||||
m
|
||||
}).into_iter().map(|i| {
|
||||
ResSumWithItems {
|
||||
summary: i.1.0,
|
||||
customer: i.1.1,
|
||||
reservations: i.1.2
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
Ok(res?)
|
||||
ret.sort_by(|a, b| a.summary.date.cmp(&b.summary.date) );
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn set_state(uuid: Uuid, state: ReservationState) -> Result<(), ServerFnError> {
|
||||
@@ -185,6 +204,29 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
async fn notify_cancel(uuid: Uuid) -> Result<(), AppError> {
|
||||
send_notify(uuid, get_message(MessageType::ReservationCanceled).await?).await
|
||||
}
|
||||
|
||||
async fn month_chart_data(year: i32) -> Result<Vec<ChartData>, AppError> {
|
||||
let pool = get_pool().await?;
|
||||
let data = query_as::<_, ChartData>(
|
||||
"SELECT count(id) as count, date_part('month', date) as period \
|
||||
FROM reservation_sum \
|
||||
WHERE date_part('year', date) = $1 GROUP BY period ORDER BY period")
|
||||
.bind(year)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn year_chart_data() -> Result<Vec<ChartData>, AppError> {
|
||||
let pool = get_pool().await?;
|
||||
let data = query_as::<_, ChartData>(
|
||||
"SELECT count(id) as count, date_part('year', date) as period FROM reservation_sum GROUP BY period ORDER BY period")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}}
|
||||
|
||||
#[server]
|
||||
@@ -327,6 +369,25 @@ pub async fn get_next_reservations() -> Result<ApiResponse<Vec<ResSumWithItems>>
|
||||
Some(ReservationState::Approved)).await?))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn num_days(month: u32, year: i32) -> i64 {
|
||||
if month == 12 {
|
||||
NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
|
||||
} else {
|
||||
NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
|
||||
}.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
|
||||
.num_days()
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_reservations_for_month(month: u32, year: i32) -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
|
||||
let data = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).unwrap(),
|
||||
&NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default()).unwrap(),
|
||||
None).await?;
|
||||
|
||||
Ok(ApiResponse::Data(data))
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn approve(uuid: String) -> Result<ApiResponse<()>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
@@ -357,4 +418,93 @@ pub async fn cancel(uuid: String) -> Result<ApiResponse<()>, ServerFnError> {
|
||||
}
|
||||
|
||||
Ok(ApiResponse::Data(()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn chart(data: &Vec<ChartData>, title: &str, month: bool) -> String {
|
||||
use charts_rs::{BarChart, THEME_ANT};
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use std::ops::Add;
|
||||
use chrono::Month;
|
||||
|
||||
let mut chart = BarChart::new_with_theme(
|
||||
vec![("Bookings", data.iter().map(|d| d.count.to_f32().unwrap_or_default()).collect()).into()],
|
||||
if month {
|
||||
data.iter().map(|d| Month::try_from(d.period.to_u8().unwrap_or_default()).unwrap_or(Month::January).name().to_string()).collect()
|
||||
} else {
|
||||
data.iter().map(|d| d.period.to_string()).collect()
|
||||
},
|
||||
THEME_ANT);
|
||||
chart.title_text = title.to_string();
|
||||
chart.legend_show = Some(false);
|
||||
chart.series_label_font_size = 8.0;
|
||||
chart.x_axis_font_size = 8.0;
|
||||
chart.title_font_size = 11.0;
|
||||
chart.font_family = "Arial".to_string();
|
||||
chart.y_axis_configs[0].axis_font_size = 8.0;
|
||||
|
||||
|
||||
chart.width = 300.0;
|
||||
chart.height = 150.0;
|
||||
|
||||
"data:image/svg+xml;base64,".to_string().add(&BASE64_STANDARD.encode(&chart.svg().unwrap_or_default()))
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn month_chart(year: i32) -> Result<ApiResponse<String>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
perm_check!(is_logged_in);
|
||||
|
||||
let data = month_chart_data(year).await?;
|
||||
Ok(ApiResponse::Data(chart(&data, "Month bookings", true)))
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn year_chart() -> Result<ApiResponse<String>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
perm_check!(is_logged_in);
|
||||
|
||||
let data = year_chart_data().await?;
|
||||
Ok(ApiResponse::Data(chart(&data, "Year bookings", false)))
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn years() -> Result<Vec<i32>, ServerFnError> {
|
||||
let pool = get_pool().await?;
|
||||
|
||||
let data: Result<Vec<(f64,)>, Error> = query_as("SELECT DISTINCT date_part('year', date) as year FROM reservation_sum ORDER BY year DESC")
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
|
||||
Ok(data?.into_iter().map(|d| d.0.to_i32().unwrap_or_default()).collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn months(year: i32) -> Result<Vec<u32>, ServerFnError> {
|
||||
let pool = get_pool().await?;
|
||||
|
||||
let data: Result<Vec<(f64,)>, Error> = query_as("SELECT DISTINCT date_part('month', date) as month \
|
||||
FROM reservation_sum \
|
||||
WHERE date_part('year', date) = $1 \
|
||||
ORDER BY month DESC")
|
||||
.bind(year)
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
|
||||
Ok(data?.into_iter().map(|d| d.0.to_u32().unwrap_or_default()).collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn reservations_in_month(year: i32, month: u32) -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
perm_check!(is_logged_in);
|
||||
|
||||
let ret = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).ok_or(ServerFnError::new("Cannot parse date"))?,
|
||||
&NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default())
|
||||
.ok_or(ServerFnError::new("Cannot parse date"))?,
|
||||
None)
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::Data(ret))
|
||||
}
|
||||
Reference in New Issue
Block a user