Added customers overview, booking detail dialog and about dialog.

main
Josef Rokos 1 year ago
parent 952f8fbe94
commit 549ec86684

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>

@ -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;

Loading…
Cancel
Save