Implemented mail settings.
This commit is contained in:
+9
-3
@@ -8,6 +8,7 @@ use crate::components::admin_portal::AdminPortal;
|
||||
use crate::components::header::Header;
|
||||
use crate::components::user_menu::MenuOpener;
|
||||
use crate::pages::login::Login;
|
||||
use crate::pages::mail_settings::MailSettings;
|
||||
use crate::pages::public::Public;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -63,9 +64,9 @@ pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
init_locales();
|
||||
provide_context(MenuHelper::new());
|
||||
provide_context(MenuOpener::new());
|
||||
provide_context(DialogHelper::new());
|
||||
provide_context(MenuHelper::new()); // Only one menu can be opened
|
||||
provide_context(MenuOpener::new()); // Drawer opener
|
||||
provide_context(DialogHelper::new()); // Gray dialog background
|
||||
|
||||
view! {
|
||||
<Header/>
|
||||
@@ -85,6 +86,11 @@ pub fn App() -> impl IntoView {
|
||||
<Settings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/mail_settings" view=|| view! {
|
||||
<AdminPortal>
|
||||
<MailSettings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
+29
-7
@@ -538,19 +538,41 @@ pub struct ResSumWithItems {
|
||||
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 {
|
||||
#[default]
|
||||
NewReservation,
|
||||
NewReservationCust,
|
||||
ReservationApp,
|
||||
ReservationCanceled,
|
||||
}
|
||||
|
||||
pub struct Message {
|
||||
id: u16,
|
||||
msg_type: MessageType,
|
||||
subject: String,
|
||||
text: String,
|
||||
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 {
|
||||
id: i32,
|
||||
pub msg_type: MessageType,
|
||||
#[validate(length(min = 1,message = "Enter mail subject"))]
|
||||
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 reservation;
|
||||
pub mod customer;
|
||||
pub mod mail;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! perm_check {
|
||||
|
||||
@@ -6,9 +6,40 @@ use crate::locales::trl;
|
||||
use crate::pages::change_pwd::ChangePassword;
|
||||
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]
|
||||
pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
let user_menu = MenuOpener::new();
|
||||
let settings_menu = MenuOpener::new();
|
||||
let (user, set_user) = create_signal(User::default());
|
||||
let editor = DialogOpener::new();
|
||||
let pw_changer = DialogOpener::new();
|
||||
@@ -38,15 +69,15 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/" class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-time"></i>
|
||||
<div data-i18n="Analytics">"Opening hours"</div>
|
||||
<a href="/bookings" class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-layer"></i>
|
||||
<div data-i18n="Analytics">"Booking summary"</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/" class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-layer"></i>
|
||||
<div data-i18n="Analytics">"Places"</div>
|
||||
<a href="/customers" class="menu-link">
|
||||
<i class="menu-icon tf-icons bx bx-face"></i>
|
||||
<div data-i18n="Analytics">"Customers"</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
@@ -73,16 +104,18 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
</div>
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
</div>
|
||||
//<!-- /Search -->
|
||||
//<!-- /Settings -->
|
||||
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
<SettingsMenu opener=settings_menu/>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use log::error;
|
||||
use rezervator::backend::company::check_company;
|
||||
use rezervator::backend::mail::check_messages;
|
||||
use rezervator::backend::user::create_admin;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -59,6 +60,9 @@ async fn main() -> std::io::Result<()> {
|
||||
if let Err(e) = check_company(&pool).await {
|
||||
error!("Error while checking company: {:?}", e);
|
||||
}
|
||||
if let Err(e) = check_messages(&pool).await {
|
||||
error!("Error while checking messages: {:?}", e);
|
||||
}
|
||||
|
||||
HttpServer::new(move || {
|
||||
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 today_reservations;
|
||||
mod new_reservations;
|
||||
pub mod mail_settings;
|
||||
mod mail_view;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user