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.
579 lines
14 KiB
Rust
579 lines
14 KiB
Rust
#![allow(unused_variables)]
|
|
|
|
use std::fmt::Display;
|
|
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 validator::{Validate, ValidationError};
|
|
use crate::error::AppError;
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
pub enum ApiResponse<T> {
|
|
Data(T),
|
|
Error(String)
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
|
pub struct Company {
|
|
id: i32,
|
|
#[validate(length(min = 1,message = "Name cannot be empty"))]
|
|
pub name: String,
|
|
#[validate(length(min = 1,message = "Street cannot be empty"))]
|
|
pub street: String,
|
|
#[validate(length(min = 1,message = "House number cannot be empty"))]
|
|
pub house_number: String,
|
|
pub zip_code: String,
|
|
pub city: String,
|
|
}
|
|
|
|
impl Company {
|
|
pub fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
|
pub struct User {
|
|
id: i32,
|
|
pub login: String,
|
|
pub password: String,
|
|
pub full_name: Option<String>,
|
|
#[validate(email(message = "Enter valid email address"))]
|
|
pub email: Option<String>,
|
|
pub admin: bool,
|
|
pub get_emails: bool,
|
|
}
|
|
|
|
impl User {
|
|
pub fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
|
pub struct UserProfile {
|
|
#[validate(length(min = 1,message = "Username cannot be empty"))]
|
|
login: String,
|
|
#[validate(must_match(other = "password_ver", message = "Passwords doesn't match"))]
|
|
password: Option<String>,
|
|
password_ver: Option<String>,
|
|
full_name: String,
|
|
#[validate(email(message = "Enter valid email address"))]
|
|
email: String,
|
|
get_emails: Option<String>,
|
|
admin: Option<String>
|
|
}
|
|
|
|
impl UserProfile {
|
|
|
|
pub fn login(&self) -> &str {
|
|
&self.login
|
|
}
|
|
|
|
pub fn full_name(&self) -> &str {
|
|
&self.full_name
|
|
}
|
|
|
|
pub fn email(&self) -> &str {
|
|
&self.email
|
|
}
|
|
|
|
pub fn get_emails(&self) -> bool {
|
|
self.get_emails.is_some()
|
|
}
|
|
pub fn admin(&self) -> bool {
|
|
self.admin.is_some()
|
|
}
|
|
pub fn password(&self) -> &Option<String> {
|
|
&self.password
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
|
pub struct PwdChange {
|
|
login: String,
|
|
old_password: String,
|
|
#[validate(length(min = 1, message = "Enter new password"),
|
|
must_match(other = "password_ver", message = "Passwords doesn't match"))]
|
|
password: String,
|
|
password_ver: String
|
|
}
|
|
|
|
impl PwdChange {
|
|
pub fn login(&self) -> &str {
|
|
&self.login
|
|
}
|
|
pub fn old_password(&self) -> &str {
|
|
&self.old_password
|
|
}
|
|
pub fn password(&self) -> &str {
|
|
&self.password
|
|
}
|
|
pub fn password_ver(&self) -> &str {
|
|
&self.password_ver
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
|
pub struct OpeningHour {
|
|
id: i32,
|
|
pub day: i32,
|
|
pub from: NaiveTime,
|
|
pub to: NaiveTime,
|
|
pub discount: Option<i32>
|
|
}
|
|
|
|
impl OpeningHour {
|
|
pub fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
pub struct DayHours(pub Vec<DayHour>);
|
|
|
|
impl DayHours {
|
|
|
|
pub fn try_new(hours: &str) -> Result<Self, AppError> {
|
|
if hours.is_empty() {
|
|
return Ok(Self(Vec::new()))
|
|
}
|
|
|
|
let times = hours.split(",")
|
|
.map(|h| h.trim())
|
|
.map(|h| DayHour::try_from(h))
|
|
.collect::<Vec<_>>();
|
|
|
|
if times.contains(&Err(AppError::HourParseError)) {
|
|
return Err(AppError::HourParseError)
|
|
}
|
|
|
|
Ok(Self(times.into_iter().map(|h| h.unwrap()).collect()))
|
|
}
|
|
|
|
pub fn hours(&self) -> &Vec<DayHour> {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Display for DayHours {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if self.0.is_empty() {
|
|
return write!(f, "{}", "".to_string())
|
|
}
|
|
|
|
let str = self.0.iter().map(|h| {
|
|
let discount = if let Some(d) = h.discount() {
|
|
format!(" ({})", d).to_string()
|
|
} else { "".to_string() };
|
|
format!("{} - {}{}", h.from().format("%H:%M"), h.to().format("%H:%M"), discount).to_string()
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join(", ");
|
|
write!(f, "{}", str)
|
|
}
|
|
}
|
|
|
|
#[derive(Eq, PartialEq, Serialize, Deserialize, Clone, Debug)]
|
|
pub struct DayHour {
|
|
from: NaiveTime,
|
|
to: NaiveTime,
|
|
discount: Option<i32>
|
|
}
|
|
|
|
impl DayHour {
|
|
pub fn new(from: NaiveTime, to: NaiveTime, discount: Option<i32>) -> Self {
|
|
Self { from, to, discount }
|
|
}
|
|
|
|
pub fn from(&self) -> NaiveTime {
|
|
self.from
|
|
}
|
|
pub fn to(&self) -> NaiveTime {
|
|
self.to
|
|
}
|
|
pub fn discount(&self) -> Option<i32> {
|
|
self.discount
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for DayHour {
|
|
type Error = AppError;
|
|
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
value.as_str().try_into()
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for DayHour {
|
|
type Error = AppError;
|
|
|
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
if value.is_empty() {
|
|
return Err(AppError::HourParseError)
|
|
}
|
|
|
|
let times = value.split("-")
|
|
.map(|t| t.trim()).collect::<Vec<_>>();
|
|
|
|
if times.len() != 2 {
|
|
return Err(AppError::HourParseError)
|
|
}
|
|
|
|
let from = NaiveTime::parse_from_str(times.get(0).unwrap(), "%H:%M");
|
|
let to = NaiveTime::parse_from_str(times.get(1).unwrap(), "%H:%M");
|
|
|
|
if from.is_err() || to.is_err() {
|
|
return Err(AppError::HourParseError)
|
|
}
|
|
|
|
Ok(DayHour {
|
|
from: from.unwrap(),
|
|
to: to.unwrap(),
|
|
discount: None
|
|
})
|
|
}
|
|
}
|
|
|
|
fn validate_hours(value: &WeekHours) -> Result<(), ValidationError> {
|
|
if value.hours().is_empty() {
|
|
return Ok(())
|
|
}
|
|
if let Ok(h) = DayHours::try_new(value.hours()) {
|
|
for hr in h.hours() {
|
|
if hr.from() >= hr.to() { return Err(ValidationError::new("TO_BEFORE_FROM")) }
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref RE_HOURS: Regex = Regex::new(r"^$|(^\d{2}:\d{2} ?- ?\d{2}:\d{2} ?(\(\d+\))?,? ?)+").unwrap();
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, Validate)]
|
|
#[validate(schema(function = "validate_hours", message = "Time 'to' must be after time 'from'"))]
|
|
pub struct WeekHours {
|
|
day: Weekday,
|
|
#[validate(regex(path = "RE_HOURS", message = "Hours must be in HH:MM - HH:MM format"))]
|
|
hours: String
|
|
}
|
|
|
|
impl Default for WeekHours {
|
|
fn default() -> Self {
|
|
Self {
|
|
day: Weekday::Mon,
|
|
hours: String::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WeekHours {
|
|
pub fn new(day: Weekday, hours: Vec<DayHour>) -> Self {
|
|
Self {
|
|
day,
|
|
hours: DayHours(hours).to_string()
|
|
}
|
|
}
|
|
pub fn day(&self) -> Weekday {
|
|
self.day
|
|
}
|
|
pub fn hours(&self) -> &str {
|
|
&self.hours
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
|
|
#[cfg_attr(feature = "ssr", sqlx(type_name = "slot_type"))]
|
|
pub enum SlotType {
|
|
Quarter,
|
|
Half,
|
|
#[default]
|
|
Hour,
|
|
Day
|
|
}
|
|
|
|
fn def_true() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Validate, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
|
pub struct ResProperty {
|
|
id: i32,
|
|
#[validate(length(min = 1,message = "Name cannot be empty"))]
|
|
pub name: String,
|
|
pub description: String,
|
|
pub price: Decimal,
|
|
pub slot: SlotType,
|
|
#[serde(default = "def_true")]
|
|
pub allow_multi: bool,
|
|
#[serde(default = "def_true")]
|
|
pub active: bool
|
|
}
|
|
|
|
impl ResProperty {
|
|
pub fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[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 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
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
|
pub struct ResSumWithItems {
|
|
pub summary: ReservationSum,
|
|
pub customer: Customer,
|
|
pub reservations: Vec<ResWithProperty>
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
|
|
#[cfg_attr(feature = "ssr", sqlx(type_name = "message_type"))]
|
|
pub enum MessageType {
|
|
#[default]
|
|
NewReservation,
|
|
NewReservationCust,
|
|
ReservationApp,
|
|
ReservationCanceled,
|
|
}
|
|
|
|
impl Display for MessageType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", match self {
|
|
MessageType::NewReservation => {"NewReservation"}
|
|
MessageType::NewReservationCust => {"NewReservationCust"}
|
|
MessageType::ReservationApp => {"ReservationApp"}
|
|
MessageType::ReservationCanceled => {"ReservationCanceled"}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize, Debug, Default, Validate)]
|
|
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
|
|
pub struct Message {
|
|
id: i32,
|
|
pub msg_type: MessageType,
|
|
#[validate(length(min = 1,message = "Enter mail subject"))]
|
|
pub subject: String,
|
|
#[validate(length(min = 1,message = "Enter text"))]
|
|
pub text: String,
|
|
}
|
|
|
|
impl Message {
|
|
pub fn id(&self) -> i32 {
|
|
self.id
|
|
}
|
|
}
|