2023-02-05 18:32:06 +01:00

402 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}