add group routes
This commit is contained in:
parent
272b5eb61c
commit
a1d5509909
@ -39,7 +39,7 @@ CREATE TABLE group_station (
|
||||
station_id INTEGER,
|
||||
points INTEGER,
|
||||
notes TEXT,
|
||||
arrived_at DATETIME,
|
||||
arrived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
started_at DATETIME,
|
||||
left_at DATETIME,
|
||||
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 maud::{html, Markup};
|
||||
use axum::{Router, body::Body, response::Response, routing::get};
|
||||
use maud::{Markup, html};
|
||||
use partials::page;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
|
||||
|
||||
pub(crate) mod group;
|
||||
mod partials;
|
||||
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_rules! err {
|
||||
@ -111,6 +120,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) {
|
||||
.route("/", get(index))
|
||||
.nest("/station", station::routes())
|
||||
.nest("/route", route::routes())
|
||||
.nest("/group", group::routes())
|
||||
.route("/pico.css", get(serve_pico_css))
|
||||
.route("/style.css", get(serve_my_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 serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod web;
|
||||
@ -9,11 +9,11 @@ mod web;
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Route {
|
||||
pub(crate) id: i64,
|
||||
name: String,
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
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;")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@ -126,7 +126,7 @@ DROP TABLE temp_pos;",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stations(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
pub(crate) async fn stations(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
sqlx::query_as::<_, Station>(
|
||||
"
|
||||
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
|
||||
.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>> {
|
||||
|
@ -1,12 +1,12 @@
|
||||
use super::Route;
|
||||
use crate::{err, page, station::Station, succ};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
@ -24,7 +24,9 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
em { "Routen " }
|
||||
"definieren welche "
|
||||
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 {
|
||||
@for route in &routes{
|
||||
@ -125,6 +127,8 @@ async fn view(
|
||||
let cur_stations = route.stations(&db).await;
|
||||
let stations_not_in_route = route.stations_not_in_route(&db).await;
|
||||
|
||||
let groups = route.groups(&db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
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)
|
||||
}
|
||||
|
@ -127,6 +127,16 @@ impl Station {
|
||||
.await
|
||||
.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>> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user