Added verify step

This commit is contained in:
xeovalyte 2024-07-23 20:42:00 +02:00
parent 7ea256f85b
commit 0f0aff81f4
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:kSQDrQDmKzljJzfGYcd3m9RqHi4h8rSwkZ3sQ9kBURo
5 changed files with 337 additions and 48 deletions

View File

@ -754,6 +754,14 @@ html {
--tw-contain-style: ; --tw-contain-style: ;
} }
@media (hover:hover) {
.table tr.hover:hover,
.table tr.hover:nth-child(even):hover {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
}
.btn { .btn {
display: inline-flex; display: inline-flex;
height: 3rem; height: 3rem;
@ -818,6 +826,57 @@ html {
content: var(--tw-content); content: var(--tw-content);
} }
.card {
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--rounded-box, 1rem);
}
.card:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.card figure {
display: flex;
align-items: center;
justify-content: center;
}
.card.image-full {
display: grid;
}
.card.image-full:before {
position: relative;
content: "";
z-index: 10;
border-radius: var(--rounded-box, 1rem);
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
opacity: 0.75;
}
.card.image-full:before,
.card.image-full > * {
grid-column-start: 1;
grid-row-start: 1;
}
.card.image-full > figure img {
height: 100%;
-o-object-fit: cover;
object-fit: cover;
}
.card.image-full > .card-body {
position: relative;
z-index: 20;
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
}
.dropdown { .dropdown {
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -1137,6 +1196,39 @@ html {
min-width: 4rem; min-width: 4rem;
} }
.table {
position: relative;
width: 100%;
border-radius: var(--rounded-box, 1rem);
text-align: left;
font-size: 0.875rem;
line-height: 1.25rem;
}
.table :where(.table-pin-rows thead tr) {
position: sticky;
top: 0px;
z-index: 1;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.table :where(.table-pin-rows tfoot tr) {
position: sticky;
bottom: 0px;
z-index: 1;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.table :where(.table-pin-cols tr th) {
position: sticky;
left: 0px;
right: 0px;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.btm-nav > *.disabled, .btm-nav > *.disabled,
.btm-nav > *[disabled] { .btm-nav > *[disabled] {
pointer-events: none; pointer-events: none;
@ -1271,6 +1363,53 @@ html {
} }
} }
.card :where(figure:first-child) {
overflow: hidden;
border-start-start-radius: inherit;
border-start-end-radius: inherit;
border-end-start-radius: unset;
border-end-end-radius: unset;
}
.card :where(figure:last-child) {
overflow: hidden;
border-start-start-radius: unset;
border-start-end-radius: unset;
border-end-start-radius: inherit;
border-end-end-radius: inherit;
}
.card:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.card.bordered {
border-width: 1px;
--tw-border-opacity: 1;
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
.card.compact .card-body {
padding: 1rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.card-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
}
.card.image-full :where(figure) {
overflow: hidden;
border-radius: inherit;
}
@keyframes checkmark { @keyframes checkmark {
0% { 0% {
background-position-y: 5px; background-position-y: 5px;
@ -1643,6 +1782,45 @@ html {
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
} }
.table:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}
.table :where(th, td) {
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
vertical-align: middle;
}
.table tr.active,
.table tr.active:nth-child(even),
.table-zebra tbody tr:nth-child(even) {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
.table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) {
border-bottom-width: 1px;
--tw-border-opacity: 1;
border-bottom-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
.table :where(thead, tfoot) {
white-space: nowrap;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 700;
color: var(--fallback-bc,oklch(var(--bc)/0.6));
}
.table :where(tfoot) {
border-top-width: 1px;
--tw-border-opacity: 1;
border-top-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
}
@keyframes toast-pop { @keyframes toast-pop {
0% { 0% {
transform: scale(0.9); transform: scale(0.9);
@ -1737,6 +1915,14 @@ html {
grid-template-rows: repeat(1, minmax(0, 1fr)); grid-template-rows: repeat(1, minmax(0, 1fr));
} }
.card-compact .card-title {
margin-bottom: 0.25rem;
}
.card-normal .card-title {
margin-bottom: 0.75rem;
}
.join.join-vertical > :where(*:not(:first-child)) { .join.join-vertical > :where(*:not(:first-child)) {
margin-left: 0px; margin-left: 0px;
margin-right: 0px; margin-right: 0px;
@ -1809,8 +1995,12 @@ html {
margin-right: auto; margin-right: auto;
} }
.mt-16 { .mb-1 {
margin-top: 4rem; margin-bottom: 0.25rem;
}
.mb-5 {
margin-bottom: 1.25rem;
} }
.mt-5 { .mt-5 {
@ -1821,6 +2011,10 @@ html {
display: flex; display: flex;
} }
.table {
display: table;
}
.contents { .contents {
display: contents; display: contents;
} }
@ -1830,6 +2024,10 @@ html {
height: 2rem; height: 2rem;
} }
.h-\[30rem\] {
height: 30rem;
}
.h-full { .h-full {
height: 100%; height: 100%;
} }
@ -1847,6 +2045,10 @@ html {
min-height: 4rem; min-height: 4rem;
} }
.w-96 {
width: 24rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -1867,6 +2069,10 @@ html {
flex-direction: column; flex-direction: column;
} }
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap { .flex-nowrap {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
@ -1891,6 +2097,14 @@ html {
gap: 0.5rem; gap: 0.5rem;
} }
.gap-5 {
gap: 1.25rem;
}
.overflow-auto {
overflow: auto;
}
.overflow-y-auto { .overflow-y-auto {
overflow-y: auto; overflow-y: auto;
} }
@ -1900,6 +2114,10 @@ html {
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
} }
.p-5 {
padding: 1.25rem;
}
.px-2 { .px-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
@ -1920,8 +2138,8 @@ html {
padding-bottom: 2.5rem; padding-bottom: 2.5rem;
} }
.pb-36 { .pb-10 {
padding-bottom: 9rem; padding-bottom: 2.5rem;
} }
.text-xl { .text-xl {

View File

@ -1,3 +1,4 @@
use crate::util::model::member::{Member, MembersMigration};
use dioxus::prelude::{dioxus_elements::FileEngine, *}; use dioxus::prelude::{dioxus_elements::FileEngine, *};
use std::sync::Arc; use std::sync::Arc;
@ -17,12 +18,13 @@ enum Steps {
#[component] #[component]
pub fn Migration() -> Element { pub fn Migration() -> Element {
let step = use_signal(|| Steps::Upload); let step = use_signal(|| Steps::Upload);
let members_migration = use_signal(|| MembersMigration::new());
rsx! { rsx! {
div { div {
class: "flex flex-col items-center justify-center h-full py-10", class: "flex flex-col items-center justify-center py-10",
ul { ul {
class: "steps pb-36", class: "steps pb-10",
li { class: "step step-primary", "Uploaden" }, li { class: "step step-primary", "Uploaden" },
li { li {
class: "step", class: "step",
@ -32,16 +34,16 @@ pub fn Migration() -> Element {
li { class: "step", "Klaar" }, li { class: "step", "Klaar" },
}, },
match *step.read() { match *step.read() {
Steps::Upload => rsx! { Upload { step: step } }, Steps::Upload => rsx! { Upload { step: step, members_migration: members_migration } },
Steps::Verify => rsx! { Verify {} }, Steps::Verify => rsx! { Verify { members_migration: members_migration} },
Steps::Done => rsx! { Verify {} }, Steps::Done => rsx! { Verify { members_migration: members_migration } },
} }
} }
} }
} }
#[component] #[component]
fn Upload(step: Signal<Steps>) -> Element { fn Upload(step: Signal<Steps>, members_migration: Signal<MembersMigration>) -> Element {
let mut file_uploaded = use_signal(|| None); let mut file_uploaded = use_signal(|| None);
let mut loading = use_signal(|| false); let mut loading = use_signal(|| false);
@ -69,23 +71,25 @@ fn Upload(step: Signal<Steps>) -> Element {
Some(file) => { Some(file) => {
loading.set(true); loading.set(true);
if let Ok(_response) = upload_members_list(file.contents.clone()).await { if let Ok(response) = upload_members_list(file.contents.clone()).await {
tracing::info!("Done"); members_migration.set(response);
step.set(Steps::Verify); step.set(Steps::Verify);
loading.set(false);
} }
loading.set(false); loading.set(false);
} }
None => tracing::info!("File doesn't exists"), None => tracing::info!("File doesn't exists"),
} }
}; };
rsx! { rsx! {
form { form {
class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2",
onsubmit: sumbit, onsubmit: sumbit,
h2 { class: "text-xl", "Selecteer het ledenbestand" }, h2 { class: "text-xl mb-5", "Selecteer de ledenlijst" },
input { input {
r#type: "file", r#type: "file",
class: "file-input file-input-bordered w-full mt-16", class: "file-input file-input-bordered w-full",
accept: ".csv", accept: ".csv",
multiple: false, multiple: false,
autocomplete: "off", autocomplete: "off",
@ -99,32 +103,81 @@ fn Upload(step: Signal<Steps>) -> Element {
} }
"Uploaden" "Uploaden"
} }
"{file_uploaded.read().is_none()}"
} }
} }
} }
#[component] #[component]
fn Verify() -> Element { fn Verify(members_migration: Signal<MembersMigration>) -> Element {
let mut loading = use_signal(|| false);
rsx! { rsx! {
div { div {
class: "flex flex-col items-center w-full h-full max-w-md mx-auto px-2", class: "flex flex-col items-center justify-center w-full mx-auto px-2",
h2 { class: "text-xl", "Controleer de actie" }, 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().inserted.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Verwijderen" }
MembersTable { members: members_migration.read().removed.clone() }
}
div {
class: "card bg-base-200 p-5",
h2 { class: "card-title mb-1", "Updaten" }
MembersTable { members: members_migration.read().updated.clone() }
}
}
button {
class: "btn btn-primary btn-wide mt-5",
disabled: loading(),
if loading() {
span { class: "loading loading-spinner" }
}
"Toepassen"
}
}
}
}
#[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] #[server]
async fn upload_members_list(input: String) -> Result<String, ServerFnError> { async fn upload_members_list(input: String) -> Result<MembersMigration, ServerFnError> {
use crate::util::model::member::Member; match Member::migrate_proposal(input).await {
Ok(r) => Ok(r),
tracing::info!("Getting members..."); Err(err) => Err(ServerFnError::new(err.to_string())),
}
let result = Member::migrate_proposal(input).await;
tracing::info!("{:?}", result);
Ok("Whoo".to_string())
} }
#[server] #[server]

View File

@ -1,2 +1 @@
#[cfg(feature = "server")]
pub mod member; pub mod member;

View File

@ -1,24 +1,32 @@
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::util::surrealdb::DB; use crate::util::surrealdb::DB;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::collections::BTreeSet; use std::collections::BTreeSet;
#[cfg(feature = "server")]
mod migration; mod migration;
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Member { pub struct Member {
id: String, pub id: String,
name: Name, pub name: Name,
hours: BTreeSet<String>, pub hours: BTreeSet<String>,
groups: BTreeSet<String>, pub groups: BTreeSet<String>,
diploma: Option<String>, pub diploma: Option<String>,
registration_token: Option<String>, pub registration_token: Option<String>,
} }
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Name { pub struct Name {
first: String, pub first: String,
full: String, pub full: String,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub struct MembersMigration {
pub inserted: Vec<Member>,
pub updated: Vec<Member>,
pub removed: Vec<Member>,
} }
#[cfg(feature = "server")] #[cfg(feature = "server")]
@ -31,3 +39,13 @@ impl Member {
Ok(members) Ok(members)
} }
} }
impl MembersMigration {
pub fn new() -> MembersMigration {
MembersMigration {
inserted: vec![],
updated: vec![],
removed: vec![],
}
}
}

View File

@ -1,9 +1,7 @@
use crate::util::surrealdb::DB; use super::{Member, MembersMigration};
use super::Member;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::{BTreeSet, HashMap, HashSet}; use std::collections::{BTreeSet, HashMap};
// Create a store for saving information when migrating to a new members list // Create a store for saving information when migrating to a new members list
static MEMBERS_STORE: Lazy<MembersStore> = Lazy::new(|| MembersStore::new()); static MEMBERS_STORE: Lazy<MembersStore> = Lazy::new(|| MembersStore::new());
@ -129,11 +127,10 @@ fn rows_to_members(rows: Vec<Row>) -> Vec<Member> {
} }
// Compare the new members list with the current // Compare the new members list with the current
// (inserted, updated, removed)
fn combine_members_lists( fn combine_members_lists(
new_members_list: Vec<Member>, new_members_list: Vec<Member>,
current_members_list: Vec<Member>, current_members_list: Vec<Member>,
) -> (Vec<Member>, Vec<Member>, Vec<Member>) { ) -> MembersMigration {
tracing::info!( tracing::info!(
"Current: {}, New: {}", "Current: {}, New: {}",
current_members_list.len(), current_members_list.len(),
@ -183,13 +180,17 @@ fn combine_members_lists(
} }
} }
(inserted_members, updated_members, removed_members) MembersMigration {
inserted: inserted_members,
updated: updated_members,
removed: removed_members,
}
} }
impl Member { impl Member {
pub async fn migrate_proposal( pub async fn migrate_proposal(
csv: String, csv: String,
) -> Result<(Vec<Member>, Vec<Member>, Vec<Member>), Box<dyn std::error::Error>> { ) -> Result<MembersMigration, Box<dyn std::error::Error>> {
let rows = csv_to_rows(csv)?; let rows = csv_to_rows(csv)?;
let new_members = rows_to_members(rows); let new_members = rows_to_members(rows);