Compare commits
10 Commits
6b7415f0dd
...
48b8ff7bd7
Author | SHA1 | Date | |
---|---|---|---|
48b8ff7bd7 | |||
ca427153d9 | |||
a3e4a77b91 | |||
d7b5694188 | |||
c0efdf8b49 | |||
b067be722d | |||
165d68ca50 | |||
6ea404845a | |||
f79cbc36c4 | |||
1cb3090f64 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
/*/target
|
||||
/target
|
||||
terminwahl_front/dist
|
||||
|
||||
db.sqlite*
|
||||
db.sqlite*
|
||||
|
2752
Cargo.lock
generated
2752
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
81
README.md
Normal 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).
|
18
install.sh
18
install.sh
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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;
|
@ -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)))
|
||||
}
|
||||
|
55
terminwahl_back/src/api/errors_alt.rs
Normal file
55
terminwahl_back/src/api/errors_alt.rs
Normal 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))
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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"))
|
||||
|
@ -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!({
|
||||
|
@ -16,7 +16,7 @@
|
||||
Elternsprechtag
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
Am 28.02.23
|
||||
Am 06.03.24
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -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 }
|
||||
|
@ -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>
|
@ -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">
|
||||
|
@ -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()
|
||||
|
9
terminwahl_front/static/.gitignore
vendored
9
terminwahl_front/static/.gitignore
vendored
@ -1,4 +1,7 @@
|
||||
bulma.sass
|
||||
sass/
|
||||
webfonts/
|
||||
scss/
|
||||
sass
|
||||
webfonts
|
||||
scss
|
||||
*.zip
|
||||
bulma
|
||||
fontawesome*
|
@ -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 }
|
||||
|
@ -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>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user