forked from Ruderverein-Donau-Linz/rowt
Compare commits
102 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b9cf98473 | |||
eaa35fb46c | |||
86470da184 | |||
ae61564ad4 | |||
82a54bdea1 | |||
2a37bcbec5 | |||
a2a39103e0 | |||
c96cc4b38f | |||
3008264261 | |||
![]() |
11025738bb | ||
![]() |
31fc0605d9 | ||
![]() |
1fdec59f77 | ||
da793fec2d | |||
8917629613 | |||
2a2c2ce9dc | |||
d82bd3ebeb | |||
10f6268e56 | |||
f0ea5823ba | |||
32800b1897 | |||
3406b66f41 | |||
cfd8b12556 | |||
2ffddda960 | |||
c7c92c83fb | |||
5cc77c39ff | |||
80d8857c6b | |||
78403e4ec5 | |||
4dd656f566 | |||
23a1a118a3 | |||
b281201906 | |||
4d58bd3cae | |||
67e790a82e | |||
63bf1015cc | |||
352dad8e6c | |||
4f42e7cb8c | |||
c6aa25fe0e | |||
9ba848cbab | |||
9047459d6c | |||
87de3859a2 | |||
b8aaf5ba2e | |||
de9ea9405e | |||
3bd229554b | |||
f9c9f7c523 | |||
0dfceec737 | |||
e5fec411f3 | |||
ac67c6cfdb | |||
a90c4fc07e | |||
52b960cec7 | |||
f7d109f1b2 | |||
63505722f9 | |||
d21272d4bb | |||
97dd7794fb | |||
cfe99c2f2a | |||
2a3f846c5c | |||
af4163a065 | |||
8a9047b3c3 | |||
ebc7c32351 | |||
1a850535ed | |||
99bbb2b088 | |||
b31209a97a | |||
![]() |
be4f302a4c | ||
e5c2bec145 | |||
0ebcd5a284 | |||
6237340f72 | |||
![]() |
5b013fe389 | ||
![]() |
022ec6bd5b | ||
09d4c0abe4 | |||
5448558085 | |||
3232a03d75 | |||
dceb57e370 | |||
f68928df00 | |||
d3bb050534 | |||
32b4131aae | |||
1d34cb5794 | |||
8a4d98a90f | |||
![]() |
213e9faad4 | ||
a9a8207813 | |||
b7b2385264 | |||
b560233acf | |||
d7187a7589 | |||
e61b16c389 | |||
2ac8a3155c | |||
d01e6ea30b | |||
f38ca09eb7 | |||
1ad4c31979 | |||
5e413d2d72 | |||
0f8e1158b9 | |||
af10399797 | |||
6344ba720d | |||
4b1dceb08a | |||
cb819c16a3 | |||
08a48cb4d2 | |||
9c36da32bd | |||
77444d25ae | |||
a683af00d0 | |||
766886d857 | |||
38703321e8 | |||
ec1c717341 | |||
22bb79bfbd | |||
eba4b77983 | |||
83d266b3e0 | |||
980bcff1d9 | |||
c15ed6e9a9 |
@ -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:
|
||||
|
1319
Cargo.lock
generated
1319
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -13,20 +13,20 @@ 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"] }
|
||||
ureq = { version = "3.0", features = ["json"] }
|
||||
regex = "1.10"
|
||||
urlencoding = "2.1"
|
||||
|
||||
|
@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
addRelationMagic(<HTMLElement>document.querySelector("body"));
|
||||
reloadPage();
|
||||
setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
|
||||
initDropdown();
|
||||
});
|
||||
|
||||
function changeTheme() {
|
||||
@ -795,3 +796,21 @@ function replaceStrings() {
|
||||
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
|
||||
});
|
||||
}
|
||||
|
||||
function initDropdown() {
|
||||
const popoverTriggerList = document.querySelectorAll('[data-dropdown]');
|
||||
|
||||
popoverTriggerList.forEach((popoverTriggerEl: Element) => {
|
||||
const id = popoverTriggerEl.getAttribute('data-dropdown');
|
||||
|
||||
if (id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
// Toggle visibility of the dropdown when clicked
|
||||
popoverTriggerEl.addEventListener('click', () => {
|
||||
element.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.goto("/auth");
|
||||
|
20
src/lib.rs
20
src/lib.rs
@ -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]
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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
58
src/model/user/fee.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -196,7 +151,8 @@ impl User {
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
|
||||
|
||||
Liebe Grüße, Philipp", self.name),
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
@ -342,7 +298,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 +337,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 +396,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 +417,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
|
||||
@ -632,18 +592,27 @@ WHERE lower(name)=?
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY last_access DESC
|
||||
Self::all_with_order(db, "last_access", false).await
|
||||
}
|
||||
|
||||
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
|
||||
let mut query = format!(
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY {}
|
||||
",
|
||||
sort
|
||||
);
|
||||
if !asc {
|
||||
query.push_str(" DESC");
|
||||
}
|
||||
|
||||
sqlx::query_as::<_, User>(&query)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
|
||||
@ -1033,39 +1002,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1172,7 +1178,7 @@ special_user!(AdminUser, +"admin");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch");
|
||||
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
|
||||
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||
special_user!(VorstandUser, +"Vorstand");
|
||||
special_user!(VorstandUser, +"admin", +"Vorstand");
|
||||
special_user!(EventUser, +"manage_events");
|
||||
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
|
||||
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
|
@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
Ok(mut response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
|
@ -99,8 +99,8 @@ fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => {
|
||||
let data: Result<Data, _> = response.into_json();
|
||||
Ok(mut response) => {
|
||||
let data: Result<Data, _> = response.body_mut().read_json();
|
||||
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
|
@ -2,7 +2,7 @@ use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@ -17,7 +17,7 @@ use sqlx::SqlitePool;
|
||||
#[get("/boat")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
@ -40,7 +40,7 @@ async fn index(
|
||||
}
|
||||
|
||||
#[get("/boat/<boat>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, admin: VorstandUser, boat: i32) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat).await;
|
||||
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
|
||||
|
||||
@ -61,7 +61,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToUpdate<'_>>,
|
||||
boat_id: i32,
|
||||
_admin: AdminUser,
|
||||
_admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat_id).await;
|
||||
let Some(boat) = boat else {
|
||||
@ -78,7 +78,7 @@ async fn update(
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToAdd<'_>>,
|
||||
_admin: AdminUser,
|
||||
_admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boat::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
|
@ -43,13 +43,17 @@ impl<'r> FromRequest<'r> for Referer {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user")]
|
||||
#[get("/user?<sort>&<asc>")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
sort: Option<String>,
|
||||
asc: bool,
|
||||
) -> Template {
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
let sort_column = sort.unwrap_or_else(|| "last_access".to_string());
|
||||
|
||||
let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
@ -408,7 +412,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 +462,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> {
|
||||
|
@ -73,7 +73,7 @@ async fn login(
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!");
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
@ -235,7 +235,7 @@ async fn create_logbook(
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
@ -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 {
|
||||
@ -356,7 +356,7 @@ async fn home_logbook(
|
||||
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,36 +4,59 @@
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Users</h1>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/admin/user/new"
|
||||
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
|
||||
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">Neue Person hinzufügen</summary>
|
||||
<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">
|
||||
class="flex mt-4 rounded-md sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Name" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="input rounded-md w-100"
|
||||
placeholder="Name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-right ml-3">
|
||||
<input value="Hinzufügen"
|
||||
type="submit"
|
||||
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{% endif %}
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<div class="search-wrapper flex">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||
|
||||
<div class="relative">
|
||||
<button id="dropdownbtn" data-dropdown="dropdown" class="btn btn-dark ml-3" type="button">
|
||||
Sortieren
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
|
||||
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
|
||||
<li>
|
||||
<a href="./user" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name&asc" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
@ -46,7 +69,7 @@
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles and user.mail %}
|
||||
{% if not user.last_access and allowed_to_edit and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
|
@ -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 %}
|
||||
|
@ -74,7 +74,7 @@
|
||||
</summary>
|
||||
<div class="pt-3">
|
||||
<p>
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an Philipp melden (Tel. nr siehe Signal, oder an <a href="mailto:it@rudernlinz.at"
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an unseren Schriftführer melden (<a href="mailto:info@rudernlinz.at"
|
||||
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
|
||||
target="_blank">it@rudernlinz.at</a>).
|
||||
<br />
|
||||
|
@ -128,7 +128,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
<a href="/admin/user"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Userverwaltung</a>
|
||||
{% endif %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
|
||||
<a href="/admin/boat"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t">Boote</a>
|
||||
{% endif %}
|
||||
|
@ -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 (✓).
|
||||
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 (✓).<br /><a href="/notification/read/all" class="underline">Du kannst hier ausnahmsweise alle als gelesen markieren.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="divide-y">
|
||||
@ -418,6 +418,9 @@
|
||||
<a href="/board/achievement"
|
||||
class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/boat" class="block w-100 py-2 hover:text-primary-600">Boote</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -426,9 +429,6 @@
|
||||
role="alert">
|
||||
<h2 class="h2">Admin</h2>
|
||||
<ul class="list-none ms-2 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<li class="py-1">
|
||||
<a href="/admin/boat" class="block w-100 py-2 hover:text-primary-600">Boote</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/user" class="block w-100 py-2 hover:text-primary-600">User</a>
|
||||
</li>
|
||||
|
@ -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;
|
||||
|
@ -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") %}
|
||||
|
@ -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 %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user