From c708a4b79b0ba082c35905400fd35b96958c96ab Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Sun, 29 Jan 2023 22:56:33 +0100 Subject: [PATCH] =?UTF-8?q?Buchung=20funktioniert=20email=20anf=C3=A4nglic?= =?UTF-8?q?h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 +- .gitignore | 3 +- Cargo.lock | 94 +++++ Cargo.toml | 5 + terminwahl_back/Cargo.toml | 9 +- terminwahl_back/src/api.rs | 1 + terminwahl_back/src/api/read.rs | 13 +- terminwahl_back/src/api/write.rs | 37 ++ terminwahl_back/src/db/read.rs | 28 +- terminwahl_back/src/db/write.rs | 17 + terminwahl_back/src/lib.rs | 1 - terminwahl_back/src/main.rs | 25 +- terminwahl_front/Cargo.toml | 9 +- terminwahl_front/index.html | 8 +- terminwahl_front/src/main.rs | 382 +++++++++++++++++- terminwahl_front/src/requests.rs | 55 +++ terminwahl_front/static/.gitignore | 4 + terminwahl_front/static/logoheader.png | Bin 0 -> 17714 bytes terminwahl_front/static/my_bulma_colors.scss | 23 ++ terminwahl_typen/Cargo.toml | 12 + .../types.rs => terminwahl_typen/src/lib.rs | 36 +- 21 files changed, 728 insertions(+), 38 deletions(-) create mode 100644 terminwahl_back/src/api/write.rs create mode 100644 terminwahl_front/src/requests.rs create mode 100644 terminwahl_front/static/.gitignore create mode 100644 terminwahl_front/static/logoheader.png create mode 100644 terminwahl_front/static/my_bulma_colors.scss create mode 100644 terminwahl_typen/Cargo.toml rename terminwahl_back/src/types.rs => terminwahl_typen/src/lib.rs (52%) diff --git a/.env b/.env index 6dd120c..fa15c38 100644 --- a/.env +++ b/.env @@ -1 +1,3 @@ -DATABASE_URL="sqlite://db.sqlite" \ No newline at end of file +DATABASE_URL="sqlite://db.sqlite" +SMTP_USER="SMTP_USERNAME" +SMTP_PASSWORD="SMTP_PASSWORD" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9257143..73545a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /*/target /target terminwahl_front/dist -db.sqlite \ No newline at end of file + +db.sqlite* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fc08466..73adb93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,6 +692,22 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.0", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -741,6 +757,15 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.25" @@ -1152,6 +1177,17 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.8" @@ -1319,6 +1355,34 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lettre" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd84a055407850bcf4791baa77cb4818d37cbb79ad4e60b9b659727b920d2c65" +dependencies = [ + "async-trait", + "base64 0.21.0", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "nom", + "once_cell", + "quoted_printable", + "rustls", + "rustls-pemfile", + "socket2", + "tokio", + "tokio-rustls", + "webpki-roots", +] + [[package]] name = "libc" version = "0.2.139" @@ -1388,6 +1452,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "memchr" version = "2.5.0" @@ -1684,6 +1754,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" + [[package]] name = "rand" version = "0.8.5" @@ -2109,10 +2185,12 @@ dependencies = [ "chrono", "dotenv", "env_logger", + "lettre", "log", "serde", "serde_json", "sqlx", + "terminwahl_typen", "uuid", ] @@ -2120,9 +2198,25 @@ dependencies = [ name = "terminwahl_front" version = "0.1.0" dependencies = [ + "chrono", + "gloo", + "js-sys", + "serde", + "serde_json", + "terminwahl_typen", + "web-sys", "yew", ] +[[package]] +name = "terminwahl_typen" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + [[package]] name = "thiserror" version = "1.0.38" diff --git a/Cargo.toml b/Cargo.toml index 44cb824..d44c03a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,7 @@ [workspace] members = ["terminwahl_back", "terminwahl_front"] + +[workspace.dependencies] +serde = {version="1.0", features = ["derive"]} +serde_json = "1.0" +chrono = {version="*", features = ["serde"]} \ No newline at end of file diff --git a/terminwahl_back/Cargo.toml b/terminwahl_back/Cargo.toml index a7cb826..8f0fc4d 100644 --- a/terminwahl_back/Cargo.toml +++ b/terminwahl_back/Cargo.toml @@ -15,10 +15,13 @@ actix-session = { version = "0.7", features = ["cookie-session"] } # traits have been altered. The overall architecture of this CRUD can still be reproduced with a # newer version of sqlx, and the version will be updated in the future. sqlx = { version = "0.6.2", features = ["sqlite", "runtime-actix-rustls", "chrono"] } -serde = {version="1.0", features = ["derive"]} -serde_json = "1.0" uuid = { version = "1.2", features = ["serde", "v4"] } dotenv = "*" env_logger = "0.10" log = "*" -chrono = {version="*", features = ["serde"]} \ No newline at end of file +lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]} + +terminwahl_typen={path="../terminwahl_typen/"} +serde = {workspace = true} +serde_json={workspace=true} +chrono={workspace=true} diff --git a/terminwahl_back/src/api.rs b/terminwahl_back/src/api.rs index 06d7429..8778692 100644 --- a/terminwahl_back/src/api.rs +++ b/terminwahl_back/src/api.rs @@ -1,2 +1,3 @@ pub mod errors; pub mod read; +pub mod write; diff --git a/terminwahl_back/src/api/read.rs b/terminwahl_back/src/api/read.rs index cecc9a4..1f199f3 100644 --- a/terminwahl_back/src/api/read.rs +++ b/terminwahl_back/src/api/read.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use actix_web::{error, web, Error, HttpResponse}; use crate::db::{self, Pool}; @@ -19,8 +17,15 @@ pub async fn get_subjects_json(pool: web::Data) -> Result) -> Result { - let tasks = db::read::get_free_slots(&pool) +pub async fn get_slots_json(pool: web::Data) -> Result { + let tasks = db::read::get_slots(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + + Ok(HttpResponse::Ok().json(tasks)) +} +pub async fn get_unavailable_json(pool: web::Data) -> Result { + let tasks = db::read::get_unavailable(&pool) .await .map_err(error::ErrorInternalServerError)?; diff --git a/terminwahl_back/src/api/write.rs b/terminwahl_back/src/api/write.rs new file mode 100644 index 0000000..f9a42c9 --- /dev/null +++ b/terminwahl_back/src/api/write.rs @@ -0,0 +1,37 @@ +use actix_web::{error, web, HttpResponse}; +use lettre::{ + transport::smtp::{authentication::Credentials, SmtpTransportBuilder}, + Message, SmtpTransport, Transport, +}; +use terminwahl_typen::{PlannedAppointment, RequestState}; + +use crate::db::{self, Pool}; + +pub async fn save_appointments_json( + pool: web::Data, + mailer: web::Data, + appointments: web::Json>, +) -> Result { + db::write::save_appointments(&pool, &appointments) + .await + .map_err(error::ErrorInternalServerError)?; + + let email = Message::builder() + .from( + "Franz Dietrich " + .parse() + .unwrap(), + ) + .to("Franz Dietrich ".parse().unwrap()) + .subject("Happy new year") + .body(String::from("Be happy!")) + .unwrap(); + + // Send the email + match mailer.send(&email) { + Ok(_) => println!("Email sent successfully!"), + Err(e) => panic!("Could not send email: {:?}", e), + } + + Ok(HttpResponse::Ok().json(RequestState::Success)) +} diff --git a/terminwahl_back/src/db/read.rs b/terminwahl_back/src/db/read.rs index 3a62c3c..343abef 100644 --- a/terminwahl_back/src/db/read.rs +++ b/terminwahl_back/src/db/read.rs @@ -1,6 +1,10 @@ +use std::collections::HashSet; + use sqlx::query_as; -use crate::types::{AppointmentSlot, AppointmentSlots, Subject, Subjects, Teacher, Teachers}; +use terminwahl_typen::{ + AppointmentSlot, AppointmentSlots, SlotId, Subject, Subjects, Teacher, Teachers, +}; use super::Pool; @@ -11,7 +15,7 @@ pub async fn get_teachers(db: &Pool) -> Result { SELECT * FROM `teachers`"#, ) - .fetch_all(&*db) + .fetch_all(db) .await } @@ -22,21 +26,35 @@ pub async fn get_subjects(db: &Pool) -> Result { SELECT * FROM `subjects`"#, ) - .fetch_all(&*db) + .fetch_all(db) .await } -pub async fn get_free_slots(db: &Pool) -> Result { +pub async fn get_slots(db: &Pool) -> Result { match query_as!( AppointmentSlot, r#" SELECT * FROM `appointment_slots`"#, ) - .fetch_all(&*db) + .fetch_all(db) .await { Ok(elems) => Ok(elems.into_iter().map(|e| (e.id, e)).collect()), Err(e) => Err(e), } } +pub async fn get_unavailable(db: &Pool) -> Result, sqlx::Error> { + match query_as!( + SlotId, + r#" + SELECT teacher_id, slot_id + FROM `appointments`"#, + ) + .fetch_all(db) + .await + { + Ok(elems) => Ok(elems.into_iter().collect()), + Err(e) => Err(e), + } +} diff --git a/terminwahl_back/src/db/write.rs b/terminwahl_back/src/db/write.rs index e69de29..488ae66 100644 --- a/terminwahl_back/src/db/write.rs +++ b/terminwahl_back/src/db/write.rs @@ -0,0 +1,17 @@ +use terminwahl_typen::PlannedAppointment; + +use super::Pool; + +pub async fn save_appointments( + pool: &Pool, + appointments: &[PlannedAppointment], +) -> Result<(), sqlx::Error> { + for appointment in appointments { + sqlx::query("INSERT INTO appointments (teacher_id, slot_id) VALUES ($1, $2)") + .bind(appointment.teacher_id) + .bind(appointment.slot_id) + .execute(pool) + .await?; + } + Ok(()) +} diff --git a/terminwahl_back/src/lib.rs b/terminwahl_back/src/lib.rs index d3ce016..0377820 100644 --- a/terminwahl_back/src/lib.rs +++ b/terminwahl_back/src/lib.rs @@ -1,3 +1,2 @@ pub mod api; pub mod db; -pub mod types; diff --git a/terminwahl_back/src/main.rs b/terminwahl_back/src/main.rs index 909c914..3e62420 100644 --- a/terminwahl_back/src/main.rs +++ b/terminwahl_back/src/main.rs @@ -7,10 +7,8 @@ use actix_web::{ web, App, HttpServer, }; use dotenv::dotenv; -use std::{ - env, - sync::{Arc, Mutex}, -}; +use lettre::{transport::smtp::authentication::Credentials, SmtpTransport}; +use std::env; use terminwahl_back::{api, db}; #[actix_web::main] @@ -22,6 +20,15 @@ async fn main() -> std::io::Result<()> { let pool = db::init_pool(&database_url) .await .expect("Failed to create pool"); + let smtp_user = env::var("SMTP_USER").expect("Failed to get smtp user"); + let smtp_password = env::var("SMTP_PASSWORD").expect("Failed to get smtp password"); + let credentials = Credentials::new(smtp_user, smtp_password); + let smtp_pool = SmtpTransport::relay("smtp.1und1.de") + .expect("Failed to connect to smtp") + // Add credentials for authentication + .credentials(credentials) + // Connection pool settings + .build(); log::info!("starting HTTP server at http://localhost:8080"); @@ -40,6 +47,7 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(web::Data::new(pool.clone())) + .app_data(web::Data::new(smtp_pool.clone())) .wrap(Logger::default()) .wrap(session_store) .wrap(error_handlers) @@ -49,9 +57,14 @@ async fn main() -> std::io::Result<()> { .service( web::resource("/get/subjects").route(web::get().to(api::read::get_subjects_json)), ) + .service(web::resource("/get/slots").route(web::get().to(api::read::get_slots_json))) .service( - web::resource("/get/free_slots") - .route(web::get().to(api::read::get_free_slots_json)), + web::resource("/get/unavailable") + .route(web::get().to(api::read::get_unavailable_json)), + ) + .service( + web::resource("/send/appointments") + .route(web::post().to(api::write::save_appointments_json)), ) .service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html")) }) diff --git a/terminwahl_front/Cargo.toml b/terminwahl_front/Cargo.toml index 3f62426..bd8e535 100644 --- a/terminwahl_front/Cargo.toml +++ b/terminwahl_front/Cargo.toml @@ -7,4 +7,11 @@ edition = "2021" [dependencies] -yew = { version = "0.20.0", features = ["csr"] } \ No newline at end of file +yew = { version = "0.20.0", features = ["csr"] } +gloo="*" +js-sys="*" +web-sys="*" +terminwahl_typen = {path="../terminwahl_typen/"} +serde = {workspace = true} +serde_json={workspace=true} +chrono={workspace=true} \ No newline at end of file diff --git a/terminwahl_front/index.html b/terminwahl_front/index.html index de24c98..33fb19b 100644 --- a/terminwahl_front/index.html +++ b/terminwahl_front/index.html @@ -1,9 +1,15 @@ - + Yew App + + + + + + \ No newline at end of file diff --git a/terminwahl_front/src/main.rs b/terminwahl_front/src/main.rs index 4190e7c..2e0b09f 100644 --- a/terminwahl_front/src/main.rs +++ b/terminwahl_front/src/main.rs @@ -1,21 +1,375 @@ +mod requests; +use std::collections::{HashMap, HashSet}; + +use gloo::console::log; +use requests::{fetch_slots, fetch_teachers, fetch_unavailable, send_appointments}; +use serde::{Deserialize, Serialize}; +use terminwahl_typen::{ + AppointmentSlot, AppointmentSlots, IdType, PlannedAppointment, RequestState, SlotId, Teacher, + Teachers, +}; +use web_sys::HtmlInputElement; use yew::prelude::*; -#[function_component] -fn App() -> Html { - let counter = use_state(|| 5); - let onclick = { - let counter = counter.clone(); - move |_| { - let value = *counter + 1; - counter.set(value); - } - }; +// Define the possible messages which can be sent to the component +pub enum Msg { + UpdateName(String), + UpdateSchüler(String), + UpdateEmail(String), + DataEntered(Nutzer), + GetTeachers, + ReceivedTeachers(Teachers), + GetSlots, + ReceivedSlots(AppointmentSlots), + Selected(PlannedAppointment), + TooMany, + SendToServer, + AppointmentsSent(RequestState), + ReceivedUnavailable(HashSet), +} - html! { -
- -

{ *counter }

+pub struct App { + nutzer: Option, + tmp_nutzer: Nutzer, + teachers: Option, + slots: Option, + appointments: HashMap, + unavailable: Option>, + successfully_saved: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Nutzer { + name: String, + schüler: String, + email: String, +} + +impl From> for Msg { + fn from(value: Result) -> Self { + match value { + Ok(m) => m, + Err(m) => m, + } + } +} + +impl Nutzer { + fn validate(&self) -> bool { + !self.name.is_empty() && !self.email.is_empty() && !self.schüler.is_empty() + } +} + +impl Component for App { + type Message = Msg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + let app = Self { + appointments: HashMap::new(), + slots: None, + unavailable: None, + teachers: None, + nutzer: None, + tmp_nutzer: Nutzer { + name: "".into(), + schüler: "".into(), + email: "".into(), + }, + successfully_saved: None, + }; + ctx.link().send_message(Msg::GetTeachers); + ctx.link().send_message(Msg::GetSlots); + app + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::Selected(planned_appointment) => { + let slot_id = + SlotId::new(planned_appointment.teacher_id, planned_appointment.slot_id); + if self.appointments.contains_key(&slot_id) { + self.appointments.remove(&slot_id); + } else { + self.appointments.insert( + SlotId::new(planned_appointment.teacher_id, planned_appointment.slot_id), + planned_appointment, + ); + }; + true + } + Msg::TooMany => todo!(), + Msg::GetTeachers => { + ctx.link().send_future(fetch_teachers()); + false + } + Msg::ReceivedTeachers(teachers) => { + self.teachers = Some(teachers); + true + } + Msg::GetSlots => { + ctx.link().send_future(fetch_slots()); + ctx.link().send_future(fetch_unavailable()); + false + } + Msg::ReceivedSlots(slots) => { + self.slots = Some(slots); + true + } + Msg::DataEntered(n) => { + self.nutzer = Some(n); + true + } + Msg::UpdateName(s) => { + log!("update name to {}", &s); + self.tmp_nutzer.name = s; + true + } + Msg::UpdateSchüler(s) => { + self.tmp_nutzer.schüler = s; + true + } + Msg::UpdateEmail(s) => { + self.tmp_nutzer.email = s; + true + } + Msg::SendToServer => { + if (1..=3).contains(&self.appointments.len()) { + let values = self.appointments.clone().into_values(); + let appointments: Vec = values.collect(); + ctx.link().send_future(send_appointments(appointments)); + true + } else { + true + } + } + Msg::AppointmentsSent(state) => { + self.successfully_saved = Some(state); + true + } + Msg::ReceivedUnavailable(r) => { + self.unavailable = Some(r); + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + html! {<> +
+
+ +

+ {"Am 28.02.23"} +

+
+
+
+
+ { + if self.nutzer.is_none(){ + self.view_eingabe_daten(ctx) + } + else + { + self.view_auswahl_termine(ctx) + } + } +
+
+ + + } + } +} + +impl App { + fn view_eingabe_daten(&self, ctx: &Context) -> Html { + html! {
+
+
+ +
+

{"Anmeldung zum Elternsprechtag!"}

{"Bitte geben Sie unbedingt eine valide Emailadresse an, + da die Termine erst nach Bestätigung über den per Email zugesandten Link gebucht sind."}

+
+
+ +

+ ().map(|input|Msg::UpdateName(input.value())) + })}/> + + + +

+
+
+ +

+ ().map(|input|Msg::UpdateSchüler(input.value())) + })}/> + + + +

+
+
+ +

+ ().map(|input|Msg::UpdateEmail(input.value())) + })}/> + + + +

+
+
+

{"Datenschutzerklärung:"}

+

{"Mit dem klick auf Weiter bestätigen Sie, dass das Lehrerkollegium die hier + und im folgenden eingegebenen Daten zur Organisation des Elternsprechtags speichert, verarbeitet und verwendet. + Die Daten werden nur für diesen Zweck verwendet."}

+
+ { + if self.tmp_nutzer.validate() { + html!{{"Weiter"}} + } else {html!{<>

{"Füllen Sie zunächst die Felder oben aus!"}

{"Weiter"}}}} +
+
+
+
+
} + } + + fn view_auswahl_termine(&self, ctx: &Context) -> Html { + html! {<> +
+ {self.anleitung()} + {if let Some(ref teachers) = self.teachers + {teachers.iter().map(|t|{self.view_teacher_card(ctx, t)}).collect()}else{html!{}}}
+
+
+ { if self.appointments.is_empty(){ + html!{<> +

{"Bitte wählen Sie mindestens einen Block aus, bevor sie buchen können."}

+
+ +
+ } }else if self.appointments.len() > 3{ + html!{<> +

{"Bitte wählen Sie höchstens drei Blöcke, bevor sie buchen können."}

+
+ +
+ + }}else{html!{<> + + + } + }} +
+
+ + } + } + + fn anleitung(&self) -> Html { + html!(
+
+
+
+ {"Hinweise"} +
+
+
+
+
    +
  • {"Termine sind erst gebucht, wenn Sie:"} +
    • {"ganz unten auf Absenden geklickt haben"}
    • +
    • {"Sie die Buchung innerhalb von 3 Stunden über den link in der email bestätigen."}
    • +
    +
  • +
  • {"Maximal 3 Termine pro Elternhaus, da die Anzahl der Termine begrenzt ist."}
  • +
  • {"Buchen Sie die Termine so, dass sie zwischen zwei Terminen mindestens einen Slot Pause haben für Raumsuche usw."}
  • +
  • {"Sprechen Sie vor allem mit den LehrerInnen, wo ihre Kinder Probleme haben."}
  • +
  • {"Sollten Sie dringenden Gesprächsbedarf haben, aber alle Termine sind voll, melden Sie sich wie gehabt bei den entsprechenden LehrerInnen"}
  • +
  • {"Sollten Sie ihren Termin nicht wahrnehmen können, sagen Sie ihn möglichst früh ab, dass er erneut belegt werden kann."}
  • +
+
+
+
+
) + } + + fn view_teacher_card(&self, ctx: &Context, teacher: &Teacher) -> Html { + html! { +
+
+
+
+ {&teacher.ansprache}{" "}{&teacher.last_name} +
+
+
+
+ { if let Some(ref slots) = self.slots + { + let mut slots:Vec<&AppointmentSlot> = slots.values().collect(); + slots.sort_by(|x,y|{x.start_time.cmp(&y.start_time)}); + let slots = slots.iter().map(|t|{self.view_teacher_slot(ctx, t, teacher.id)}); + let first = slots.clone().take(4); + let rest = slots.skip(4); + first.chain([html!{
{"Pause"}
}]).chain(rest).collect()}else{html!{}} } +
+
+
+
+ } + } + fn view_teacher_slot( + &self, + ctx: &Context, + slot: &AppointmentSlot, + teacher_id: IdType, + ) -> Html { + let slot_id = slot.id; + let onclick = ctx.link().callback(move |_| { + Msg::Selected(PlannedAppointment { + teacher_id, + slot_id, + }) + }); + let text = format!( + "{} – {}", + slot.start_time.time().format("%H:%M"), + slot.end_time.time().format("%H:%M") + ); + let slot_id = SlotId::new(teacher_id, slot.id); + let (color, disabled) = if self.appointments.contains_key(&slot_id) { + ("is-primary", false) + } else if let Some(unavailable) = &self.unavailable { + if unavailable.contains(&slot_id) { + ("is-danger", true) + } else { + ("", false) + } + } else { + ("", false) + }; + html! {} } } diff --git a/terminwahl_front/src/requests.rs b/terminwahl_front/src/requests.rs new file mode 100644 index 0000000..ae0dabf --- /dev/null +++ b/terminwahl_front/src/requests.rs @@ -0,0 +1,55 @@ +use gloo::net::http::{Method, Request}; +use terminwahl_typen::{PlannedAppointment, RequestState}; + +use crate::Msg; + +pub async fn fetch_teachers() -> Result { + // Send the request to the specified URL. + let response = Request::new("/get/teachers").send().await; + // Return the ZuordnungMessage with the given network object and the response. + let response = response + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))? + .json() + .await + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))?; + Ok(Msg::ReceivedTeachers(response)) +} + +pub async fn fetch_slots() -> Result { + // Send the request to the specified URL. + let response = Request::new("/get/slots").send().await; + // Return the ZuordnungMessage with the given network object and the response. + let response = response + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))? + .json() + .await + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))?; + Ok(Msg::ReceivedSlots(response)) +} + +pub async fn fetch_unavailable() -> Result { + // Send the request to the specified URL. + let response = Request::new("/get/unavailable").send().await; + // Return the ZuordnungMessage with the given network object and the response. + let response = response + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))? + .json() + .await + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))?; + Ok(Msg::ReceivedUnavailable(response)) +} + +pub async fn send_appointments(appointments: Vec) -> Result { + let response = Request::new("/send/appointments") + .method(Method::POST) + .json(&appointments) + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))? + .send() + .await; + let response = response + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))? + .json() + .await + .map_err(|_| Msg::AppointmentsSent(RequestState::Error))?; + Ok(Msg::AppointmentsSent(response)) +} diff --git a/terminwahl_front/static/.gitignore b/terminwahl_front/static/.gitignore new file mode 100644 index 0000000..6608948 --- /dev/null +++ b/terminwahl_front/static/.gitignore @@ -0,0 +1,4 @@ +bulma.sass +sass/ +webfonts/ +scss/ \ No newline at end of file diff --git a/terminwahl_front/static/logoheader.png b/terminwahl_front/static/logoheader.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6e3fa3c3c5e5ac009028bf867bffa530c8fc4e GIT binary patch literal 17714 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 pXYK2`kN&PIG|>M)h4<`F)EuNJ+; @@ -37,3 +42,32 @@ pub struct AppointmentSlot { pub start_time: NaiveDateTime, pub end_time: NaiveDateTime, } + +#[derive(Debug, Deserialize, Serialize)] +pub struct OccupiedSlot { + pub id: IdType, + pub teacher_id: IdType, + pub slot_id: IdType, +} + +#[derive(Debug, Deserialize, Serialize, Hash, Clone, Copy, Eq, PartialEq)] +pub struct SlotId { + pub teacher_id: IdType, + pub slot_id: IdType, +} + +impl SlotId { + pub fn new(teacher: IdType, slot: IdType) -> Self { + Self { + teacher_id: teacher, + slot_id: slot, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum RequestState { + Success, + Message(String), + Error, +}