402 lines
16 KiB
Rust
402 lines
16 KiB
Rust
mod requests;
|
||
use std::collections::{HashMap, HashSet};
|
||
|
||
use gloo::console::log;
|
||
use requests::{fetch_slots, fetch_teachers, fetch_unavailable, send_appointments};
|
||
use terminwahl_typen::{
|
||
AppointmentSlot, AppointmentSlots, IdType, Nutzer, PlannedAppointment, RequestState, SlotId,
|
||
Teacher, Teachers,
|
||
};
|
||
use web_sys::HtmlInputElement;
|
||
use yew::prelude::*;
|
||
|
||
// 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>),
|
||
}
|
||
|
||
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>,
|
||
}
|
||
|
||
impl From<Result<Msg, Msg>> for Msg {
|
||
fn from(value: Result<Msg, Msg>) -> Self {
|
||
match value {
|
||
Ok(m) => m,
|
||
Err(m) => m,
|
||
}
|
||
}
|
||
}
|
||
|
||
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,
|
||
self.nutzer
|
||
.as_ref()
|
||
.expect("This should always exist")
|
||
.clone(),
|
||
));
|
||
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 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)
|
||
}
|
||
}
|
||
</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 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">
|
||
<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">{"E-Mail"}</label>
|
||
<p class="control has-icons-left">
|
||
<input class="input" type="email" placeholder="E-Mail" 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 Elternsprechtages speichert, verarbeitet und nutzt.
|
||
Die Daten werden ausschließlich zu diesem 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."}</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."}</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 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 {
|
||
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>{"Die 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 E-Mail 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 eine Pause für die Raumsuche 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 gewohnt bei den entsprechenden LehrerInnen"}</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>
|
||
</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>}
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
yew::Renderer::<App>::new().render();
|
||
}
|