Added begin of messaging proposal

This commit is contained in:
xeovalyte 2024-12-12 14:16:38 +01:00
parent a75df84dcb
commit 924b98f7e6
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:kSQDrQDmKzljJzfGYcd3m9RqHi4h8rSwkZ3sQ9kBURo
12 changed files with 381 additions and 219 deletions

View File

@ -2,5 +2,5 @@ pub mod admin;
pub mod agenda;
pub mod home;
pub mod layout;
pub mod news;
pub mod messages;
pub mod settings;

View File

@ -17,10 +17,10 @@ pub fn Navbar() -> Element {
"Home"
},
Link {
to: Route::News {},
to: Route::MessagesPage {},
class: "btn btn-ghost flex-col flex-nowrap py-1 gap-1 font-normal h-max",
icons::Newspaper {},
div { class: "font-normal", "Nieuws" }
div { class: "font-normal", "Berichten" }
},
Link {
to: Route::Agenda {},

View File

@ -3,7 +3,7 @@ use dioxus::prelude::*;
pub mod create;
#[component]
pub fn News() -> Element {
pub fn MessagesPage() -> Element {
rsx! {
div {
h1 { class: "text-xl font-bold text-primary", "Nieuws" }

View File

@ -0,0 +1,126 @@
use crate::util::model::message::{Message, Target, TargetKind};
use crate::util::model::session::Session;
use std::collections::HashMap;
use dioxus::prelude::*;
mod content;
mod targets;
#[derive(PartialEq)]
enum Steps {
Message,
Targets,
Verify,
Done,
}
#[derive(Props, Clone, PartialEq, Debug)]
struct Form {
title: Signal<String>,
body: Signal<String>,
targets: Signal<HashMap<u32, Target>>,
}
impl Default for Form {
fn default() -> Self {
Self {
title: use_signal(|| String::new()),
body: use_signal(|| String::new()),
targets: use_signal(|| {
HashMap::from([(
0,
Target {
kind: TargetKind::None,
value: String::new(),
},
)])
}),
}
}
}
#[component]
pub fn MessagesCreatePage() -> Element {
let mut step = use_signal(|| Steps::Message);
let form = Form::default();
let submit_proposal = move |_| async move {
let targets = (form.targets)();
if let Ok(amount) = message_proposal(
(form.title)(),
(form.body)(),
targets.into_values().collect(),
)
.await
{
tracing::info!("{}", amount);
step.set(Steps::Verify);
} else {
tracing::info!("Error occured")
}
};
rsx! {
div {
class: "w-full max-w-2xl space-y-3 flex flex-col",
h1 { class: "text-xl font-bold text-primary", "Nieuw bericht" }
ul {
class: "steps pb-10 mx-auto",
li { class: "step step-primary", "Inhoud" },
li {
class: "step",
class: if let Steps::Targets | Steps::Done | Steps::Verify = *step.read() { "step-primary" },
"Naar"
}
li {
class: "step",
class: if let Steps::Verify | Steps::Done = *step.read() { "step-primary" },
"Controleren"
}
li {
class: "step",
class: if let Steps::Done = *step.read() { "step-primary" },
"Klaar"
}
}
div {
class: "flex flex-col gap-y-5",
div {
class: if let Steps::Message = *step.read() { "" } else { "hidden" },
content::Content {
step: step,
title: form.title,
body: form.body,
}
}
div {
class: if let Steps::Targets = *step.read() { "" } else { "hidden" },
targets::Targets {
step: step,
targets: form.targets,
onsubmit: submit_proposal,
}
}
}
}
}
}
#[server]
async fn message_proposal(
title: String,
body: String,
targets: Vec<Target>,
) -> Result<usize, ServerFnError> {
let user = Session::fetch_current_user().await?;
if !user.admin {
return Err(crate::Error::NoPermissions.into());
}
let amount = Message::create_proposal(title, body, targets).await?;
Ok(amount)
}

View File

@ -0,0 +1,51 @@
use super::Steps;
use dioxus::prelude::*;
#[component]
pub fn Content(step: Signal<Steps>, title: Signal<String>, body: Signal<String>) -> Element {
let submit = move |event: FormEvent| {
title.set(event.values()["title"].as_value());
body.set(event.values()["body"].as_value());
step.set(Steps::Targets);
};
rsx! {
form {
class: "flex flex-col w-full gap-y-5",
onsubmit: submit,
label {
class: "form-control w-full",
div {
class: "label",
span { class: "label-text", "Titel" }
},
input {
r#type: "text",
required: true,
name: "title",
class: "input input-bordered w-full",
}
}
label {
class: "form-control w-full",
div {
class: "label",
span { class: "label-text", "Bericht" }
},
textarea {
name: "body",
class: "textarea textarea-bordered w-full",
required: true,
}
},
div {
class: "w-full flex gap-x-3 justify-end",
input {
r#type: "submit",
class: "btn btn-primary",
value: "Volgende",
}
}
}
}
}

View File

@ -1,142 +1,15 @@
use crate::util::model::news::{Target, TargetKind};
use std::collections::HashMap;
use dioxus::prelude::*;
#[derive(PartialEq)]
enum Steps {
Message,
Targets,
Verify,
Done,
}
use super::Steps;
use crate::util::model::message::{Message, Target, TargetKind};
use std::collections::HashMap;
#[derive(Props, Clone, PartialEq, Debug)]
struct Form {
title: Signal<String>,
body: Signal<String>,
#[component]
pub fn Targets(
step: Signal<Steps>,
targets: Signal<HashMap<u32, Target>>,
}
impl Default for Form {
fn default() -> Self {
Self {
title: use_signal(|| String::new()),
body: use_signal(|| String::new()),
targets: use_signal(|| {
HashMap::from([(
0,
Target {
kind: TargetKind::None,
value: String::new(),
},
)])
}),
}
}
}
#[component]
pub fn NewsCreate() -> Element {
let step = use_signal(|| Steps::Message);
let form = Form::default();
rsx! {
div {
class: "w-full max-w-2xl space-y-3 flex flex-col",
h1 { class: "text-xl font-bold text-primary", "Nieuw bericht" }
ul {
class: "steps pb-10 mx-auto",
li { class: "step step-primary", "Inhoud" },
li {
class: "step",
class: if let Steps::Targets | Steps::Done | Steps::Verify = *step.read() { "step-primary" },
"Naar"
}
li {
class: "step",
class: if let Steps::Verify | Steps::Done = *step.read() { "step-primary" },
"Controleren"
}
li {
class: "step",
class: if let Steps::Done = *step.read() { "step-primary" },
"Klaar"
}
}
div {
class: "flex flex-col gap-y-5",
div {
class: if let Steps::Message = *step.read() { "" } else { "hidden" },
Message {
step: step,
title: form.title,
body: form.body,
}
}
div {
class: if let Steps::Targets = *step.read() { "" } else { "hidden" },
TargetSelect {
step: step,
targets: form.targets,
}
}
}
}
}
}
#[component]
fn Message(step: Signal<Steps>, title: Signal<String>, body: Signal<String>) -> Element {
let submit = move |event: FormEvent| {
title.set(event.values()["title"].as_value());
body.set(event.values()["body"].as_value());
step.set(Steps::Targets);
};
rsx! {
form {
class: "flex flex-col w-full gap-y-5",
onsubmit: submit,
label {
class: "form-control w-full",
div {
class: "label",
span { class: "label-text", "Titel" }
},
input {
r#type: "text",
required: true,
name: "title",
class: "input input-bordered w-full",
}
}
label {
class: "form-control w-full",
div {
class: "label",
span { class: "label-text", "Bericht" }
},
textarea {
name: "body",
class: "textarea textarea-bordered w-full",
required: true,
}
},
div {
class: "w-full flex gap-x-3 justify-end",
input {
r#type: "submit",
class: "btn btn-primary",
value: "Volgende",
}
}
}
}
}
#[component]
fn TargetSelect(step: Signal<Steps>, targets: Signal<HashMap<u32, Target>>) -> Element {
onsubmit: EventHandler<FormEvent>,
) -> Element {
let mut target_id = use_signal(|| 1);
let target_ids = use_memo(move || {
@ -147,14 +20,10 @@ fn TargetSelect(step: Signal<Steps>, targets: Signal<HashMap<u32, Target>>) -> E
filtered_targets
});
let submit = move |_| {
step.set(Steps::Verify);
};
rsx! {
form {
class: "w-full",
onsubmit: submit,
onsubmit: move |event| onsubmit.call(event),
label {
class: "form-control w-full",
div {

View File

@ -18,8 +18,8 @@ use components::agenda::Agenda;
use components::home::Home;
use components::layout::AdminLayout;
use components::layout::Global;
use components::news::create::NewsCreate;
use components::news::News;
use components::messages::create::MessagesCreatePage;
use components::messages::MessagesPage;
use components::settings::Settings;
use tracing::Level;
@ -31,10 +31,10 @@ pub enum Route {
Home {},
#[route("/agenda")]
Agenda {},
#[route("/news")]
News {},
#[route("/news/create")]
NewsCreate {},
#[route("/messages")]
MessagesPage {},
#[route("/messages/create")]
MessagesCreatePage {},
#[route("/settings")]
Settings {},

View File

@ -1,4 +1,4 @@
pub mod member;
pub mod news;
pub mod message;
pub mod session;
pub mod user;

147
src/util/model/message.rs Normal file
View File

@ -0,0 +1,147 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use crate::util::surrealdb::{thing_to_string, DB};
#[cfg(feature = "server")]
use surrealdb::sql::statements::{BeginStatement, CommitStatement};
#[derive(Debug, Serialize, Deserialize)]
pub struct PushSubscription {
endpoint: String,
keys_256dh: String,
keys_auth: String,
encodings: String,
}
#[derive(PartialEq, Clone, Copy, Debug, Deserialize, Serialize, Eq)]
pub enum TargetKind {
None,
All,
Group,
Hourgroup,
Hour,
Member,
Account,
Day,
}
impl TargetKind {
pub fn from_string(input: &str) -> Self {
match input {
"all" => Self::All,
"group" => Self::Group,
"hourgroup" => Self::Hourgroup,
"hour" => Self::Hour,
"member" => Self::Member,
"account" => Self::Account,
"day" => Self::Day,
_ => Self::None,
}
}
}
#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Eq)]
pub struct Target {
pub kind: TargetKind,
pub value: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Message {
#[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))]
pub id: String,
pub title: String,
pub body: String,
pub tagets: Vec<Target>,
}
#[cfg(feature = "server")]
impl Message {
pub async fn create_proposal(
title: String,
body: String,
targets: Vec<Target>,
) -> Result<usize, crate::Error> {
let member_ids = fetch_members(targets).await?;
Ok(member_ids.len())
}
pub async fn insert(
title: String,
body: String,
channel: String,
member_ids: Vec<String>,
) -> Result<(), crate::Error> {
let mut transaction = DB
.query(BeginStatement::default())
.query("let $message = CREATE ONLY message SET title = $title, body = $body, channel = $channel RETURN id;")
.bind(("title", title))
.bind(("body", body))
.bind(("channel", channel));
for member_id in member_ids {
transaction = transaction.query(format!("RELATE ONLY (type::thing('member', $member_id_{}))->member_to_message->(type::thing('message', $message.id));", member_id))
.bind((format!("member_id_{}", member_id), member_id));
}
let res = transaction.query(CommitStatement::default()).await?;
res.check()?;
Ok(())
}
}
/// Retrieve a vec of all the members that will recieve the message
#[cfg(feature = "server")]
async fn fetch_members(targets: Vec<Target>) -> Result<Vec<String>, surrealdb::Error> {
let mut transaction = DB.query(BeginStatement::default());
for target in targets {
match target.kind {
TargetKind::All => {
transaction = transaction.query("SELECT VALUE meta::id(id) FROM member;");
break;
}
_ => {}
};
}
let mut res = transaction.query(CommitStatement::default()).await?;
res = res.check()?;
let members: Vec<String> = res.take(0)?;
Ok(members)
}
#[cfg(feature = "server")]
async fn fetch_pushsubscriptions(
targets: Vec<Target>,
) -> Result<Vec<PushSubscription>, surrealdb::Error> {
let mut transaction = DB.query(BeginStatement::default());
for target in targets {
match target.kind {
TargetKind::All => {
transaction = transaction
.query("RETURN array::group(SELECT VALUE pushsubscriptions FROM user);");
break;
}
_ => {}
};
}
let mut res = transaction
.query(CommitStatement::default())
.await?
.check()?;
let pushsubscriptions: Vec<PushSubscription> = res.take(0)?;
Ok(pushsubscriptions)
}

View File

@ -1,68 +0,0 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use crate::util::surrealdb::{thing_to_string, DB};
#[cfg(feature = "server")]
use surrealdb::sql::{
statements::{BeginStatement, CommitStatement},
Thing,
};
#[derive(PartialEq, Clone, Copy, Debug, Deserialize, Serialize, Eq)]
pub enum TargetKind {
None,
All,
Group,
Hourgroup,
Hour,
Member,
Account,
Day,
}
impl TargetKind {
pub fn from_string(input: &str) -> Self {
match input {
"all" => Self::All,
"group" => Self::Group,
"hourgroup" => Self::Hourgroup,
"hour" => Self::Hour,
"member" => Self::Member,
"account" => Self::Account,
"day" => Self::Day,
_ => Self::None,
}
}
}
#[derive(PartialEq, Debug, Deserialize, Serialize, Clone, Eq)]
pub struct Target {
pub kind: TargetKind,
pub value: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct News {
#[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))]
pub id: String,
pub title: String,
pub body: String,
pub tagets: Vec<Target>,
}
#[cfg(feature = "server")]
impl News {
// pub async fn new(
// title: String,
// body: String,
// targets: Vec<Target>,
// ) -> Result<Self, crate::Error> {
// }
}
#[cfg(feature = "server")]
fn fetch_targets(targets: Vec<Target>) {
let mut transaction = DB.query(BeginStatement::default());
for target in targets {}
}

View File

@ -8,6 +8,8 @@ use surrealdb::sql::{
Thing,
};
use super::message::PushSubscription;
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
#[cfg_attr(feature = "server", serde(deserialize_with = "thing_to_string"))]
@ -174,4 +176,19 @@ impl User {
Ok(())
}
pub async fn insert_pushsubscription(
&self,
pushsubscription: PushSubscription,
) -> Result<(), crate::Error> {
let res = DB
.query("UPDATE $user SET pushsubscriptions += $pushsubscription;")
.bind(("user", Thing::from(("user", self.id.as_str()))))
.bind(("pushsubscription", pushsubscription))
.await?;
res.check()?;
Ok(())
}
}

View File

@ -47,6 +47,12 @@ async fn apply_queries() -> surrealdb::Result<()> {
DEFINE FIELD password ON TABLE user TYPE string;
DEFINE FIELD admin ON TABLE user TYPE bool
DEFAULT false;
DEFINE FIELD pushsubscriptions ON TABLE user TYPE array<object>
DEFAULT [];
DEFINE FIELD pushsubscriptions.*.endpoint ON TABLE user TYPE string;
DEFINE FIELD pushsubscriptions.*.keys_256dh ON TABLE user TYPE string;
DEFINE FIELD pushsubscriptions.*.keys_auth ON TABLE user TYPE string;
DEFINE FIELD pushsubscriptions.*.encodings ON TABLE user TYPE string;
DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE;
",
@ -94,5 +100,19 @@ async fn apply_queries() -> surrealdb::Result<()> {
)
.await?;
// Define messages table
DB.query(
"
DEFINE TABLE OVERWRITE message SCHEMAFULL;
DEFINE FIELD title ON TABLE message TYPE string;
DEFINE FIELD body ON TABLE message TYPE string;
DEFINE FIELD channel ON TABLE message TYPE string;
DEFINE TABLE OVERWRITE member_to_message SCHEMAFULL TYPE RELATION FROM member TO message ENFORCED;
",
)
.await?;
Ok(())
}