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 0000000..3c6e3fa Binary files /dev/null and b/terminwahl_front/static/logoheader.png differ diff --git a/terminwahl_front/static/my_bulma_colors.scss b/terminwahl_front/static/my_bulma_colors.scss new file mode 100644 index 0000000..52374ab --- /dev/null +++ b/terminwahl_front/static/my_bulma_colors.scss @@ -0,0 +1,23 @@ +@charset "utf-8"; + + +$blue: #083aa6; +$yellow: #f2b227; +$cyan: #1c83c5; + +@import "./bulma.sass"; + +#headerlogo { + background-color: $blue; + border-radius: 79px; + padding: 15px; +} + +$fa-font-path: "./webfonts"; + +// importing core styling file +@import "./scss/fontawesome.scss"; + +// our project needs Solid + Brands +@import "./scss/solid.scss"; +@import "./scss/brands.scss"; \ No newline at end of file diff --git a/terminwahl_typen/Cargo.toml b/terminwahl_typen/Cargo.toml new file mode 100644 index 0000000..8fa8585 --- /dev/null +++ b/terminwahl_typen/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "terminwahl_typen" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +serde = {workspace = true} +serde_json={workspace=true} +chrono={workspace=true} \ No newline at end of file diff --git a/terminwahl_back/src/types.rs b/terminwahl_typen/src/lib.rs similarity index 52% rename from terminwahl_back/src/types.rs rename to terminwahl_typen/src/lib.rs index 01cc3ed..799a957 100644 --- a/terminwahl_back/src/types.rs +++ b/terminwahl_typen/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use chrono::{DateTime, Local, NaiveDateTime}; +use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; pub type IdType = i64; @@ -28,6 +28,11 @@ pub struct Appointment { pub teacher_id: IdType, pub slot_id: IdType, } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PlannedAppointment { + pub teacher_id: IdType, + pub slot_id: IdType, +} pub type AppointmentSlots = HashMap; @@ -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, +}