allow cancel of events #522

Merged
philipp merged 3 commits from allow-cancel-of-events into staging 2024-05-21 22:39:59 +02:00
9 changed files with 246 additions and 69 deletions
Showing only changes of commit 1f0de7abf4 - Show all commits

View File

@ -152,6 +152,31 @@ ORDER BY read_at DESC, created_at DESC;
} }
} }
} }
// Cox read notification about cancelled event
let re = Regex::new(r"^remove_trip_by_planned_event:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() {
let _ = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
self.user_id,
number
)
.execute(db)
.await
.unwrap();
} }
} }
} }
}
}
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",
action
)
.execute(db)
.await
.unwrap();
}
}

View File

@ -8,7 +8,7 @@ use ics::{
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
use super::{tripdetails::TripDetails, triptype::TripType, user::User}; use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] #[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct PlannedEvent { pub struct PlannedEvent {
@ -17,7 +17,7 @@ pub struct PlannedEvent {
planned_amount_cox: i64, planned_amount_cox: i64,
trip_details_id: i64, trip_details_id: i64,
pub planned_starting_time: String, pub planned_starting_time: String,
max_people: i64, pub(crate) max_people: i64,
pub day: String, pub day: String,
pub notes: Option<String>, pub notes: Option<String>,
pub allow_guests: bool, pub allow_guests: bool,
@ -227,6 +227,9 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
let tripdetails = self.trip_details(db).await;
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people, max_people,
@ -238,6 +241,69 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
if max_people == 0 && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match notes {
Some(n) if !n.is_empty() => n,
_ => ".",
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!("remove_trip_by_planned_event:{}", self.id)),
)
.await;
}
}
let rower = Registration::all_rower(db, self.trip_details_id).await;
for user in rower {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match notes {
Some(n) if !n.is_empty() => n,
_ => ".",
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
tripdetails.id
)),
)
.await;
}
}
}
if max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
)
.await;
Notification::delete_by_action(
db,
&format!("remove_trip_by_planned_event:{}", self.id),
)
.await;
}
} }
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> { pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
@ -250,7 +316,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(), "Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
); );
} }
if Registration::all_cox(db, self.trip_details_id).await.len() > 0 { if Registration::all_cox(db, self.id).await.len() > 0 {
return Err( return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist." "Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
.into(), .into(),
@ -326,7 +392,7 @@ mod test {
let pool = testdb!(); let pool = testdb!();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await; planned_event.delete(&pool).await.unwrap();
let res = let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;

View File

@ -137,6 +137,10 @@ WHERE trip.id=?
return Err(CoxHelpError::DetailsLocked); return Err(CoxHelpError::DetailsLocked);
} }
if planned_event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent);
}
match sqlx::query!( match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id, cox.id,
@ -193,6 +197,9 @@ WHERE day=?
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
}; };
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people, max_people,
@ -206,7 +213,7 @@ WHERE day=?
.await .await
.unwrap(); //Okay, as trip_details can only be created with proper DB backing .unwrap(); //Okay, as trip_details can only be created with proper DB backing
if max_people == 0 { if max_people == 0 && !was_already_cancelled {
let rowers = TripWithUserAndType::from(db, trip.clone()).await.rower; let rowers = TripWithUserAndType::from(db, trip.clone()).await.rower;
for user in rowers { for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await { if let Some(user) = User::find_by_name(db, &user.name).await {
@ -234,6 +241,14 @@ WHERE day=?
} }
} }
if max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
trip_details.check_free_spaces(db).await; trip_details.check_free_spaces(db).await;
@ -318,6 +333,7 @@ pub enum CoxHelpError {
AlreadyRegisteredAsRower, AlreadyRegisteredAsRower,
AlreadyRegisteredAsCox, AlreadyRegisteredAsCox,
DetailsLocked, DetailsLocked,
CanceledEvent,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]

View File

@ -435,7 +435,7 @@ WHERE id like ?
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE name like ? WHERE name=?
", ",
name name
) )

View File

@ -97,6 +97,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }

View File

@ -8,7 +8,9 @@
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2">Gruppe</h2> <h2 class="h2">Gruppe</h2>
<form action="/admin/notification/group" method="post" class="grid gap-3 p-3"> <form action="/admin/notification/group"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Gruppe", data=roles, name="role_id") }} {{ macros::select(label="Gruppe", data=roles, name="role_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }} {{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }} {{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
@ -18,7 +20,9 @@
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2">Person</h2> <h2 class="h2">Person</h2>
<form action="/admin/notification/user" method="post" class="grid gap-3 p-3"> <form action="/admin/notification/user"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Person", data=users, name="user_id") }} {{ macros::select(label="Person", data=users, name="user_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }} {{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }} {{ macros::input(label="Nachricht", name="message", type="text", required=true) }}

View File

@ -46,20 +46,42 @@
<div class="p-3"> <div class="p-3">
Folgende personenbezogenen haben wir von dir gespeichert: Folgende personenbezogenen haben wir von dir gespeichert:
<ul> <ul>
<li><strong>Name:</strong> {{ loggedin_user.name }}</li> <li>
<li><strong>Passwort:</strong> (verschlüsselt als argon Hash)</li> <strong>Name:</strong> {{ loggedin_user.name }}
<li><strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}</li> </li>
<li><strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}</li> <li>
<li><strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}</li> <strong>Passwort:</strong> (verschlüsselt als argon Hash)
<li><strong>Mail:</strong> {{ loggedin_user.mail }}</li> </li>
{% if loggedin_user.nickname %}<li><strong>Spitzname:</strong> {{ loggedin_user.nickname }}</li>{% endif %} <li>
<li><strong>Telefonnummer:</strong> {{ loggedin_user.phone }}</li> <strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}
<li><strong>Adresse:</strong> {{ loggedin_user.address }}</li> </li>
<li>
<strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}
</li>
<li>
<strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}
</li>
<li>
<strong>Mail:</strong> {{ loggedin_user.mail }}
</li>
{% if loggedin_user.nickname %}
<li>
<strong>Spitzname:</strong> {{ loggedin_user.nickname }}
</li>
{% endif %}
<li>
<strong>Telefonnummer:</strong> {{ loggedin_user.phone }}
</li>
<li>
<strong>Adresse:</strong> {{ loggedin_user.address }}
</li>
<li>(Beitrittserklärung)</li> <li>(Beitrittserklärung)</li>
{% if loggedin_user.family_id %} {% if loggedin_user.family_id %}
<li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li> <li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li>
{% endif %} {% endif %}
<li><strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)</li> <li>
<strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)
</li>
<li>Anmeldungen zu Ausfahrten</li> <li>Anmeldungen zu Ausfahrten</li>
<li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li> <li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li>
<li>Logbucheinträge</li> <li>Logbucheinträge</li>

View File

@ -3,7 +3,8 @@
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div> <div>
<span class="text-[#ff0000]">&hearts;</span> <span class="text-[#ff0000]">&hearts;</span>
Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline" onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a> Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline"
onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a>
</div> </div>
<div> <div>
<button id="theme-toggle-js" <button id="theme-toggle-js"

View File

@ -99,6 +99,17 @@
style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}"> style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="mr-1"> <div class="mr-1">
{% if planned_event.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888; Absage
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-[#f43f5e]">({{ planned_event.name }}
{%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
{%- endif -%}
)</small>
{% else %}
<strong class="text-primary-900 dark:text-white"> <strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }} {{ planned_event.planned_starting_time }}
Uhr Uhr
@ -108,6 +119,7 @@
- {{ 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>
{% endif %}
<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 }})
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %} {% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
@ -165,16 +177,22 @@
<div id="event{{ planned_event.trip_details_id }}"> <div id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #} {# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %} {% if planned_event.planned_amount_cox > 0 %}
{% if planned_event.max_people == 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
{% else %}
{% if amount_cox_missing > 0 %} {% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }} {{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %} {% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }} {{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{# --- END List Coxes --- #} {# --- END List Coxes --- #}
{# --- START List Rowers --- #} {# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %} {% set amount_cur_rower = planned_event.rower | length %}
{% if planned_event.max_people == 0 %}
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=planned_event.rower, trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% else %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }} {{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% endif %} {% endif %}
{# --- END List Rowers --- #} {# --- END List Rowers --- #}
@ -208,13 +226,35 @@
</div> </div>
{# --- END Edit Form --- #} {# --- END Edit Form --- #}
{# --- START Delete Btn --- #} {# --- START Delete Btn --- #}
<div class="text-right"> {% if planned_event.rower | length == 0 and amount_cur_cox == 0 %}
<div class="text-right mt-6">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" <a href="/admin/planned-event/{{ planned_event.id }}/delete"
class="inline-block btn btn-alert"> class="inline-block btn btn-alert">
{% include "includes/delete-icon" %} {% include "includes/delete-icon" %}
Termin löschen Termin löschen
</a> </a>
</div> </div>
{% else %}
{% if planned_event.max_people == 0 %}
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
{% else %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Event absagen</h3>
<form action="/admin/planned-event" method="post" class="grid">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ planned_event.id }}" />
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='', name='name', type='hidden', value=planned_event.name) }}
{{ macros::input(label='', name='max_people', type='hidden', value=planned_event.max_people) }}
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=planned_event.planned_amount_cox) }}
{{ macros::input(label='', name='notes', type='hidden', value=planned_event.notes) }}
{{ macros::input(label='', name='always_show', type='hidden', value=planned_event.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=planned_event.is_locked) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
</form>
</div>
{% endif %}
{% endif %}
{% endif %} {% endif %}
{# --- END Delete Btn --- #} {# --- END Delete Btn --- #}
</div> </div>