Implemented mail settings.

main
Josef Rokos 1 year ago
parent 88ac17008c
commit d18ef72d03

@ -8,6 +8,7 @@ use crate::components::admin_portal::AdminPortal;
use crate::components::header::Header; use crate::components::header::Header;
use crate::components::user_menu::MenuOpener; use crate::components::user_menu::MenuOpener;
use crate::pages::login::Login; use crate::pages::login::Login;
use crate::pages::mail_settings::MailSettings;
use crate::pages::public::Public; use crate::pages::public::Public;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -63,9 +64,9 @@ pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc. // Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context(); provide_meta_context();
init_locales(); init_locales();
provide_context(MenuHelper::new()); provide_context(MenuHelper::new()); // Only one menu can be opened
provide_context(MenuOpener::new()); provide_context(MenuOpener::new()); // Drawer opener
provide_context(DialogHelper::new()); provide_context(DialogHelper::new()); // Gray dialog background
view! { view! {
<Header/> <Header/>
@ -85,6 +86,11 @@ pub fn App() -> impl IntoView {
<Settings/> <Settings/>
</AdminPortal> </AdminPortal>
}/> }/>
<Route path="admin/mail_settings" view=|| view! {
<AdminPortal>
<MailSettings/>
</AdminPortal>
}/>
</Routes> </Routes>
</main> </main>
</Router> </Router>

@ -538,19 +538,41 @@ pub struct ResSumWithItems {
pub reservations: Vec<ResWithProperty> pub reservations: Vec<ResWithProperty>
} }
/* #[derive(Clone, Serialize, Deserialize, Debug, Default)]
#[cfg_attr(feature = "ssr", derive(sqlx::Type))]
#[cfg_attr(feature = "ssr", sqlx(type_name = "message_type"))]
pub enum MessageType { pub enum MessageType {
#[default]
NewReservation, NewReservation,
NewReservationCust, NewReservationCust,
ReservationApp, ReservationApp,
ReservationCanceled, ReservationCanceled,
} }
impl Display for MessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
MessageType::NewReservation => {"NewReservation"}
MessageType::NewReservationCust => {"NewReservationCust"}
MessageType::ReservationApp => {"ReservationApp"}
MessageType::ReservationCanceled => {"ReservationCanceled"}
})
}
}
#[derive(Clone, Serialize, Deserialize, Debug, Default, Validate)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Message { pub struct Message {
id: u16, id: i32,
msg_type: MessageType, pub msg_type: MessageType,
subject: String, #[validate(length(min = 1,message = "Enter mail subject"))]
text: String, pub subject: String,
#[validate(length(min = 1,message = "Enter text"))]
pub text: String,
} }
*/ impl Message {
pub fn id(&self) -> i32 {
self.id
}
}

@ -0,0 +1,81 @@
use cfg_if::cfg_if;
use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, Message, MessageType};
use crate::components::data_form::ForValidation;
cfg_if! { if #[cfg(feature = "ssr")] {
use sqlx::{PgPool, query, query_as};
use sqlx::Error;
use crate::backend::get_pool;
use crate::error::AppError;
use log::info;
pub async fn message_for_type(msg_type: &MessageType, pool: &PgPool) -> Result<Message, Error> {
Ok(query_as::<_, Message>("SELECT * FROM message WHERE msg_type = $1")
.bind(msg_type)
.fetch_one(pool)
.await?)
}
pub async fn init_message(msg_type: &MessageType, pool: &PgPool) -> Result<(), Error> {
query("INSERT INTO message(msg_type, subject, text) VALUES($1, '', '')")
.bind(msg_type)
.execute(pool)
.await?;
Ok(())
}
pub async fn check_messages(pool: &PgPool) -> Result<(), AppError> {
let types = [
MessageType::NewReservation,
MessageType::NewReservationCust,
MessageType::ReservationApp,
MessageType::ReservationCanceled];
for msg_type in types {
let msg = message_for_type(&msg_type, pool).await;
if let Err(e) = msg {
if matches!(e, Error::RowNotFound) {
info!("Creating initial message for type {:?}", msg_type);
init_message(&msg_type, pool).await?;
} else {
return Err(e.into())
}
}
}
Ok(())
}
}}
#[server]
pub async fn get_message(msg_type: MessageType) -> Result<Message, ServerFnError> {
let pool = get_pool().await?;
Ok(message_for_type(&msg_type, &pool).await?)
}
#[server]
pub async fn update_message(message: Message) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check;
perm_check!(is_admin);
let pool = get_pool().await?;
query("UPDATE message SET subject = $1, text = $2 WHERE msg_type = $3")
.bind(message.subject)
.bind(message.text)
.bind(message.msg_type)
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}
impl ForValidation for UpdateMessage {
fn entity(&self) -> &dyn Validate {
&self.message
}
}

@ -7,6 +7,7 @@ pub mod opening_hours;
pub mod property; pub mod property;
pub mod reservation; pub mod reservation;
pub mod customer; pub mod customer;
pub mod mail;
#[macro_export] #[macro_export]
macro_rules! perm_check { macro_rules! perm_check {

@ -6,9 +6,40 @@ use crate::locales::trl;
use crate::pages::change_pwd::ChangePassword; use crate::pages::change_pwd::ChangePassword;
use crate::pages::profile_edit::ProfileEdit; use crate::pages::profile_edit::ProfileEdit;
#[component]
fn settings_menu(opener: MenuOpener) -> impl IntoView {
view! {
<ul
class=move || {
if opener.visible() {
"dropdown-menu dropdown-menu-end show"
} else {
"dropdown-menu dropdown-menu-end"
}
}
data-bs-popper="none"
on:mouseleave=move |_| opener.close()
>
<li>
<a class="dropdown-item" href="/admin/settings">
<i class="bx bx-cog me-2"></i>
<span class="align-middle">{trl("Base settings")}</span>
</a>
</li>
<li>
<a class="dropdown-item" href="/admin/mail_settings">
<i class="bx bx-envelope me-2"></i>
<span class="align-middle">{trl("Mail settings")}</span>
</a>
</li>
</ul>
}
}
#[component] #[component]
pub fn AdminPortal(children: Children) -> impl IntoView { pub fn AdminPortal(children: Children) -> impl IntoView {
let user_menu = MenuOpener::new(); let user_menu = MenuOpener::new();
let settings_menu = MenuOpener::new();
let (user, set_user) = create_signal(User::default()); let (user, set_user) = create_signal(User::default());
let editor = DialogOpener::new(); let editor = DialogOpener::new();
let pw_changer = DialogOpener::new(); let pw_changer = DialogOpener::new();
@ -38,15 +69,15 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
</a> </a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="/" class="menu-link"> <a href="/bookings" class="menu-link">
<i class="menu-icon tf-icons bx bx-time"></i> <i class="menu-icon tf-icons bx bx-layer"></i>
<div data-i18n="Analytics">"Opening hours"</div> <div data-i18n="Analytics">"Booking summary"</div>
</a> </a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
<a href="/" class="menu-link"> <a href="/customers" class="menu-link">
<i class="menu-icon tf-icons bx bx-layer"></i> <i class="menu-icon tf-icons bx bx-face"></i>
<div data-i18n="Analytics">"Places"</div> <div data-i18n="Analytics">"Customers"</div>
</a> </a>
</li> </li>
<li class="menu-item"> <li class="menu-item">
@ -73,16 +104,18 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
</div> </div>
<div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse"> <div class="navbar-nav-right d-flex align-items-center" id="navbar-collapse">
//<!-- Search --> //<!-- Title -->
<div class="navbar-nav align-items-center ms-auto mt-auto"> <div class="navbar-nav align-items-center ms-auto mt-auto">
<div class="nav-item d-flex align-items-center mt-auto"> <div class="nav-item d-flex align-items-center mt-auto">
<h4 class="mt-3"><i class="bx bx-desktop fs-4 lh-0"></i>" Admin portal"</h4> <h4 class="mt-3"><i class="bx bx-desktop fs-4 lh-0"></i>" Admin portal"</h4>
</div> </div>
</div> </div>
//<!-- /Search --> //<!-- /Settings -->
<ul class="navbar-nav flex-row align-items-center ms-auto"> <ul class="navbar-nav flex-row align-items-center ms-auto">
<SettingsMenu opener=settings_menu/>
<li class="nav-item navbar-dropdown dropdown-user dropdown"> <li class="nav-item navbar-dropdown dropdown-user dropdown">
<a class="nav-link dropdown-toggle hide-arrow" href="/admin/settings" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle hide-arrow" href="#" on:click=move |_| settings_menu.toggle()>
<i class="bx bx-cog fs-3 lh-0"></i> <i class="bx bx-cog fs-3 lh-0"></i>
</a> </a>
</li> </li>

@ -1,5 +1,6 @@
use log::error; use log::error;
use rezervator::backend::company::check_company; use rezervator::backend::company::check_company;
use rezervator::backend::mail::check_messages;
use rezervator::backend::user::create_admin; use rezervator::backend::user::create_admin;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
@ -59,6 +60,9 @@ async fn main() -> std::io::Result<()> {
if let Err(e) = check_company(&pool).await { if let Err(e) = check_company(&pool).await {
error!("Error while checking company: {:?}", e); error!("Error while checking company: {:?}", e);
} }
if let Err(e) = check_messages(&pool).await {
error!("Error while checking messages: {:?}", e);
}
HttpServer::new(move || { HttpServer::new(move || {
let leptos_options = &conf.leptos_options; let leptos_options = &conf.leptos_options;

@ -0,0 +1,27 @@
use leptos::*;
use crate::backend::data::MessageType;
use crate::locales::trl;
use crate::pages::mail_view::MailView;
#[component]
pub fn mail_settings() -> impl IntoView {
view! {
<h1>{trl("Mail settings")}</h1>
<div class="row mb-5">
<div class="col-md">
<MailView title="New booking" mail_type=MessageType::NewReservation/>
</div>
<div class="col-md">
<MailView title="New booking - for customer" mail_type=MessageType::NewReservationCust/>
</div>
</div>
<div class="row mb-5">
<div class="col-md">
<MailView title="Booking approved" mail_type=MessageType::ReservationApp/>
</div>
<div class="col-md">
<MailView title="Booking canceled" mail_type=MessageType::ReservationCanceled/>
</div>
</div>
}
}

@ -0,0 +1,81 @@
use leptos::*;
use crate::backend::data::{Message, MessageType};
use crate::backend::mail::{get_message, UpdateMessage};
use crate::components::data_form::DataForm;
use crate::components::modal_box::DialogOpener;
use crate::locales::trl;
#[component]
fn mail_edit(opener: DialogOpener, mail: ReadSignal<Message>) -> impl IntoView {
let update = create_server_action::<UpdateMessage>();
view! {
<DataForm opener=opener title="Edit mail" action=update>
<input type="hidden" prop:value={move || mail.get().id()} name="message[id]"/>
<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>
<input
type="text"
id="nameWithTitle"
class="form-control"
placeholder="Enter Subject"
prop:value={move || mail.get().subject}
name="message[subject]"
/>
</div>
</div>
<div class="row">
<div class="col mb-3">
<label for="text" class="form-label">"Text"</label>
<textarea
id="text"
class="form-control"
prop:value={move || mail.get().text}
name="message[text]"
/>
</div>
</div>
</DataForm>
}
}
#[component]
pub fn mail_view(title: &'static str, mail_type: MessageType) -> impl IntoView {
let editor = DialogOpener::new();
let mail = create_blocking_resource( move || editor.visible(), move |_| get_message(mail_type.clone()));
let for_edit = create_rw_signal(Message::default());
view! {
<MailEdit opener=editor mail={for_edit.read_only()}/>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><i class="bx bx-buildings"></i>" "{trl(title)}</h5>
<p class="card-text">
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{
mail.get().map(|m| match m {
Ok(m) => {
for_edit.set(m.clone());
view! {
<div>
<p>{trl("Subject: ")}{m.subject}</p>
<p>{m.text}</p>
</div>
}
}
Err(_) => {
view! {
<div>"Error loading data"</div>
}
}})
}
</Transition>
</p>
<a href="javascript:void(0)" class="card-link" on:click = move |_| editor.show()>
<i class="bx bx-edit-alt fs-4 lh-0"></i></a>
</div>
</div>
}
}

@ -17,4 +17,6 @@ mod property_delete;
mod res_dialogs; mod res_dialogs;
mod today_reservations; mod today_reservations;
mod new_reservations; mod new_reservations;
pub mod mail_settings;
mod mail_view;

Loading…
Cancel
Save