Improved closing days. Add more than one closing days interval is now possible.

main
Josef Rokos 4 weeks ago
parent 2cb0a31abf
commit 0f940a9f5e

@ -349,7 +349,7 @@ pub struct PublicFormData {
pub property: ResProperty, pub property: ResProperty,
pub hours: Vec<DayHour>, pub hours: Vec<DayHour>,
pub reservations: Vec<Reservation>, pub reservations: Vec<Reservation>,
pub closing_days: Option<ClosingTime> pub closing_days: Vec<ClosingTime>
} }
fn empty_slots() -> Vec<String> { fn empty_slots() -> Vec<String> {

@ -8,6 +8,7 @@ use crate::components::data_form::ForValidation;
cfg_if! { if #[cfg(feature = "ssr")] { cfg_if! { if #[cfg(feature = "ssr")] {
use crate::error::AppError; use crate::error::AppError;
use chrono::Local;
pub async fn hours_for_day(day: Weekday) -> Result<Vec<DayHour>, AppError> { pub async fn hours_for_day(day: Weekday) -> Result<Vec<DayHour>, AppError> {
use crate::backend::get_pool; use crate::backend::get_pool;
@ -21,6 +22,18 @@ cfg_if! { if #[cfg(feature = "ssr")] {
Ok(hours.into_iter().map(|h| { DayHour::new(h.from, h.to, h.discount)}).collect()) Ok(hours.into_iter().map(|h| { DayHour::new(h.from, h.to, h.discount)}).collect())
} }
async fn purge_closing_days() -> Result<(), AppError> {
use crate::backend::get_pool;
let pool = get_pool().await?;
sqlx::query("DELETE FROM closing_time WHERE to_date < $1")
.bind(Local::now())
.execute(&pool)
.await?;
Ok(())
}
}} }}
#[server] #[server]
@ -97,7 +110,7 @@ pub async fn get_closing_time() -> Result<Option<ClosingTime>, ServerFnError> {
use sqlx::{Error, query_as}; use sqlx::{Error, query_as};
let pool = get_pool().await?; let pool = get_pool().await?;
let ct = query_as::<_, ClosingTime>("SELECT * FROM closing_time") let ct = query_as::<_, ClosingTime>("SELECT * FROM closing_time ORDER BY from_date")
.fetch_one(&pool) .fetch_one(&pool)
.await; .await;
@ -113,44 +126,49 @@ pub async fn get_closing_time() -> Result<Option<ClosingTime>, ServerFnError> {
} }
#[server] #[server]
pub async fn update_closing_time(time: ClosingTime) -> Result<ApiResponse<()>, ServerFnError> { pub async fn get_closing_times() -> Result<Vec<ClosingTime>, ServerFnError> {
use crate::backend::get_pool;
use sqlx::query_as;
let pool = get_pool().await?;
purge_closing_days().await?;
Ok(query_as::<_, ClosingTime>("SELECT * FROM closing_time ORDER BY from_date").fetch_all(&pool).await?)
}
#[server]
pub async fn insert_closing_time(time: ClosingTime) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check; use crate::perm_check;
use crate::backend::get_pool; use crate::backend::get_pool;
perm_check!(is_admin); perm_check!(is_admin);
let pool = get_pool().await?; let pool = get_pool().await?;
let mut tx = pool.begin().await?;
sqlx::query("DELETE FROM closing_time")
.execute(&mut *tx)
.await?;
sqlx::query("INSERT INTO closing_time(from_date, to_date) VALUES($1, $2)") sqlx::query("INSERT INTO closing_time(from_date, to_date) VALUES($1, $2)")
.bind(time.from_date) .bind(time.from_date)
.bind(time.to_date) .bind(time.to_date)
.execute(&mut *tx) .execute(&pool)
.await?; .await?;
tx.commit().await?;
Ok(ApiResponse::Data(())) Ok(ApiResponse::Data(()))
} }
impl ForValidation for UpdateClosingTime { impl ForValidation for InsertClosingTime {
fn entity(&self) -> &dyn Validate { fn entity(&self) -> &dyn Validate {
&self.time &self.time
} }
} }
#[server] #[server]
pub async fn delete_closing_time() -> Result<ApiResponse<()>, ServerFnError> { pub async fn delete_closing_time(id: i32) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check; use crate::perm_check;
use crate::backend::get_pool; use crate::backend::get_pool;
perm_check!(is_admin); perm_check!(is_admin);
let pool = get_pool().await?; let pool = get_pool().await?;
sqlx::query("DELETE FROM closing_time") sqlx::query("DELETE FROM closing_time WHERE id = $1")
.bind(id)
.execute(&pool) .execute(&pool)
.await?; .await?;

@ -26,7 +26,6 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use crate::backend::user::emails_for_notify; use crate::backend::user::emails_for_notify;
use crate::locales::trl; use crate::locales::trl;
use rust_decimal::prelude::ToPrimitive; use rust_decimal::prelude::ToPrimitive;
use crate::backend::opening_hours::get_closing_time;
async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result<ReservationSum, Error> { 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") let reservation = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1")
@ -236,12 +235,13 @@ cfg_if! { if #[cfg(feature = "ssr")] {
pub async fn get_public_form_data(day: NaiveDate) -> Result<ApiResponse<Vec<PublicFormData>>, ServerFnError> { pub async fn get_public_form_data(day: NaiveDate) -> Result<ApiResponse<Vec<PublicFormData>>, ServerFnError> {
use crate::backend::opening_hours::hours_for_day; use crate::backend::opening_hours::hours_for_day;
use crate::backend::property::get_props; use crate::backend::property::get_props;
use crate::backend::opening_hours::get_closing_times;
use chrono::Datelike; use chrono::Datelike;
let hours = hours_for_day(day.weekday()).await?; let hours = hours_for_day(day.weekday()).await?;
let props = get_props(Some("active = true")).await?; let props = get_props(Some("active = true")).await?;
let reservations = reservations_for_day(&day).await?; let reservations = reservations_for_day(&day).await?;
let closing_days = get_closing_time().await?; let closing_days = get_closing_times().await?;
info!("Loading public form data"); info!("Loading public form data");

@ -54,7 +54,7 @@ fn settings_menu(opener: MenuOpener) -> impl IntoView {
</li> </li>
<li> <li>
<a class="dropdown-item" href="/admin/appearance"> <a class="dropdown-item" href="/admin/appearance">
<i class="bx bx-envelope me-2"></i> <i class="bx bx-show me-2"></i>
<span class="align-middle">{trl("Appearance")}</span> <span class="align-middle">{trl("Appearance")}</span>
</a> </a>
</li> </li>

@ -167,7 +167,9 @@ lazy_static! {
("Bad username or password", "Špatné uživatelské jméno nebo heslo"), ("Bad username or password", "Špatné uživatelské jméno nebo heslo"),
("You can't escalate your privileges", "Nemůžete povýšit práva sami sobě"), ("You can't escalate your privileges", "Nemůžete povýšit práva sami sobě"),
("Username already exists", "Uživatel již existuje"), ("Username already exists", "Uživatel již existuje"),
("You can't delete yourself", "Nemůžete smazat sami sebe") ("You can't delete yourself", "Nemůžete smazat sami sebe"),
("Are you sure you want to delete closing days from ", "Opravdu chcete smazat zavírací dny od "),
(" to ", " do ")
])), ])),
("sk", HashMap::from( [ ("sk", HashMap::from( [
("Dashboard", "Prehlad"), ("Dashboard", "Prehlad"),

@ -0,0 +1,116 @@
use chrono::Local;
use leptos::*;
use crate::backend::data::ClosingTime;
use crate::backend::opening_hours::{get_closing_times, DeleteClosingTime, InsertClosingTime};
use crate::components::data_form::{DataForm, QuestionDialog};
use crate::components::modal_box::DialogOpener;
use crate::locales::{loc_date, trl};
#[component]
pub fn del_closing_day(closing_time: ReadSignal<ClosingTime>, opener: DialogOpener) -> impl IntoView {
let delete = create_server_action::<DeleteClosingTime>();
view! {
<QuestionDialog opener=opener action=delete title="Delete closing days">
<input type="hidden" prop:value={move || closing_time.get().id()} name="id"/>
<div>{trl("Are you sure you want to delete closing days from ")}{move || loc_date(closing_time.get().from_date)}{trl(" to ")}{move || loc_date(closing_time.get().to_date)}"?"</div>
</QuestionDialog>
}
}
#[component]
pub fn insert_closing_days(opener: DialogOpener) -> impl IntoView {
let insert_day = create_server_action::<InsertClosingTime>();
view! {
<DataForm opener=opener action=insert_day title="Closing days">
<input type="hidden" value=0 name="time[id]"/>
<div class="row">
<div class="col mb-3">
<label for="from_day" class="form-label">{trl("From")}</label>
<input
type="date"
id="from_day"
class="form-control"
prop:value={move || Local::now().date_naive().format("%Y-%m-%d").to_string()}
name="time[from_date]"
/>
<label for="to_day" class="form-label">{trl("To")}</label>
<input
type="date"
id="to_day"
class="form-control"
prop:value={move || Local::now().date_naive().format("%Y-%m-%d").to_string()}
name="time[to_date]"
/>
</div>
</div>
</DataForm>
}
}
#[component]
pub fn closing_days() -> impl IntoView {
let delete_dialog = DialogOpener::new();
let editor = DialogOpener::new();
let times = create_blocking_resource(move || editor.visible() || delete_dialog.visible(), move |_| {get_closing_times()});
let time_to_del = create_rw_signal(ClosingTime::default());
view! {
<DelClosingDay closing_time=time_to_del.read_only() opener=delete_dialog/>
<InsertClosingDays opener=editor/>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-calendar-x"></i>" "{trl("Closing days")}</h5>
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
<table class="table card-table">
<thead>
<tr>
<th>{trl("From")}</th>
<th>{trl("To")}</th>
<th>{trl("Actions")}</th>
</tr>
</thead>
{move || {
times.get().map(|c| match c {
Err(e) => {
let err = if e.to_string().contains("403") {
"Only admin can edit closing times".to_string()
} else {
e.to_string()
};
view! {<tbody class="table-border-bottom-0">
<tr><td colspan=3>{trl(&err)}</td></tr></tbody>}}
Ok(c) => {
view! {<tbody class="table-border-bottom-0">
<For each=move || c.clone()
key=|ct| ct.id()
children=move |ct: ClosingTime| {
let ct_delete = ct.clone();
view! {
<tr>
<td>{loc_date(ct.from_date)}</td>
<td>{loc_date(ct.to_date)}</td>
<td>
<a class="dropdown-item text-danger" href="javascript:void(0);" on:click=move |_| {
time_to_del.set(ct_delete.clone());
delete_dialog.show();
}>
<i class="bx bx-trash me-1"></i> {trl("Delete")}</a>
</td>
</tr>
}
}/></tbody>
}
}
})
}}
</table>
</Transition>
<a href="#" class="card-link" on:click=move |_| editor.show()>
<i class="bx bx-plus-circle fs-4 lh-0"></i>
</a>
</div>
</div>
}
}

@ -1,7 +1,6 @@
use chrono::Local;
use leptos::*; use leptos::*;
use crate::backend::data::WeekHours; use crate::backend::data::WeekHours;
use crate::backend::opening_hours::{DeleteClosingTime, UpdateClosingTime, UpdateHours}; use crate::backend::opening_hours::{DeleteClosingTime, UpdateHours};
use crate::components::data_form::{DataForm, QuestionDialog}; use crate::components::data_form::{DataForm, QuestionDialog};
use crate::components::modal_box::DialogOpener; use crate::components::modal_box::DialogOpener;
use crate::locales::trl; use crate::locales::trl;
@ -30,37 +29,6 @@ pub fn EditHours(opener: DialogOpener, hours: ReadSignal<WeekHours>) -> impl Int
} }
} }
#[component]
pub fn closing_days(opener: DialogOpener) -> impl IntoView {
let update_days = create_server_action::<UpdateClosingTime>();
view! {
<DataForm opener=opener action=update_days title="Closing days">
<input type="hidden" value=0 name="time[id]"/>
<div class="row">
<div class="col mb-3">
<label for="from_day" class="form-label">{trl("From")}</label>
<input
type="date"
id="from_day"
class="form-control"
prop:value={move || Local::now().date_naive().format("%Y-%m-%d").to_string()}
name="time[from_date]"
/>
<label for="to_day" class="form-label">{trl("To")}</label>
<input
type="date"
id="to_day"
class="form-control"
prop:value={move || Local::now().date_naive().format("%Y-%m-%d").to_string()}
name="time[to_date]"
/>
</div>
</div>
</DataForm>
}
}
#[component] #[component]
pub fn del_closing_days(opener: DialogOpener) -> impl IntoView { pub fn del_closing_days(opener: DialogOpener) -> impl IntoView {
let delete = create_server_action::<DeleteClosingTime>(); let delete = create_server_action::<DeleteClosingTime>();

@ -22,4 +22,5 @@ mod mail_view;
pub mod all_reservations; pub mod all_reservations;
pub mod customers; pub mod customers;
pub mod appearance_settings; pub mod appearance_settings;
mod closing_days;

@ -1,10 +1,10 @@
use chrono::Weekday; use chrono::Weekday;
use leptos::*; use leptos::*;
use crate::backend::data::{DayHours, WeekHours}; use crate::backend::data::{DayHours, WeekHours};
use crate::backend::opening_hours::{get_closing_time, get_hours}; use crate::backend::opening_hours::get_hours;
use crate::components::modal_box::DialogOpener; use crate::components::modal_box::DialogOpener;
use crate::locales::{loc_date, show_day, trl}; use crate::locales::{show_day, trl};
use crate::pages::hours_edit::{ClosingDays, DelClosingDays, EditHours}; use crate::pages::hours_edit::{DelClosingDays, EditHours};
fn show_time(tm: &str) -> impl Fn() -> String { fn show_time(tm: &str) -> impl Fn() -> String {
if tm.is_empty() { if tm.is_empty() {
@ -17,15 +17,12 @@ fn show_time(tm: &str) -> impl Fn() -> String {
#[component] #[component]
pub fn OpeningHours() -> impl IntoView { pub fn OpeningHours() -> impl IntoView {
let editor = DialogOpener::new(); let editor = DialogOpener::new();
let closing_editor = DialogOpener::new();
let closing_delete = DialogOpener::new(); let closing_delete = DialogOpener::new();
let hours = create_blocking_resource(move || editor.visible(), move |_| {get_hours()}); let hours = create_blocking_resource(move || editor.visible(), move |_| {get_hours()});
let closing_days = create_blocking_resource(move || closing_editor.visible() | closing_delete.visible(), move |_| get_closing_time());
let hrs = create_rw_signal(WeekHours::default()); let hrs = create_rw_signal(WeekHours::default());
view! { view! {
<EditHours opener=editor hours=hrs.read_only() /> <EditHours opener=editor hours=hrs.read_only() />
<ClosingDays opener=closing_editor/>
<DelClosingDays opener=closing_delete/> <DelClosingDays opener=closing_delete/>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
@ -68,28 +65,6 @@ pub fn OpeningHours() -> impl IntoView {
}} }}
</Transition> </Transition>
</p> </p>
<div align="center">
{trl("Closing days: ")}
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{
closing_days.get().map(|cd| match cd {
Ok(cd) => {
if let Some(cd) = cd {
view! {<p>{loc_date(cd.from_date)}" - "{loc_date(cd.to_date)}</p>}
} else {
view! {<p></p>}
}
}
Err(_) => {view! {<p></p>}}})
}
</Transition>
<a href="javascript:void(0)" class="card-link" on:click = move |_| {
closing_editor.show();
}><i class="bx bx-edit-alt me-1"></i></a>
<a class="card-link text-danger" href="javascript:void(0);" on:click=move |_| {
closing_delete.show();
}><i class="bx bx-trash me-1"></i> </a>
</div>
</div> </div>
</div> </div>
} }

@ -22,12 +22,15 @@ fn time_selector(
slots: RwSignal<Vec<String>>, slots: RwSignal<Vec<String>>,
price: RwSignal<Decimal>, price: RwSignal<Decimal>,
day: ReadSignal<NaiveDate>, day: ReadSignal<NaiveDate>,
closing_days: Option<ClosingTime>) -> impl IntoView { closing_days: Vec<ClosingTime>) -> impl IntoView {
let closed = if let Some(c) = closing_days { let mut closed = false;
day.get() >= c.from_date && day.get() <= c.to_date for cd in closing_days {
} else { if day.get() >= cd.from_date && day.get() <= cd.to_date {
false closed = true;
}; break
}
}
let checks = if !closed { let checks = if !closed {
hours.into_iter().map(|h| { hours.into_iter().map(|h| {
match property.slot { match property.slot {

@ -1,5 +1,6 @@
use leptos::*; use leptos::*;
use crate::locales::trl; use crate::locales::trl;
use crate::pages::closing_days::ClosingDays;
use crate::pages::company_info::CompanyInfo; use crate::pages::company_info::CompanyInfo;
use crate::pages::opening_hours::OpeningHours; use crate::pages::opening_hours::OpeningHours;
use crate::pages::users::Users; use crate::pages::users::Users;
@ -25,5 +26,11 @@ pub fn Settings() -> impl IntoView {
<Properties/> <Properties/>
</div> </div>
</div> </div>
<div class="row mb-5">
<div class="col-md">
<ClosingDays/>
</div>
</div>
} }
} }
Loading…
Cancel
Save