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"
|
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
|
||||||
/target
|
/target
|
||||||
terminwahl_front/dist
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
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]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.31"
|
version = "0.8.31"
|
||||||
@ -741,6 +757,15 @@ version = "2.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.25"
|
version = "1.0.25"
|
||||||
@ -1152,6 +1177,17 @@ dependencies = [
|
|||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
@ -1319,6 +1355,34 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
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]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.139"
|
version = "0.2.139"
|
||||||
@ -1388,6 +1452,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "match_cfg"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -1684,6 +1754,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -2109,10 +2185,12 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"terminwahl_typen",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2120,9 +2198,25 @@ dependencies = [
|
|||||||
name = "terminwahl_front"
|
name = "terminwahl_front"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"gloo",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"terminwahl_typen",
|
||||||
|
"web-sys",
|
||||||
"yew",
|
"yew",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminwahl_typen"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["terminwahl_back", "terminwahl_front"]
|
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
|
# 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.
|
# newer version of sqlx, and the version will be updated in the future.
|
||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-actix-rustls", "chrono"] }
|
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"] }
|
uuid = { version = "1.2", features = ["serde", "v4"] }
|
||||||
dotenv = "*"
|
dotenv = "*"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
log = "*"
|
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 errors;
|
||||||
pub mod read;
|
pub mod read;
|
||||||
|
pub mod write;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use actix_web::{error, web, Error, HttpResponse};
|
use actix_web::{error, web, Error, HttpResponse};
|
||||||
|
|
||||||
use crate::db::{self, Pool};
|
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))
|
Ok(HttpResponse::Ok().json(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_free_slots_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
|
pub async fn get_slots_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
|
||||||
let tasks = db::read::get_free_slots(&pool)
|
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
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.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 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;
|
use super::Pool;
|
||||||
|
|
||||||
@ -11,7 +15,7 @@ pub async fn get_teachers(db: &Pool) -> Result<Teachers, sqlx::Error> {
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM `teachers`"#,
|
FROM `teachers`"#,
|
||||||
)
|
)
|
||||||
.fetch_all(&*db)
|
.fetch_all(db)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,21 +26,35 @@ pub async fn get_subjects(db: &Pool) -> Result<Subjects, sqlx::Error> {
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM `subjects`"#,
|
FROM `subjects`"#,
|
||||||
)
|
)
|
||||||
.fetch_all(&*db)
|
.fetch_all(db)
|
||||||
.await
|
.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!(
|
match query_as!(
|
||||||
AppointmentSlot,
|
AppointmentSlot,
|
||||||
r#"
|
r#"
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM `appointment_slots`"#,
|
FROM `appointment_slots`"#,
|
||||||
)
|
)
|
||||||
.fetch_all(&*db)
|
.fetch_all(db)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(elems) => Ok(elems.into_iter().map(|e| (e.id, e)).collect()),
|
Ok(elems) => Ok(elems.into_iter().map(|e| (e.id, e)).collect()),
|
||||||
Err(e) => Err(e),
|
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 api;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod types;
|
|
||||||
|
@ -7,10 +7,8 @@ use actix_web::{
|
|||||||
web, App, HttpServer,
|
web, App, HttpServer,
|
||||||
};
|
};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use std::{
|
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
|
||||||
env,
|
use std::env;
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
use terminwahl_back::{api, db};
|
use terminwahl_back::{api, db};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@ -22,6 +20,15 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let pool = db::init_pool(&database_url)
|
let pool = db::init_pool(&database_url)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create pool");
|
.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");
|
log::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
@ -40,6 +47,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(pool.clone()))
|
.app_data(web::Data::new(pool.clone()))
|
||||||
|
.app_data(web::Data::new(smtp_pool.clone()))
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(session_store)
|
.wrap(session_store)
|
||||||
.wrap(error_handlers)
|
.wrap(error_handlers)
|
||||||
@ -49,9 +57,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(
|
.service(
|
||||||
web::resource("/get/subjects").route(web::get().to(api::read::get_subjects_json)),
|
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(
|
.service(
|
||||||
web::resource("/get/free_slots")
|
web::resource("/get/unavailable")
|
||||||
.route(web::get().to(api::read::get_free_slots_json)),
|
.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"))
|
.service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html"))
|
||||||
})
|
})
|
||||||
|
@ -8,3 +8,10 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
yew = { version = "0.20.0", features = ["csr"] }
|
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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Yew App</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
</html>
|
</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::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[function_component]
|
// Define the possible messages which can be sent to the component
|
||||||
fn App() -> Html {
|
pub enum Msg {
|
||||||
let counter = use_state(|| 5);
|
UpdateName(String),
|
||||||
let onclick = {
|
UpdateSchüler(String),
|
||||||
let counter = counter.clone();
|
UpdateEmail(String),
|
||||||
move |_| {
|
DataEntered(Nutzer),
|
||||||
let value = *counter + 1;
|
GetTeachers,
|
||||||
counter.set(value);
|
ReceivedTeachers(Teachers),
|
||||||
}
|
GetSlots,
|
||||||
};
|
ReceivedSlots(AppointmentSlots),
|
||||||
|
Selected(PlannedAppointment),
|
||||||
|
TooMany,
|
||||||
|
SendToServer,
|
||||||
|
AppointmentsSent(RequestState),
|
||||||
|
ReceivedUnavailable(HashSet<SlotId>),
|
||||||
|
}
|
||||||
|
|
||||||
html! {
|
pub struct App {
|
||||||
<div>
|
nutzer: Option<Nutzer>,
|
||||||
<button {onclick}>{ "+1" }</button>
|
tmp_nutzer: Nutzer,
|
||||||
<p>{ *counter }</p>
|
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>
|
||||||
|
<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 std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Local, NaiveDateTime};
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub type IdType = i64;
|
pub type IdType = i64;
|
||||||
@ -28,6 +28,11 @@ pub struct Appointment {
|
|||||||
pub teacher_id: IdType,
|
pub teacher_id: IdType,
|
||||||
pub slot_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>;
|
pub type AppointmentSlots = HashMap<IdType, AppointmentSlot>;
|
||||||
|
|
||||||
@ -37,3 +42,32 @@ pub struct AppointmentSlot {
|
|||||||
pub start_time: NaiveDateTime,
|
pub start_time: NaiveDateTime,
|
||||||
pub end_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