Added customers overview, booking detail dialog and about dialog.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use crate::components::admin_portal::AdminPortal;
|
||||
use crate::components::header::Header;
|
||||
use crate::components::user_menu::MenuOpener;
|
||||
use crate::pages::all_reservations::Bookings;
|
||||
use crate::pages::customers::Customers;
|
||||
use crate::pages::login::Login;
|
||||
use crate::pages::mail_settings::MailSettings;
|
||||
use crate::pages::public::Public;
|
||||
@@ -97,6 +98,11 @@ pub fn App() -> impl IntoView {
|
||||
<Bookings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/customers" view=|| view! {
|
||||
<AdminPortal>
|
||||
<Customers/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
+20
-1
@@ -1,11 +1,14 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{server, ServerFnError};
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::backend::data::Customer;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use sqlx::{query_as, query};
|
||||
use sqlx::Error;
|
||||
use crate::backend::data::Customer;
|
||||
use std::ops::DerefMut;
|
||||
use leptos::expect_context;
|
||||
|
||||
pub async fn find_customer_by_email(email: &str, tx: &mut Transaction<'_, Postgres>) -> Option<Customer> {
|
||||
let customer = query_as::<_, Customer>("SELECT * FROM customer WHERE email = $1")
|
||||
@@ -48,3 +51,19 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
Ok(find_customer_by_email(email, tx).await.ok_or(Error::RowNotFound)?)
|
||||
}
|
||||
}}
|
||||
|
||||
#[server]
|
||||
pub async fn get_customers() -> Result<ApiResponse<Vec<Customer>>, ServerFnError> {
|
||||
use crate::backend::get_pool;
|
||||
use crate::perm_check;
|
||||
|
||||
perm_check!(is_logged_in);
|
||||
|
||||
let pool = get_pool().await?;
|
||||
|
||||
let customers = query_as::<_, Customer>("SELECT * FROM customer")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::Data(customers))
|
||||
}
|
||||
@@ -1,11 +1,30 @@
|
||||
use leptos::*;
|
||||
use crate::backend::data::User;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog};
|
||||
use crate::components::user_menu::{MenuOpener, UserMenu};
|
||||
use crate::locales::trl;
|
||||
use crate::pages::change_pwd::ChangePassword;
|
||||
use crate::pages::profile_edit::ProfileEdit;
|
||||
|
||||
#[component]
|
||||
fn about(opener: DialogOpener) -> impl IntoView {
|
||||
view! {
|
||||
<ModalDialog opener=opener title="">
|
||||
<ModalBody>
|
||||
<img src="/rezervovator_l.svg" width="180"/> <br /><br/>
|
||||
<p>
|
||||
{trl("Online booking application for sports facilities and service providers.")}<br/><br/>
|
||||
<div align="center">
|
||||
<a href="https://www.rust-lang.org" target="_blank"><img src="/rust.png" height="40"/></a>" "
|
||||
<a href="https://leptos.dev" target="_blank"><img src="/Leptos_logo.png" height="40"/></a> <br/><br/>
|
||||
"(c) 2023 - 2024"
|
||||
</div>
|
||||
</p>
|
||||
</ModalBody>
|
||||
</ModalDialog>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn settings_menu(opener: MenuOpener) -> impl IntoView {
|
||||
view! {
|
||||
@@ -44,6 +63,7 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
let editor = DialogOpener::new();
|
||||
let pw_changer = DialogOpener::new();
|
||||
let drawer = use_context::<MenuOpener>().expect("No drawer opener");
|
||||
let about_dlg = DialogOpener::new();
|
||||
|
||||
view! {
|
||||
<div class="layout-wrapper layout-content-navbar">
|
||||
@@ -69,21 +89,21 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/admin/bookings" class="menu-link">
|
||||
<a href="/admin/bookings" class="menu-link" on:click=move |_| drawer.close()>
|
||||
<i class="menu-icon tf-icons bx bx-layer"></i>
|
||||
<div data-i18n="Analytics">"Booking summary"</div>
|
||||
<div data-i18n="Analytics">{trl("Booking summary")}</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/admin/customers" class="menu-link">
|
||||
<a href="/admin/customers" class="menu-link" on:click=move |_| drawer.close()>
|
||||
<i class="menu-icon tf-icons bx bx-face"></i>
|
||||
<div data-i18n="Analytics">"Customers"</div>
|
||||
<div data-i18n="Analytics">{trl("Customers")}</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/" class="menu-link">
|
||||
<a href="#" class="menu-link" on:click=move |_| about_dlg.show()>
|
||||
<i class="menu-icon tf-icons bx bx-info-circle"></i>
|
||||
<div data-i18n="Analytics">"About"</div>
|
||||
<div data-i18n="Analytics">{trl("About")}</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -141,6 +161,7 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
<div class="container-xxl flex-grow-1 container-p-y">
|
||||
<ProfileEdit user={user} opener=editor/>
|
||||
<ChangePassword user={user} opener=pw_changer/>
|
||||
<About opener=about_dlg/>
|
||||
{children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +78,28 @@ lazy_static! {
|
||||
("Your reservation has been successfully saved.", "Vaše rezervace byla úspěšně uložena."),
|
||||
("We look forward to seeing you on", "Těšíme se na vaši návštěvu"),
|
||||
("Create booking", "Vytvořit rezervaci"),
|
||||
("Online booking application for sports facilities and service providers.", "Online rezervační aplikace pro sportovní zařízení a provozovatele služeb."),
|
||||
("About", "O aplikaci"),
|
||||
("Booking summary", "Všechny rezervace"),
|
||||
("Customers", "Zákazníci"),
|
||||
("Customer", "Zákazník"),
|
||||
("Booking overview", "Rezervace"),
|
||||
("Base settings", "Základní nastavení"),
|
||||
("Mail settings", "Nastavení e-mailů"),
|
||||
("New booking", "Nová rezervace"),
|
||||
("New booking - for customer", "Nová rezervace - zákazníkovi"),
|
||||
("Booking approved", "Schválení rezervace"),
|
||||
("Booking canceled", "Zrušení rezervace"),
|
||||
("Subject", "Předmět"),
|
||||
("Subject: ", "Předmět: "),
|
||||
("Edit mail", "Upravit e-mail"),
|
||||
("Year: ", "Rok: "),
|
||||
("Month: ", "Měsíc: "),
|
||||
("New", "Nová"),
|
||||
("Approved", "Potvrzená"),
|
||||
("Canceled", "Zrušená"),
|
||||
("Booking detail", "Detail rezervace"),
|
||||
("State", "Stav")
|
||||
])),
|
||||
("sk", HashMap::from( [
|
||||
("Dashboard", "Prehlad"),
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use leptos::*;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::backend::data::{ApiResponse, ResSumWithItems};
|
||||
use crate::backend::reservation::{month_chart, reservations_in_month, year_chart, years};
|
||||
use crate::locales::{loc_date, trl};
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog};
|
||||
use crate::locales::{loc_date, show_day, trl};
|
||||
|
||||
#[component]
|
||||
fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
|
||||
view! {
|
||||
<ModalDialog title="Booking detail" opener=opener>
|
||||
<ModalBody>
|
||||
<p>
|
||||
{move || {
|
||||
let detail = reservation.get();
|
||||
view! {
|
||||
<b>{show_day(&detail.summary.date.weekday())}" - "{loc_date(detail.summary.date)}</b><br/>
|
||||
<For each=move || detail.reservations.clone()
|
||||
key=|item| item.reservation.id()
|
||||
let:item>
|
||||
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
|
||||
</For>
|
||||
{trl("Customer: ")}{detail.customer.full_name}", "<a href={format!("mailto:{}", detail.customer.email)}>{detail.customer.email}</a>", "{detail.customer.phone}<br/>
|
||||
{
|
||||
let note = detail.summary.note.clone();
|
||||
let show = note.is_some() && !note.clone().unwrap().is_empty();
|
||||
view! {
|
||||
<Show when=move || show>
|
||||
{trl("Note: ")}{note.clone()}<br/>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
{trl("Price: ")}{detail.summary.price.to_string()}<br/>
|
||||
}
|
||||
}}
|
||||
</p>
|
||||
</ModalBody>
|
||||
</ModalDialog>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn bookings() -> impl IntoView {
|
||||
@@ -13,15 +48,18 @@ pub fn bookings() -> impl IntoView {
|
||||
let chart = create_blocking_resource(move || year.get(),move |y| month_chart(y));
|
||||
let reservations = create_blocking_resource(move || (year.get(), month.get()), move |p| reservations_in_month(p.0, p.1));
|
||||
let all_months: Vec<u32> = vec![1,2,3,4,5,6,7,8,9,10,11,12];
|
||||
let res_detail = create_rw_signal(ResSumWithItems::default());
|
||||
let detail_dlg = DialogOpener::new();
|
||||
|
||||
view! {
|
||||
<h1>{trl("Booking overview")}</h1>
|
||||
<BookingDetail reservation=res_detail.read_only() opener=detail_dlg />
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-5">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<div class="nav-link">
|
||||
"Rok: "
|
||||
{trl("Year: ")}
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -47,7 +85,7 @@ pub fn bookings() -> impl IntoView {
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="nav-link">
|
||||
"Měsíc: "
|
||||
{trl("Month: ")}
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -121,12 +159,15 @@ pub fn bookings() -> impl IntoView {
|
||||
let:data>
|
||||
<tr>
|
||||
<td>{loc_date(data.summary.date)}</td>
|
||||
<td>{data.customer.full_name}</td>
|
||||
<td>{data.customer.full_name.clone()}</td>
|
||||
<td>{data.summary.price.to_string()}</td>
|
||||
<td>{data.summary.state.to_string()}</td>
|
||||
<td><button type="button" class="btn p-0 dropdown-toggle hide-arrow">
|
||||
//on:click=move |_| menu.toggle()>
|
||||
<i class="bx bx-dots-vertical-rounded"></i>
|
||||
<td>{trl(&data.summary.state.to_string())}</td>
|
||||
<td><button type="button" class="btn"
|
||||
on:click=move |_| {
|
||||
res_detail.set(data.clone());
|
||||
detail_dlg.show();
|
||||
}>
|
||||
<i class="bx bx-show"></i>
|
||||
</button></td>
|
||||
</tr>
|
||||
</For>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use leptos::*;
|
||||
use crate::backend::customer::get_customers;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn customers() -> impl IntoView {
|
||||
let customers = create_blocking_resource(||(), |_| get_customers());
|
||||
|
||||
view! {
|
||||
<h1>{trl("Customers")}</h1>
|
||||
<div class="row mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Transition fallback=move || view! {<div>"Loading"</div>}>
|
||||
<table class="table card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{trl("Full name")}</th>
|
||||
<th>{trl("E-mail")}</th>
|
||||
<th>{trl("Phone")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{
|
||||
customers.get().map(|c| match c {
|
||||
Ok(c) => {match c {
|
||||
ApiResponse::Data(c) => {
|
||||
view! {
|
||||
<tbody>
|
||||
<For each=move || c.clone() key=|i| i.id() let:data>
|
||||
<tr>
|
||||
<td>{data.full_name}</td>
|
||||
<td>{data.email}</td>
|
||||
<td>{data.phone}</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
}
|
||||
}
|
||||
ApiResponse::Error(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
}
|
||||
})
|
||||
}
|
||||
</table>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ fn mail_edit(opener: DialogOpener, mail: ReadSignal<Message>) -> impl IntoView {
|
||||
<input type="hidden" prop:value={move || mail.get().msg_type.to_string()} name="message[msg_type]"/>
|
||||
<div class="row">
|
||||
<div class="col mb-3">
|
||||
<label for="subject" class="form-label">"Subject"</label>
|
||||
<label for="subject" class="form-label">{trl("Subject")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="nameWithTitle"
|
||||
|
||||
@@ -20,4 +20,5 @@ mod new_reservations;
|
||||
pub mod mail_settings;
|
||||
mod mail_view;
|
||||
pub mod all_reservations;
|
||||
pub mod customers;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user