Implemented settings for properties of reservation. Fixed bugs on empty database.

main
Josef Rokos 1 year ago
parent d7e33e4342
commit 55ca93406a

@ -13,15 +13,20 @@ CREATE TABLE "user" (
password VARCHAR NOT NULL,
full_name VARCHAR,
email VARCHAR,
admin bool,
get_emails bool
admin bool NOT NULL default false,
get_emails bool NOT NULL default false
);
CREATE TYPE slot_type AS ENUM ('Quarter', 'Half', 'Hour', 'Day');
CREATE TABLE property (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
description VARCHAR,
price NUMERIC(9, 2) NOT NULL
price NUMERIC(9, 2) NOT NULL,
slot slot_type NOT NULL default 'Hour',
allow_multi BOOLEAN NOT NULL default true,
active BOOLEAN NOT NULL default true
);
CREATE TYPE message_type AS ENUM ('NewReservation', 'NewReservationCust', 'ReservationApp', 'ReservationCanceled');

@ -1,8 +1,29 @@
use cfg_if::cfg_if;
use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, Company};
use crate::components::data_form::ForValidation;
cfg_if! { if #[cfg(feature = "ssr")] {
use sqlx::{query_as, PgPool, query};
use crate::error::AppError;
use log::info;
pub async fn check_company(pool: &PgPool) -> Result<(), AppError> {
let count: (i64,) = query_as("SELECT COUNT(id) FROM company")
.fetch_one(pool)
.await?;
if count.0 == 0 {
info!("Creating initial company");
query("INSERT INTO company(name, street, house_number, zip_code, city) VALUES('Company name', '', '', '', '')")
.execute(pool).await?;
}
Ok(())
}
}}
#[server(GetCompany, "/api", "Url", "get_company")]
pub async fn get_company() -> Result<ApiResponse<Company>, ServerFnError> {
use crate::backend::AppData;

@ -2,9 +2,11 @@
//use rust_decimal::Decimal;
#![allow(unused_variables)]
use std::fmt::Display;
use chrono::{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};
@ -162,19 +164,21 @@ impl DayHours {
}
}
impl ToString for DayHours {
fn to_string(&self) -> String {
impl Display for DayHours {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.is_empty() {
return "".to_string()
return write!(f, "{}", "".to_string())
}
self.0.iter().map(|h| {
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()})
format!("{} - {}{}", h.from().format("%H:%M"), h.to().format("%H:%M"), discount).to_string()
})
.collect::<Vec<String>>()
.join(", ")
.join(", ");
write!(f, "{}", str)
}
}
@ -289,13 +293,43 @@ impl WeekHours {
}
}
/*pub struct Property {
id: u16,
name: String,
description: String,
price: Decimal
#[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
}
}
/*
pub enum MessageType {
NewReservation,
NewReservationCust,

@ -4,6 +4,7 @@ pub mod company;
pub mod user;
pub mod auth_middleware;
pub mod opening_hours;
pub mod property;
#[macro_export]
macro_rules! perm_check {

@ -0,0 +1,82 @@
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> {
use crate::backend::get_pool;
let pool = get_pool().await?;
let props = sqlx::query_as::<_, ResProperty>("SELECT * FROM property").fetch_all(&pool).await?;
Ok(ApiResponse::Data(props))
}
#[server]
pub async fn create_property(property: ResProperty) ->Result<ApiResponse<()>, ServerFnError> {
use crate::backend::get_pool;
use crate::perm_check;
perm_check!(is_admin);
let pool = get_pool().await?;
sqlx::query("INSERT INTO property(name, description, price, slot) VALUES($1, $2, $3, $4)")
.bind(&property.name)
.bind(&property.description)
.bind(&property.price)
.bind(&property.slot)
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}
#[server]
pub async fn edit_property(property: ResProperty) -> Result<ApiResponse<()>, ServerFnError> {
use crate::backend::get_pool;
use crate::perm_check;
perm_check!(is_admin);
let pool = get_pool().await?;
sqlx::query("UPDATE property SET name = $1, description = $2, price = $3, active = $4, slot = $5 WHERE id = $6")
.bind(&property.name)
.bind(&property.description)
.bind(&property.price)
.bind(property.active)
.bind(&property.slot)
.bind(property.id())
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}
#[server]
pub async fn delete_property(id: i32) -> Result<ApiResponse<()>, ServerFnError> {
use crate::backend::get_pool;
use crate::perm_check;
perm_check!(is_admin);
let pool = get_pool().await?;
sqlx::query("DELETE FROM property WHERE id = $1")
.bind(id)
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}
impl ForValidation for CreateProperty {
fn entity(&self) -> &dyn Validate {
&self.property
}
}
impl ForValidation for EditProperty {
fn entity(&self) -> &dyn Validate {
&self.property
}
}

@ -9,6 +9,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use actix_session::*;
use leptos_actix::{extract, redirect};
use log::{info, warn};
use crate::error::AppError;
pub async fn has_admin_user(pool: &PgPool) -> Result<bool, Error> {
let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#)
@ -19,7 +20,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
Ok(count.0 > 0)
}
pub async fn create_admin(pool: &PgPool) -> Result<(), Error> {
pub async fn create_admin(pool: &PgPool) -> Result<(), AppError> {
if !has_admin_user(pool).await? {
let pwd = pwhash::bcrypt::hash("admin");
query(r#"INSERT INTO "user"(login, password, full_name, admin) VALUES($1, $2, $3, $4)"#)
@ -44,6 +45,11 @@ cfg_if! { if #[cfg(feature = "ssr")] {
extract(|session: Session| async move {
session.get::<User>("user").unwrap_or(None)
}).await.unwrap_or(None)
/*let mut usr = User::default();
usr.full_name = Some("PokAdm".to_string());
usr.admin = true;
Some(usr)*/
}
pub async fn is_logged_in() -> bool {

@ -1,15 +1,21 @@
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use leptos::ServerFnError;
#[derive(Debug, Eq, PartialEq)]
pub enum AppError {
HourParseError
HourParseError,
ServerError(String),
FatalError(String)
}
impl AppError {
fn as_string(&self) -> String {
//match self { AppError::HourParseError => {"Hour parse error"} }
"Hours parse error".to_string()
match self {
AppError::HourParseError => {"Hour parse error".to_string()},
AppError::ServerError(e) => {format!("Server error: {}", e)},
AppError::FatalError(e) => {format!("Fatal error: {}", e)}
}
}
}
@ -19,4 +25,17 @@ impl Display for AppError {
}
}
impl Error for AppError {}
impl Error for AppError {}
impl From<ServerFnError> for AppError {
fn from(value: ServerFnError) -> Self {
AppError::ServerError(value.to_string())
}
}
#[cfg(feature = "ssr")]
impl From<sqlx::Error> for AppError {
fn from(value: sqlx::Error) -> Self {
AppError::FatalError(value.to_string())
}
}

@ -12,6 +12,8 @@ lazy_static! {
("Search...", "Najít..."),
("Close", "Zavřít"),
("Save changes", "Uložit změny"),
("Yes", "Ano"),
("No", "Ne"),
("Company info", "Organizace"),
("Name cannot be empty", "Jméno nesmí být prázdné"),
("Invalid old password", "Neplatné staré heslo"),
@ -25,6 +27,23 @@ lazy_static! {
("Saturday", "Sobota"),
("Sunday", "Neděle"),
("Opening hours", "Otvírací hodiny"),
("Username", "Uživatel"),
("Password", "Heslo"),
("Sign in", "Přihlásit"),
("Create user", "Vytvořit uživatele"),
("Delete user", "Smazat uživatele"),
("Users", "Uživatelé"),
("Full name", "Celé jméno"),
("Actions", "Akce"),
("Edit", "Upravit"),
("Change password", "Změnit heslo"),
("Delete", "Smazat"),
("Properties", "Předměty"),
("Name", "Jméno"),
("Description", "Popis"),
("Price", "Cena"),
("Edit hours", "Upravit hodiny"),
("Hours", "Hodiny"),
])),
("sk", HashMap::from( [
("Dashboard", "Prehlad"),

@ -1,3 +1,7 @@
use log::error;
use rezervator::backend::company::check_company;
use rezervator::backend::user::create_admin;
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@ -34,7 +38,7 @@ async fn main() -> std::io::Result<()> {
let cfg_path = matches.opt_str("c").unwrap_or("config.toml".to_string());
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();
info!("Starting server");
let conf = get_configuration(None).await.unwrap();
@ -49,6 +53,13 @@ async fn main() -> std::io::Result<()> {
migrate!().run(&pool).await.expect("could not run SQLx migrations");
if let Err(e) = create_admin(&pool).await {
error!("Error while checking admin user: {:?}", e);
}
if let Err(e) = check_company(&pool).await {
error!("Error while checking company: {:?}", e);
}
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;

@ -11,4 +11,7 @@ mod user_edit;
mod user_delete;
mod opening_hours;
mod hours_edit;
mod properties;
mod property_edit;
mod property_delete;

@ -0,0 +1,111 @@
use leptos::*;
use leptos_use::use_media_query;
use crate::backend::data::{ApiResponse, ResProperty};
use crate::backend::property::get_properties;
use crate::components::modal_box::DialogOpener;
use crate::components::user_menu::MenuOpener;
use crate::locales::trl;
use crate::pages::property_delete::PropertyDelete;
use crate::pages::property_edit::PropertyEdit;
#[component]
pub fn properties() -> impl IntoView {
let is_wide = use_media_query("(min-width: 500px)");
let properties = create_rw_signal::<Vec<ResProperty>>(vec![]);
let prop = create_rw_signal(ResProperty::default());
let empty_prop = create_rw_signal(ResProperty::default());
let create_form = DialogOpener::new();
let edit_form = DialogOpener::new();
let delete_dlg = DialogOpener::new();
let props = create_blocking_resource(move || create_form.visible() || edit_form.visible() || delete_dlg.visible(), move |_| get_properties());
view! {
<PropertyEdit property=prop.read_only() opener=edit_form edit=true/>
<PropertyEdit property=empty_prop.read_only() opener=create_form edit=false/>
<PropertyDelete property=prop.read_only() opener=delete_dlg/>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-basket"></i>" "{trl("Properties")}</h5>
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
<table class="table card-table">
<thead>
<tr>
<th>{trl("Name")}</th>
{move || if is_wide.get() {view! {<th>{trl("Description")}</th>}}
else {view! {<th></th>}} }
<th>{trl("Price")}</th>
<th>{trl("Actions")}</th>
</tr>
</thead>
{move || {
props.get().map(|u| match u {
Err(e) => {
view! {<tbody class="table-border-bottom-0">
<tr><td colspan=4>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}}
Ok(u) => {
match u {
ApiResponse::Data(p) => {
properties.set(p.clone());
view! {<tbody class="table-border-bottom-0">
<For each=move || properties.get()
key=|prop| prop.id()
let:data>
{move || {
let menu = MenuOpener::new();
let data = data.clone();
let prop_for_edit = data.clone();
let prop_for_delet = data.clone();
view! {
<tr>
<td>{data.name.clone()}</td>
{move || if is_wide.get() {view! {<td>{data.description.clone()}</td>}}
else {view! {<td></td>}} }
<td>{data.price.to_string()}</td>
<td>
<div class="dropdown">
<button type="button" class="btn p-0 dropdown-toggle hide-arrow"
on:click=move |_| menu.toggle()>
<i class="bx bx-dots-vertical-rounded"></i>
</button>
<div class={move || if menu.visible() {"dropdown-menu show"} else {"dropdown-menu"} }
style="position: absolute; insert: 0px 0px auto; margin: 0px; transform: translate3d(-160px, 0px, 0px);"
on:mouseleave=move |_| menu.toggle()>
<a class="dropdown-item" href="javascript:void(0);" on:click=move |_| {
prop.set(prop_for_edit.clone());
edit_form.show();
}>
<i class="bx bx-edit-alt me-1"></i> {trl("Edit")}</a>
<a class="dropdown-item text-danger" href="javascript:void(0);" on:click=move |_| {
prop.set(prop_for_delet.clone());
delete_dlg.show();
}>
<i class="bx bx-trash me-1"></i> {trl("Delete")}</a>
</div>
</div>
</td>
</tr>}
}}
</For>
</tbody>
}
},
ApiResponse::Error(s) => {
view! {<tbody class="table-border-bottom-0">
<tr><td colspan=4>{trl(&s)}</td></tr></tbody>}
}
}
}
})
}
}
</table>
</Transition>
<a href="#" class="card-link" on:click=move |_| {
empty_prop.set(ResProperty::default());
create_form.show();}>
<i class="bx bx-plus-circle fs-4 lh-0"></i>
</a>
</div>
</div>
}
}

@ -0,0 +1,17 @@
use leptos::*;
use crate::backend::data::ResProperty;
use crate::backend::property::DeleteProperty;
use crate::components::data_form::QuestionDialog;
use crate::components::modal_box::DialogOpener;
#[component]
pub fn property_delete(property: ReadSignal<ResProperty>, opener: DialogOpener) -> impl IntoView {
let del_property = create_server_action::<DeleteProperty>();
view! {
<QuestionDialog opener=opener action=del_property title="Delete property">
<input type="hidden" prop:value={move || property.get().id()} name="id"/>
<div>"Are you sure you want to delete property "{move || property.get().name}"?"</div>
</QuestionDialog>
}
}

@ -0,0 +1,105 @@
use leptos::*;
use crate::backend::data::{ResProperty, SlotType};
use crate::backend::property::{CreateProperty, EditProperty};
use crate::components::data_form::DataForm;
use crate::components::modal_box::DialogOpener;
#[component]
fn form_inner(property: ReadSignal<ResProperty>) -> impl IntoView {
let active_str = create_rw_signal(if property.get().active
{ "true".to_string() } else { "false".to_string() });
view! {
<input type="hidden" prop:value={move || property.get().id()} name="property[id]"/>
<div class="row">
<div class="col mb-3">
<label for="name" class="form-label">"Name"</label>
<input
type="text"
id="name"
class="form-control"
placeholder="Enter name"
prop:value={move || property.get().name}
name="property[name]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="description" class="form-label">"Description"</label>
<input
type="text"
id="name"
class="form-control"
placeholder="Enter description"
prop:value={move || property.get().description}
name="property[description]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="price" class="form-label">"Price"</label>
<input
type="text"
id="name"
class="form-control"
placeholder=""
prop:value={move || property.get().price.to_string()}
name="property[price]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="slot" class="form-label">Time slot</label>
<select id="slot" name="property[slot]" class="form-select">
<option value="Quarter" selected=move || property.get().slot == SlotType::Quarter>"Quarter an hour"</option>
<option value="Half" selected=move || property.get().slot == SlotType::Half>"Half an hour"</option>
<option value="Hour" selected=move || property.get().slot == SlotType::Hour>"Hour"</option>
<option value="Day" selected=move || property.get().slot == SlotType::Day>"Day"</option>
</select>
</div>
</div>
{move || {
if property.get().id() != 0 {
view! {
<div class="row">
<div class="col mb-3">
<input
type="checkbox"
id="active"
class="form-check-input"
prop:checked={move || property.get().active}
on:click=move |_| active_str.set(if active_str.get() == "true".to_string()
{ "false".to_string() } else { "true".to_string() })
/>
<label for="active" class="form-label">"Active"</label>
<input type="hidden" prop:value=active_str name="property[active]"/>
</div>
</div>
}
} else {
view! {<div></div>}
}
}
}}
}
#[component]
pub fn property_edit(property: ReadSignal<ResProperty>, edit: bool, opener: DialogOpener) -> impl IntoView {
let action_create = create_server_action::<CreateProperty>();
let action_edit = create_server_action::<EditProperty>();
view! {
{move ||
if edit {
view! {<DataForm opener=opener action=action_edit title="Edit property">
<FormInner property=property/>
</DataForm>}}
else {
view! {<DataForm opener=opener action=action_create title="Create property">
<FormInner property=property/>
</DataForm>}}
}
}
}

@ -3,6 +3,7 @@ use crate::locales::trl;
use crate::pages::company_info::CompanyInfo;
use crate::pages::opening_hours::OpeningHours;
use crate::pages::users::Users;
use crate::pages::properties::Properties;
#[component]
pub fn Settings() -> impl IntoView {
@ -21,7 +22,7 @@ pub fn Settings() -> impl IntoView {
<OpeningHours/>
</div>
<div class="col-md">
<Properties/>
</div>
</div>
}

Loading…
Cancel
Save