diff --git a/assets/tailwind.css b/assets/tailwind.css index 97cd06d..8beadf2 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -1401,6 +1401,27 @@ html { margin-inline-start: calc(var(--border-btn) * -1); } +.loading { + pointer-events: none; + display: inline-block; + aspect-ratio: 1 / 1; + width: 1.5rem; + background-color: currentColor; + -webkit-mask-size: 100%; + mask-size: 100%; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +.loading-spinner { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + .mockup-browser .mockup-browser-toolbar .input { position: relative; margin-left: auto; @@ -1899,8 +1920,8 @@ html { padding-bottom: 2.5rem; } -.pb-16 { - padding-bottom: 4rem; +.pb-36 { + padding-bottom: 9rem; } .text-xl { diff --git a/src/components/admin/members.rs b/src/components/admin/members.rs index 5b73ff8..92618a8 100644 --- a/src/components/admin/members.rs +++ b/src/components/admin/members.rs @@ -7,10 +7,45 @@ struct UploadedFile { contents: String, } +#[derive(PartialEq)] +enum Steps { + Upload, + Verify, + Done, +} + #[component] pub fn Migration() -> Element { + let step = use_signal(|| Steps::Upload); + + rsx! { + div { + class: "flex flex-col items-center justify-center h-full py-10", + ul { + class: "steps pb-36", + li { class: "step step-primary", "Uploaden" }, + li { + class: "step", + class: if *step.read() == Steps::Verify { "step-primary" }, + "Controleren" + }, + li { class: "step", "Klaar" }, + }, + match *step.read() { + Steps::Upload => rsx! { Upload { step: step } }, + Steps::Verify => rsx! { Verify {} }, + Steps::Done => rsx! { Verify {} }, + } + } + } +} + +#[component] +fn Upload(step: Signal) -> Element { let mut file_uploaded = use_signal(|| None); + let mut loading = use_signal(|| false); + let read_files = move |file_engine: Arc| async move { let files = file_engine.files(); for file_name in &files { @@ -32,47 +67,68 @@ pub fn Migration() -> Element { 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 { - tracing::info!("Loaded!!"); + tracing::info!("Done"); + step.set(Steps::Verify); } + 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", "Selecteer het ledenbestand" }, + input { + r#type: "file", + class: "file-input file-input-bordered w-full mt-16", + 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" + } + "{file_uploaded.read().is_none()}" + } + } +} + +#[component] +fn Verify() -> Element { rsx! { div { - class: "flex flex-col items-center justify-center h-full py-10", - ul { - class: "steps pb-16", - li { class: "step step-primary", "Uploaden" }, - li { class: "step", "Controleren" }, - li { class: "step", "Klaar" }, - } - form { - class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", - onsubmit: sumbit, - h2 { class: "text-xl", "Selecteer het ledenbestand" }, - input { - r#type: "file", - class: "file-input file-input-bordered w-full mt-16", - accept: ".csv", - multiple: false, - autocomplete: false, - onchange: upload_files, - } - input { - r#type: "submit", - class: "btn btn-primary btn-wide mt-5", - disabled: file_uploaded.read().is_none(), - value: "Uploaden" - } - } + class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", + h2 { class: "text-xl", "Controleer de actie" }, } } } #[server] async fn upload_members_list(input: String) -> Result { + use crate::util::model::member::Member; + tracing::info!("Getting members..."); + + let result = Member::migrate_proposal(input).await; + + tracing::info!("{:?}", result); + Ok("Whoo".to_string()) } + +#[server] +async fn check_members_list(correct: bool, id: u32) -> Result<(), ServerFnError> { + tracing::info!("boe"); + Ok(()) +} diff --git a/src/util/model/member.rs b/src/util/model/member.rs index e693dba..30bf640 100644 --- a/src/util/model/member.rs +++ b/src/util/model/member.rs @@ -5,7 +5,7 @@ use std::collections::BTreeSet; mod migration; -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] pub struct Member { id: String, name: Name, @@ -15,7 +15,7 @@ pub struct Member { registration_token: Option, } -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] pub struct Name { first: String, full: String, diff --git a/src/util/model/member/migration.rs b/src/util/model/member/migration.rs index 2e65ae5..3743e60 100644 --- a/src/util/model/member/migration.rs +++ b/src/util/model/member/migration.rs @@ -1,3 +1,5 @@ +use crate::util::surrealdb::DB; + use super::Member; use once_cell::sync::Lazy; @@ -126,6 +128,78 @@ fn rows_to_members(rows: Vec) -> Vec { members } +// Compare the new members list with the current +// (inserted, updated, removed) +fn combine_members_lists( + new_members_list: Vec, + current_members_list: Vec, +) -> (Vec, Vec, Vec) { + tracing::info!( + "Current: {}, New: {}", + current_members_list.len(), + new_members_list.len() + ); + + let current_members_map: HashMap = current_members_list + .clone() + .into_iter() + .map(|m| (m.id.clone(), m)) + .collect(); + + let new_members_map: HashMap = new_members_list + .clone() + .into_iter() + .map(|m| (m.id.clone(), m)) + .collect(); + + let mut inserted_members: Vec = vec![]; + let mut updated_members: Vec = vec![]; + let mut removed_members: Vec = vec![]; + + for current_member in current_members_list { + if let Some(new_member) = new_members_map.get(¤t_member.id) { + // Update existing member + let new_member_clone = new_member.clone(); + + updated_members.push(Member { + id: current_member.id, + name: new_member_clone.name, + 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); + } + } + + 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); + } + } + + (inserted_members, updated_members, removed_members) +} + +impl Member { + pub async fn migrate_proposal( + csv: String, + ) -> Result<(Vec, Vec, Vec), Box> { + let rows = csv_to_rows(csv)?; + + let new_members = rows_to_members(rows); + + let current_members = Member::fetch_all().await?; + + Ok(combine_members_lists(new_members, current_members)) + } +} + #[cfg(test)] mod tests { use super::*;