Opening hours completed. Leptos upgraded to 0.5.2.
This commit is contained in:
+176
-9
@@ -1,9 +1,14 @@
|
||||
//use chrono::{NaiveDate, NaiveTime, Weekday};
|
||||
//use rust_decimal::Decimal;
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use chrono::{NaiveTime, Weekday};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
//use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
use validator::{Validate, ValidationError};
|
||||
use crate::error::AppError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum ApiResponse<T> {
|
||||
@@ -114,6 +119,176 @@ impl PwdChange {
|
||||
}
|
||||
}
|
||||
|
||||
#[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 ToString for DayHours {
|
||||
fn to_string(&self) -> String {
|
||||
if self.0.is_empty() {
|
||||
return "".to_string()
|
||||
}
|
||||
|
||||
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(", ")
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
/*pub struct Property {
|
||||
id: u16,
|
||||
name: String,
|
||||
@@ -135,14 +310,6 @@ pub struct Message {
|
||||
text: String,
|
||||
}
|
||||
|
||||
pub struct OpeningHour {
|
||||
id: u16,
|
||||
day: Weekday,
|
||||
from: NaiveTime,
|
||||
to: NaiveTime,
|
||||
discount: u8
|
||||
}
|
||||
|
||||
pub struct Customer {
|
||||
id: u128,
|
||||
full_name: String,
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod data;
|
||||
pub mod company;
|
||||
pub mod user;
|
||||
pub mod auth_middleware;
|
||||
pub mod opening_hours;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! perm_check {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::collections::HashMap;
|
||||
use chrono::Weekday;
|
||||
use leptos::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, DayHour, WeekHours};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
#[server]
|
||||
pub async fn get_hours() -> Result<HashMap<Weekday, Vec<DayHour>>, ServerFnError> {
|
||||
use crate::backend::get_pool;
|
||||
use crate::backend::data::OpeningHour;
|
||||
|
||||
let pool = get_pool().await?;
|
||||
let hours = sqlx::query_as::<_, OpeningHour>("SELECT * FROM opening_hour").fetch_all(&pool).await?;
|
||||
|
||||
let mut ret: HashMap<Weekday, Vec<DayHour>> = hours.into_iter().fold(HashMap::new(), |mut map, v| {
|
||||
map.entry(Weekday::try_from(v.day as u8).unwrap_or(Weekday::Mon))
|
||||
.and_modify(|h| h.push(DayHour::new(v.from, v.to, v.discount)))
|
||||
.or_insert(vec![DayHour::new(v.from, v.to, v.discount)]);
|
||||
map
|
||||
});
|
||||
|
||||
for d in 0..6 {
|
||||
if let None = ret.get(&Weekday::try_from(d as u8).unwrap()) {
|
||||
ret.insert(Weekday::try_from(d as u8).unwrap(), Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn update_hours(hours: WeekHours) -> Result<ApiResponse<()>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
use crate::backend::get_pool;
|
||||
use crate::backend::data::DayHours;
|
||||
|
||||
perm_check!(is_admin);
|
||||
|
||||
let hr = DayHours::try_new(hours.hours())?;
|
||||
let pool = get_pool().await?;
|
||||
let day = hours.day();
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
sqlx::query("DELETE FROM opening_hour WHERE day = $1")
|
||||
.bind(day.num_days_from_monday() as i32)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for h in hr.hours() {
|
||||
sqlx::query(r#"INSERT INTO opening_hour(day, "from", "to", discount) VALUES($1, $2, $3, $4)"#)
|
||||
.bind(day.num_days_from_monday() as i32)
|
||||
.bind(h.from())
|
||||
.bind(h.to())
|
||||
.bind(h.discount())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(ApiResponse::Data(()))
|
||||
}
|
||||
|
||||
impl ForValidation for UpdateHours {
|
||||
fn entity(&self) -> &dyn Validate {
|
||||
&self.hours
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum AppError {
|
||||
HourParseError
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
fn as_string(&self) -> String {
|
||||
//match self { AppError::HourParseError => {"Hour parse error"} }
|
||||
"Hours parse error".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AppError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AppError {}
|
||||
@@ -4,6 +4,7 @@ pub mod backend;
|
||||
mod pages;
|
||||
mod components;
|
||||
mod validator;
|
||||
pub mod error;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ lazy_static! {
|
||||
("Name cannot be empty", "Jméno nesmí být prázdné"),
|
||||
("Invalid old password", "Neplatné staré heslo"),
|
||||
("Please sign-in to your account", "Přihlaste se prosím k uživatelskému účtu"),
|
||||
("Closed", "Zavřeno"),
|
||||
("Monday", "Pondělí"),
|
||||
("Tuesday", "Úterý"),
|
||||
("Wednesday", "Středa"),
|
||||
("Thursday", "Čtvrtek"),
|
||||
("Friday", "Pátek"),
|
||||
("Saturday", "Sobota"),
|
||||
("Sunday", "Neděle"),
|
||||
("Opening hours", "Otvírací hodiny"),
|
||||
])),
|
||||
("sk", HashMap::from( [
|
||||
("Dashboard", "Prehlad"),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
use leptos::*;
|
||||
use crate::backend::data::WeekHours;
|
||||
use crate::backend::opening_hours::UpdateHours;
|
||||
use crate::components::data_form::DataForm;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
|
||||
#[component]
|
||||
pub fn EditHours(opener: DialogOpener, hours: ReadSignal<WeekHours>) -> impl IntoView {
|
||||
let update_hours = create_server_action::<UpdateHours>();
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=update_hours title="Edit hours">
|
||||
<input type="hidden" value={move || hours.get().day().to_string()} name="hours[day]"/>
|
||||
<div class="row">
|
||||
<div class="col mb-3">
|
||||
<label for="hours" class="form-label">"Hours"</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hours"
|
||||
class="form-control"
|
||||
placeholder="12:00 - 15:00, 17:00 - 21:00"
|
||||
prop:value={move || hours.get().hours().to_string()}
|
||||
name="hours[hours]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DataForm>
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -1,11 +1,14 @@
|
||||
pub mod home_page;
|
||||
pub mod settings;
|
||||
pub mod company_info;
|
||||
mod company_info;
|
||||
mod company_edit;
|
||||
pub mod login;
|
||||
pub mod public;
|
||||
pub mod profile_edit;
|
||||
pub mod change_pwd;
|
||||
pub mod users;
|
||||
pub mod user_edit;
|
||||
pub mod user_delete;
|
||||
mod users;
|
||||
mod user_edit;
|
||||
mod user_delete;
|
||||
mod opening_hours;
|
||||
mod hours_edit;
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use chrono::Weekday;
|
||||
use leptos::*;
|
||||
use crate::backend::data::{DayHours, WeekHours};
|
||||
use crate::backend::opening_hours::get_hours;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use crate::locales::trl;
|
||||
use crate::pages::hours_edit::EditHours;
|
||||
|
||||
fn show_time(tm: &str) -> impl Fn() -> String {
|
||||
if tm.is_empty() {
|
||||
trl("Closed")
|
||||
} else {
|
||||
trl(tm)
|
||||
}
|
||||
}
|
||||
|
||||
fn show_day(day: &Weekday) -> impl Fn() -> String {
|
||||
match day {
|
||||
Weekday::Mon => { trl("Monday") }
|
||||
Weekday::Tue => { trl("Tuesday") }
|
||||
Weekday::Wed => { trl("Wednesday") }
|
||||
Weekday::Thu => { trl("Thursday") }
|
||||
Weekday::Fri => { trl("Friday") }
|
||||
Weekday::Sat => { trl("Saturday") }
|
||||
Weekday::Sun => { trl("Sunday") }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn OpeningHours() -> impl IntoView {
|
||||
let editor = DialogOpener::new();
|
||||
let hours = create_blocking_resource(move || editor.visible(), move |_| {get_hours()});
|
||||
let hrs = create_rw_signal(WeekHours::default());
|
||||
|
||||
view! {
|
||||
<EditHours opener=editor hours=hrs.read_only() />
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bx bxs-watch"></i>" "{trl("Opening hours")}</h5>
|
||||
<p class="card-text">
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{move || {
|
||||
hours.get().map(|h| match h {
|
||||
Ok(h) => {
|
||||
let h = create_rw_signal(h);
|
||||
let d = create_rw_signal((0..7).collect::<Vec<u8>>());
|
||||
view! {
|
||||
<div>
|
||||
<table class="table card-table">
|
||||
<For each=move || d.get() key=|day| *day
|
||||
children=move |day| {
|
||||
let week_day = Weekday::try_from(day).unwrap_or(Weekday::Mon);
|
||||
let hr_c = h.get().get(&week_day).unwrap_or(&Vec::new()).clone();
|
||||
let hr_day = hr_c.clone();
|
||||
view! {
|
||||
<tr>
|
||||
<td>{show_day(&week_day)}</td>
|
||||
<td>{show_time(&DayHours(hr_day).to_string())}</td>
|
||||
<td><a href="javascript:void(0)" class="card-link" on:click = move |_| {
|
||||
hrs.set(WeekHours::new(week_day, hr_c.clone()));
|
||||
editor.show();
|
||||
}><i class="bx bx-edit-alt me-1"></i></a></td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||
<p>{e.to_string()}</p></div>
|
||||
}}
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use leptos::*;
|
||||
use crate::locales::trl;
|
||||
use crate::pages::company_info::CompanyInfo;
|
||||
use crate::pages::opening_hours::OpeningHours;
|
||||
use crate::pages::users::Users;
|
||||
|
||||
#[component]
|
||||
@@ -15,5 +16,13 @@ pub fn Settings() -> impl IntoView {
|
||||
<Users/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-5">
|
||||
<div class="col-md">
|
||||
<OpeningHours/>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user