Buchung funktioniert email anfänglich
This commit is contained in:
parent
0cd75a9ff5
commit
c708a4b79b
2
.env
2
.env
@ -1 +1,3 @@
|
||||
DATABASE_URL="sqlite://db.sqlite"
|
||||
SMTP_USER="SMTP_USERNAME"
|
||||
SMTP_PASSWORD="SMTP_PASSWORD"
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/*/target
|
||||
/target
|
||||
terminwahl_front/dist
|
||||
db.sqlite
|
||||
|
||||
db.sqlite*
|
94
Cargo.lock
generated
94
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"]}
|
@ -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"]}
|
||||
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}
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod errors;
|
||||
pub mod read;
|
||||
pub mod write;
|
||||
|
@ -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<Pool>) -> Result<HttpResponse, Er
|
||||
Ok(HttpResponse::Ok().json(tasks))
|
||||
}
|
||||
|
||||
pub async fn get_free_slots_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
|
||||
let tasks = db::read::get_free_slots(&pool)
|
||||
pub async fn get_slots_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
|
||||
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<Pool>) -> Result<HttpResponse, Error> {
|
||||
let tasks = db::read::get_unavailable(&pool)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
|
37
terminwahl_back/src/api/write.rs
Normal file
37
terminwahl_back/src/api/write.rs
Normal file
@ -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<Pool>,
|
||||
mailer: web::Data<SmtpTransport>,
|
||||
appointments: web::Json<Vec<PlannedAppointment>>,
|
||||
) -> Result<HttpResponse, error::Error> {
|
||||
db::write::save_appointments(&pool, &appointments)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(
|
||||
"Franz Dietrich <franz.dietrich@uhlandshoehe.de>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("Franz Dietrich <dietrich@teilgedanken.de>".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))
|
||||
}
|
@ -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<Teachers, sqlx::Error> {
|
||||
SELECT *
|
||||
FROM `teachers`"#,
|
||||
)
|
||||
.fetch_all(&*db)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -22,21 +26,35 @@ pub async fn get_subjects(db: &Pool) -> Result<Subjects, sqlx::Error> {
|
||||
SELECT *
|
||||
FROM `subjects`"#,
|
||||
)
|
||||
.fetch_all(&*db)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_free_slots(db: &Pool) -> Result<AppointmentSlots, sqlx::Error> {
|
||||
pub async fn get_slots(db: &Pool) -> Result<AppointmentSlots, sqlx::Error> {
|
||||
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<HashSet<SlotId>, 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),
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
@ -1,3 +1,2 @@
|
||||
pub mod api;
|
||||
pub mod db;
|
||||
pub mod types;
|
||||
|
@ -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"))
|
||||
})
|
||||
|
@ -8,3 +8,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
|
||||
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}
|
@ -1,9 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew App</title>
|
||||
<link data-trunk rel="scss" href="static/my_bulma_colors.scss" />
|
||||
<link data-trunk rel="copy-file" href="static/logoheader.png" />
|
||||
<link data-trunk rel="copy-dir" href="static/webfonts" />
|
||||
<meta name="keywords" content="Termine,Waldorfschule,Lehrersprechtag,Lehrerinnensprechtag" />
|
||||
<meta name="author" content="Franz Dietrich" />
|
||||
<meta name="description" content="Termine buchen für den Lehrersprechtag der Waldorfschule Uhlandshöhe" />
|
||||
</head>
|
||||
|
||||
</html>
|
@ -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<SlotId>),
|
||||
}
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<button {onclick}>{ "+1" }</button>
|
||||
<p>{ *counter }</p>
|
||||
pub struct App {
|
||||
nutzer: Option<Nutzer>,
|
||||
tmp_nutzer: Nutzer,
|
||||
teachers: Option<Teachers>,
|
||||
slots: Option<AppointmentSlots>,
|
||||
appointments: HashMap<SlotId, PlannedAppointment>,
|
||||
unavailable: Option<HashSet<SlotId>>,
|
||||
successfully_saved: Option<RequestState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct Nutzer {
|
||||
name: String,
|
||||
schüler: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl From<Result<Msg, Msg>> for Msg {
|
||||
fn from(value: Result<Msg, Msg>) -> 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>) -> 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<Self>, 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<PlannedAppointment> = 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<Self>) -> Html {
|
||||
html! {<>
|
||||
<section class="hero is-warning">
|
||||
<div class="hero-body">
|
||||
<p class="title has-text-link">
|
||||
{"Elternsprechtag"}
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
{"Am 28.02.23"}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
{
|
||||
if self.nutzer.is_none(){
|
||||
self.view_eingabe_daten(ctx)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.view_auswahl_termine(ctx)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn view_eingabe_daten(&self, ctx: &Context<Self>) -> Html {
|
||||
html! { <div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<div class="notification is-light">
|
||||
<figure class="image container is-128x128" id="headerlogo">
|
||||
<img src="/logoheader.png" />
|
||||
</figure>
|
||||
<div class="box mt-3 is-light">
|
||||
<p>{"Anmeldung zum Elternsprechtag!"}</p><p>{"Bitte geben Sie unbedingt eine valide Emailadresse an,
|
||||
da die Termine erst nach Bestätigung über den per Email zugesandten Link gebucht sind."}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{"Voller Name"}</label>
|
||||
<p class="control has-icons-left has-icons-right">
|
||||
<input class="input" type="text" placeholder="Voller Name" value={self.tmp_nutzer.name.to_string()}
|
||||
oninput={ctx.link().batch_callback(|event:InputEvent| {
|
||||
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateName(input.value()))
|
||||
})}/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-signature"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{"Alle betreffenden SchülerInnen"}</label>
|
||||
<p class="control has-icons-left has-icons-right">
|
||||
<input class="input" type="email" placeholder="Schülerinnen" value={self.tmp_nutzer.schüler.to_string()}
|
||||
oninput={ctx.link().batch_callback(|event:InputEvent| {
|
||||
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateSchüler(input.value()))
|
||||
})}/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{"Email"}</label>
|
||||
<p class="control has-icons-left">
|
||||
<input class="input" type="email" placeholder="email" value={self.tmp_nutzer.email.to_string()}
|
||||
oninput={ctx.link().batch_callback(|event:InputEvent| {
|
||||
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateEmail(input.value()))
|
||||
})}/>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-at"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="box mt-3 is-light">
|
||||
<h4 class="title is-4">{"Datenschutzerklärung:"}</h4>
|
||||
<p>{"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."}</p>
|
||||
<div class="has-text-right mt-6 mr-6">
|
||||
{
|
||||
if self.tmp_nutzer.validate() {
|
||||
html!{<a class="button is-link is-medium" onclick={
|
||||
let tmp_nutzer = self.tmp_nutzer.clone();
|
||||
ctx.link().callback(move |_|{Msg::DataEntered(tmp_nutzer.clone()) })
|
||||
}>{"Weiter"}</a>}
|
||||
} else {html!{<><p>{"Füllen Sie zunächst die Felder oben aus!"}</p><a class="button is-link is-medium" disabled=true>{"Weiter"}</a></>}}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
}
|
||||
|
||||
fn view_auswahl_termine(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {<>
|
||||
<div id="app" class="row columns is-multiline">
|
||||
{self.anleitung()}
|
||||
{if let Some(ref teachers) = self.teachers
|
||||
{teachers.iter().map(|t|{self.view_teacher_card(ctx, t)}).collect()}else{html!{}}}
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
{ if self.appointments.is_empty(){
|
||||
html!{<>
|
||||
<p>{"Bitte wählen Sie mindestens einen Block aus, bevor sie buchen können."}</p>
|
||||
<div class="section">
|
||||
<button class="button" disabled=true>{"Buchungen reservieren"}</button>
|
||||
</div></>
|
||||
} }else if self.appointments.len() > 3{
|
||||
html!{<>
|
||||
<p>{"Bitte wählen Sie höchstens drei Blöcke, bevor sie buchen können."}</p>
|
||||
<div class="section">
|
||||
<button class="button" disabled=true>{"Buchungen reservieren"}</button>
|
||||
</div></>
|
||||
|
||||
}}else{html!{<>
|
||||
<button class="button is-primary" onclick={ctx.link().callback(|_|Msg::SendToServer)}>{"Buchungen reservieren"}</button></>
|
||||
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn anleitung(&self) -> Html {
|
||||
html!( <div class="column is-12">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-centered">
|
||||
{"Hinweise"}
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li>{"Termine sind erst gebucht, wenn Sie:"}
|
||||
<ul><li>{"ganz unten auf Absenden geklickt haben"}</li>
|
||||
<li>{"Sie die Buchung innerhalb von 3 Stunden über den link in der email bestätigen."}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>{"Maximal 3 Termine pro Elternhaus, da die Anzahl der Termine begrenzt ist."}</li>
|
||||
<li>{"Buchen Sie die Termine so, dass sie zwischen zwei Terminen mindestens einen Slot Pause haben für Raumsuche usw."}</li>
|
||||
<li>{"Sprechen Sie vor allem mit den LehrerInnen, wo ihre Kinder Probleme haben."}</li>
|
||||
<li>{"Sollten Sie dringenden Gesprächsbedarf haben, aber alle Termine sind voll, melden Sie sich wie gehabt bei den entsprechenden LehrerInnen"}</li>
|
||||
<li>{"Sollten Sie ihren Termin nicht wahrnehmen können, sagen Sie ihn möglichst früh ab, dass er erneut belegt werden kann."}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
fn view_teacher_card(&self, ctx: &Context<Self>, teacher: &Teacher) -> Html {
|
||||
html! {
|
||||
<div class="column is-3">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-header-title is-centered">
|
||||
{&teacher.ansprache}{" "}{&teacher.last_name}
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
{ 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!{<div class="has-text-centered mb-3">{"Pause"}</div>}]).chain(rest).collect()}else{html!{}} }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
fn view_teacher_slot(
|
||||
&self,
|
||||
ctx: &Context<Self>,
|
||||
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! {<button class={format!("button is-fullwidth mb-2 {color}")} disabled={disabled} {onclick}>{text}</button>}
|
||||
}
|
||||
}
|
||||
|
||||
|
55
terminwahl_front/src/requests.rs
Normal file
55
terminwahl_front/src/requests.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use gloo::net::http::{Method, Request};
|
||||
use terminwahl_typen::{PlannedAppointment, RequestState};
|
||||
|
||||
use crate::Msg;
|
||||
|
||||
pub async fn fetch_teachers() -> Result<Msg, Msg> {
|
||||
// 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<Msg, Msg> {
|
||||
// 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<Msg, Msg> {
|
||||
// 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<PlannedAppointment>) -> Result<Msg, Msg> {
|
||||
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))
|
||||
}
|
4
terminwahl_front/static/.gitignore
vendored
Normal file
4
terminwahl_front/static/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
bulma.sass
|
||||
sass/
|
||||
webfonts/
|
||||
scss/
|
BIN
terminwahl_front/static/logoheader.png
Normal file
BIN
terminwahl_front/static/logoheader.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
23
terminwahl_front/static/my_bulma_colors.scss
Normal file
23
terminwahl_front/static/my_bulma_colors.scss
Normal file
@ -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";
|
12
terminwahl_typen/Cargo.toml
Normal file
12
terminwahl_typen/Cargo.toml
Normal file
@ -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}
|
@ -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<IdType, AppointmentSlot>;
|
||||
|
||||
@ -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,
|
||||
}
|
Loading…
Reference in New Issue
Block a user