Funktionierende zu und absage.

This commit is contained in:
Franz Dietrich 2023-02-02 12:56:42 +01:00
parent 87c0a2d541
commit 43d08a014b
Signed by: dietrich
GPG Key ID: F0CE5A20AB5C4B27
9 changed files with 200 additions and 48 deletions

View File

@ -8,14 +8,11 @@ use log::debug;
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use terminwahl_typen::{ use terminwahl_typen::{AppointmentSlot, Nutzer, PlannedAppointment, RequestState, Teacher};
AppointmentSlot, IdType, Nutzer, PlannedAppointment, RequestState, Teacher,
};
use crate::db::{ use crate::db::{
self, self,
read::{get_slot_by_id, get_teacher_by_id}, read::{get_slot_by_id, get_teacher_by_id},
write::confirm_appointments,
Pool, Pool,
}; };
@ -26,7 +23,7 @@ pub struct FullAppointment {
} }
impl FullAppointment { impl FullAppointment {
async fn get(pool: &Pool, teacher_id: i64, slot_id: i64) -> Result<Self, Box<dyn Error>> { pub async fn get(pool: &Pool, teacher_id: i64, slot_id: i64) -> Result<Self, Box<dyn Error>> {
Ok(Self { Ok(Self {
teacher: get_teacher_by_id(pool, teacher_id) teacher: get_teacher_by_id(pool, teacher_id)
.await .await

View File

@ -76,7 +76,7 @@ pub async fn get_unavailable(db: &Pool) -> Result<HashSet<SlotId>, sqlx::Error>
SlotId, SlotId,
r#" r#"
SELECT teacher_id, slot_id SELECT teacher_id, slot_id
FROM `appointments` WHERE datetime(expires) > datetime('now');"#, FROM `appointments` WHERE datetime(expires) > datetime('now') OR expires is NULL;"#,
) )
.fetch_all(db) .fetch_all(db)
.await .await

View File

@ -1,7 +1,10 @@
use chrono::{Duration, Local}; use chrono::{Duration, Local};
use sqlx::query; use futures::future;
use sqlx::{query, query_as};
use terminwahl_typen::{IdType, Nutzer, PlannedAppointment}; use terminwahl_typen::{IdType, Nutzer, PlannedAppointment};
use crate::api::write::FullAppointment;
use super::Pool; use super::Pool;
pub async fn save_appointments( pub async fn save_appointments(
@ -51,12 +54,69 @@ pub async fn save_nutzer(pool: &Pool, nutzer: &Nutzer) -> Result<IdType, sqlx::E
Ok(db_nutzer.id) Ok(db_nutzer.id)
} }
pub async fn confirm_appointments(pool: &Pool, validation_key: &str) -> Result<(), sqlx::Error> { pub async fn confirm_appointments(
pool: &Pool,
validation_key: &str,
) -> Result<Vec<FullAppointment>, sqlx::Error> {
let _ = query!( let _ = query!(
"UPDATE appointments SET expires = NULL WHERE validation_key = ?", "UPDATE appointments SET expires = NULL WHERE validation_key = ?",
validation_key validation_key
) )
.execute(pool) .execute(pool)
.await?; .await?;
Ok(()) let appointments = query_as!(
PlannedAppointment,
"Select teacher_id, slot_id from appointments WHERE validation_key = ? and expires is NULL",
validation_key
)
.fetch_all(pool)
.await?;
let full_appointments = future::try_join_all(appointments.into_iter().map(
|PlannedAppointment {
teacher_id,
slot_id,
}| { FullAppointment::get(pool, teacher_id, slot_id) },
))
.await
.expect("Failed to get full list of appointments");
Ok(full_appointments)
}
pub async fn cancel_appointment(
pool: &Pool,
teacher_id: IdType,
slot_id: IdType,
validation_key: &str,
) -> Result<Vec<FullAppointment>, sqlx::Error> {
let _ = query!(
"DELETE FROM appointments WHERE teacher_id = ? AND slot_id = ? AND validation_key = ?",
teacher_id,
slot_id,
validation_key
)
.execute(pool)
.await?;
// Fetch the remaining appointments
let appointments = query_as!(
PlannedAppointment,
"Select teacher_id, slot_id from appointments WHERE validation_key = ? and expires is NULL",
validation_key
)
.fetch_all(pool)
.await?;
// Fetch the teacher names and times
let full_appointments = future::try_join_all(appointments.into_iter().map(
|PlannedAppointment {
teacher_id,
slot_id,
}| { FullAppointment::get(pool, teacher_id, slot_id) },
))
.await
.expect("Failed to get full list of appointments");
Ok(full_appointments)
} }

View File

@ -2,3 +2,19 @@ pub mod api;
pub mod db; pub mod db;
pub mod handlebars_helper; pub mod handlebars_helper;
pub mod views; pub mod views;
pub struct CssPath {
pub path: String,
}
impl Default for CssPath {
fn default() -> Self {
let css_path = glob::glob("terminwahl_front/dist/my_bulma_colors*.css")
.expect("Failed to find css file")
.next()
.expect("Failed to find file")
.expect("Failed to find file");
let css_file = css_path.file_name().unwrap().to_str().unwrap().to_owned();
Self { path: css_file }
}
}

View File

@ -11,7 +11,7 @@ use handlebars::Handlebars;
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport}; use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
use log::debug; use log::debug;
use std::env; use std::env;
use terminwahl_back::{api, db, handlebars_helper::TimeOfDate, views}; use terminwahl_back::{api, db, handlebars_helper::TimeOfDate, views, CssPath};
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -58,6 +58,7 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
.app_data(web::Data::new(smtp_pool.clone())) .app_data(web::Data::new(smtp_pool.clone()))
.app_data(web::Data::new(handlebars.clone())) .app_data(web::Data::new(handlebars.clone()))
.app_data(web::Data::new(CssPath::default()))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(session_store) .wrap(session_store)
.wrap(error_handlers) .wrap(error_handlers)
@ -80,6 +81,10 @@ async fn main() -> std::io::Result<()> {
web::resource("/confirm/{validation_key}") web::resource("/confirm/{validation_key}")
.route(web::get().to(views::confirm_validation_key)), .route(web::get().to(views::confirm_validation_key)),
) )
.service(
web::resource("/cancel/{teacher_id}/{slot_id}/{validation_key}")
.route(web::get().to(views::delete_appointment)),
)
.service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html")) .service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html"))
}) })
.bind(("127.0.0.1", 8080))? .bind(("127.0.0.1", 8080))?

View File

@ -1,29 +1,54 @@
use actix_web::{error, web, HttpResponse}; use actix_web::{error, web, HttpResponse};
use glob::glob;
use handlebars::Handlebars; use handlebars::Handlebars;
use lettre::SmtpTransport; use lettre::SmtpTransport;
use serde_json::json; use serde_json::json;
use terminwahl_typen::RequestState; use terminwahl_typen::IdType;
use crate::db::{write::confirm_appointments, Pool}; use crate::{
db::{
write::{cancel_appointment, confirm_appointments},
Pool,
},
CssPath,
};
pub async fn confirm_validation_key( pub async fn confirm_validation_key(
pool: web::Data<Pool>, pool: web::Data<Pool>,
mailer: web::Data<SmtpTransport>, _mailer: web::Data<SmtpTransport>,
css: web::Data<CssPath>,
handlebars: web::Data<Handlebars<'_>>, handlebars: web::Data<Handlebars<'_>>,
validation_key: web::Path<String>, validation_key: web::Path<String>,
) -> Result<HttpResponse, error::Error> { ) -> Result<HttpResponse, error::Error> {
let css_path = glob("terminwahl_front/dist/my_bulma_colors*.css")
.expect("Failed to find css file")
.next()
.expect("Failed to find file")
.expect("Failed to find file");
let css_file = css_path.file_name().unwrap().to_str();
let data = json!({
"css_file" : css_file,
});
match confirm_appointments(&pool, &validation_key).await { match confirm_appointments(&pool, &validation_key).await {
Ok(_) => Ok(HttpResponse::Ok().body(handlebars.render("confirmed.html", &data).unwrap())), Ok(appointments) => Ok({
let data = json!({
"css_file" : css.path,
"appointments": appointments,
"validation_key": validation_key.into_inner(),
});
HttpResponse::Ok().body(handlebars.render("confirmed.html", &data).unwrap())
}),
Err(e) => Err(error::ErrorBadRequest(e)),
}
}
pub async fn delete_appointment(
pool: web::Data<Pool>,
_mailer: web::Data<SmtpTransport>,
handlebars: web::Data<Handlebars<'_>>,
css: web::Data<CssPath>,
path: web::Path<(IdType, IdType, String)>,
) -> Result<HttpResponse, error::Error> {
let (teacher_id, slot_id, validation_key) = path.into_inner();
match cancel_appointment(&pool, teacher_id, slot_id, &validation_key).await {
Ok(appointments) => {
let data = json!({
"css_file" : css.path,
"appointments": appointments,
"validation_key": validation_key,
});
Ok(HttpResponse::Ok().body(handlebars.render("confirmed.html", &data).unwrap()))
}
Err(e) => Err(error::ErrorBadRequest(e)), Err(e) => Err(error::ErrorBadRequest(e)),
} }
} }

View File

@ -23,7 +23,25 @@
<div class="container"> <div class="container">
<div class="section"> <div class="section">
<h1 class="title is-1">Bestätigt</h1> <h1 class="title is-1">Bestätigt</h1>
<p>Die unten stehenden Treffen sind bestätigt.</p>
<p> Sollten Sie eines oder mehrere Treffen doch nicht wahrnehmen können,
sagen Sie diese bitte möglichst früh ab, sodass Sie von anderen wiederum gebucht werden können.</p>
<div class="mt-6">
{{#each appointments as |appoint|}}
<div class="columns is-centered"><div class="column is-6">
<div class="box"><div class="columns is-vcentered">
<div class="column">
<b>{{appoint.teacher.ansprache}} {{appoint.teacher.last_name}}: {{time_of appoint.slot.start_time}} - {{time_of appoint.slot.end_time}}</b>
</div> </div>
<div class="column">
<a class="button is-small" href="/cancel/{{appoint.teacher.id}}/{{appoint.slot.id}}/{{ ../validation_key }}">
<span class="fas fa-ban fa-fw"></span>
<span class="ml-2">Dieses Treffen Absagen</span>
</a></div></div></div></div></div>
{{/each}}
</div></div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,21 +1,21 @@
Sehr geehrte/r {{ nutzer.name }}, Sehr geehrte/r {{ nutzer.name }},
bitte bestätigen Sie durch klick auf den folgenden Link ihre Anmeldung für die Termine bitte bestätigen Sie Ihre Anmeldung zu folgenden Terminen durch Anklicken des unten stehenden Links:
Alle Treffen bestätigen:
https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }}
Wenn Sie ein Treffen absagen wollen klicken Sie trotzdem auf den obigen Bestätigungslink und zusätzlich auf den passenden Absage-Link unten.
Auch bestätigte Treffen können abgesagt werden.
Liste der Treffen - mit Absage-Links, sollten Sie zu einem Treffen doch nicht können.
{{#each appointments as |appoint|}} {{#each appointments as |appoint|}}
{{appoint.teacher.ansprache}} {{appoint.teacher.last_name}}: {{time_of appoint.slot.start_time}} - {{time_of appoint.slot.end_time}} {{appoint.teacher.ansprache}} {{appoint.teacher.last_name}}: {{time_of appoint.slot.start_time}} - {{time_of appoint.slot.end_time}}
Dieses Treffen Absagen:
https://elternsprechtag.uhle.cloud/cancel/{{appoint.teacher.id}}/{{appoint.slot.id}}/{{ ../validation_key }}
{{/each}} {{/each}}
Alle Termine bestätigen:
https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }}
Wenn Sie einen Termin absagen möchten, klicken Sie bitte trotzdem auf den Bestätigungslink oben.
Dort können Sie dann einzelne oder alle Termine stornieren.
Dies funktioniert auch, wenn Sie die Termine bereits bestätigt haben.
Wenn Sie länger als 3 Stunden gewartet haben, werden die Termine wieder freigegeben.
Neue Termine müssen Sie wieder über die ursprüngliche Seite buchen.
Vielen Dank für Ihre Anmeldung! Vielen Dank für Ihre Anmeldung!
Das Oberstufenkollegium Das Oberstufenkollegium

View File

@ -161,7 +161,7 @@ impl Component for App {
<div class="container"> <div class="container">
<div class="section"> <div class="section">
{ {
if self.nutzer.is_none(){ 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) self.view_eingabe_daten(ctx)
} }
else else
@ -186,8 +186,8 @@ impl App {
<img src="/logoheader.png" /> <img src="/logoheader.png" />
</figure> </figure>
<div class="box mt-3 is-light"> <div class="box mt-3 is-light">
<p>{"Anmeldung zum Elternsprechtag!"}</p><p>{"Bitte geben Sie unbedingt eine valide Emailadresse an, <p>{"Anmeldung zum Elternsprechtag!"}</p><p>{"Bitte geben Sie unbedingt eine gültige E-Mail-Adresse an,
da die Termine erst nach Bestätigung über den per Email zugesandten Link gebucht sind."}</p> da die Termine erst nach Bestätigung über den per E-Mail zugesandten Link gebucht werden."}</p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">{"Voller Name"}</label> <label class="label">{"Voller Name"}</label>
@ -227,9 +227,9 @@ impl App {
</div> </div>
<div class="box mt-3 is-light"> <div class="box mt-3 is-light">
<h4 class="title is-4">{"Datenschutzerklärung:"}</h4> <h4 class="title is-4">{"Datenschutzerklärung:"}</h4>
<p>{"Mit dem klick auf Weiter bestätigen Sie, dass das Lehrerkollegium die hier <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. und im Folgenden eingegebenen Daten zur Organisation des Elternsprechtages speichert, verarbeitet und nutzt.
Die Daten werden nur für diesen Zweck verwendet."}</p> Die Daten werden ausschließlich zu diesem Zweck verwendet."}</p>
<div class="has-text-right mt-6 mr-6"> <div class="has-text-right mt-6 mr-6">
{ {
if self.tmp_nutzer.validate() { if self.tmp_nutzer.validate() {
@ -278,6 +278,37 @@ impl App {
} }
} }
fn view_dank_dialog(&self, _ctx: &Context<Self>) -> Html {
html! {<>
<div id="app" class="row columns is-multiline">
<div class="column is-12">
<div class="card">
<header class="card-header">
<div class="card-header-title is-centered">
{"Erfolgreich abgeschickt, aber noch nicht fertig!"}
</div>
</header>
<div class="card-content">
<div class="content">
<p>{"Sie haben die Termine erfolgreich vorgemerkt. Um sie endgültig zu buchen müssen Sie:"}</p>
<p><ol>
<li>{"In Ihren E-Mails die E-Mail mit dem Betreff \"Elternsprechtag: Bestätigen Sie Ihre Termine\" suchen."}</li>
<li>{"Innerhalb von 3 Stunden auf den darin enthaltenen "}<b>{"Link klicken."}</b></li>
</ol></p>
<p>{"Sie können auch mehrmals auf den Link klicken, den Sie erhalten haben. Sie können dort Ihre Termine einsehen und gegebenenfalls absagen.
Wenn Sie einen Termin absagen, tun Sie dies bitte so früh wie möglich, damit der Termin wieder für andere frei wird."}</p>
<p>{"Wenn Sie einen anderen Termin buchen möchten, müssen Sie sich erneut über diese Seite anmelden, Sie erhalten dann einen zweiten Link.
Achtung: Die Termine, die Sie zuerst gebucht haben, werden dadurch "}<b>{"nicht"}</b>{" gelöscht."}</p>
</div>
</div>
</div>
</div>
</div>
</>
}
}
fn anleitung(&self) -> Html { fn anleitung(&self) -> Html {
html!( <div class="column is-12"> html!( <div class="column is-12">
<div class="card"> <div class="card">
@ -289,16 +320,16 @@ impl App {
<div class="card-content"> <div class="card-content">
<div class="content"> <div class="content">
<ul> <ul>
<li>{"Termine sind erst gebucht, wenn Sie:"} <li>{"Die Termine sind erst gebucht, wenn Sie:"}
<ul><li>{"ganz unten auf Absenden geklickt haben"}</li> <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> <li>{"Sie die Buchung innerhalb von 3 Stunden über den Link in der E-Mail bestätigen."}</li>
</ul> </ul>
</li> </li>
<li>{"Maximal 3 Termine pro Elternhaus, da die Anzahl der Termine begrenzt ist."}</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>{"Buchen Sie die Termine so, dass Sie zwischen zwei Terminen eine Pause für die Raumsuche haben."}</li>
<li>{"Sprechen Sie vor allem mit den LehrerInnen, wo ihre Kinder Probleme haben."}</li> <li>{"Sprechen Sie vor allem mit den LehrerInnen, deren Fächer Ihren Kindern Probleme bereiten."}</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 dringenden Gesprächsbedarf haben, aber alle Termine sind voll, melden Sie sich wie gewohnt 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> <li>{"Wenn Sie Ihren Termin nicht wahrnehmen können, sagen Sie ihn so früh wie möglich ab, damit er neu vergeben werden kann."}</li>
</ul> </ul>
</div> </div>
</div> </div>