Buchung funktioniert email anfänglich

This commit is contained in:
2023-01-29 22:56:33 +01:00
parent 0cd75a9ff5
commit c708a4b79b
21 changed files with 728 additions and 38 deletions
+368 -14
View File
@@ -1,21 +1,375 @@
mod requests;
use std::collections::{HashMap, HashSet};
use gloo::console::log;
use requests::{fetch_slots, fetch_teachers, fetch_unavailable, send_appointments};
use serde::{Deserialize, Serialize};
use terminwahl_typen::{
AppointmentSlot, AppointmentSlots, IdType, PlannedAppointment, RequestState, SlotId, Teacher,
Teachers,
};
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component]
fn App() -> Html {
let counter = use_state(|| 5);
let onclick = {
let counter = counter.clone();
move |_| {
let value = *counter + 1;
counter.set(value);
}
};
// Define the possible messages which can be sent to the component
pub enum Msg {
UpdateName(String),
UpdateSchüler(String),
UpdateEmail(String),
DataEntered(Nutzer),
GetTeachers,
ReceivedTeachers(Teachers),
GetSlots,
ReceivedSlots(AppointmentSlots),
Selected(PlannedAppointment),
TooMany,
SendToServer,
AppointmentsSent(RequestState),
ReceivedUnavailable(HashSet<SlotId>),
}
html! {
<div>
<button {onclick}>{ "+1" }</button>
<p>{ *counter }</p>
pub struct App {
nutzer: Option<Nutzer>,
tmp_nutzer: Nutzer,
teachers: Option<Teachers>,
slots: Option<AppointmentSlots>,
appointments: HashMap<SlotId, PlannedAppointment>,
unavailable: Option<HashSet<SlotId>>,
successfully_saved: Option<RequestState>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Nutzer {
name: String,
schüler: String,
email: String,
}
impl From<Result<Msg, Msg>> for Msg {
fn from(value: Result<Msg, Msg>) -> Self {
match value {
Ok(m) => m,
Err(m) => m,
}
}
}
impl Nutzer {
fn validate(&self) -> bool {
!self.name.is_empty() && !self.email.is_empty() && !self.schüler.is_empty()
}
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let app = Self {
appointments: HashMap::new(),
slots: None,
unavailable: None,
teachers: None,
nutzer: None,
tmp_nutzer: Nutzer {
name: "".into(),
schüler: "".into(),
email: "".into(),
},
successfully_saved: None,
};
ctx.link().send_message(Msg::GetTeachers);
ctx.link().send_message(Msg::GetSlots);
app
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Selected(planned_appointment) => {
let slot_id =
SlotId::new(planned_appointment.teacher_id, planned_appointment.slot_id);
if self.appointments.contains_key(&slot_id) {
self.appointments.remove(&slot_id);
} else {
self.appointments.insert(
SlotId::new(planned_appointment.teacher_id, planned_appointment.slot_id),
planned_appointment,
);
};
true
}
Msg::TooMany => todo!(),
Msg::GetTeachers => {
ctx.link().send_future(fetch_teachers());
false
}
Msg::ReceivedTeachers(teachers) => {
self.teachers = Some(teachers);
true
}
Msg::GetSlots => {
ctx.link().send_future(fetch_slots());
ctx.link().send_future(fetch_unavailable());
false
}
Msg::ReceivedSlots(slots) => {
self.slots = Some(slots);
true
}
Msg::DataEntered(n) => {
self.nutzer = Some(n);
true
}
Msg::UpdateName(s) => {
log!("update name to {}", &s);
self.tmp_nutzer.name = s;
true
}
Msg::UpdateSchüler(s) => {
self.tmp_nutzer.schüler = s;
true
}
Msg::UpdateEmail(s) => {
self.tmp_nutzer.email = s;
true
}
Msg::SendToServer => {
if (1..=3).contains(&self.appointments.len()) {
let values = self.appointments.clone().into_values();
let appointments: Vec<PlannedAppointment> = values.collect();
ctx.link().send_future(send_appointments(appointments));
true
} else {
true
}
}
Msg::AppointmentsSent(state) => {
self.successfully_saved = Some(state);
true
}
Msg::ReceivedUnavailable(r) => {
self.unavailable = Some(r);
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {<>
<section class="hero is-warning">
<div class="hero-body">
<p class="title has-text-link">
{"Elternsprechtag"}
</p>
<p class="subtitle">
{"Am 28.02.23"}
</p>
</div>
</section>
<div class="container">
<div class="section">
{
if self.nutzer.is_none(){
self.view_eingabe_daten(ctx)
}
else
{
self.view_auswahl_termine(ctx)
}
}
</div>
</div>
</>
}
}
}
impl App {
fn view_eingabe_daten(&self, ctx: &Context<Self>) -> Html {
html! { <div class="columns is-centered">
<div class="column is-half">
<div class="notification is-light">
<figure class="image container is-128x128" id="headerlogo">
<img src="/logoheader.png" />
</figure>
<div class="box mt-3 is-light">
<p>{"Anmeldung zum Elternsprechtag!"}</p><p>{"Bitte geben Sie unbedingt eine valide Emailadresse an,
da die Termine erst nach Bestätigung über den per Email zugesandten Link gebucht sind."}</p>
</div>
<div class="field">
<label class="label">{"Voller Name"}</label>
<p class="control has-icons-left has-icons-right">
<input class="input" type="text" placeholder="Voller Name" value={self.tmp_nutzer.name.to_string()}
oninput={ctx.link().batch_callback(|event:InputEvent| {
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateName(input.value()))
})}/>
<span class="icon is-small is-left">
<i class="fas fa-signature"></i>
</span>
</p>
</div>
<div class="field">
<label class="label">{"Alle betreffenden SchülerInnen"}</label>
<p class="control has-icons-left has-icons-right">
<input class="input" type="email" placeholder="Schülerinnen" value={self.tmp_nutzer.schüler.to_string()}
oninput={ctx.link().batch_callback(|event:InputEvent| {
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateSchüler(input.value()))
})}/>
<span class="icon is-small is-left">
<i class="fas fa-graduation-cap"></i>
</span>
</p>
</div>
<div class="field">
<label class="label">{"Email"}</label>
<p class="control has-icons-left">
<input class="input" type="email" placeholder="email" value={self.tmp_nutzer.email.to_string()}
oninput={ctx.link().batch_callback(|event:InputEvent| {
event.target_dyn_into::<HtmlInputElement>().map(|input|Msg::UpdateEmail(input.value()))
})}/>
<span class="icon is-small is-left">
<i class="fas fa-at"></i>
</span>
</p>
</div>
<div class="box mt-3 is-light">
<h4 class="title is-4">{"Datenschutzerklärung:"}</h4>
<p>{"Mit dem klick auf Weiter bestätigen Sie, dass das Lehrerkollegium die hier
und im folgenden eingegebenen Daten zur Organisation des Elternsprechtags speichert, verarbeitet und verwendet.
Die Daten werden nur für diesen Zweck verwendet."}</p>
<div class="has-text-right mt-6 mr-6">
{
if self.tmp_nutzer.validate() {
html!{<a class="button is-link is-medium" onclick={
let tmp_nutzer = self.tmp_nutzer.clone();
ctx.link().callback(move |_|{Msg::DataEntered(tmp_nutzer.clone()) })
}>{"Weiter"}</a>}
} else {html!{<><p>{"Füllen Sie zunächst die Felder oben aus!"}</p><a class="button is-link is-medium" disabled=true>{"Weiter"}</a></>}}}
</div>
</div>
</div>
</div>
</div>}
}
fn view_auswahl_termine(&self, ctx: &Context<Self>) -> Html {
html! {<>
<div id="app" class="row columns is-multiline">
{self.anleitung()}
{if let Some(ref teachers) = self.teachers
{teachers.iter().map(|t|{self.view_teacher_card(ctx, t)}).collect()}else{html!{}}}
</div>
<div class="container">
<div class="section">
{ if self.appointments.is_empty(){
html!{<>
<p>{"Bitte wählen Sie mindestens einen Block aus, bevor sie buchen können."}</p>
<div class="section">
<button class="button" disabled=true>{"Buchungen reservieren"}</button>
</div></>
} }else if self.appointments.len() > 3{
html!{<>
<p>{"Bitte wählen Sie höchstens drei Blöcke, bevor sie buchen können."}</p>
<div class="section">
<button class="button" disabled=true>{"Buchungen reservieren"}</button>
</div></>
}}else{html!{<>
<button class="button is-primary" onclick={ctx.link().callback(|_|Msg::SendToServer)}>{"Buchungen reservieren"}</button></>
}
}}
</div>
</div>
</>
}
}
fn anleitung(&self) -> Html {
html!( <div class="column is-12">
<div class="card">
<header class="card-header">
<div class="card-header-title is-centered">
{"Hinweise"}
</div>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>{"Termine sind erst gebucht, wenn Sie:"}
<ul><li>{"ganz unten auf Absenden geklickt haben"}</li>
<li>{"Sie die Buchung innerhalb von 3 Stunden über den link in der email bestätigen."}</li>
</ul>
</li>
<li>{"Maximal 3 Termine pro Elternhaus, da die Anzahl der Termine begrenzt ist."}</li>
<li>{"Buchen Sie die Termine so, dass sie zwischen zwei Terminen mindestens einen Slot Pause haben für Raumsuche usw."}</li>
<li>{"Sprechen Sie vor allem mit den LehrerInnen, wo ihre Kinder Probleme haben."}</li>
<li>{"Sollten Sie dringenden Gesprächsbedarf haben, aber alle Termine sind voll, melden Sie sich wie gehabt bei den entsprechenden LehrerInnen"}</li>
<li>{"Sollten Sie ihren Termin nicht wahrnehmen können, sagen Sie ihn möglichst früh ab, dass er erneut belegt werden kann."}</li>
</ul>
</div>
</div>
</div>
</div>)
}
fn view_teacher_card(&self, ctx: &Context<Self>, teacher: &Teacher) -> Html {
html! {
<div class="column is-3">
<div class="card">
<header class="card-header">
<div class="card-header-title is-centered">
{&teacher.ansprache}{" "}{&teacher.last_name}
</div>
</header>
<div class="card-content">
<div class="content">
{ if let Some(ref slots) = self.slots
{
let mut slots:Vec<&AppointmentSlot> = slots.values().collect();
slots.sort_by(|x,y|{x.start_time.cmp(&y.start_time)});
let slots = slots.iter().map(|t|{self.view_teacher_slot(ctx, t, teacher.id)});
let first = slots.clone().take(4);
let rest = slots.skip(4);
first.chain([html!{<div class="has-text-centered mb-3">{"Pause"}</div>}]).chain(rest).collect()}else{html!{}} }
</div>
</div>
</div>
</div>
}
}
fn view_teacher_slot(
&self,
ctx: &Context<Self>,
slot: &AppointmentSlot,
teacher_id: IdType,
) -> Html {
let slot_id = slot.id;
let onclick = ctx.link().callback(move |_| {
Msg::Selected(PlannedAppointment {
teacher_id,
slot_id,
})
});
let text = format!(
"{} – {}",
slot.start_time.time().format("%H:%M"),
slot.end_time.time().format("%H:%M")
);
let slot_id = SlotId::new(teacher_id, slot.id);
let (color, disabled) = if self.appointments.contains_key(&slot_id) {
("is-primary", false)
} else if let Some(unavailable) = &self.unavailable {
if unavailable.contains(&slot_id) {
("is-danger", true)
} else {
("", false)
}
} else {
("", false)
};
html! {<button class={format!("button is-fullwidth mb-2 {color}")} disabled={disabled} {onclick}>{text}</button>}
}
}
+55
View 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))
}