add group routes
This commit is contained in:
		@@ -39,7 +39,7 @@ CREATE TABLE group_station (
 | 
				
			|||||||
    station_id INTEGER,
 | 
					    station_id INTEGER,
 | 
				
			||||||
    points INTEGER,
 | 
					    points INTEGER,
 | 
				
			||||||
    notes TEXT,
 | 
					    notes TEXT,
 | 
				
			||||||
    arrived_at DATETIME,
 | 
					    arrived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 | 
				
			||||||
    started_at DATETIME,
 | 
					    started_at DATETIME,
 | 
				
			||||||
    left_at DATETIME,
 | 
					    left_at DATETIME,
 | 
				
			||||||
    PRIMARY KEY (group_id, station_id),
 | 
					    PRIMARY KEY (group_id, station_id),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										173
									
								
								src/group/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								src/group/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					use crate::{route::Route, station::Station};
 | 
				
			||||||
 | 
					use axum::Router;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use sqlx::{FromRow, SqlitePool};
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(FromRow, Debug, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub(crate) struct Group {
 | 
				
			||||||
 | 
					    pub(crate) id: i64,
 | 
				
			||||||
 | 
					    pub(crate) name: String,
 | 
				
			||||||
 | 
					    notes: Option<String>,
 | 
				
			||||||
 | 
					    amount_people: Option<i64>,
 | 
				
			||||||
 | 
					    first_station_id: i64,
 | 
				
			||||||
 | 
					    route_id: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum CreateError {
 | 
				
			||||||
 | 
					    NoStationForRoute,
 | 
				
			||||||
 | 
					    DuplicateName(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Group {
 | 
				
			||||||
 | 
					    pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as::<_, Self>(
 | 
				
			||||||
 | 
					            "SELECT id, name, notes, amount_people, first_station_id, route_id FROM 'group';",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) async fn all_with_route(db: &SqlitePool, route: &Route) -> Vec<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as!(
 | 
				
			||||||
 | 
					            Group,
 | 
				
			||||||
 | 
					            "select id, name, notes, amount_people, first_station_id, route_id from 'group' where route_id = ?;",
 | 
				
			||||||
 | 
					            route.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) async fn all_with_first_station(db: &SqlitePool, station: &Station) -> Vec<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as!(
 | 
				
			||||||
 | 
					            Group,
 | 
				
			||||||
 | 
					            "select id, name, notes, amount_people, first_station_id, route_id from 'group' where first_station_id = ?;",
 | 
				
			||||||
 | 
					            station.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "SELECT id, name, notes, amount_people, first_station_id, route_id FROM 'group' WHERE id = ?",
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn create(db: &SqlitePool, name: &str, route: &Route) -> Result<(), CreateError> {
 | 
				
			||||||
 | 
					        // get next station id which has the lowest amount of groups to have in the first place
 | 
				
			||||||
 | 
					        // assigned
 | 
				
			||||||
 | 
					        let Some(station) = route.get_next_first_station(db).await else {
 | 
				
			||||||
 | 
					            return Err(CreateError::NoStationForRoute);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "INSERT INTO 'group'(name, route_id, first_station_id) VALUES (?, ?, ?)",
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            route.id,
 | 
				
			||||||
 | 
					            station.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_err(|e| CreateError::DuplicateName(e.to_string()))?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_name(&self, db: &SqlitePool, name: &str) {
 | 
				
			||||||
 | 
					        sqlx::query!("UPDATE 'group' SET name = ? WHERE id = ?", name, self.id)
 | 
				
			||||||
 | 
					            .execute(db)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_notes(&self, db: &SqlitePool, notes: &str) {
 | 
				
			||||||
 | 
					        sqlx::query!("UPDATE 'group' SET notes = ? WHERE id = ?", notes, self.id)
 | 
				
			||||||
 | 
					            .execute(db)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_amount_people(&self, db: &SqlitePool, amount_people: i64) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE 'group' SET amount_people = ? WHERE id = ?",
 | 
				
			||||||
 | 
					            amount_people,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_route(&self, db: &SqlitePool, route: &Route) -> Result<String, ()> {
 | 
				
			||||||
 | 
					        let Some(station) = route.get_next_first_station(db).await else {
 | 
				
			||||||
 | 
					            return Err(());
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE 'group' SET route_id = ?, first_station_id = ? WHERE id = ?",
 | 
				
			||||||
 | 
					            route.id,
 | 
				
			||||||
 | 
					            station.id,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(station.name)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_first_station(&self, db: &SqlitePool, station: &Station) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE 'group' SET first_station_id = ? WHERE id = ?",
 | 
				
			||||||
 | 
					            station.id,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn update_amount_people_reset(&self, db: &SqlitePool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE 'group' SET amount_people = NULL WHERE id = ?",
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
 | 
				
			||||||
 | 
					        sqlx::query!("DELETE FROM 'group' WHERE id = ?", self.id)
 | 
				
			||||||
 | 
					            .execute(db)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| e.to_string())?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn first_station(&self, db: &SqlitePool) -> Station {
 | 
				
			||||||
 | 
					        Station::find_by_id(db, self.first_station_id)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect("db constraints")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn route(&self, db: &SqlitePool) -> Route {
 | 
				
			||||||
 | 
					        Route::find_by_id(db, self.route_id)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect("db constraints")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
				
			||||||
 | 
					    web::routes()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										534
									
								
								src/group/web.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								src/group/web.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,534 @@
 | 
				
			|||||||
 | 
					use super::{CreateError, Group};
 | 
				
			||||||
 | 
					use crate::{err, partials::page, pl, route::Route, station::Station, succ};
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    Form, Router,
 | 
				
			||||||
 | 
					    extract::State,
 | 
				
			||||||
 | 
					    response::{IntoResponse, Redirect},
 | 
				
			||||||
 | 
					    routing::{get, post},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use maud::{Markup, PreEscaped, html};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use tower_sessions::Session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct CreateForm {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    route_id: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn create(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    Form(form): Form<CreateForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(route) = Route::find_by_id(&db, form.route_id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit {} konnte nicht erstellt werden, da keine Route mit ID {} existiert",
 | 
				
			||||||
 | 
					            form.name,
 | 
				
			||||||
 | 
					            form.route_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match Group::create(&db, &form.name, &route).await {
 | 
				
			||||||
 | 
					        Ok(()) => succ!(session, "Gruppe '{}' erfolgreich erstellt!", form.name),
 | 
				
			||||||
 | 
					        Err(CreateError::DuplicateName(e)) => err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe '{}' konnte _NICHT_ erstellt werden, da es bereits eine Gruppe mit diesem Namen gibt ({e})!",
 | 
				
			||||||
 | 
					            form.name
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        Err(CreateError::NoStationForRoute) => err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe '{}' konnte _NICHT_ erstellt werden, da in der angegebenen Route '{}' noch keine Stationen vorkommen",
 | 
				
			||||||
 | 
					            form.name,
 | 
				
			||||||
 | 
					            route.name
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to("/group")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn delete(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht gelöscht werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match group.delete(&db).await {
 | 
				
			||||||
 | 
					        Ok(()) => succ!(session, "Gruppe '{}' erfolgreich gelöscht!", group.name),
 | 
				
			||||||
 | 
					        Err(e) => err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe '{}' kann nicht gelöscht werden, da sie bereits verwendet wird. ({e})",
 | 
				
			||||||
 | 
					            group.name
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to("/group")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn view(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					) -> Result<Markup, impl IntoResponse> {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Err(Redirect::to("/group"));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let first_station = group.first_station(&db).await;
 | 
				
			||||||
 | 
					    let routes = Route::all(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let stations = group.route(&db).await.stations(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // maybe switch to maud-display impl of group
 | 
				
			||||||
 | 
					    let content = html! {
 | 
				
			||||||
 | 
					        h1 {
 | 
				
			||||||
 | 
					            a href="/group" { "↩️" }
 | 
				
			||||||
 | 
					            "Gruppe " (group.name)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        article {
 | 
				
			||||||
 | 
					            details {
 | 
				
			||||||
 | 
					                summary { "Gruppennamen bearbeiten ✏️" }
 | 
				
			||||||
 | 
					                form action=(format!("/group/{}/name", group.id)) method="post" {
 | 
				
			||||||
 | 
					                    input type="text" name="name" value=(group.name) required;
 | 
				
			||||||
 | 
					                    input type="submit" value="Speichern";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        table {
 | 
				
			||||||
 | 
					            tbody {
 | 
				
			||||||
 | 
					                tr {
 | 
				
			||||||
 | 
					                    th scope="row" { "Notizen" };
 | 
				
			||||||
 | 
					                    td {
 | 
				
			||||||
 | 
					                        @match &group.notes {
 | 
				
			||||||
 | 
					                            Some(notes) => {
 | 
				
			||||||
 | 
					                                (notes)
 | 
				
			||||||
 | 
					                                details {
 | 
				
			||||||
 | 
					                                summary { "✏️" }
 | 
				
			||||||
 | 
					                                form action=(format!("/group/{}/notes", group.id)) method="post" {
 | 
				
			||||||
 | 
					                                        textarea name="notes" required rows="10" { (notes) };
 | 
				
			||||||
 | 
					                                        input type="submit" value="Speichern";
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                            None => details {
 | 
				
			||||||
 | 
					                                summary { "Neue Notiz hinzufügen" }
 | 
				
			||||||
 | 
					                                form action=(format!("/group/{}/notes", group.id)) method="post" {
 | 
				
			||||||
 | 
					                                        textarea name="notes" required rows="10" {};
 | 
				
			||||||
 | 
					                                        input type="submit" value="Speichern";
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                tr {
 | 
				
			||||||
 | 
					                    th scope="row" { "Anzahl Gruppenmitglieder" };
 | 
				
			||||||
 | 
					                    td {
 | 
				
			||||||
 | 
					                        @match group.amount_people {
 | 
				
			||||||
 | 
					                            Some(amount) => (amount),
 | 
				
			||||||
 | 
					                            None => "?",
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                                details {
 | 
				
			||||||
 | 
					                                summary { "✏️" }
 | 
				
			||||||
 | 
					                                form action=(format!("/group/{}/amount-people", group.id)) method="post" {
 | 
				
			||||||
 | 
					                                    input type="number" name="amount_people" min="0" max="10";
 | 
				
			||||||
 | 
					                                    input type="submit" value="Speichern";
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                                a href=(format!("/group/{}/amount-people-reset", group.id)) {
 | 
				
			||||||
 | 
					                                    button class="error" {
 | 
				
			||||||
 | 
					                                        em data-tooltip="Ich weiß noch nicht wv. Personen diese Gruppe beherbergt." {
 | 
				
			||||||
 | 
					                                            "?"
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                tr {
 | 
				
			||||||
 | 
					                    th scope="row" { "Route" };
 | 
				
			||||||
 | 
					                    td {
 | 
				
			||||||
 | 
					                            a href=(format!("/route/{}", &group.route(&db).await.id)) {
 | 
				
			||||||
 | 
					                                (&group.route(&db).await.name)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        @if routes.len() > 1 {
 | 
				
			||||||
 | 
					                            details {
 | 
				
			||||||
 | 
					                                summary { "✏️" }
 | 
				
			||||||
 | 
					                                form action=(format!("/group/{}/update-route", group.id)) method="post" {
 | 
				
			||||||
 | 
					                                        select name="route_id" aria-label="Route auswählen" required {
 | 
				
			||||||
 | 
					                                            @for route in &routes {
 | 
				
			||||||
 | 
					                                                @if route.id != group.route(&db).await.id {
 | 
				
			||||||
 | 
					                                                    option value=(route.id) {
 | 
				
			||||||
 | 
					                                                        (route.name)
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        input type="submit" value="Gruppe speichern";
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                tr {
 | 
				
			||||||
 | 
					                    th scope="row" {
 | 
				
			||||||
 | 
					                        "Erste Station"
 | 
				
			||||||
 | 
					                        article {
 | 
				
			||||||
 | 
					                            "Die erste Station wird beim Anlegen einer Gruppe automatisch an diejenige Station vergeben, die aktuell am wenigsten Startgruppen hat. Diese Zuteilung kannst du hier auch manuell verändern."
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    td {
 | 
				
			||||||
 | 
					                        a href=(format!("/station/{}", first_station.id)) {
 | 
				
			||||||
 | 
					                            (first_station.name)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        @if stations.len() > 1 {
 | 
				
			||||||
 | 
					                            details {
 | 
				
			||||||
 | 
					                                summary { "✏️" }
 | 
				
			||||||
 | 
					                                form action=(format!("/group/{}/update-first-station", group.id)) method="post" {
 | 
				
			||||||
 | 
					                                        select name="first_station_id" aria-label="Station auswählen" required {
 | 
				
			||||||
 | 
					                                            @for station in &stations {
 | 
				
			||||||
 | 
					                                                @if station.id != first_station.id {
 | 
				
			||||||
 | 
					                                                    option value=(station.id) {
 | 
				
			||||||
 | 
					                                                        (station.name)
 | 
				
			||||||
 | 
					                                                        @let amount_start_groups = Group::all_with_first_station(&db, station).await.len();
 | 
				
			||||||
 | 
					                                                        @if amount_start_groups > 0 {
 | 
				
			||||||
 | 
					                                                        (format!(" (schon {amount_start_groups} {})", pl(amount_start_groups, "Startgruppe")))
 | 
				
			||||||
 | 
					                                                        }
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                        input type="submit" value="Station speichern";
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    Ok(page(content, session, true).await)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UpdateNameForm {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async fn update_name(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					    Form(form): Form<UpdateNameForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group.update_name(&db, &form.name).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    succ!(
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					        "Gruppe '{}' heißt ab sofort '{}'.",
 | 
				
			||||||
 | 
					        group.name,
 | 
				
			||||||
 | 
					        form.name
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UpdateNotesForm {
 | 
				
			||||||
 | 
					    notes: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async fn update_notes(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					    Form(form): Form<UpdateNotesForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group.update_notes(&db, &form.notes).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    succ!(
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					        "Notizen für die Gruppe '{}' wurden erfolgreich bearbeitet!",
 | 
				
			||||||
 | 
					        group.name
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UpdateAmountPeopleForm {
 | 
				
			||||||
 | 
					    amount_people: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async fn update_amount_people(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					    Form(form): Form<UpdateAmountPeopleForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group.update_amount_people(&db, form.amount_people).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    succ!(
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					        "Anzahl an Mitglieder für die Gruppe '{}' wurden erfolgreich bearbeitet!",
 | 
				
			||||||
 | 
					        group.name
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UpdateRouteForm {
 | 
				
			||||||
 | 
					    route_id: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async fn update_route(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					    Form(form): Form<UpdateRouteForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    // TODO: move sanity checks into mod.rs
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let Some(route) = Route::find_by_id(&db, form.route_id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Konnte Gruppe mit ID {id} konnte nicht zur Route mit ID {} bearbeiten, da diese Route nicht existiert.",
 | 
				
			||||||
 | 
					            form.route_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to(&format!("/group/{id}"));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match group.update_route(&db, &route).await {
 | 
				
			||||||
 | 
					        Ok(new_first_station_name) => succ!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe '{}' laufen ab sofort die Route '{}'. Auch die erste Station hat sich dadurch auf {} verändert!!",
 | 
				
			||||||
 | 
					            group.name,
 | 
				
			||||||
 | 
					            route.name,
 | 
				
			||||||
 | 
					            new_first_station_name
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        Err(()) => err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Konnte für die Gruppe {} nicht die Route {} auswählen, da Route {} keine Stationen zugeteilt hat.",
 | 
				
			||||||
 | 
					            group.name,
 | 
				
			||||||
 | 
					            route.name,
 | 
				
			||||||
 | 
					            route.name
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UpdateFirstStationForm {
 | 
				
			||||||
 | 
					    first_station_id: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async fn update_first_station(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					    Form(form): Form<UpdateFirstStationForm>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let Some(station) = Station::find_by_id(&db, form.first_station_id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Konnte die erste Station (ID={}) der Gruppe mit ID {} nicht bearbeiten, da diese Station nicht existiert.",
 | 
				
			||||||
 | 
					            form.first_station_id,
 | 
				
			||||||
 | 
					            group.id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to(&format!("/group/{id}"));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !station.is_in_route(&db, &group.route(&db).await).await {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Konnte Station {} nicht der Gruppe {} hinzufügen, weil die Gruppe bei Route {} und nicht bei Route {} mitläuft.",
 | 
				
			||||||
 | 
					            station.name,
 | 
				
			||||||
 | 
					            group.name,
 | 
				
			||||||
 | 
					            group.route(&db).await.name,
 | 
				
			||||||
 | 
					            group.name
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to(&format!("/group/{id}"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group.update_first_station(&db, &station).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    succ!(
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					        "Erste Station der Gruppe {} ist ab sofort {}",
 | 
				
			||||||
 | 
					        group.name,
 | 
				
			||||||
 | 
					        station.name
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn update_amount_people_reset(
 | 
				
			||||||
 | 
					    State(db): State<Arc<SqlitePool>>,
 | 
				
			||||||
 | 
					    session: Session,
 | 
				
			||||||
 | 
					    axum::extract::Path(id): axum::extract::Path<i64>,
 | 
				
			||||||
 | 
					) -> impl IntoResponse {
 | 
				
			||||||
 | 
					    let Some(group) = Group::find_by_id(&db, id).await else {
 | 
				
			||||||
 | 
					        err!(
 | 
				
			||||||
 | 
					            session,
 | 
				
			||||||
 | 
					            "Gruppe mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Redirect::to("/group");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    group.update_amount_people_reset(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    succ!(
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					        "Anzahl an Mitglieder für die Gruppe '{}' wurden erfolgreich bearbeitet!",
 | 
				
			||||||
 | 
					        group.name
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(&format!("/group/{id}"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
 | 
				
			||||||
 | 
					    let groups = Group::all(&db).await;
 | 
				
			||||||
 | 
					    let routes = Route::all(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let content = html! {
 | 
				
			||||||
 | 
					        h1 {
 | 
				
			||||||
 | 
					            a href="/" { "↩️" }
 | 
				
			||||||
 | 
					            "Gruppen"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        article {
 | 
				
			||||||
 | 
					            em { "Gruppen " }
 | 
				
			||||||
 | 
					            "sind eine Menge an Personen, die verschiedene "
 | 
				
			||||||
 | 
					            a href="/station" { "Stationen" }
 | 
				
			||||||
 | 
					            " ablaufen. Welche Stationen, entscheidet sich je nachdem, welcher "
 | 
				
			||||||
 | 
					            a href="/route" { "Route" }
 | 
				
			||||||
 | 
					            " sie zugewiesen sind."
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ol {
 | 
				
			||||||
 | 
					            @for group in &groups{
 | 
				
			||||||
 | 
					                li {
 | 
				
			||||||
 | 
					                     a href=(format!("/group/{}", group.id)){
 | 
				
			||||||
 | 
					                        (group.name)
 | 
				
			||||||
 | 
					                     }
 | 
				
			||||||
 | 
					                     a href=(format!("/group/{}/delete", group.id))
 | 
				
			||||||
 | 
					                       onclick="return confirm('Bist du sicher, dass die Gruppe gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden.');" {
 | 
				
			||||||
 | 
					                         "🗑️"
 | 
				
			||||||
 | 
					                     }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        @if groups.is_empty() {
 | 
				
			||||||
 | 
					            article class="warning" {
 | 
				
			||||||
 | 
					                "Es gibt noch keine Gruppen. "
 | 
				
			||||||
 | 
					                @if !routes.is_empty() {
 | 
				
			||||||
 | 
					                    "Das kannst du hier ändern ⤵️"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        h2 { "Neue Gruppe" }
 | 
				
			||||||
 | 
					        @if routes.is_empty() {
 | 
				
			||||||
 | 
					            article class="error" {
 | 
				
			||||||
 | 
					                (PreEscaped("Bevor du einer Gruppe erstellen kannst, musst du zumindest eine Route erstellen, die die Gruppe gehen kann → "))
 | 
				
			||||||
 | 
					                a role="button" href="/route" {
 | 
				
			||||||
 | 
					                    "Gruppe erstellen"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } @else {
 | 
				
			||||||
 | 
					            form action="/group" method="post" {
 | 
				
			||||||
 | 
					                @if routes.len() == 1 {
 | 
				
			||||||
 | 
					                    fieldset role="group" {
 | 
				
			||||||
 | 
					                        input type="text" name="name" placeholder="Gruppenname" required;
 | 
				
			||||||
 | 
					                        input type="hidden" name="route_id" value=(routes[0].id) ;
 | 
				
			||||||
 | 
					                        input type="submit" value="Neue Gruppe";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } @else {
 | 
				
			||||||
 | 
					                    input type="text" name="name" placeholder="Gruppenname" required;
 | 
				
			||||||
 | 
					                    select name="route_id" aria-label="Route auswählen" required {
 | 
				
			||||||
 | 
					                        @for route in &routes {
 | 
				
			||||||
 | 
					                            option value=(route.id) {
 | 
				
			||||||
 | 
					                                (route.name)
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    input type="submit" value="Neue Gruppe";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    page(content, session, false).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
				
			||||||
 | 
					    Router::new()
 | 
				
			||||||
 | 
					        .route("/", get(index))
 | 
				
			||||||
 | 
					        .route("/", post(create))
 | 
				
			||||||
 | 
					        .route("/{id}", get(view))
 | 
				
			||||||
 | 
					        .route("/{id}/delete", get(delete))
 | 
				
			||||||
 | 
					        .route("/{id}/name", post(update_name))
 | 
				
			||||||
 | 
					        .route("/{id}/notes", post(update_notes))
 | 
				
			||||||
 | 
					        .route("/{id}/amount-people", post(update_amount_people))
 | 
				
			||||||
 | 
					        .route("/{id}/amount-people-reset", get(update_amount_people_reset))
 | 
				
			||||||
 | 
					        .route("/{id}/update-route", post(update_route))
 | 
				
			||||||
 | 
					        .route("/{id}/update-first-station", post(update_first_station))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/lib.rs
									
									
									
									
									
								
							@@ -1,14 +1,23 @@
 | 
				
			|||||||
use axum::{body::Body, response::Response, routing::get, Router};
 | 
					use axum::{Router, body::Body, response::Response, routing::get};
 | 
				
			||||||
use maud::{html, Markup};
 | 
					use maud::{Markup, html};
 | 
				
			||||||
use partials::page;
 | 
					use partials::page;
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
use tokio::net::TcpListener;
 | 
					use tokio::net::TcpListener;
 | 
				
			||||||
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
 | 
					use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod group;
 | 
				
			||||||
mod partials;
 | 
					mod partials;
 | 
				
			||||||
pub(crate) mod route;
 | 
					pub(crate) mod route;
 | 
				
			||||||
pub(crate) mod station; // TODO: find nicer name for this 'associative table'
 | 
					pub(crate) mod station;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn pl(amount: usize, single: &str) -> String {
 | 
				
			||||||
 | 
					    if amount == 1 {
 | 
				
			||||||
 | 
					        single.into()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        format!("{single}n")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[macro_export]
 | 
					#[macro_export]
 | 
				
			||||||
macro_rules! err {
 | 
					macro_rules! err {
 | 
				
			||||||
@@ -111,6 +120,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) {
 | 
				
			|||||||
        .route("/", get(index))
 | 
					        .route("/", get(index))
 | 
				
			||||||
        .nest("/station", station::routes())
 | 
					        .nest("/station", station::routes())
 | 
				
			||||||
        .nest("/route", route::routes())
 | 
					        .nest("/route", route::routes())
 | 
				
			||||||
 | 
					        .nest("/group", group::routes())
 | 
				
			||||||
        .route("/pico.css", get(serve_pico_css))
 | 
					        .route("/pico.css", get(serve_pico_css))
 | 
				
			||||||
        .route("/style.css", get(serve_my_css))
 | 
					        .route("/style.css", get(serve_my_css))
 | 
				
			||||||
        .route("/leaflet.css", get(serve_leaflet_css))
 | 
					        .route("/leaflet.css", get(serve_leaflet_css))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
use crate::station::Station;
 | 
					use crate::{group::Group, station::Station};
 | 
				
			||||||
use axum::Router;
 | 
					use axum::Router;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use sqlx::{FromRow, SqlitePool};
 | 
					use sqlx::{FromRow, Row, SqlitePool};
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod web;
 | 
					mod web;
 | 
				
			||||||
@@ -9,11 +9,11 @@ mod web;
 | 
				
			|||||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
 | 
					#[derive(FromRow, Debug, Serialize, Deserialize)]
 | 
				
			||||||
pub(crate) struct Route {
 | 
					pub(crate) struct Route {
 | 
				
			||||||
    pub(crate) id: i64,
 | 
					    pub(crate) id: i64,
 | 
				
			||||||
    name: String,
 | 
					    pub(crate) name: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Route {
 | 
					impl Route {
 | 
				
			||||||
    async fn all(db: &SqlitePool) -> Vec<Self> {
 | 
					    pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
 | 
				
			||||||
        sqlx::query_as::<_, Self>("SELECT id, name FROM route;")
 | 
					        sqlx::query_as::<_, Self>("SELECT id, name FROM route;")
 | 
				
			||||||
            .fetch_all(db)
 | 
					            .fetch_all(db)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
@@ -126,7 +126,7 @@ DROP TABLE temp_pos;",
 | 
				
			|||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn stations(&self, db: &SqlitePool) -> Vec<Station> {
 | 
					    pub(crate) async fn stations(&self, db: &SqlitePool) -> Vec<Station> {
 | 
				
			||||||
        sqlx::query_as::<_, Station>(
 | 
					        sqlx::query_as::<_, Station>(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
            SELECT s.id, s.name, s.notes, s.amount_people, s.last_login, s.pw, s.lat, s.lng
 | 
					            SELECT s.id, s.name, s.notes, s.amount_people, s.last_login, s.pw, s.lat, s.lng
 | 
				
			||||||
@@ -160,6 +160,39 @@ DROP TABLE temp_pos;",
 | 
				
			|||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) async fn groups(&self, db: &SqlitePool) -> Vec<Group> {
 | 
				
			||||||
 | 
					        Group::all_with_route(db, self).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) async fn get_next_first_station(&self, db: &SqlitePool) -> Option<Station> {
 | 
				
			||||||
 | 
					        let Ok(row) = sqlx::query(&format!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					            SELECT 
 | 
				
			||||||
 | 
					                s.id AS next_first_station_id,
 | 
				
			||||||
 | 
					                COUNT(g.id) AS group_count
 | 
				
			||||||
 | 
					            FROM station s
 | 
				
			||||||
 | 
					            JOIN route_station rs ON s.id = rs.station_id
 | 
				
			||||||
 | 
					            LEFT JOIN 'group' g ON s.id = g.first_station_id
 | 
				
			||||||
 | 
					            WHERE rs.route_id = {}
 | 
				
			||||||
 | 
					            GROUP BY s.id
 | 
				
			||||||
 | 
					            ORDER BY group_count ASC, s.id ASC
 | 
				
			||||||
 | 
					           LIMIT 1",
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					        .fetch_one(db)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            return None; // No station for route exists
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let next_first_station_id = row.get::<i64, _>("next_first_station_id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Some(
 | 
				
			||||||
 | 
					            Station::find_by_id(db, next_first_station_id)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .expect("db constraints"),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
					pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
use super::Route;
 | 
					use super::Route;
 | 
				
			||||||
use crate::{err, page, station::Station, succ};
 | 
					use crate::{err, page, station::Station, succ};
 | 
				
			||||||
use axum::{
 | 
					use axum::{
 | 
				
			||||||
 | 
					    Form, Router,
 | 
				
			||||||
    extract::State,
 | 
					    extract::State,
 | 
				
			||||||
    response::{IntoResponse, Redirect},
 | 
					    response::{IntoResponse, Redirect},
 | 
				
			||||||
    routing::{get, post},
 | 
					    routing::{get, post},
 | 
				
			||||||
    Form, Router,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use maud::{html, Markup, PreEscaped};
 | 
					use maud::{Markup, PreEscaped, html};
 | 
				
			||||||
use serde::Deserialize;
 | 
					use serde::Deserialize;
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
use std::sync::Arc;
 | 
					use std::sync::Arc;
 | 
				
			||||||
@@ -24,7 +24,9 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
 | 
				
			|||||||
            em { "Routen " }
 | 
					            em { "Routen " }
 | 
				
			||||||
            "definieren welche "
 | 
					            "definieren welche "
 | 
				
			||||||
            a href="/station" { "Stationen" }
 | 
					            a href="/station" { "Stationen" }
 | 
				
			||||||
            " von den Teilnehmern in welcher Reihenfolge abgeklappert werden sollen. Wenn es verschiedene Kategorien (zB Kinder- und Erwachsenenwertung) gibt, kannst du auch mehrere Routen mit (teils) überlappenden Stationen erstellen."
 | 
					            " von den "
 | 
				
			||||||
 | 
					            a href="/group" { "Gruppen" }
 | 
				
			||||||
 | 
					            " in welcher Reihenfolge abgeklappert werden sollen. Wenn es verschiedene Kategorien (zB Kinder- und Erwachsenenwertung) gibt, kannst du auch mehrere Routen mit (teils) überlappenden Stationen erstellen."
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        ol {
 | 
					        ol {
 | 
				
			||||||
            @for route in &routes{
 | 
					            @for route in &routes{
 | 
				
			||||||
@@ -125,6 +127,8 @@ async fn view(
 | 
				
			|||||||
    let cur_stations = route.stations(&db).await;
 | 
					    let cur_stations = route.stations(&db).await;
 | 
				
			||||||
    let stations_not_in_route = route.stations_not_in_route(&db).await;
 | 
					    let stations_not_in_route = route.stations_not_in_route(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let groups = route.groups(&db).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let content = html! {
 | 
					    let content = html! {
 | 
				
			||||||
        h1 {
 | 
					        h1 {
 | 
				
			||||||
            a href="/route" { "↩️" }
 | 
					            a href="/route" { "↩️" }
 | 
				
			||||||
@@ -185,6 +189,27 @@ async fn view(
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        h2 { "Gruppen mit dieser Route" }
 | 
				
			||||||
 | 
					        @if groups.is_empty() {
 | 
				
			||||||
 | 
					            article {
 | 
				
			||||||
 | 
					                "Noch keine Gruppe ist dieser Route zugeteilt."
 | 
				
			||||||
 | 
					                a role="button" href="/group" {
 | 
				
			||||||
 | 
					                    "Zu den Gruppen"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } @else {
 | 
				
			||||||
 | 
					            ol {
 | 
				
			||||||
 | 
					                @for group in &groups {
 | 
				
			||||||
 | 
					                    li {
 | 
				
			||||||
 | 
					                        a href=(format!("/group/{}", group.id)) {
 | 
				
			||||||
 | 
					                            (group.name)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    Ok(page(content, session, false).await)
 | 
					    Ok(page(content, session, false).await)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,6 +127,16 @@ impl Station {
 | 
				
			|||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn is_in_route(&self, db: &SqlitePool, route: &Route) -> bool {
 | 
				
			||||||
 | 
					        for r in self.routes(db).await {
 | 
				
			||||||
 | 
					            if r.id == route.id {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
					pub(super) fn routes() -> Router<Arc<SqlitePool>> {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user