main #1

Open
philipp wants to merge 71 commits from Ruderverein-Donau-Linz/rowt:main into main
33 changed files with 1306 additions and 809 deletions

View File

@ -11,7 +11,7 @@ env:
jobs:
test:
runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-2024-12-05
container: git.hofer.link/philipp/ci-images:rust-latest
steps:
- uses: actions/checkout@v3
- name: Run Test DB Script
@ -37,7 +37,7 @@ jobs:
deploy-staging:
runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-2024-12-05
container: git.hofer.link/philipp/ci-images:rust-latest
needs: [test]
if: github.ref == 'refs/heads/staging'
steps:
@ -80,7 +80,7 @@ jobs:
deploy-main:
runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-2024-12-05
container: git.hofer.link/philipp/ci-images:rust-latest
needs: [test]
if: github.ref == 'refs/heads/main'
steps:

1146
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,18 +13,18 @@ rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
log = "0.4"
env_logger = "0.11"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]}
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"]}
chrono-tz = "0.9"
chrono-tz = "0.10"
tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5"
futures = "0.3"
lettre = "0.11"
csv = "1.3"
itertools = "0.13"
itertools = "0.14"
job_scheduler_ng = "2.0"
ureq = { version = "2.9", features = ["json"] }
regex = "1.10"

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test";
import { test, expect, } from "@playwright/test";
import type { Page } from "@playwright/test";
test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth");

View File

@ -11,16 +11,16 @@ pub mod rest;
pub mod scheduled;
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
pub(crate) const RENNRUDERBEITRAG: i32 = 11000;
pub(crate) const BOAT_STORAGE: i32 = 4500;
pub(crate) const FAMILY_TWO: i32 = 30000;
pub(crate) const FAMILY_THREE_OR_MORE: i32 = 35000;
pub(crate) const STUDENT_OR_PUPIL: i32 = 8000;
pub(crate) const REGULAR: i32 = 22000;
pub(crate) const UNTERSTUETZEND: i32 = 2500;
pub(crate) const FOERDERND: i32 = 8500;
pub(crate) const SCHECKBUCH: i32 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i32 = 3000;
pub(crate) const RENNRUDERBEITRAG: i64 = 11000;
pub(crate) const BOAT_STORAGE: i64 = 4500;
pub(crate) const FAMILY_TWO: i64 = 30000;
pub(crate) const FAMILY_THREE_OR_MORE: i64 = 35000;
pub(crate) const STUDENT_OR_PUPIL: i64 = 8000;
pub(crate) const REGULAR: i64 = 22000;
pub(crate) const UNTERSTUETZEND: i64 = 2500;
pub(crate) const FOERDERND: i64 = 8500;
pub(crate) const SCHECKBUCH: i64 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000;
#[cfg(test)]
#[macro_export]

View File

@ -1,5 +1,3 @@
use std::collections::HashMap;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
@ -7,6 +5,93 @@ use crate::tera::board::boathouse::FormBoathouseToAdd;
use super::boat::Boat;
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathousePlace {
boat: Boat,
boathouse_id: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseRack {
boats: [Option<BoathousePlace>; 12],
}
impl BoathouseRack {
fn new() -> Self {
let boats = [
None, None, None, None, None, None, None, None, None, None, None, None,
];
Self { boats }
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
self.boats[boathouse.level as usize] = Some(BoathousePlace {
boat: Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
boathouse_id: boathouse.id,
});
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseSide {
mountain: BoathouseRack,
water: BoathouseRack,
}
impl BoathouseSide {
fn new() -> Self {
Self {
mountain: BoathouseRack::new(),
water: BoathouseRack::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.side.as_str() {
"mountain" => self.mountain.add(db, boathouse).await,
"water" => self.water.add(db, boathouse).await,
_ => panic!("db constraint failed"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseAisles {
mountain: BoathouseSide,
middle: BoathouseSide,
water: BoathouseSide,
}
impl BoathouseAisles {
fn new() -> Self {
Self {
mountain: BoathouseSide::new(),
middle: BoathouseSide::new(),
water: BoathouseSide::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.aisle.as_str() {
"water" => self.water.add(db, boathouse).await,
"middle" => self.middle.add(db, boathouse).await,
"mountain" => self.mountain.add(db, boathouse).await,
_ => panic!("db constraint failed"),
};
}
pub async fn from(db: &SqlitePool, boathouses: Vec<Boathouse>) -> Self {
let mut ret = BoathouseAisles::new();
for boathouse in boathouses {
ret.add(db, boathouse).await;
}
ret
}
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boathouse {
pub id: i64,
@ -17,54 +102,7 @@ pub struct Boathouse {
}
impl Boathouse {
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
let mut mountain = HashMap::new();
mountain.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
mountain.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("mountain-aisle", mountain);
let mut middle = HashMap::new();
middle.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
middle.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("middle-aisle", middle);
let mut water = HashMap::new();
water.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
water.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("water-aisle", water);
pub async fn get(db: &SqlitePool) -> BoathouseAisles {
let boathouses = sqlx::query_as!(
Boathouse,
"SELECT id, boat_id, aisle, side, level FROM boathouse"
@ -73,21 +111,7 @@ impl Boathouse {
.await
.unwrap(); //TODO: fixme
for boathouse in boathouses {
let aisle = ret
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
.unwrap();
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
side[boathouse.level as usize] = Some((
boathouse.id,
Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
));
}
ret
BoathouseAisles::from(db, boathouses).await
}
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {

View File

@ -56,6 +56,44 @@ impl BoatReservation {
.await
.ok()
}
pub async fn for_day(db: &SqlitePool, day: NaiveDate) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
Self,
"
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE end_date >= ? AND start_date <= ?
", day, day
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in boatreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
.await
.unwrap();
res.push(BoatReservationWithDetails {
reservation,
boat,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
@ -95,13 +133,13 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
}
res
}
pub async fn all_future_with_groups(
db: &SqlitePool,
pub fn with_groups(
reservations: Vec<BoatReservationWithDetails>,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
@ -120,6 +158,12 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
grouped_reservations
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let reservations = Self::all_future(db).await;
Self::with_groups(reservations)
}
pub async fn create(
db: &SqlitePool,

View File

@ -96,8 +96,8 @@ FROM trip WHERE planned_event_id = ?
.unwrap()
.into_iter()
.map(|r| Registration {
name: r.name,
registered_at: r.registered_at,
name: r.name.unwrap(),
registered_at: r.registered_at.unwrap(),
is_guest: false,
is_real_guest: false,
})

View File

@ -74,7 +74,7 @@ GROUP BY family.id;"
}
}
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
pub async fn amount_family_members(&self, db: &SqlitePool) -> i64 {
sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id

View File

@ -33,8 +33,7 @@ impl PartialEq for Logbook {
pub(crate) enum Filter {
SingleDayOnly,
MultiDazOnly,
None,
MultiDayOnly,
}
#[derive(FromForm, Debug, Clone)]
@ -362,12 +361,13 @@ ORDER BY departure DESC
}
}
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year(
db: &SqlitePool,
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
min_distance: i32,
year: i32,
filter: Filter,
exclude_last_log: bool,
) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = sqlx::query_as(
&format!("
@ -378,7 +378,7 @@ ORDER BY departure DESC
ORDER BY arrival DESC
", user.id, min_distance, year)
)
.fetch_all(db)
.fetch_all(db.deref_mut())
.await
.unwrap(); //TODO: fixme
@ -389,19 +389,20 @@ ORDER BY departure DESC
match filter {
Filter::SingleDayOnly => {
if trip_days == 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
Filter::MultiDazOnly => {
Filter::MultiDayOnly => {
if trip_days > 0 {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
Filter::None => {
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
}
}
}
if exclude_last_log {
ret.pop();
}
ret
}

View File

@ -298,7 +298,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
"Dieser gilt für die gesamte Familie ({}). Diese Mail wird an alle Familienmitglieder verschickt, bezahlen müsst ihr natürlich nur 1x.\n",
fees.name
))
}

View File

@ -11,6 +11,8 @@ use self::{
waterlevel::Waterlevel,
weather::Weather,
};
use boatreservation::{BoatReservation, BoatReservationWithDetails};
use std::collections::HashMap;
pub mod boat;
pub mod boatdamage;
@ -48,6 +50,7 @@ pub struct Day {
regular_sees_this_day: bool,
max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>,
boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
}
impl Day {
@ -64,6 +67,9 @@ impl Day {
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
}
} else {
Self {
@ -74,6 +80,9 @@ impl Day {
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
}
}
}

View File

@ -194,6 +194,15 @@ ORDER BY read_at DESC, created_at DESC;
}
}
}
pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
let notifications = Self::for_user(db, user).await;
for notification in notifications {
notification.mark_read(db).await;
}
}
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",

View File

@ -1,63 +1,64 @@
use crate::model::{logbook::Logbook, stat::Stat, user::User};
use serde::Serialize;
#[derive(Serialize, PartialEq, Debug)]
pub(crate) enum Level {
NONE,
BRONZE,
SILVER,
GOLD,
DIAMOND,
DONE,
None,
Bronze,
Silver,
Gold,
Diamond,
Done,
}
impl Level {
fn required_km(&self) -> i32 {
match self {
Level::BRONZE => 40000,
Level::SILVER => 80000,
Level::GOLD => 100000,
Level::DIAMOND => 200000,
Level::DONE => 0,
Level::NONE => 0,
Level::Bronze => 40_000,
Level::Silver => 80_000,
Level::Gold => 100_000,
Level::Diamond => 200_000,
Level::Done => 0,
Level::None => 0,
}
}
fn next_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() {
Level::BRONZE
} else if km < Level::SILVER.required_km() {
Level::SILVER
} else if km < Level::GOLD.required_km() {
Level::GOLD
} else if km < Level::DIAMOND.required_km() {
Level::DIAMOND
if km < Level::Bronze.required_km() {
Level::Bronze
} else if km < Level::Silver.required_km() {
Level::Silver
} else if km < Level::Gold.required_km() {
Level::Gold
} else if km < Level::Diamond.required_km() {
Level::Diamond
} else {
Level::DONE
Level::Done
}
}
pub(crate) fn curr_level(km: i32) -> Self {
if km < Level::BRONZE.required_km() {
Level::NONE
} else if km < Level::SILVER.required_km() {
Level::BRONZE
} else if km < Level::GOLD.required_km() {
Level::SILVER
} else if km < Level::DIAMOND.required_km() {
Level::GOLD
if km < Level::Bronze.required_km() {
Level::None
} else if km < Level::Silver.required_km() {
Level::Bronze
} else if km < Level::Gold.required_km() {
Level::Silver
} else if km < Level::Diamond.required_km() {
Level::Gold
} else {
Level::DIAMOND
Level::Diamond
}
}
pub(crate) fn desc(&self) -> &str {
match self {
Level::BRONZE => "Bronze",
Level::SILVER => "Silber",
Level::GOLD => "Gold",
Level::DIAMOND => "Diamant",
Level::DONE => "",
Level::NONE => "-",
Level::Bronze => "Bronze",
Level::Silver => "Silber",
Level::Gold => "Gold",
Level::Diamond => "Diamant",
Level::Done => "",
Level::None => "-",
}
}
}
@ -85,3 +86,19 @@ impl Next {
}
}
}
pub(crate) async fn new_level_with_last_log(
db: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
user: &User,
) -> Option<String> {
let rowed_km = Stat::total_km_tx(db, user).await.rowed_km;
if let Some(last_logbookentry) = Logbook::completed_with_user_tx(db, user).await.last() {
let last_trip_km = last_logbookentry.logbook.distance_in_km.unwrap();
if Level::curr_level(rowed_km) != Level::curr_level(rowed_km - last_trip_km as i32) {
return Some(Level::curr_level(rowed_km).desc().to_string());
}
}
None
}

View File

@ -2,7 +2,7 @@ use std::cmp;
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::SqlitePool;
use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
@ -111,11 +111,44 @@ pub(crate) struct Status {
}
impl Status {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
fn calc(
agebracket: &AgeBracket,
rowed_km: i32,
single_day_trips_over_required_distance: usize,
multi_day_trips_over_required_distance: usize,
year: i32,
) -> Self {
let category = agebracket.cat().to_string();
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance >= 1
|| single_day_trips_over_required_distance >= 2);
Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance: vec![],
single_day_trips_over_required_distance: vec![],
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
}
}
pub(crate) async fn for_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
exclude_last_log: bool,
) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else {
return None;
};
let category = agebracket.cat().to_string();
let year = if Local::now().month() == 1 {
Local::now().year() - 1
@ -123,44 +156,66 @@ impl Status {
Local::now().year()
};
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km;
let single_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_single_day_in_km(),
year,
Filter::SingleDayOnly,
exclude_last_log,
)
.await;
let multi_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_multi_day_in_km(),
year,
Filter::MultiDazOnly,
Filter::MultiDayOnly,
exclude_last_log,
)
.await;
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance.len() >= 1
|| single_day_trips_over_required_distance.len() >= 2);
let ret = Self::calc(
&agebracket,
rowed_km,
single_day_trips_over_required_distance.len(),
multi_day_trips_over_required_distance.len(),
year,
);
Some(Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance,
single_day_trips_over_required_distance,
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
..ret
})
}
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_user_tx(&mut tx, user, false).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn completed_with_last_log(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(status) = Self::for_user_tx(db, user, false).await {
// if user has agebracket...
if status.achieved {
// ... and has achieved the 'Fahrtenabzeichen'
let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap();
if !without_last_entry.achieved {
// ... and this wasn't the case before the last logentry
return true;
}
}
}
false
}
}

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::{collections::HashMap, ops::DerefMut};
use crate::model::user::User;
use chrono::Datelike;
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction};
use super::boat::Boat;
@ -98,6 +98,7 @@ ORDER BY
#[derive(FromRow, Serialize, Clone)]
pub struct Stat {
name: String,
pub(crate) amount_trips: i32,
pub(crate) rowed_km: i32,
}
@ -108,9 +109,11 @@ impl Stat {
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let rowed_km = sqlx::query(&format!(
// proper guests
let guests = sqlx::query(&format!(
"
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km,
SUM(b.amount_seats - COALESCE(m.member_count, 0)) AS amount_trips
FROM logbook l
JOIN boat b ON l.boat_id = b.id
LEFT JOIN (
@ -123,12 +126,15 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.exter
))
.fetch_one(db)
.await
.unwrap()
.get::<i64, usize>(0) as i32;
.unwrap();
let rowed_km_guests = sqlx::query(&format!(
let guest_km: i32 = guests.get(0);
let guest_amount_trips: i32 = guests.get(1);
// e.g. scheckbücher
let guest_user = sqlx::query(&format!(
"
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM user u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
@ -145,15 +151,27 @@ AND u.name != 'Externe Steuerperson';
))
.fetch_one(db)
.await
.unwrap()
.get::<i64, usize>(0) as i32;
.unwrap();
let guest_user_km: i32 = guest_user.get(0);
let guest_user_amount_trips: i32 = guest_user.get(1);
Stat {
name: "Gäste".into(),
rowed_km: rowed_km + rowed_km_guests,
amount_trips: guest_amount_trips + guest_user_amount_trips,
rowed_km: guest_km + guest_user_km,
}
}
pub async fn trips_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.amount_trips;
}
sum
}
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
@ -172,7 +190,7 @@ AND u.name != 'Externe Steuerperson';
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM (
SELECT * FROM user
WHERE id IN (
@ -194,16 +212,17 @@ ORDER BY rowed_km DESC, u.name;
.into_iter()
.map(|row| Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
})
.collect()
}
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat {
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM (
SELECT * FROM user
WHERE id={}
@ -214,17 +233,29 @@ WHERE l.distance_in_km IS NOT NULL;
",
user.id
))
.fetch_one(db)
.fetch_one(db.deref_mut())
.await
.unwrap();
Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::total_km_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn person_tx(
db: &mut Transaction<'_, Sqlite>,
year: Option<i32>,
user: &User,
) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
@ -232,7 +263,7 @@ WHERE l.distance_in_km IS NOT NULL;
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM (
SELECT * FROM user
WHERE id={}
@ -243,15 +274,23 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
",
user.id
))
.fetch_one(db)
.fetch_one(db.deref_mut())
.await
.unwrap();
Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::person_tx(&mut tx, year, user).await;
tx.commit().await.unwrap();
ret
}
}
#[derive(Debug, Serialize)]

View File

@ -287,10 +287,8 @@ WHERE day=?
return Err(TripUpdateError::NotYourTrip);
}
if update.trip_type != Some(4) {
if !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed);
}
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed);
}
let Some(trip_details_id) = update.trip.trip_details_id else {

58
src/model/user/fee.rs Normal file
View File

@ -0,0 +1,58 @@
use super::User;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i64,
pub parts: Vec<(String, i64)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i64) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}

View File

@ -15,14 +15,25 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
stat::Stat, tripdetails::TripDetails, Day,
family::Family,
log::Log,
logbook::Logbook,
mail::Mail,
notification::Notification,
personal::{equatorprice, rowingbadge},
role::Role,
stat::Stat,
tripdetails::TripDetails,
Day,
};
use crate::{
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
};
use fee::Fee;
mod fee;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
@ -49,7 +60,7 @@ pub struct User {
pub struct UserWithDetails {
#[serde(flatten)]
pub user: User,
pub amount_unread_notifications: i32,
pub amount_unread_notifications: i64,
pub allowed_to_steer: bool,
pub on_water: bool,
pub roles: Vec<String>,
@ -83,62 +94,6 @@ pub enum LoginError {
DeserializationError,
}
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i32) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}
impl User {
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
@ -342,7 +297,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
}
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await {
if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await
{
return None;
}
if self.deleted {
@ -378,13 +336,16 @@ ASKÖ Ruderverein Donau Linz", self.name),
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await {
if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await
{
return fee;
}
if self.has_role(db, "Rennrudern").await {
if self.has_role(db, "half-rennrudern").await {
fee.add("Rennruderbeitrag (1/2 Preis) ".into(), RENNRUDERBEITRAG / 2);
} else {
} else if !self.has_role(db, "renntrainer").await {
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
}
}
@ -434,19 +395,17 @@ ASKÖ Ruderverein Donau Linz", self.name),
}
} else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0);
} else if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else {
if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
}
fee
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i32 {
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
self.id
@ -457,7 +416,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
.count
}
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i64 {
sqlx::query!(
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
self.id
@ -1033,39 +992,76 @@ ORDER BY last_access DESC
smtp_pw: &str,
) {
if self.has_role_tx(db, "scheckbuch").await {
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len();
if amount_trips == 5 {
if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
let amount_trips = Logbook::completed_with_user_tx(db, self).await.len();
match amount_trips {
5 => {
if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
}
Notification::create_for_steering_people_tx(
db,
&format!(
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
self.name
),
"Scheckbuch fertig",
None,None
)
.await;
}
Notification::create_for_steering_people_tx(
db,
&format!(
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
self.name
),
"Scheckbuch fertig",
None,None
)
.await;
} else if amount_trips > 5 {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
self.name, amount_trips
),
"Scheckbuch überfertig",
None,None
)
.await;
a if a > 5 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
self.name, amount_trips
),
"Scheckbuch überfertig",
None,None
)
.await;
}
_ => {}
}
}
// TODO: check fahrtenabzeichen fertig?
// TODO: check äquatorpreis geschafft?
// check fahrtenabzeichen fertig
if rowingbadge::Status::completed_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für das diesjährige Fahrtenabzeichen erfüllt.",
self.name
),
"Fahrtenabzeichen geschafft",
None,None
)
.await;
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
}
// check äquatorpreis geschafft?
if let Some(level) = equatorprice::new_level_with_last_log(db, self).await {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&board,
&format!(
"Lieber Vorstand, zur Info: {} hat gerade alle Anforderungen für den Äquatorpreis in {level} geschafft.",
self.name
),
"Äquatorpreis",
None,None
)
.await;
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
}
}
}

View File

@ -408,7 +408,7 @@ async fn create_scheckbuch(
format!("{} created new scheckbuch: {data:?}", admin.name),
)
.await;
Flash::success(Redirect::to("/admin/user/scheckbuch"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
}
#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
@ -458,7 +458,7 @@ async fn schnupper_to_scheckbuch(
),
)
.await;
Flash::success(Redirect::to("/admin/schnupper"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
}
pub fn routes() -> Vec<Route> {

View File

@ -18,12 +18,12 @@ async fn index(
context.insert("flash", &msg.into_inner());
}
let role = Role::find_by_name(&db, "Donau Linz").await.unwrap();
let users = User::all_with_role(&db, &role).await;
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let users = User::all_with_role(db, &role).await;
let mut people = Vec::new();
let mut rowingbadge_year = None;
for user in users {
let achievement = Achievements::for_user(&db, &user).await;
let achievement = Achievements::for_user(db, &user).await;
if let Some(badge) = &achievement.rowingbadge {
rowingbadge_year = Some(badge.year);
}

View File

@ -148,13 +148,13 @@ async fn fixed<'r>(
#[derive(FromForm)]
pub struct FormBoatDamageVerified<'r> {
pub desc: &'r str,
desc: &'r str,
}
#[post("/<boatdamage_id>/verified", data = "<data>")]
async fn verified<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageFixed<'r>>,
data: Form<FormBoatDamageVerified<'r>>,
boatdamage_id: i32,
techuser: TechUser,
) -> Flash<Redirect> {

View File

@ -217,7 +217,7 @@ async fn new_thirty(
eprintln!("Failed to persist file: {:?}", e);
}
let result = data.result.trim_start_matches(|c| c == '0' || c == ' ');
let result = data.result.trim_start_matches(['0', ' ']);
sqlx::query!(
"UPDATE user SET dirty_thirty = ? where id = ?",
@ -318,7 +318,7 @@ async fn new_dozen(
if let Err(e) = data.proof.move_copy_to(file_path).await {
eprintln!("Failed to persist file: {:?}", e);
}
let result = data.result.trim_start_matches(|c| c == '0' || c == ' ');
let result = data.result.trim_start_matches(['0', ' ']);
let result = if result.contains(":") || result.contains(".") {
format_time(result)
} else {

View File

@ -27,7 +27,7 @@ use crate::{
},
logtype::LogType,
trip::Trip,
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
user::{DonauLinzUser, User, UserWithDetails, VorstandUser},
},
tera::Config,
};
@ -118,7 +118,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
}
#[get("/show?<year>", rank = 2)]
async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Template {
async fn show_for_year(db: &State<SqlitePool>, user: VorstandUser, year: i32) -> Template {
let logs = Logbook::completed_in_year(db, year).await;
Template::render(
@ -312,7 +312,7 @@ async fn update(
let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
};
match logbook.update(db, data.clone(), &user.user).await {

View File

@ -19,7 +19,7 @@ async fn cal_registered(
return Err("Invalid".into());
};
if &user.user_token != uuid {
if user.user_token != uuid {
return Err("Invalid".into());
}

View File

@ -27,6 +27,12 @@ async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) ->
}
}
pub fn routes() -> Vec<Route> {
routes![mark_read]
#[get("/read/all")]
async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
Notification::mark_all_read(db, &user).await;
Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
}
pub fn routes() -> Vec<Route> {
routes![mark_read, mark_all_read]
}

View File

@ -32,13 +32,14 @@ async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Templa
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await;
let club_trips = Stat::trips_people(db, year).await;
let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user).await;
let kiosk = false;
Template::render(
"stat.people",
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km, club_trips),
)
}

View File

@ -5,6 +5,7 @@
<h1 class="h1">Users</h1>
{% if allowed_to_edit %}
<form action="/admin/user/new"
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
method="post"
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
<div class="w-full">

View File

@ -4,13 +4,12 @@
{% extends "base" %}
{% macro show_place(aisle_name, side_name, level) %}
<li class="truncate p-2 flex relative w-full">
{% set aisle = aisle_name ~ "-aisle" %}
{% set place = boathouse[aisle][side_name] %}
{% set place = boathouse[aisle_name][side_name].boats %}
{% if place[level] %}
{{ place[level].1.name }}
{{ place[level].boat.name }}
{% if "admin" in loggedin_user.roles %}
<a class="btn btn-primary absolute end-0"
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
{% endif %}
{% elif boats | length > 0 %}
{% if "admin" in loggedin_user.roles %}

View File

@ -32,7 +32,7 @@
<h2 class="h2">Nachrichten</h2>
{% if loggedin_user.amount_unread_notifications > 10 %}
<div class="text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-center pb-3 px-3">
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte einen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).
Du hast viele ungelesene Benachrichtigungen. Um deine Oberfläche übersichtlich zu halten und wichtige Updates nicht zu verpassen, nimm dir bitte in Zukunft einen kurzen Moment Zeit sie zu überprüfen und als gelesen zu markieren (&#10003;).<br /><a href="/notification/read/all" class="underline">Du kannst hier ausnahmsweise alle als gelesen markieren.</a>
</div>
{% endif %}
<div class="divide-y">

View File

@ -5,7 +5,7 @@
<div class="max-w-screen-lg w-full">
<h1 class="h1">
Logbuch
{% if loggedin_user and "admin" in loggedin_user.roles %}
{% if loggedin_user and "Vorstand" in loggedin_user.roles %}
<select id="yearSelect"
onchange="changeYear()"
style="background: transparent;

View File

@ -89,8 +89,29 @@
</small>
{% endif %}
</h2>
{% if day.events | length > 0 or day.trips | length > 0 %}
{% if day.events | length > 0 or day.trips | length > 0 or day.boat_reservations | length > 0 %}
<div class="grid grid-cols-1 gap-3 mb-3">
{# --- START Boatreservations--- #}
{% for _, reservations_for_event in day.boat_reservations %}
{% set reservation = reservations_for_event[0] %}
<div class="pt-2 px-3 border-gray-200">
<div class="flex justify-between items-center">
<div class="mr-1">
<span class="text-primary-900 dark:text-white">
⏳ {{ reservation.time_desc }} <small class="text-gray-600 dark:text-gray-100">({{ reservation.user_applicant.name }})</small><br/>
<strong>
{% for reservation in reservations_for_event -%}
{{ reservation.boat.name }}
{%- if not loop.last %} + {% endif -%}
{% endfor -%}
</strong>
</span>
<small class="text-gray-600 dark:text-gray-100">(Reservierung - {{ reservation.usage}})</small>
</div>
</div>
</div>
{% endfor %}
{# --- END Boatreservations--- #}
{# --- START Events --- #}
{% if day.events | length > 0 %}
{% for event in day.events | sort(attribute="planned_starting_time") %}

View File

@ -20,7 +20,15 @@
</div>
<div id="filter-result-js" class="search-result"></div>
<div class="border-r border-l border-gray-200 dark:border-primary-600">
{% set_global km = 0 %} {% set_global index = 1 %}
<div class="border-t border-gray-200 dark:border-primary-600 bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="false"
data-filter="Header">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"><b>#</b></span>
<span class="grow"><b>Name</b></span>
<span class="pl-3 w-20 text-right"><b>km</b></span>
<span class="pl-3 w-20 text-right"><b>Fahrten</b></span>
</div>
{% set_global km = 0 %} {% set_global km = 0 %} {% set_global index = 1 %}
{% for s in stat %}
<div class="border-t border-gray-200 dark:border-primary-600 bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="true"
@ -34,68 +42,62 @@
{% endif %}
</span>
<span class="grow">{{ s.name }}</span>
<span>{{ s.rowed_km }} km</span>
<span class="pl-3 w-20 text-right">{{ s.rowed_km }}</span>
<span class="pl-3 w-20 text-right">{{ s.amount_trips }}</span>
{% set_global km = s.rowed_km %}
</div>
{% endfor %}
<div class="border-t border-gray-200 dark:border-primary-600 bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="true"
<div class="border-t border-black dark:border-white bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="false"
data-filter="Summe Vereinsmitglieder">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"></span>
<span class="grow"><b>Summe Vereinsmitglieder</b></span>
<span><b>{{ club_km }} km</b></span>
<span class="pl-3 w-20 text-right"><b>{{ club_km }}</b></span>
<span class="pl-3 w-20 text-right"><b>{{ club_trips }}</b></span>
</div>
<div class="border-t border-gray-200 dark:border-primary-600 bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="true"
data-filterable="false"
data-filter="Summe {{ guest_km.name }}">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"></span>
<span class="grow"><b>Summe {{ guest_km.name }}</b></span>
<span><b>{{ guest_km.rowed_km }} km</b></span>
<span class="pl-3 w-20 text-right"><b>{{ guest_km.rowed_km }}</b></span>
<span class="pl-3 w-20 text-right"><b>{{ guest_km.amount_trips }}</b></span>
</div>
<div class="border-t border-gray-200 dark:border-primary-600 border-b bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
data-filterable="true"
data-filterable="false"
data-filter="Gesamtsumme">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"></span>
<span class="grow"><b>Gesamtsumme</b></span>
<span><b>{{ club_km + guest_km.rowed_km }} km</b></span>
<span class="pl-3 w-20 text-right"><b>{{ club_km + guest_km.rowed_km }}</b></span>
<span class="pl-3 w-20 text-right"><b>{{ guest_km.amount_trips + club_trips }}</b></span>
</div>
</div>
<div id="container" class="w-full"></div>
</div>
<script>
{% if personal %}
const data = [
{%- for p in personal %}{ date: '{{p.date}}', km: {{p.km}} },{%- endfor %}
]
sessionStorage.setItem('userStats', JSON.stringify(data));
{% endif %}
function getYearFromURL() {
var queryParams = new URLSearchParams(window.location.search);
return queryParams.get('year');
}
function populateYears() {
var select = document.getElementById('yearSelect');
var currentYear = new Date().getFullYear();
var selectedYear = getYearFromURL() || currentYear;
for (var year = 1977; year <= currentYear; year++) {
var option = document.createElement('option');
option.value = option.textContent = year;
if (year == selectedYear) {
option.selected = true;
}
select.appendChild(option);
}
}
function changeYear() {
var selectedYear = document.getElementById('yearSelect').value;
window.location.href = '?year=' + selectedYear;
}
// Call this function when the page loads
populateYears();
function getYearFromURL() {
var queryParams = new URLSearchParams(window.location.search);
return queryParams.get('year');
}
function populateYears() {
var select = document.getElementById('yearSelect');
var currentYear = new Date().getFullYear();
var selectedYear = getYearFromURL() || currentYear;
for (var year = 1977; year <= currentYear; year++) {
var option = document.createElement('option');
option.value = option.textContent = year;
if (year == selectedYear) {
option.selected = true;
}
select.appendChild(option);
}
}
function changeYear() {
var selectedYear = document.getElementById('yearSelect').value;
window.location.href = '?year=' + selectedYear;
}
populateYears();
</script>
<script src="/public/logbook.js"></script>
{% endblock content %}