Compare commits

..

22 Commits

Author SHA1 Message Date
2ebfe7564a Merge branch 'staging' into notification
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 14:00:25 +01:00
68a1153885 improve boathouse functionality, fixes #261
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m22s
CI/CD Pipeline / deploy-staging (push) Successful in 4m4s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 13:58:42 +01:00
c1411b3a76 re-wording
Some checks failed
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 8m8s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
2024-03-20 13:41:14 +01:00
5533106aca show total club km
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-03-20 13:34:21 +01:00
a17a08d018 create temporary boat reservation feature
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m13s
CI/CD Pipeline / deploy-staging (push) Successful in 4m4s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 09:15:58 +01:00
c9eecf0a29 create temporary boat reservation feature
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-03-20 09:14:26 +01:00
9c1bcbc5f5 don't count 'externe steuerkilometer' as guest kms
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m16s
CI/CD Pipeline / deploy-staging (push) Successful in 4m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 08:58:51 +01:00
5cedbc078d fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m11s
CI/CD Pipeline / deploy-staging (push) Successful in 19m53s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 00:48:27 +01:00
0aa32654b0 push
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m43s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-20 00:11:11 +01:00
39a8a1563c bit nicer logs
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m54s
CI/CD Pipeline / deploy-staging (push) Successful in 19m33s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-19 09:59:46 +01:00
8645612718 spacing
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m41s
CI/CD Pipeline / deploy-staging (push) Successful in 6m19s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-17 21:08:55 +01:00
c068713572 add more logs
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m15s
CI/CD Pipeline / deploy-staging (push) Successful in 4m56s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-16 20:42:41 +01:00
d1fa3e0336 board members can open and close trips for others
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m15s
CI/CD Pipeline / deploy-staging (push) Successful in 5m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-16 19:47:04 +01:00
4d634ce313 spacing again
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m25s
CI/CD Pipeline / deploy-staging (push) Successful in 4m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-16 10:56:37 +01:00
09a0354eee fix whitespace
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m35s
CI/CD Pipeline / deploy-staging (push) Successful in 5m0s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-16 09:55:58 +01:00
730559f2f4 a bit nicer mail sending layout
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m49s
CI/CD Pipeline / deploy-staging (push) Successful in 4m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-15 15:29:50 +01:00
2ab2472e66 fix typo
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Successful in 5m10s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-15 11:49:27 +01:00
c9d10f81a9 add mail for requesting fee
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-03-15 11:41:03 +01:00
413d08f538 show which schnupperant already paid
Some checks failed
CI/CD Pipeline / test (push) Successful in 9m46s
CI/CD Pipeline / deploy-staging (push) Failing after 22m31s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-15 10:52:13 +01:00
5e24f9ce04 update docs
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m47s
CI/CD Pipeline / deploy-staging (push) Successful in 4m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-15 09:54:36 +01:00
f70766e817 move to hetzner server
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m11s
CI/CD Pipeline / deploy-staging (push) Successful in 4m46s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-15 09:10:56 +01:00
09e11dbb2b show two rowes of boats for the 3 most left 'aisles'
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Successful in 4m5s
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-03-09 18:16:32 +01:00
22 changed files with 386 additions and 80 deletions

View File

@ -26,8 +26,8 @@ jobs:
~/.cargo/registry/cache/ ~/.cargo/registry/cache/
~/.cargo/git/db/ ~/.cargo/git/db/
target/ target/
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-debug-rowt-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-debug- restore-keys: ${{ runner.os }}-cargo-debug-rowt-
- name: Build - name: Build
run: | run: |
@ -65,8 +65,8 @@ jobs:
~/.cargo/registry/cache/ ~/.cargo/registry/cache/
~/.cargo/git/db/ ~/.cargo/git/db/
target/ target/
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-release-rowt-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release- restore-keys: ${{ runner.os }}-cargo-release-rowt-
- name: Build - name: Build
run: | run: |
cargo build --release --target $CARGO_TARGET cargo build --release --target $CARGO_TARGET
@ -80,15 +80,15 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/ scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/ scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/ scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/ scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm /home/philipp/rowing-staging/db.sqlite && cp /home/philipp/rowing/db.sqlite /home/philipp/rowing-staging/db.sqlite && mkdir -p /home/philipp/rowing-staging/svelte/build && mkdir -p /home/philipp/rowing-staging/data-ergo/thirty && mkdir -p /home/philipp/rowing-staging/data-ergo/dozen && sqlite3 /home/philipp/rowing-staging/db.sqlite < /home/philipp/rowing-staging/staging-diff.sql' ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing-staging/rot-updating /home/philipp/rowing-staging/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
@ -116,8 +116,8 @@ jobs:
~/.cargo/registry/cache/ ~/.cargo/registry/cache/
~/.cargo/git/db/ ~/.cargo/git/db/
target/ target/
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-release-rowt-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release- restore-keys: ${{ runner.os }}-cargo-release-rowt-
- name: Build - name: Build
run: | run: |
@ -132,13 +132,13 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing/ scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing/ scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing/ scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/philipp/rowing/svelte/build && mkdir -p /home/philipp/rowing/data-ergo/thirty && mkdir -p /home/philipp/rowing/data-ergo/dozen' ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing/rot-updating /home/philipp/rowing/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

View File

@ -22,3 +22,25 @@
- Rust: `cargo check` - Rust: `cargo check`
- Tera files: `djlint **.html.tera --profile=jinja --reformat` - Tera files: `djlint **.html.tera --profile=jinja --reformat`
- Typescript: `prettier -w *.ts` - Typescript: `prettier -w *.ts`
# Dependencies
- `sqlite3`
- `rust`
# Nginx config
```
server {
server_name staging.rudernlinz.at;
location / {
proxy_pass http://localhost:7999/; # The / is important!
}
}
server {
server_name app.rudernlinz.at;
location / {
proxy_pass http://localhost:8001/; # The / is important!
}
}
```

View File

@ -146,7 +146,7 @@ CREATE TABLE IF NOT EXISTS "boathouse" (
"boat_id" INTEGER NOT NULL REFERENCES boat(id), "boat_id" INTEGER NOT NULL REFERENCES boat(id),
"aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')), "aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')),
"side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')), "side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')),
"level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 3), "level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 11),
CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space
); );

View File

@ -4,12 +4,15 @@ Description=Rot
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/philipp/rowing WorkingDirectory=/home/rowing
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001" Environment="ROCKET_PORT=8001"
Environment="RUST_LOG=info" Environment="RUST_LOG=info"
ExecStart=/home/k004373/rowing/rot ExecStart=/home/rowing/rot
Restart=always
RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -4,12 +4,14 @@ Description=Rot Staging
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/philipp/rowing-staging WorkingDirectory=/home/rowing-staging
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=7999" Environment="ROCKET_PORT=7999"
Environment="ROCKET_LOG=info" Environment="ROCKET_LOG=info"
ExecStart=/home/philipp/rowing-staging/rot ExecStart=/home/rowing-staging/rot
Restart=always
RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -17,22 +17,52 @@ pub struct Boathouse {
} }
impl Boathouse { impl Boathouse {
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 4]>> { pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 4]>> = HashMap::new(); let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
let mut mountain = HashMap::new(); let mut mountain = HashMap::new();
mountain.insert("mountain", [None, None, None, None]); mountain.insert(
mountain.insert("water", [None, None, None, None]); "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); ret.insert("mountain-aisle", mountain);
let mut middle = HashMap::new(); let mut middle = HashMap::new();
middle.insert("mountain", [None, None, None, None]); middle.insert(
middle.insert("water", [None, None, None, None]); "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); ret.insert("middle-aisle", middle);
let mut water = HashMap::new(); let mut water = HashMap::new();
water.insert("mountain", [None, None, None, None]); water.insert(
water.insert("water", [None, None, None, None]); "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); ret.insert("water-aisle", water);
let boathouses = sqlx::query_as!( let boathouses = sqlx::query_as!(

View File

@ -472,7 +472,7 @@ ORDER BY departure DESC
mut log: LogToFinalize, mut log: LogToFinalize,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
//TODO: extract common tests with `create()` //TODO: extract common tests with `create()`
if user.id != self.shipmaster { if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
return Err(LogbookUpdateError::NotYourEntry); return Err(LogbookUpdateError::NotYourEntry);
} }
@ -549,7 +549,10 @@ ORDER BY departure DESC
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
Log::create(db, format!("{user:?} deleted trip: {self:?}")).await; Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
if user.has_role(db, "admin").await || user.id == self.shipmaster { if user.has_role(db, "admin").await
|| user.has_role(db, "Vorstand").await
|| user.id == self.shipmaster
{
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db) .execute(db)
.await .await

View File

@ -182,4 +182,116 @@ Der Vorstand
} }
} }
} }
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await;
for user in users {
if let Some(fee) = user.fee(db).await {
if !fee.paid {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\n\
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
Mit freundlichen Grüßen,\n\
Der Vorstand");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds = Credentials::new(
"no-reply@rudernlinz.at".to_owned(),
smtp_pw.clone(),
);
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
}
}
}
}
}
} }

View File

@ -73,7 +73,8 @@ WHERE u.id NOT IN (
WHERE ro.name = 'Donau Linz' WHERE ro.name = 'Donau Linz'
) )
AND l.distance_in_km IS NOT NULL AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%'; AND l.arrival LIKE '{year}-%'
AND u.name != 'Externe Steuerperson';
" "
)) ))
.fetch_one(db) .fetch_one(db)
@ -87,6 +88,16 @@ AND l.arrival LIKE '{year}-%';
} }
} }
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.rowed_km;
}
sum
}
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> { pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
let year = match year { let year = match year {
Some(year) => year, Some(year) => year,

View File

@ -25,7 +25,7 @@ const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500; const UNTERSTUETZEND: i32 = 2500;
const FOERDERND: i32 = 8500; const FOERDERND: i32 = 8500;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct User { pub struct User {
pub id: i64, pub id: i64,
pub name: String, pub name: String,
@ -106,6 +106,7 @@ pub struct Fee {
pub name: String, pub name: String,
pub user_ids: String, pub user_ids: String,
pub paid: bool, pub paid: bool,
pub users: Vec<User>,
} }
impl Default for Fee { impl Default for Fee {
@ -121,6 +122,7 @@ impl Fee {
name: "".into(), name: "".into(),
parts: Vec::new(), parts: Vec::new(),
user_ids: "".into(), user_ids: "".into(),
users: Vec::new(),
paid: false, paid: false,
} }
} }
@ -139,6 +141,7 @@ impl Fee {
self.name.push_str(&user.name); self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id)); self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
} }
pub fn paid(&mut self) { pub fn paid(&mut self) {
@ -509,6 +512,8 @@ ORDER BY last_access DESC
if ![ if ![
"n-sageder", "n-sageder",
"p-hofer", "p-hofer",
"daniel-kortschak",
"rudernlinz",
"m-birner", "m-birner",
"s-sollberger", "s-sollberger",
"d-kortschak", "d-kortschak",

View File

@ -6,6 +6,7 @@ use rocket::{post, FromForm};
use rocket_dyn_templates::{tera::Context, Template}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::log::Log;
use crate::model::mail::Mail; use crate::model::mail::Mail;
use crate::model::role::Role; use crate::model::role::Role;
use crate::model::user::AdminUser; use crate::model::user::AdminUser;
@ -34,11 +35,23 @@ async fn index(
} }
#[get("/mail/fee")] #[get("/mail/fee")]
async fn fee(db: &State<SqlitePool>, _admin: AdminUser, config: &State<Config>) -> &'static str { async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
Log::create(db, format!("{admin:?} trying to send fee")).await;
Mail::fees(db, config.smtp_pw.clone()).await; Mail::fees(db, config.smtp_pw.clone()).await;
"SUCC" "SUCC"
} }
#[get("/mail/fee-final")]
async fn fee_final(
db: &State<SqlitePool>,
admin: AdminUser,
config: &State<Config>,
) -> &'static str {
Log::create(db, format!("{admin:?} trying to send fee_final")).await;
Mail::fees_final(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct MailToSend<'a> { pub struct MailToSend<'a> {
pub(crate) role_id: i32, pub(crate) role_id: i32,
@ -52,18 +65,21 @@ async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<MailToSend<'_>>, data: Form<MailToSend<'_>>,
config: &State<Config>, config: &State<Config>,
_admin: AdminUser, admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let d = data.into_inner(); let d = data.into_inner();
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
if Mail::send(db, d, config.smtp_pw.clone()).await { if Mail::send(db, d, config.smtp_pw.clone()).await {
Log::create(db, "Mail successfully sent".into()).await;
Flash::success(Redirect::to("/admin/mail"), "Mail versendet") Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} else { } else {
Log::create(db, "Error sending the mail".into()).await;
Flash::error(Redirect::to("/admin/mail"), "Fehler") Flash::error(Redirect::to("/admin/mail"), "Fehler")
} }
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, update, fee] routes![index, update, fee, fee_final]
} }
#[cfg(test)] #[cfg(test)]

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use crate::model::{ use crate::model::{
family::Family, family::Family,
log::Log,
logbook::Logbook, logbook::Logbook,
role::Role, role::Role,
user::{AdminUser, User, UserWithRoles, VorstandUser}, user::{AdminUser, User, UserWithRoles, VorstandUser},
@ -163,7 +164,7 @@ async fn scheckbuch(
#[get("/user/fees/paid?<user_ids>")] #[get("/user/fees/paid?<user_ids>")]
async fn fees_paid( async fn fees_paid(
db: &State<SqlitePool>, db: &State<SqlitePool>,
_admin: AdminUser, admin: AdminUser,
user_ids: Vec<i32>, user_ids: Vec<i32>,
referer: Referer, referer: Referer,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
@ -172,9 +173,19 @@ async fn fees_paid(
let user = User::find_by_id(db, user_id).await.unwrap(); let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name)); res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await { if user.has_role(db, "paid").await {
Log::create(
db,
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
)
.await;
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap()) user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await; .await;
} else { } else {
Log::create(
db,
format!("{} set fees paid for '{}'", admin.user.name, user.name),
)
.await;
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap()) user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await; .await;
} }
@ -204,8 +215,9 @@ async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<
} }
#[get("/user/<user>/delete")] #[get("/user/<user>/delete")]
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user { match user {
Some(user) => { Some(user) => {
user.delete(db).await; user.delete(db).await;
@ -239,9 +251,14 @@ pub struct UserEditForm {
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UserEditForm>, data: Form<UserEditForm>,
_admin: AdminUser, admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user = User::find_by_id(db, data.id).await; let user = User::find_by_id(db, data.id).await;
Log::create(
db,
format!("{} updated user from {user:?} to {data:?}", admin.user.name),
)
.await;
let Some(user) = user else { let Some(user) = user else {
return Flash::error( return Flash::error(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
@ -254,7 +271,7 @@ async fn update(
Flash::success(Redirect::to("/admin/user"), "Successfully updated user") Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
} }
#[derive(FromForm)] #[derive(FromForm, Debug)]
struct UserAddForm<'r> { struct UserAddForm<'r> {
name: &'r str, name: &'r str,
} }
@ -263,9 +280,14 @@ struct UserAddForm<'r> {
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>, data: Form<UserAddForm<'_>>,
_admin: AdminUser, admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
if User::create(db, data.name).await { if User::create(db, data.name).await {
Log::create(
db,
format!("{} created new user: {data:?}", admin.user.name),
)
.await;
Flash::success(Redirect::to("/admin/user"), "Successfully created user") Flash::success(Redirect::to("/admin/user"), "Successfully created user")
} else { } else {
Flash::error( Flash::error(

View File

@ -35,25 +35,27 @@ async fn index_boat_kiosk(
#[get("/?<year>", rank = 2)] #[get("/?<year>", rank = 2)]
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template { async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await; let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await;
let guest_km = Stat::guest(db, year).await; let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user).await; let personal = stat::get_personal(db, &user).await;
let kiosk = false; let kiosk = false;
Template::render( Template::render(
"stat.people", "stat.people",
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km), context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
) )
} }
#[get("/?<year>")] #[get("/?<year>")]
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template { async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await; let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await;
let guest_km = Stat::guest(db, year).await; let guest_km = Stat::guest(db, year).await;
let kiosk = true; let kiosk = true;
Template::render( Template::render(
"stat.people", "stat.people",
context!(stat, kiosk, show_kiosk_header: true, guest_km), context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
) )
} }

View File

@ -2,16 +2,20 @@
{% import "includes/forms/boat" as boat %} {% import "includes/forms/boat" as boat %}
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full"> <div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Mail</h1> <h1 class="h1">Mail</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Mail senden</h2>
<form action="/admin/mail" method="post" enctype="multipart/form-data"> <form action="/admin/mail" method="post" enctype="multipart/form-data">
<select name="role_id"> {{ macros::select(label="Gruppe", data=roles, name="role_id") }}
{% for role in roles %}<option value="{{ role.id }}">{{ role.name }}</option>{% endfor %} {{ macros::input(label="Betreff", name="subject", type="text", required=true) }}
</select> <textarea name="body" rows="4" cols="50" class="dark:text-white"></textarea>
<input type="text" name="subject" />
<textarea name="body" rows="4" cols="50"></textarea>
<input type="file" name="files" multiple /> <input type="file" name="files" multiple />
<input type="submit" /> <input type="submit" value="Abschicken" />
</form> </form>
</div> </div>
</div>
</div>
{% endblock content %} {% endblock content %}

View File

@ -9,7 +9,12 @@
<h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2> <h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2>
<div class="text-sm p-3"> <div class="text-sm p-3">
<ol class="ms-2" style="list-style: number;"> <ol class="ms-2" style="list-style: number;">
{% for user in schnupperanten %}<li class="py-1">{{ user.name }} ({{ user.mail }} | {{ user.notes }})</li>{% endfor %} {% for user in schnupperanten %}
<li class="py-1"
{% if "paid" in user.roles %}style="background-color: green;"{% endif %}>
{{ user.name }} ({{ user.mail }} | {{ user.notes }})
</li>
{% endfor %}
</ol> </ol>
</div> </div>
</div> </div>

View File

@ -7,8 +7,10 @@
{% set aisle = aisle_name ~ "-aisle" %} {% set aisle = aisle_name ~ "-aisle" %}
{% set place = boathouse[aisle][side_name] %} {% set place = boathouse[aisle][side_name] %}
{% if place[level] %} {% if place[level] %}
{{ place[level].1.name }} <a class="btn btn-primary absolute end-0" href="/board/boathouse/{{ place[level].0 }}/delete">X</a> {{ place[level].1.name }} <a class="btn btn-primary absolute end-0"
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
{% elif boats | length > 0 %} {% elif boats | length > 0 %}
{% if "admin" in loggedin_user.roles %}
<details> <details>
<summary>Kein Boot</summary> <summary>Kein Boot</summary>
<form action="/board/boathouse" method="post" class="grid gap-3"> <form action="/board/boathouse" method="post" class="grid gap-3">
@ -24,6 +26,9 @@
{% else %} {% else %}
Kein Boot Kein Boot
{% endif %} {% endif %}
{% else %}
Kein Boot
{% endif %}
</li> </li>
{% endmacro show_place %} {% endmacro show_place %}
{% macro show_side(aisle_name, side_name) %} {% macro show_side(aisle_name, side_name) %}
@ -33,11 +38,41 @@
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 1) }} {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 1) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 2) }} {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 2) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 3) }} {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 3) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 4) }}
{% if aisle_name != 'water' or side_name != 'water' %}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 5) }}
{% endif %}
{% set show_additional = false %}
{% if aisle_name == "mountain" %}
{% set show_additional = true %}
{% elif aisle_name == "middle" and side_name == "mountain" %}
{% set show_additional = true %}
{% endif %}
{% if show_additional %}
<hr />
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 6) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 7) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 8) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 9) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 10) }}
{{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 11) }}
{% endif %}
</ol> </ol>
</div> </div>
{% endmacro show_side %} {% endmacro show_side %}
{% macro show_aisle(name, last=false) %} {% macro show_aisle(name, last=false) %}
<div id="{{ name }}-aisle" class="grid grid-cols-2 gap-4 {% if not last %}md:border-r{% endif %}"> <div id="{{ name }}-aisle"
class="grid grid-cols-2 gap-4 {% if not last %}md:border-r{% endif %}">
<h1 class="col-span-2 text-center">
{% if name == "water" %}
🌊
{% elif name == "middle" %}
{% else %}
⛰️
{% endif %}
- Gang
</h1>
{{ self::show_side(aisle_name = name, side_name = "mountain") }} {{ self::show_side(aisle_name = name, side_name = "mountain") }}
{{ self::show_side(aisle_name = name, side_name = "water") }} {{ self::show_side(aisle_name = name, side_name = "water") }}
</div> </div>

View File

@ -137,9 +137,11 @@
{{ rower.name }} {{ rower.name }}
{% if rower.id == log.shipmaster or rower.id == log.steering_person %} {% if rower.id == log.shipmaster or rower.id == log.steering_person %}
<small class="text-gray-600 dark:text-primary-100">( <small class="text-gray-600 dark:text-primary-100">(
{% if rower.id == log.shipmaster %}Schiffsführer{% endif %} {%- if rower.id == log.shipmaster %}Schiffsführer
{% endif -%}
{% if rower.id == log.shipmaster and rower.id == log.steering_person %}/{% endif %} {% if rower.id == log.shipmaster and rower.id == log.steering_person %}/{% endif %}
{% if rower.id == log.steering_person %}Steuerperson{% endif %} {%- if rower.id == log.steering_person %}Steuerperson
{%- endif -%}
)</small> )</small>
{% endif %} {% endif %}
</p> </p>

View File

@ -1,3 +1,17 @@
{% macro boatreservation() %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3"
style="margin-top: 10px">
<h2 class="h2">Bootsreservierungen</h2>
<div class="p2" style="margin-bottom: 10px;">
<ul style="display: flex;
justify-content: space-around;
padding: 0;
list-style: none">
<li style="display: inline-block;">22.04. | Christian Gusenbauer | Boot: Linz + kleiner Hänger</li>
</ul>
</div>
</div>
{% endmacro boatreservation %}
{% macro header(loggedin_user) %} {% macro header(loggedin_user) %}
<header class="bg-primary-900 text-white flex justify-center p-3 fixed w-full z-10"> <header class="bg-primary-900 text-white flex justify-center p-3 fixed w-full z-10">
<div class="max-w-screen-xl w-full flex justify-between items-center"> <div class="max-w-screen-xl w-full flex justify-between items-center">

View File

@ -9,6 +9,7 @@
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
</div> </div>
{% endif %} {% endif %}
{{ macros::boatreservation() }}
<div class="w-full grid md:grid-cols-5 gap-3 mt-5"> <div class="w-full grid md:grid-cols-5 gap-3 mt-5">
<div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow"> <div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow">
<h2 class="h2">Boote</h2> <h2 class="h2">Boote</h2>

View File

@ -4,6 +4,7 @@
{% block content %} {% block content %}
<div class="w-full"> <div class="w-full">
<h1 class="h1">Logbuch</h1> <h1 class="h1">Logbuch</h1>
{{ macros::boatreservation() }}
<div class="w-full grid md:grid-cols-5 gap-3 mt-5"> <div class="w-full grid md:grid-cols-5 gap-3 mt-5">
<div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow"> <div class="bg-white dark:bg-primary-900 rounded-md hidden md:block shadow">
<h2 class="h2">Boote</h2> <h2 class="h2">Boote</h2>
@ -19,6 +20,8 @@
{% for log in on_water %} {% for log in on_water %}
{% if log.shipmaster == loggedin_user.id %} {% if log.shipmaster == loggedin_user.id %}
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }} {{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
{% elif "Vorstand" in loggedin_user.roles %}
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
{% else %} {% else %}
{{ log::show(log=log, state="on_water", only_ones=true) }} {{ log::show(log=log, state="on_water", only_ones=true) }}
{% endif %} {% endif %}

View File

@ -109,9 +109,9 @@
Uhr Uhr
</strong> </strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }} <small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
{% if planned_event.trip_type %} {%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
{% endif %} {%- endif -%}
)</small> )</small>
<br /> <br />
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}) <a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})

View File

@ -22,7 +22,7 @@
<div class="border-r border-l border-gray-200 dark:border-primary-600"> <div class="border-r border-l border-gray-200 dark:border-primary-600">
{% set_global km = 0 %} {% set_global index = 1 %} {% set_global km = 0 %} {% set_global index = 1 %}
{% for s in stat %} {% for s in stat %}
<div class="border-t border-gray-200 dark:border-primary-600 {% if loop.last %}border-b{% endif %} bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-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="true" data-filterable="true"
data-filter="{{ s.name }}"> data-filter="{{ s.name }}">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"> <span class="text-sm text-gray-600 dark:text-gray-100 w-10">
@ -38,12 +38,26 @@
{% set_global km = s.rowed_km %} {% set_global km = s.rowed_km %}
</div> </div>
{% endfor %} {% endfor %}
<div class="border-t border-gray-200 dark:border-primary-600 {% if loop.last %}border-b{% endif %} bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-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="true" data-filterable="true"
data-filter="{{ guest_km.name }}"> data-filter="Summe Vereinsmitglieder">
<span class="text-sm text-gray-600 dark:text-gray-100 w-10"></span> <span class="text-sm text-gray-600 dark:text-gray-100 w-10"></span>
<span class="grow">{{ guest_km.name }}</span> <span class="grow"><b>Summe Vereinsmitglieder</b></span>
<span>{{ guest_km.rowed_km }} km</span> <span><b>{{ club_km }} km</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-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>
</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-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>
</div> </div>
</div> </div>
<div id="container" class="w-full"></div> <div id="container" class="w-full"></div>