Compare commits

..

3 Commits

Author SHA1 Message Date
cf12fc51d7
Added basic navigation components 2024-07-26 13:39:50 +02:00
6d6b2c57c5
Members migration cleanup 2024-07-26 13:27:59 +02:00
b36fb6af89
Added support for groups and hours 2024-07-26 13:17:58 +02:00
11 changed files with 377 additions and 336 deletions

View File

@ -1,3 +1,6 @@
pub mod admin; pub mod admin;
pub mod agenda;
pub mod home; pub mod home;
pub mod layout; pub mod layout;
pub mod news;
pub mod settings;

View File

@ -1,231 +1 @@
use crate::util::model::member::{Member, MembersMigration}; pub mod migration;
use dioxus::prelude::{dioxus_elements::FileEngine, *};
use manganis::ImageAsset;
use std::sync::Arc;
#[derive(Debug)]
struct UploadedFile {
name: String,
contents: String,
}
#[derive(PartialEq)]
enum Steps {
Upload,
Verify,
Done,
}
#[component]
pub fn Migration() -> Element {
let step = use_signal(|| Steps::Upload);
let members_migration = use_signal(|| (0, MembersMigration::new()));
rsx! {
div {
class: "flex flex-col items-center justify-center py-10",
ul {
class: "steps pb-10",
li { class: "step step-primary", "Uploaden" },
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"
},
},
match *step.read() {
Steps::Upload => rsx! { Upload { step: step, members_migration: members_migration } },
Steps::Verify => rsx! { Verify { step: step, members_migration: members_migration } },
Steps::Done => rsx! { Done {} },
}
}
}
}
#[component]
fn Upload(step: Signal<Steps>, members_migration: Signal<(u16, MembersMigration)>) -> Element {
let mut file_uploaded = use_signal(|| None);
let mut loading = use_signal(|| false);
let read_files = move |file_engine: Arc<dyn FileEngine>| async move {
let files = file_engine.files();
for file_name in &files {
if let Some(contents) = file_engine.read_file_to_string(file_name).await {
file_uploaded.set(Some(UploadedFile {
name: file_name.clone(),
contents,
}));
}
}
};
let upload_files = move |evt: FormEvent| async move {
if let Some(file_engine) = evt.files() {
read_files(file_engine).await;
}
};
let sumbit = move |_evt: FormEvent| async move {
match &*file_uploaded.read() {
Some(file) => {
loading.set(true);
if let Ok(response) = upload_members_list(file.contents.clone()).await {
members_migration.set(response);
step.set(Steps::Verify);
loading.set(false);
}
loading.set(false);
}
None => tracing::info!("File doesn't exists"),
}
};
rsx! {
form {
class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2",
onsubmit: sumbit,
h2 { class: "text-xl mb-5", "Selecteer de ledenlijst" },
input {
r#type: "file",
class: "file-input file-input-bordered w-full",
accept: ".csv",
multiple: false,
autocomplete: "off",
onchange: upload_files,
}
button {
class: "btn btn-primary btn-wide mt-5",
disabled: file_uploaded.read().is_none() || loading(),
if loading() {
span { class: "loading loading-spinner" }
}
"Uploaden"
}
}
}
}
#[component]
fn Verify(step: Signal<Steps>, members_migration: Signal<(u16, MembersMigration)>) -> Element {
let mut loading = use_signal(|| false);
let submit_accept = move |_| async move {
loading.set(true);
if let Ok(_response) = migration_response(true, members_migration.read().0).await {
step.set(Steps::Done);
loading.set(false);
};
loading.set(false);
};
rsx! {
div {
class: "flex flex-col items-center justify-center w-full mx-auto px-2",
h2 { class: "text-xl mb-5", "Controleer de verandering" },
div {
class: "flex flex-wrap gap-5",
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Toevoegen" }
MembersTable { members: members_migration.read().1.inserted.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Verwijderen" }
MembersTable { members: members_migration.read().1.removed.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Updaten" }
MembersTable { members: members_migration.read().1.updated.clone() }
}
}
button {
class: "btn btn-primary btn-wide mt-5",
onclick: submit_accept,
disabled: loading(),
if loading() {
span { class: "loading loading-spinner" }
}
"Toepassen"
}
}
}
}
#[component]
fn Done() -> Element {
rsx! {
div {
class: "flex flex-col items-center justify-center w-full mx-auto px-2",
h2 { class: "text-xl mb-5", "Ledenlijst is geüpdate" },
div {
class: "w-80 mt-10",
img {
src: "/gifs/nick.webp",
}
}
button {
class: "btn btn-primary btn-wide mt-5",
"Terug naar Start"
}
}
}
}
#[component]
fn MembersTable(members: Vec<Member>) -> Element {
rsx! {
div {
class: "h-[30rem] w-96 overflow-auto font-normal",
table {
class: "table table-pin-rows",
thead {
tr {
th { "Relatiecode" }
th { "Naam" }
}
}
tbody {
for member in members {
tr {
th { "{member.id}" }
th { "{member.name.full}" }
}
}
}
}
}
}
}
#[server]
async fn upload_members_list(input: String) -> Result<(u16, MembersMigration), ServerFnError> {
match Member::migrate_proposal(input).await {
Ok(r) => Ok(r),
Err(err) => Err(ServerFnError::new(err.to_string())),
}
}
#[server]
async fn migration_response(correct: bool, id: u16) -> Result<(), ServerFnError> {
if correct {
match Member::migrate(id).await {
Err(err) => Err(ServerFnError::new(err.to_string())),
Ok(_) => Ok(()),
}?;
} else {
tracing::info!("Migrations denied");
}
Ok(())
}

View File

@ -0,0 +1,230 @@
use crate::util::model::member::{Member, MembersMigration};
use dioxus::prelude::{dioxus_elements::FileEngine, *};
use std::sync::Arc;
#[derive(Debug)]
struct UploadedFile {
name: String,
contents: String,
}
#[derive(PartialEq)]
enum Steps {
Upload,
Verify,
Done,
}
#[component]
pub fn Migration() -> Element {
let step = use_signal(|| Steps::Upload);
let members_migration = use_signal(|| (0, MembersMigration::new()));
rsx! {
div {
class: "flex flex-col items-center justify-center py-10",
ul {
class: "steps pb-10",
li { class: "step step-primary", "Uploaden" },
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"
},
},
match *step.read() {
Steps::Upload => rsx! { Upload { step: step, members_migration: members_migration } },
Steps::Verify => rsx! { Verify { step: step, members_migration: members_migration } },
Steps::Done => rsx! { Done {} },
}
}
}
}
#[component]
fn Upload(step: Signal<Steps>, members_migration: Signal<(u16, MembersMigration)>) -> Element {
let mut file_uploaded = use_signal(|| None);
let mut loading = use_signal(|| false);
let read_files = move |file_engine: Arc<dyn FileEngine>| async move {
let files = file_engine.files();
for file_name in &files {
if let Some(contents) = file_engine.read_file_to_string(file_name).await {
file_uploaded.set(Some(UploadedFile {
name: file_name.clone(),
contents,
}));
}
}
};
let upload_files = move |evt: FormEvent| async move {
if let Some(file_engine) = evt.files() {
read_files(file_engine).await;
}
};
let sumbit = move |_evt: FormEvent| async move {
match &*file_uploaded.read() {
Some(file) => {
loading.set(true);
if let Ok(response) = upload_members_list(file.contents.clone()).await {
members_migration.set(response);
step.set(Steps::Verify);
loading.set(false);
}
loading.set(false);
}
None => tracing::info!("File doesn't exists"),
}
};
rsx! {
form {
class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2",
onsubmit: sumbit,
h2 { class: "text-xl mb-5", "Selecteer de ledenlijst" },
input {
r#type: "file",
class: "file-input file-input-bordered w-full",
accept: ".csv",
multiple: false,
autocomplete: "off",
onchange: upload_files,
}
button {
class: "btn btn-primary btn-wide mt-5",
disabled: file_uploaded.read().is_none() || loading(),
if loading() {
span { class: "loading loading-spinner" }
}
"Uploaden"
}
}
}
}
#[component]
fn Verify(step: Signal<Steps>, members_migration: Signal<(u16, MembersMigration)>) -> Element {
let mut loading = use_signal(|| false);
let submit_accept = move |_| async move {
loading.set(true);
if let Ok(_response) = migration_response(true, members_migration.read().0).await {
step.set(Steps::Done);
loading.set(false);
};
loading.set(false);
};
rsx! {
div {
class: "flex flex-col items-center justify-center w-full mx-auto px-2",
h2 { class: "text-xl mb-5", "Controleer de verandering" },
div {
class: "flex flex-wrap gap-5",
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Toevoegen" }
MembersTable { members: members_migration.read().1.inserted.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Verwijderen" }
MembersTable { members: members_migration.read().1.removed.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Updaten" }
MembersTable { members: members_migration.read().1.updated.clone() }
}
}
button {
class: "btn btn-primary btn-wide mt-5",
onclick: submit_accept,
disabled: loading(),
if loading() {
span { class: "loading loading-spinner" }
}
"Toepassen"
}
}
}
}
#[component]
fn Done() -> Element {
rsx! {
div {
class: "flex flex-col items-center justify-center w-full mx-auto px-2",
h2 { class: "text-xl mb-5", "Ledenlijst is geüpdate" },
div {
class: "w-80 mt-10",
img {
src: "/gifs/nick.webp",
}
}
button {
class: "btn btn-primary btn-wide mt-5",
"Terug naar Start"
}
}
}
}
#[component]
fn MembersTable(members: Vec<Member>) -> Element {
rsx! {
div {
class: "h-[30rem] w-96 overflow-auto font-normal",
table {
class: "table table-pin-rows",
thead {
tr {
th { "Relatiecode" }
th { "Naam" }
}
}
tbody {
for member in members {
tr {
th { "{member.id}" }
th { "{member.name.full}" }
}
}
}
}
}
}
}
#[server]
async fn upload_members_list(input: String) -> Result<(u16, MembersMigration), ServerFnError> {
match MembersMigration::migrate_proposal(input).await {
Ok(r) => Ok(r),
Err(err) => Err(ServerFnError::new(err.to_string())),
}
}
#[server]
async fn migration_response(correct: bool, id: u16) -> Result<(), ServerFnError> {
if correct {
match MembersMigration::migrate(id).await {
Err(err) => Err(ServerFnError::new(err.to_string())),
Ok(_) => Ok(()),
}?;
} else {
tracing::info!("Migrations denied");
}
Ok(())
}

10
src/components/agenda.rs Normal file
View File

@ -0,0 +1,10 @@
use dioxus::prelude::*;
#[component]
pub fn Agenda() -> Element {
rsx! {
div {
h1 { class: "text-xl font-bold text-primary", "Agenda" }
}
}
}

View File

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

View File

@ -21,7 +21,7 @@ pub fn Topbar() -> Element {
} }
Link { Link {
class: "btn btn-square btn-ghost", class: "btn btn-square btn-ghost",
to: Route::Home {}, to: Route::Settings {},
icons::Cog {} icons::Cog {}
} }
} }

10
src/components/news.rs Normal file
View File

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

View File

@ -0,0 +1,10 @@
use dioxus::prelude::*;
#[component]
pub fn Settings() -> Element {
rsx! {
div {
h1 { class: "text-xl font-bold text-primary", "Settings" }
}
}
}

View File

@ -7,14 +7,23 @@ use dioxus::prelude::*;
use tracing::{info, Level}; use tracing::{info, Level};
// Use routes // Use routes
use components::admin::members::Migration; use components::admin::members::migration::Migration;
use components::agenda::Agenda;
use components::home::Home; use components::home::Home;
use components::news::News;
use components::settings::Settings;
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Route { pub enum Route {
#[layout(Wrapper)] #[layout(Wrapper)]
#[route("/")] #[route("/")]
Home {}, Home {},
#[route("/agenda")]
Agenda {},
#[route("/news")]
News {},
#[route("/settings")]
Settings {},
#[route("/admin/members/migration")] #[route("/admin/members/migration")]
Migration {}, Migration {},
} }

View File

@ -1,7 +1,6 @@
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::util::surrealdb::DB; use crate::util::surrealdb::DB;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod migration; mod migration;
@ -10,10 +9,10 @@ mod migration;
pub struct Member { pub struct Member {
pub id: String, pub id: String,
pub name: Name, pub name: Name,
// pub hours: BTreeSet<String>, pub hours: Vec<String>,
// pub groups: BTreeSet<String>, pub groups: Vec<String>,
// pub diploma: Option<String>, pub diploma: Option<String>,
// pub registration_token: Option<String>, pub registration_token: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]

View File

@ -64,16 +64,16 @@ impl Row {
first: self.first_name.clone(), first: self.first_name.clone(),
full: self.generate_full_name(), full: self.generate_full_name(),
}, },
// hours: self.generate_hours(), hours: self.generate_hours(),
// groups: BTreeSet::new(), groups: Vec::new(),
// diploma: self.diploma.clone(), diploma: self.diploma.clone(),
// registration_token: None, registration_token: None,
} }
} }
// Get the hour data from the raw string // Get the hour data from the raw string
fn generate_hours(&self) -> BTreeSet<String> { fn generate_hours(&self) -> Vec<String> {
let mut hours: BTreeSet<String> = BTreeSet::new(); let mut hours: Vec<String> = Vec::new();
let group_parts: Vec<&str> = self.hours.split(", ").collect(); let group_parts: Vec<&str> = self.hours.split(", ").collect();
@ -82,7 +82,7 @@ impl Row {
for part in hour_parts { for part in hour_parts {
if &*part != "Groep" { if &*part != "Groep" {
hours.insert(part.to_string()); hours.push(part.to_string());
} }
} }
} }
@ -104,103 +104,103 @@ impl Row {
} }
} }
// Convert the raw csv file to rust objects impl MembersMigration {
fn csv_to_rows(input: String) -> Result<Vec<Row>, Box<dyn std::error::Error>> { // Convert the raw csv file to rust objects
let mut members: Vec<Row> = vec![]; fn csv_to_rows(input: String) -> Result<Vec<Row>, Box<dyn std::error::Error>> {
let mut members: Vec<Row> = vec![];
let mut rdr = csv::Reader::from_reader(input.as_bytes()); let mut rdr = csv::Reader::from_reader(input.as_bytes());
for result in rdr.deserialize() { for result in rdr.deserialize() {
let row: Row = result?; let row: Row = result?;
members.push(row); members.push(row);
}
Ok(members)
} }
Ok(members) // Covert the rows to formatted members
} fn rows_to_members(rows: Vec<Row>) -> Vec<Member> {
let mut members: Vec<Member> = vec![];
// Covert the rows to formatted members for row in rows {
fn rows_to_members(rows: Vec<Row>) -> Vec<Member> { members.push(row.to_member());
let mut members: Vec<Member> = vec![]; }
for row in rows { members
members.push(row.to_member());
} }
members // Compare the new members list with the current
} fn combine_members_lists(
new_members_list: Vec<Member>,
current_members_list: Vec<Member>,
) -> MembersMigration {
tracing::info!(
"Current: {}, New: {}",
current_members_list.len(),
new_members_list.len()
);
// Compare the new members list with the current let current_members_map: HashMap<String, Member> = current_members_list
fn combine_members_lists( .clone()
new_members_list: Vec<Member>, .into_iter()
current_members_list: Vec<Member>, .map(|m| (m.id.clone(), m))
) -> MembersMigration { .collect();
tracing::info!(
"Current: {}, New: {}",
current_members_list.len(),
new_members_list.len()
);
let current_members_map: HashMap<String, Member> = current_members_list let new_members_map: HashMap<String, Member> = new_members_list
.clone() .clone()
.into_iter() .into_iter()
.map(|m| (m.id.clone(), m)) .map(|m| (m.id.clone(), m))
.collect(); .collect();
let new_members_map: HashMap<String, Member> = new_members_list let mut inserted_members: Vec<Member> = vec![];
.clone() let mut updated_members: Vec<Member> = vec![];
.into_iter() let mut removed_members: Vec<Member> = vec![];
.map(|m| (m.id.clone(), m))
.collect();
let mut inserted_members: Vec<Member> = vec![]; for current_member in current_members_list {
let mut updated_members: Vec<Member> = vec![]; if let Some(new_member) = new_members_map.get(&current_member.id) {
let mut removed_members: Vec<Member> = vec![]; // Update existing member
let new_member_clone = new_member.clone();
for current_member in current_members_list { updated_members.push(Member {
if let Some(new_member) = new_members_map.get(&current_member.id) { id: current_member.id,
// Update existing member name: new_member_clone.name,
let new_member_clone = new_member.clone(); hours: new_member_clone.hours,
groups: current_member.groups,
diploma: new_member_clone.diploma,
registration_token: current_member.diploma,
})
} else {
// Remove member
removed_members.push(current_member);
}
}
updated_members.push(Member { for new_member in new_members_list {
id: current_member.id, // Insert new member
name: new_member_clone.name, // TODO Generate registration token
// hours: new_member_clone.hours, if !current_members_map.contains_key(&new_member.id) {
// groups: current_member.groups, inserted_members.push(new_member);
// diploma: new_member_clone.diploma, }
// registration_token: current_member.diploma, }
})
} else { Self {
// Remove member inserted: inserted_members,
removed_members.push(current_member); updated: updated_members,
removed: removed_members,
} }
} }
for new_member in new_members_list {
// Insert new member
// TODO Generate registration token
if !current_members_map.contains_key(&new_member.id) {
inserted_members.push(new_member);
}
}
MembersMigration {
inserted: inserted_members,
updated: updated_members,
removed: removed_members,
}
}
impl Member {
pub async fn migrate_proposal( pub async fn migrate_proposal(
csv: String, csv: String,
) -> Result<(u16, MembersMigration), Box<dyn std::error::Error>> { ) -> Result<(u16, MembersMigration), Box<dyn std::error::Error>> {
let rows = csv_to_rows(csv)?; let rows = Self::csv_to_rows(csv)?;
let new_members = rows_to_members(rows); let new_members = Self::rows_to_members(rows);
let current_members = Member::fetch_all().await?; let current_members = Member::fetch_all().await?;
let members_migration = combine_members_lists(new_members, current_members); let members_migration = Self::combine_members_lists(new_members, current_members);
let count = MEMBERS_STORE.lock().await.insert(members_migration.clone()); let count = MEMBERS_STORE.lock().await.insert(members_migration.clone());
@ -221,8 +221,8 @@ impl Member {
// TODO add hours and diploma support // TODO add hours and diploma support
query = query query = query
+ format!( + format!(
"UPDATE member:{} SET name.first = \"{}\", name.full = \"{}\";", "UPDATE member:{} SET name.first = \"{}\", name.full = \"{}\", hours = {:?}, groups = {:?};",
member.id, member.name.first, member.name.full member.id, member.name.first, member.name.full, member.hours, member.groups
) )
.as_str(); .as_str();
} }
@ -231,8 +231,8 @@ impl Member {
// TODO add hours and diploma support // TODO add hours and diploma support
query = query query = query
+ format!( + format!(
"CREATE member:{} SET name.first = \"{}\", name.full = \"{}\";", "CREATE member:{} SET name.first = \"{}\", name.full = \"{}\", hours = {:?}, groups = {:?};",
member.id, member.name.first, member.name.full member.id, member.name.first, member.name.full, member.hours, member.groups
) )
.as_str(); .as_str();
} }
@ -267,14 +267,14 @@ mod tests {
first: "First".to_string(), first: "First".to_string(),
full: "First Last".to_string(), full: "First Last".to_string(),
}, },
// hours: BTreeSet::from([ hours: Vec::from([
// "Wedstrijd".to_string(), "Wedstrijd".to_string(),
// "Z5".to_string(), "Z5".to_string(),
// "Zaterdag".to_string(), "Zaterdag".to_string(),
// ]), ]),
// groups: BTreeSet::new(), groups: vec![],
// diploma: Some("LS1".to_string()), diploma: Some("LS1".to_string()),
// registration_token: None, registration_token: None,
}, },
Member { Member {
id: "D000002".to_string(), id: "D000002".to_string(),
@ -282,10 +282,10 @@ mod tests {
first: "First2".to_string(), first: "First2".to_string(),
full: "First2 Last2".to_string(), full: "First2 Last2".to_string(),
}, },
// hours: BTreeSet::from(["Z5".to_string(), "Zaterdag".to_string()]), hours: Vec::from(["Z5".to_string(), "Zaterdag".to_string()]),
// groups: BTreeSet::new(), groups: Vec::new(),
// diploma: Some("ZR4".to_string()), diploma: Some("ZR4".to_string()),
// registration_token: None, registration_token: None,
}, },
]; ];