Merge branch 'main' of gitlab.com:PhilippHofer/rot
This commit is contained in:
commit
b3dd1cdfe5
@ -4,11 +4,7 @@
|
|||||||
|
|
||||||
## New large features
|
## New large features
|
||||||
### Logbuch
|
### Logbuch
|
||||||
- Next: Allow editing of rowers on "Ausfahrt beenden"
|
- Only layout + tests missing :-)
|
||||||
- Then
|
|
||||||
- Allow editing own logbook entries of same day
|
|
||||||
- Stats (Personenliste mit Gesamt-KM vom Jahr)
|
|
||||||
|
|
||||||
### Guest-Scheckbuch
|
### Guest-Scheckbuch
|
||||||
- guest_trip
|
- guest_trip
|
||||||
- guest_user_id
|
- guest_user_id
|
||||||
|
@ -30,6 +30,7 @@ pub struct LogbookWithBoatAndRowers {
|
|||||||
|
|
||||||
pub enum LogbookUpdateError {
|
pub enum LogbookUpdateError {
|
||||||
NotYourEntry,
|
NotYourEntry,
|
||||||
|
TooManyRowers(usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LogbookCreateError {
|
pub enum LogbookCreateError {
|
||||||
@ -170,6 +171,13 @@ impl Logbook {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_rowers(&self, db: &SqlitePool) {
|
||||||
|
sqlx::query!("DELETE FROM rower WHERE logbook_id=?", self.id)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn home(
|
pub async fn home(
|
||||||
&self,
|
&self,
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
@ -178,10 +186,21 @@ impl Logbook {
|
|||||||
distance_in_km: i64,
|
distance_in_km: i64,
|
||||||
comments: Option<String>,
|
comments: Option<String>,
|
||||||
logtype: Option<i64>,
|
logtype: Option<i64>,
|
||||||
|
rower: Vec<i64>,
|
||||||
) -> Result<(), LogbookUpdateError> {
|
) -> Result<(), LogbookUpdateError> {
|
||||||
if user.id != self.shipmaster {
|
if user.id != self.shipmaster {
|
||||||
return Err(LogbookUpdateError::NotYourEntry);
|
return Err(LogbookUpdateError::NotYourEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap(); //ok
|
||||||
|
|
||||||
|
if rower.len() > boat.amount_seats as usize - 1 {
|
||||||
|
return Err(LogbookUpdateError::TooManyRowers(
|
||||||
|
boat.amount_seats as usize,
|
||||||
|
rower.len() + 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: check current date
|
//TODO: check current date
|
||||||
|
|
||||||
let arrival = format!("{}", chrono::offset::Local::now().format("%Y-%m-%d %H:%M"));
|
let arrival = format!("{}", chrono::offset::Local::now().format("%Y-%m-%d %H:%M"));
|
||||||
@ -197,6 +216,13 @@ impl Logbook {
|
|||||||
)
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await.unwrap(); //TODO: fixme
|
.await.unwrap(); //TODO: fixme
|
||||||
|
|
||||||
|
self.remove_rowers(db).await;
|
||||||
|
|
||||||
|
for rower in &rower {
|
||||||
|
Rower::create(db, self.id, *rower).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ pub mod logbook;
|
|||||||
pub mod logtype;
|
pub mod logtype;
|
||||||
pub mod planned_event;
|
pub mod planned_event;
|
||||||
pub mod rower;
|
pub mod rower;
|
||||||
|
pub mod stat;
|
||||||
pub mod trip;
|
pub mod trip;
|
||||||
pub mod tripdetails;
|
pub mod tripdetails;
|
||||||
pub mod triptype;
|
pub mod triptype;
|
||||||
|
35
src/model/stat.rs
Normal file
35
src/model/stat.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::{FromRow, SqlitePool};
|
||||||
|
|
||||||
|
#[derive(FromRow, Serialize, Clone)]
|
||||||
|
pub struct Stat {
|
||||||
|
name: String,
|
||||||
|
rowed_km: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stat {
|
||||||
|
pub async fn get_rowed_km(db: &SqlitePool) -> Vec<Stat> {
|
||||||
|
sqlx::query!(
|
||||||
|
"SELECT u.name AS name, COALESCE(SUM(distance_in_km), 0) as rowed_km
|
||||||
|
FROM user u
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT shipmaster AS user_id, distance_in_km
|
||||||
|
FROM logbook
|
||||||
|
UNION
|
||||||
|
SELECT r.rower_id AS user_id, l.distance_in_km
|
||||||
|
FROM logbook l
|
||||||
|
INNER JOIN rower r ON r.logbook_id = l.id
|
||||||
|
) AS subquery ON u.id = subquery.user_id
|
||||||
|
GROUP BY u.id ORDER BY rowed_km DESC;"
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| Stat {
|
||||||
|
name: row.name,
|
||||||
|
rowed_km: row.rowed_km.unwrap_or(0),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,29 @@ pub enum LoginError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
pub async fn rowed_km(&self, db: &SqlitePool) -> i32 {
|
||||||
|
sqlx::query!(
|
||||||
|
"SELECT COALESCE(SUM(distance_in_km),0) as rowed_km
|
||||||
|
FROM (
|
||||||
|
SELECT distance_in_km
|
||||||
|
FROM logbook
|
||||||
|
WHERE shipmaster = ?1
|
||||||
|
UNION
|
||||||
|
SELECT l.distance_in_km
|
||||||
|
FROM logbook l
|
||||||
|
INNER JOIN rower r ON r.logbook_id = l.id
|
||||||
|
WHERE r.rower_id = ?1
|
||||||
|
|
||||||
|
);",
|
||||||
|
self.id,
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.rowed_km
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
User,
|
User,
|
||||||
|
@ -98,6 +98,7 @@ struct LogHomeForm {
|
|||||||
distance_in_km: i64,
|
distance_in_km: i64,
|
||||||
comments: Option<String>,
|
comments: Option<String>,
|
||||||
logtype: Option<i64>,
|
logtype: Option<i64>,
|
||||||
|
rower: Vec<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/<logbook_id>", data = "<data>")]
|
#[post("/<logbook_id>", data = "<data>")]
|
||||||
@ -123,6 +124,7 @@ async fn home(
|
|||||||
data.distance_in_km,
|
data.distance_in_km,
|
||||||
data.comments.clone(), //TODO: fixme
|
data.comments.clone(), //TODO: fixme
|
||||||
data.logtype,
|
data.logtype,
|
||||||
|
data.rower.clone(), //TODO: fixme
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -24,6 +24,7 @@ mod auth;
|
|||||||
mod cox;
|
mod cox;
|
||||||
mod log;
|
mod log;
|
||||||
mod misc;
|
mod misc;
|
||||||
|
mod stat;
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||||
@ -116,6 +117,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
|||||||
.mount("/", routes![index, join, remove])
|
.mount("/", routes![index, join, remove])
|
||||||
.mount("/auth", auth::routes())
|
.mount("/auth", auth::routes())
|
||||||
.mount("/log", log::routes())
|
.mount("/log", log::routes())
|
||||||
|
.mount("/stat", stat::routes())
|
||||||
.mount("/cox", cox::routes())
|
.mount("/cox", cox::routes())
|
||||||
.mount("/admin", admin::routes())
|
.mount("/admin", admin::routes())
|
||||||
.mount("/", misc::routes())
|
.mount("/", misc::routes())
|
||||||
|
19
src/tera/stat.rs
Normal file
19
src/tera/stat.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use rocket::{get, routes, Route, State};
|
||||||
|
use rocket_dyn_templates::{context, Template};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
use crate::model::{stat::Stat, user::User};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(db: &State<SqlitePool>, user: User) -> Template {
|
||||||
|
let stat = Stat::get_rowed_km(db).await;
|
||||||
|
|
||||||
|
Template::render("stat", context!(loggedin_user: &user, stat))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![index]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {}
|
@ -13,6 +13,10 @@
|
|||||||
<span class="sr-only">FAQs</span>
|
<span class="sr-only">FAQs</span>
|
||||||
</a>
|
</a>
|
||||||
{% if loggedin_user.is_admin %}
|
{% if loggedin_user.is_admin %}
|
||||||
|
<a href="/stat" class="inline-flex justify-center rounded-md bg-primary-600 mx-1 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">
|
||||||
|
STATS
|
||||||
|
<span class="sr-only">Logbuch</span>
|
||||||
|
</a>
|
||||||
<a href="/log" class="inline-flex justify-center rounded-md bg-primary-600 mx-1 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">
|
<a href="/log" class="inline-flex justify-center rounded-md bg-primary-600 mx-1 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">
|
||||||
LOGBUCH
|
LOGBUCH
|
||||||
<span class="sr-only">Logbuch</span>
|
<span class="sr-only">Logbuch</span>
|
||||||
|
@ -43,16 +43,12 @@
|
|||||||
<input type="submit" />
|
<input type="submit" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// Get the current date and time
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
const localTime = new Date(currentDate.getTime() - (currentDate.getTimezoneOffset() * 60000));
|
||||||
// Format the date and time as a string in the format "YYYY-MM-DDTHH:mm"
|
const formattedDate = localTime.toISOString().slice(0, 16);
|
||||||
const formattedDate = currentDate.toISOString().slice(0, 16);
|
|
||||||
|
|
||||||
// Set the formatted string as the value of the input field
|
// Set the formatted string as the value of the input field
|
||||||
document.getElementById("datetime-dep").value = formattedDate;
|
document.getElementById("datetime-dep").value = formattedDate;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -89,6 +85,17 @@
|
|||||||
{{ macros::input(label="Distanz", name="distance_in_km", id="distance_in_km_home", type="number", min=0, value=log.distance_in_km) }}
|
{{ macros::input(label="Distanz", name="distance_in_km", id="distance_in_km_home", type="number", min=0, value=log.distance_in_km) }}
|
||||||
{{ macros::input(label="Kommentar", name="comments", type="text", value=log.comments) }}
|
{{ macros::input(label="Kommentar", name="comments", type="text", value=log.comments) }}
|
||||||
{{ macros::select(data=logtypes, select_name='logtype', default="Normal", selected_id=log.logtype) }}
|
{{ macros::select(data=logtypes, select_name='logtype', default="Normal", selected_id=log.logtype) }}
|
||||||
|
<select multiple="multiple" name="rower[]">
|
||||||
|
{% for user in users %}
|
||||||
|
{% set_global selected = false %}
|
||||||
|
{% for rower in log.rowers %}
|
||||||
|
{% if rower.id == user.id %}
|
||||||
|
{% set_global selected = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<option value="{{ user.id }}" {% if selected %}selected{% endif %} onmousedown="event.preventDefault(); this.selected = !this.selected; return false;">{{user.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
<input type="submit" value="AUSFAHRT BEENDEN"/>
|
<input type="submit" value="AUSFAHRT BEENDEN"/>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
16
templates/stat.html.tera
Normal file
16
templates/stat.html.tera
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% import "includes/macros" as macros %}
|
||||||
|
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="max-w-screen-lg w-full">
|
||||||
|
<h1 class="h1">Statstik</h1>
|
||||||
|
<ol>
|
||||||
|
{% for s in stat %}
|
||||||
|
<li>{{s.name}}: {{s.rowed_km}}km</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
{% endblock content%}
|
Loading…
Reference in New Issue
Block a user