Compare commits

...

10 Commits

22 changed files with 2418 additions and 914 deletions

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
/*/target
/target
terminwahl_front/dist
db.sqlite*
db.sqlite*

2752
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
[workspace]
members = ["terminwahl_back", "terminwahl_front"]
workspace.resolver = "2"
[workspace.dependencies]
serde = {version="1.0", features = ["derive"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = {version="*", features = ["serde"]}
chrono = { version = "*", features = ["serde"] }

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# Project Name
## Description
This project is a webpage that simplifies management of parentsday at our school.
## Installation
To install this project, follow these steps:
1. TODO
## Setup
to insert teachers use:
```sql
insert into teachers (ansprache, last_name, subject_id) values ('Frau', 'Bücher', 2);
insert into teachers (ansprache, last_name, subject_id) values ('Frau', 'Klemm', 2);
insert into teachers (ansprache, last_name, subject_id) values ('Frau', 'Vietzen/Pfab', 2);
insert into teachers (ansprache, last_name, subject_id) values ('Frau', 'Bärtle', 2);
insert into teachers (ansprache, last_name, subject_id) values ('Frau', 'Wörner', 2);
```
to make a teacher available in the current year:
```sql
insert into teacher_dates values (1,3);
insert into teacher_dates values (2,3);
insert into teacher_dates values (3,3);
insert into teacher_dates values (4,3);
insert into teacher_dates values (5,3);
insert into teacher_dates values (7,3);
insert into teacher_dates values (12,3);
insert into teacher_dates values (8,3);
insert into teacher_dates values (9,3);
insert into teacher_dates values (10,3);
insert into teacher_dates values (11,3);
insert into teacher_dates values (13,3);
insert into teacher_dates values (14,3);
insert into teacher_dates values (15,3);
insert into teacher_dates values (16,3);
```
to generate slots use:
```sql
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 14:00:00', '2023-02-28 14:15:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 14:15:00', '2023-02-28 14:30:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 14:30:00', '2023-02-28 14:45:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 14:45:00', '2023-02-28 15:00:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 15:15:00', '2023-02-28 15:30:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 15:30:00', '2023-02-28 15:45:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 15:45:00', '2023-02-28 16:00:00', 3);
insert into appointment_slots (start_time, end_time, date_id) values ('2023-02-28 16:00:00', '2023-02-28 16:15:00', 3);
```
## Usage
To use this project, follow these steps:
1. ...
2. ...
3. ...
## Contributing
If you would like to contribute to this project, please follow these guidelines:
1. Fork the repository.
2. Create a new branch.
3. ...
4. Submit a pull request.
## License
This project is licensed under the [MIT License](LICENSE).
## Contact
For any questions or inquiries, please contact us at [franz.dietrich@uhlandshoehe.de](mailto:franz.dietrich@uhlandshoehe.de).

View File

@ -1,9 +1,21 @@
#!/bin/bash
if [ ! -f terminwahl_front/static/bulma.sass ]; then
pushd terminwahl_front/static/ || exit
wget https://github.com/jgthms/bulma/releases/download/0.9.4/bulma-0.9.4.zip
unzip bulma-0.9.4.zip
ln -s bulma/sass/ sass
ln -s bulma/bulma.sass bulma.sass
wget https://use.fontawesome.com/releases/v6.3.0/fontawesome-free-6.3.0-web.zip
unzip fontawesome-free-6.3.0-web.zip
ln -s fontawesome-free-6.3.0-web/scss/ scss
ln -s fontawesome-free-6.3.0-web/webfonts webfonts
popd || exit
fi
systemctl stop Terminwahl
cp -v target/release/terminwahl_back /usr/local/bin/terminwahl_back && chown terminwahl.terminwahl /usr/local/bin/terminwahl_back
cp -v target/release/terminwahl_back /usr/local/bin/terminwahl_back && chown terminwahl:terminwahl /usr/local/bin/terminwahl_back
echo You may want to copy the database
echo cp -v terminwahl_back/db.sqlite /var/local/terminwahl/db.sqlite && chown terminwahl.terminwahl /var/local/terminwahl/db.sqlite
echo cp -v terminwahl_back/db.sqlite /var/local/terminwahl/db.sqlite && chown terminwahl:terminwahl /var/local/terminwahl/db.sqlite
rsync -va --delete terminwahl_back/templates/ /var/local/terminwahl/templates/
rsync -va --delete terminwahl_front/dist/ /var/local/terminwahl/dist/
systemctl start Terminwahl
systemctl start Terminwahl

View File

@ -1,31 +1,41 @@
[package]
name = "terminwahl_back"
version = "0.1.0"
edition = "2021"
edition = "2024"
default-run = "terminwahl_back"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "*"
actix-web = "4.3"
actix-web = "4.5"
actix-rt = "2.8"
actix-files = "0.6.2"
actix-session = { version = "0.7", features = ["cookie-session"] }
actix-session = { version = "0.9", features = ["cookie-session"] }
# sqlx is currently on version 0.3.5 in this project due to breaking changes introduced in versions
# beyond 0.4.0, which changed the return type of 'exectute' to a 'Done'. Also the row parsing related
# 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"] }
sqlx = { version = "0.7", features = [
"sqlite",
"runtime-tokio-rustls",
"chrono",
] }
uuid = { version = "1.2", features = ["serde", "v4"] }
dotenv = "*"
env_logger = "0.10"
env_logger = "0.11"
log = "*"
lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]}
lettre = { version = "0.11", default-features = false, features = [
"smtp-transport",
"tokio1-rustls-tls",
"hostname",
"builder",
"pool",
] }
rand = "*"
handlebars = {version="4.3", features=["dir_source"]}
handlebars = { version = "5.1", features = ["dir_source"] }
glob = "*"
terminwahl_typen={path="../terminwahl_typen/"}
serde = {workspace = true}
serde_json={workspace=true}
chrono={workspace=true}
terminwahl_typen = { path = "../terminwahl_typen/" }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }

View File

@ -0,0 +1,49 @@
-- Add migration script here
CREATE TABLE date (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
subtitle TEXT NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL
);
INSERT INTO date (id, name, subtitle, start_time, end_time)
VALUES (
1,
'OS 2023',
'2023',
'2023-02-28 12:00:00',
'2023-02-28 18:00:00'
);
-- Create a temporary table
CREATE TEMPORARY TABLE temp_appointment_slots AS
SELECT *
FROM appointment_slots;
DROP TABLE appointment_slots;
-- Recreate the appointments table with the new column and foreign key constraint
CREATE TABLE appointment_slots (
id INTEGER PRIMARY KEY,
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
date_id INTEGER DEFAULT 1 Not NULL,
-- Add the new column
FOREIGN KEY (date_id) REFERENCES date(id) -- Add the foreign key constraint
);
-- Insert data back into the new appointments table from the temporary table
INSERT INTO appointment_slots
SELECT *,
1
FROM temp_appointment_slots;
-- Drop the temporary table
DROP TABLE temp_appointment_slots;
CREATE TABLE teacher_dates (
teacher_id INTEGER NOT NULL,
date_id INTEGER NOT NULL,
PRIMARY KEY (teacher_id, date_id),
FOREIGN KEY (teacher_id) REFERENCES teachers(id),
FOREIGN KEY (date_id) REFERENCES date(id)
);
INSERT INTO teacher_dates (teacher_id, date_id)
SELECT teachers.id AS teacher_id,
date.id AS date_id
FROM teachers,
date;

View File

@ -1,26 +1,32 @@
use actix_files::NamedFile;
use actix_web::{dev, middleware::ErrorHandlerResponse, Result};
use actix_web::{dev, middleware::ErrorHandlerResponse, Responder as _, Result};
pub fn bad_request<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
let new_resp = NamedFile::open("static/errors/400.html")?
.set_status_code(res.status())
.into_response(res.request())
.customize()
.with_status(actix_web::http::StatusCode::OK)
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}
pub fn not_found<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
let new_resp = NamedFile::open("static/errors/404.html")?
.set_status_code(res.status())
.into_response(res.request())
.customize()
.with_status(actix_web::http::StatusCode::OK)
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}
pub fn internal_server_error<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
let new_resp = NamedFile::open("static/errors/500.html")?
.set_status_code(res.status())
.into_response(res.request())
.customize()
.with_status(actix_web::http::StatusCode::OK)
.respond_to(res.request())
.map_into_boxed_body()
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(res.into_response(new_resp)))
}

View File

@ -0,0 +1,55 @@
use std::io::Read as _;
use actix_files::NamedFile;
use actix_web::{
body::MessageBody,
dev::{self, ServiceResponse},
middleware::ErrorHandlerResponse,
Responder, Result,
};
pub fn bad_request<B: MessageBody>(
mut res: dev::ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> {
let mut new_resp = NamedFile::open("static/errors/400.html")?;
let mut body = String::new();
new_resp.read_to_string(&mut body);
res.response_mut().set_body(body);
/* let (req, res) = res.into_parts();
res.set_body(body);
let response = ServiceResponse::new(req, res)
.map_into_boxed_body()
.map_into_right_body(); */
Ok(ErrorHandlerResponse::Response(
res.map_into_boxed_body().map_into_right_body(),
))
}
pub fn not_found<B>(res: dev::ServiceResponse<B>) -> Result<impl Responder> {
let new_resp = NamedFile::open("static/errors/404.html")?
.customize()
.with_status(res.status())
.respond_to(res.request());
Ok(new_resp)
}
pub fn internal_server_error<B: MessageBody>(
res: dev::ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> {
let mut new_resp = NamedFile::open("static/errors/500.html")?;
let mut body = String::new();
new_resp.read_to_string(&mut body);
let (req, res) = res.into_parts();
res.set_body(body);
let response = ServiceResponse::new(req, res)
.map_into_boxed_body()
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(response))
}

View File

@ -1,9 +1,14 @@
use actix_web::{error, web, Error, HttpResponse};
use terminwahl_typen::IdType;
use crate::db::{self, Pool};
pub async fn get_teachers_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
let tasks = db::read::get_teachers(&pool)
pub async fn get_teachers_json(
pool: web::Data<Pool>,
path: web::Path<IdType>,
) -> Result<HttpResponse, Error> {
let date_id = path.into_inner();
let tasks = db::read::get_teachers(&pool, date_id)
.await
.map_err(error::ErrorInternalServerError)?;
@ -17,8 +22,12 @@ pub async fn get_subjects_json(pool: web::Data<Pool>) -> Result<HttpResponse, Er
Ok(HttpResponse::Ok().json(tasks))
}
pub async fn get_slots_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
let tasks = db::read::get_slots(&pool)
pub async fn get_slots_json(
pool: web::Data<Pool>,
path: web::Path<IdType>,
) -> Result<HttpResponse, Error> {
let date_id = path.into_inner();
let tasks = db::read::get_slots(&pool, date_id)
.await
.map_err(error::ErrorInternalServerError)?;
@ -31,3 +40,11 @@ pub async fn get_unavailable_json(pool: web::Data<Pool>) -> Result<HttpResponse,
Ok(HttpResponse::Ok().json(tasks))
}
pub async fn get_dates_json(pool: web::Data<Pool>) -> Result<HttpResponse, Error> {
let dates = db::read::get_dates(&pool)
.await
.map_err(error::ErrorInternalServerError)?;
Ok(HttpResponse::Ok().json(dates))
}

View File

@ -7,7 +7,7 @@ use lettre::{
message::header::ContentType, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
};
use log::debug;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::{distr::Alphanumeric, rng, Rng};
use serde::Serialize;
use serde_json::json;
use terminwahl_typen::{AppointmentSlot, Nutzer, PlannedAppointment, RequestState, Teacher};
@ -48,7 +48,7 @@ pub async fn save_appointments_json(
.await
.map_err(error::ErrorInternalServerError)?;
debug!("Saving appointments");
let validation_key: String = thread_rng()
let validation_key: String = rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)

View File

@ -5,17 +5,23 @@ use serde::{Deserialize, Serialize};
use sqlx::query_as;
use terminwahl_typen::{
AppointmentSlot, AppointmentSlots, IdType, Nutzer, SlotId, Subject, Subjects, Teacher, Teachers,
AppointmentSlot, AppointmentSlots, Date, Dates, IdType, Nutzer, SlotId, Subject, Subjects,
Teacher, Teachers,
};
use super::Pool;
pub async fn get_teachers(db: &Pool) -> Result<Teachers, sqlx::Error> {
pub async fn get_teachers(db: &Pool, date_id: IdType) -> Result<Teachers, sqlx::Error> {
query_as!(
Teacher,
r#"
SELECT *
FROM `teachers`"#,
SELECT
id,
ansprache,
last_name,
subject_id
FROM `teachers` JOIN teacher_dates ON teachers.id = teacher_dates.teacher_id where date_id = ?"#,
date_id
)
.fetch_all(db)
.await
@ -44,12 +50,13 @@ pub async fn get_subjects(db: &Pool) -> Result<Subjects, sqlx::Error> {
.await
}
pub async fn get_slots(db: &Pool) -> Result<AppointmentSlots, sqlx::Error> {
pub async fn get_slots(db: &Pool, date_id: IdType) -> Result<AppointmentSlots, sqlx::Error> {
match query_as!(
AppointmentSlot,
r#"
SELECT *
FROM `appointment_slots`"#,
SELECT id, start_time, end_time, date_id
FROM `appointment_slots` where date_id = ?"#,
date_id
)
.fetch_all(db)
.await
@ -87,7 +94,31 @@ pub async fn get_unavailable(db: &Pool) -> Result<HashSet<SlotId>, sqlx::Error>
Err(e) => Err(e),
}
}
pub async fn get_dates(db: &Pool) -> Result<Dates, sqlx::Error> {
match query_as!(
Date,
r#"
SELECT *
FROM `date` WHERE end_time > datetime('now', '-14 days');"#,
)
.fetch_all(db)
.await
{
Ok(elems) => Ok(elems.into_iter().collect()),
Err(e) => Err(e),
}
}
pub async fn get_date(db: &Pool, date_id: IdType) -> Result<Date, sqlx::Error> {
query_as!(
Date,
r#"
SELECT *
FROM `date` WHERE id = ?"#,
date_id
)
.fetch_one(db)
.await
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TeacherWithAppointments {
teacher: Teacher,
@ -109,8 +140,11 @@ pub struct AssignedAppointment {
nutzer: Nutzer,
}
pub async fn get_all_teachers(db: &Pool) -> Result<Vec<TeacherWithAppointments>, sqlx::Error> {
let teachers = get_teachers(db).await?;
pub async fn get_all_teachers(
db: &Pool,
date_id: IdType,
) -> Result<Vec<TeacherWithAppointments>, sqlx::Error> {
let teachers = get_teachers(db, date_id).await?;
let mut response = Vec::new();
for teacher in teachers.into_iter() {

View File

@ -7,7 +7,7 @@ use actix_web::{
web, App, HttpServer,
};
use dotenv::dotenv;
use handlebars::Handlebars;
use handlebars::{DirectorySourceOptions, Handlebars};
use lettre::{transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor};
use log::debug;
use std::env;
@ -41,8 +41,12 @@ async fn main() -> std::io::Result<()> {
let mut handlebars = Handlebars::new();
handlebars.register_helper("time_of", Box::new(TimeOfDate));
let handlebars_source = DirectorySourceOptions {
tpl_extension: ".hbs".to_string(),
..Default::default()
};
handlebars
.register_templates_directory(".hbs", handlebars_templates)
.register_templates_directory(handlebars_templates, handlebars_source)
.unwrap();
log::info!("starting HTTP server at http://localhost:8080");
@ -69,13 +73,18 @@ async fn main() -> std::io::Result<()> {
.wrap(Logger::default())
.wrap(session_store)
.wrap(error_handlers)
.service(web::resource("/get/dates").route(web::get().to(api::read::get_dates_json)))
.service(
web::resource("/get/teachers").route(web::get().to(api::read::get_teachers_json)),
web::resource("/get/teachers/{date_key}")
.route(web::get().to(api::read::get_teachers_json)),
)
.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/slots/{date_key}")
.route(web::get().to(api::read::get_slots_json)),
)
.service(
web::resource("/get/unavailable")
.route(web::get().to(api::read::get_unavailable_json)),
@ -93,7 +102,7 @@ async fn main() -> std::io::Result<()> {
.route(web::get().to(views::delete_appointment)),
)
.service(
web::resource("/export/all/{password}")
web::resource("/export/all/{password}/{id}")
.route(web::get().to(views::export_appointments)),
)
.service(Files::new("/", wasm_statics.clone()).index_file("index.html"))

View File

@ -60,12 +60,12 @@ pub async fn export_appointments(
_mailer: web::Data<AsyncSmtpTransport<Tokio1Executor>>,
handlebars: web::Data<Handlebars<'_>>,
css: web::Data<CssPath>,
path: web::Path<String>,
path: web::Path<(String, IdType)>,
) -> Result<HttpResponse, error::Error> {
let password = path.into_inner();
let (password, date_id) = path.into_inner();
dbg!(&password);
if password == "AllExport1517" {
match get_all_teachers(&pool).await {
match get_all_teachers(&pool, date_id).await {
Ok(teachers) => {
dbg!(&teachers);
let data = json!({

View File

@ -16,7 +16,7 @@
Elternsprechtag
</p>
<p class="subtitle">
Am 28.02.23
Am 06.03.24
</p>
</div>
</section>

View File

@ -1,17 +1,17 @@
[package]
name = "terminwahl_front"
version = "0.1.0"
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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}
gloo = "*"
js-sys = "*"
web-sys = "*"
terminwahl_typen = { path = "../terminwahl_typen/" }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Yew App</title>
<title>Elternsprechtag</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" />
@ -12,4 +12,6 @@
<meta name="description" content="Termine buchen für den Lehrersprechtag der Waldorfschule Uhlandshöhe" />
</head>
<body></body>
</html>

View File

@ -2,10 +2,10 @@ mod requests;
use std::collections::{HashMap, HashSet};
use gloo::console::log;
use requests::{fetch_slots, fetch_teachers, fetch_unavailable, send_appointments};
use requests::{fetch_dates, fetch_slots, fetch_teachers, fetch_unavailable, send_appointments};
use terminwahl_typen::{
AppointmentSlot, AppointmentSlots, IdType, Nutzer, PlannedAppointment, RequestState, SlotId,
Teacher, Teachers,
AppointmentSlot, AppointmentSlots, Date, Dates, IdType, Nutzer, PlannedAppointment,
RequestState, SlotId, Teacher, Teachers,
};
use web_sys::HtmlInputElement;
use yew::prelude::*;
@ -16,9 +16,12 @@ pub enum Msg {
UpdateSchüler(String),
UpdateEmail(String),
DataEntered(Nutzer),
GetTeachers,
GetTeachers(IdType),
ReceivedTeachers(Teachers),
GetSlots,
GetDates,
ReceivedDates(Dates),
SelectDate(IdType),
GetSlots(IdType),
ReceivedSlots(AppointmentSlots),
Selected(PlannedAppointment),
TooMany,
@ -30,6 +33,8 @@ pub enum Msg {
pub struct App {
nutzer: Option<Nutzer>,
tmp_nutzer: Nutzer,
dates: Option<Dates>,
selected_date: Option<Date>,
teachers: Option<Teachers>,
slots: Option<AppointmentSlots>,
appointments: HashMap<SlotId, PlannedAppointment>,
@ -54,6 +59,8 @@ impl Component for App {
let app = Self {
appointments: HashMap::new(),
slots: None,
dates: None,
selected_date: None,
unavailable: None,
teachers: None,
nutzer: None,
@ -64,8 +71,8 @@ impl Component for App {
},
successfully_saved: None,
};
ctx.link().send_message(Msg::GetTeachers);
ctx.link().send_message(Msg::GetSlots);
ctx.link().send_message(Msg::GetDates);
app
}
@ -85,16 +92,38 @@ impl Component for App {
true
}
Msg::TooMany => todo!(),
Msg::GetTeachers => {
ctx.link().send_future(fetch_teachers());
Msg::GetTeachers(id) => {
ctx.link().send_future(fetch_teachers(id));
false
}
Msg::ReceivedTeachers(teachers) => {
self.teachers = Some(teachers);
true
}
Msg::GetSlots => {
ctx.link().send_future(fetch_slots());
Msg::GetDates => {
ctx.link().send_future(fetch_dates());
false
}
Msg::ReceivedDates(dates) => {
if dates.len() == 1 {
ctx.link()
.send_message(Msg::SelectDate(dates.first().unwrap().id))
}
self.dates = Some(dates);
true
}
Msg::SelectDate(date_id) => {
ctx.link().send_message(Msg::GetTeachers(date_id));
ctx.link().send_message(Msg::GetSlots(date_id));
let date = self
.dates
.as_ref()
.map(|dts| dts.iter().find(|d| d.id == date_id).unwrap());
self.selected_date = Some(date.expect("A date should be found").clone());
true
}
Msg::GetSlots(id) => {
ctx.link().send_future(fetch_slots(id));
ctx.link().send_future(fetch_unavailable());
false
}
@ -150,24 +179,45 @@ impl Component for App {
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>
{if let Some(d) = self.selected_date.as_ref(){ html!{
<><p class="title has-text-link">
{&d.name}
</p><p class="subtitle">
{&d.subtitle}
</p><p class="subtitle">
{"Am "}{d.start_time.format("%d.%m.%Y")}
</p></>}}else{html!(<p class="title has-text-link">{"Elternsprechtag"}</p>)}}
</div>
</section>
<div class="container">
<div class="section">
{
if let Some(_saved) = self.successfully_saved.as_ref(){self.view_dank_dialog(ctx)} else if self.nutzer.is_none(){
self.view_eingabe_daten(ctx)
}
else
{
self.view_auswahl_termine(ctx)
}
if let Some(dates) = self.dates.as_ref(){
if let Some(date) = self.selected_date.as_ref(){
if date.end_time < (chrono::Local::now() + chrono::Duration::days(4)).naive_local(){
html!(
<div class="columns is-centered">
<div class="column is-half">
<div class="notification is-light">
<h1>{"Die Anmeldung ist bereits geschlossen."}</h1>
</div>
</div>
</div>
)
} else if let Some(_saved) = self.successfully_saved.as_ref(){
self.view_dank_dialog(ctx)
} else if self.nutzer.is_none(){
self.view_eingabe_daten(ctx)
}
else
{
self.view_auswahl_termine(ctx)
}
} else {
self.view_auswahl_date(dates, ctx)
}
}else{html!(<h1>{"Loading"}</h1>)}
}
</div>
</div>
@ -178,6 +228,35 @@ impl Component for App {
}
impl App {
fn view_auswahl_date(&self, dates: &Dates, ctx: &Context<Self>) -> Html {
let onchange = ctx.link().callback(|e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
Msg::SelectDate(input.value().parse().unwrap())
});
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 wählen Sie den Sprechtag zu welchem Sie sich anmelden möchten:"}</p>
</div>
<div class="select is-rounded is-fullwidth">
<select onchange={onchange}>
<option selected=true value="--">{"Bitte wählen Sie einen Termin"}</option>
{dates.iter().map(|dt|html!{
<option value={dt.id.to_string()}>{&dt.name}{" "}{&dt.start_time.format("%d.%m.%Y")}</option>
}).collect::<Html>()}
</select>
</div>
</div>
</div>
</div>
}
}
fn view_eingabe_daten(&self, ctx: &Context<Self>) -> Html {
html! { <div class="columns is-centered">
<div class="column is-half">
@ -186,7 +265,7 @@ impl App {
<img src="/logoheader.png" />
</figure>
<div class="box mt-3 is-light">
<p>{"Anmeldung zum Elternsprechtag!"}</p><p>{"Bitte geben Sie unbedingt eine gültige E-Mail-Adresse an,
<p>{"Anmeldung zum "} {&self.dates.as_ref().expect("termin").first().expect("termin").name}{" am "}{&self.dates.as_ref().expect("termin").first().expect("termin").start_time.format("%d.%m.%Y")}{"!"}</p><p>{"Bitte geben Sie unbedingt eine gültige E-Mail-Adresse an,
da die Termine erst nach Bestätigung über den per E-Mail zugesandten Link gebucht werden."}</p>
</div>
<div class="field">

View File

@ -1,11 +1,23 @@
use gloo::net::http::{Method, Request};
use terminwahl_typen::{Nutzer, PlannedAppointment, RequestState};
use gloo::net::http::Request;
use terminwahl_typen::{IdType, Nutzer, PlannedAppointment, RequestState};
use crate::Msg;
pub async fn fetch_teachers() -> Result<Msg, Msg> {
pub async fn fetch_dates() -> Result<Msg, Msg> {
// Send the request to the specified URL.
let response = Request::new("/get/teachers").send().await;
let response = Request::get("/get/dates").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::ReceivedDates(response))
}
pub async fn fetch_teachers(id: IdType) -> Result<Msg, Msg> {
// Send the request to the specified URL.
let response = Request::get(&format!("/get/teachers/{}", id)).send().await;
// Return the ZuordnungMessage with the given network object and the response.
let response = response
.map_err(|_| Msg::AppointmentsSent(RequestState::Error))?
@ -15,9 +27,9 @@ pub async fn fetch_teachers() -> Result<Msg, Msg> {
Ok(Msg::ReceivedTeachers(response))
}
pub async fn fetch_slots() -> Result<Msg, Msg> {
pub async fn fetch_slots(id: IdType) -> Result<Msg, Msg> {
// Send the request to the specified URL.
let response = Request::new("/get/slots").send().await;
let response = Request::get(&format!("/get/slots/{}", id)).send().await;
// Return the ZuordnungMessage with the given network object and the response.
let response = response
.map_err(|_| Msg::AppointmentsSent(RequestState::Error))?
@ -29,7 +41,7 @@ pub async fn fetch_slots() -> Result<Msg, Msg> {
pub async fn fetch_unavailable() -> Result<Msg, Msg> {
// Send the request to the specified URL.
let response = Request::new("/get/unavailable").send().await;
let response = Request::get("/get/unavailable").send().await;
// Return the ZuordnungMessage with the given network object and the response.
let response = response
.map_err(|_| Msg::AppointmentsSent(RequestState::Error))?
@ -43,8 +55,7 @@ pub async fn send_appointments(
appointments: Vec<PlannedAppointment>,
nutzer: Nutzer,
) -> Result<Msg, Msg> {
let response = Request::new("/send/appointments")
.method(Method::POST)
let response = Request::post("/send/appointments")
.json(&(&appointments, &nutzer))
.map_err(|_| Msg::AppointmentsSent(RequestState::Error))?
.send()

View File

@ -1,4 +1,7 @@
bulma.sass
sass/
webfonts/
scss/
sass
webfonts
scss
*.zip
bulma
fontawesome*

View File

@ -1,12 +1,12 @@
[package]
name = "terminwahl_typen"
version = "0.1.0"
edition = "2021"
edition = "2024"
# 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}
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }

View File

@ -41,6 +41,7 @@ pub struct AppointmentSlot {
pub id: IdType,
pub start_time: NaiveDateTime,
pub end_time: NaiveDateTime,
pub date_id: IdType,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
@ -84,3 +85,14 @@ impl Nutzer {
!self.name.is_empty() && !self.email.is_empty() && !self.schueler.is_empty()
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Date {
pub id: IdType,
pub name: String,
pub subtitle: String,
pub start_time: NaiveDateTime,
pub end_time: NaiveDateTime,
}
pub type Dates = Vec<Date>;