Buchung funktioniert email anfänglich
This commit is contained in:
+368
-14
@@ -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>}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user