force-action-on-important-notifications #1176

Merged
philipp merged 4 commits from force-action-on-important-notifications into staging 2026-01-03 22:11:58 +01:00
5 changed files with 62 additions and 15 deletions

View File

@@ -26,6 +26,22 @@ impl Notification {
.await .await
.ok() .ok()
} }
pub async fn oldest_unread_with_action(db: &SqlitePool, user_id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading
FROM notification
WHERE user_id = ? AND read_at IS NULL AND action_after_reading IS NOT NULL
ORDER BY created_at ASC
LIMIT 1",
user_id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn create_with_tx( pub async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,

View File

@@ -104,9 +104,11 @@ pub struct Stat {
impl Stat { impl Stat {
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat { pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
let year = match year { let year = year.unwrap_or_else(|| chrono::Local::now().year());
Some(year) => year, let year_filter = if year == 0 {
None => chrono::Local::now().year(), String::new()
} else {
format!("AND l.arrival LIKE '{}-%'", year)
}; };
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
// proper guests // proper guests
@@ -121,7 +123,7 @@ LEFT JOIN (
FROM rower FROM rower
GROUP BY logbook_id GROUP BY logbook_id
) m ON l.id = m.logbook_id ) m ON l.id = m.logbook_id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external; WHERE l.distance_in_km IS NOT NULL {year_filter} AND not b.external;
" "
)) ))
.fetch_one(db) .fetch_one(db)
@@ -145,7 +147,7 @@ WHERE u.id NOT IN (
WHERE ro.name = 'Donau Linz' WHERE ro.name = 'Donau Linz'
) )
AND l.distance_in_km IS NOT NULL AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%' {year_filter}
AND u.name != 'Externe Steuerperson'; AND u.name != 'Externe Steuerperson';
" "
)) ))
@@ -183,25 +185,27 @@ AND u.name != 'Externe Steuerperson';
} }
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> { pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
let year = match year { let year = year.unwrap_or_else(|| chrono::Local::now().year());
Some(year) => year, let year_filter = if year == 0 {
None => chrono::Local::now().year(), String::new()
} else {
format!("AND l.arrival LIKE '{}-%'", year)
}; };
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!( sqlx::query(&format!(
" "
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM ( FROM (
SELECT * FROM user SELECT * FROM user
WHERE id IN ( WHERE id IN (
SELECT user_id FROM user_role SELECT user_id FROM user_role
JOIN role ON user_role.role_id = role.id JOIN role ON user_role.role_id = role.id
WHERE role.name = 'Donau Linz' WHERE role.name = 'Donau Linz'
) )
) u ) u
INNER JOIN rower r ON u.id = r.rower_id INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson' WHERE l.distance_in_km IS NOT NULL {year_filter} AND u.name != 'Externe Steuerperson'
GROUP BY u.name GROUP BY u.name
ORDER BY rowed_km DESC, u.name; ORDER BY rowed_km DESC, u.name;
" "

View File

@@ -88,17 +88,20 @@ pub struct UserWithDetails {
pub allowed_to_steer: bool, pub allowed_to_steer: bool,
pub on_water: bool, pub on_water: bool,
pub roles: Vec<String>, pub roles: Vec<String>,
pub action_notification: Option<Notification>,
} }
impl UserWithDetails { impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self { pub async fn from_user(user: User, db: &SqlitePool) -> Self {
let allowed_to_steer = user.allowed_to_steer(db).await; let allowed_to_steer = user.allowed_to_steer(db).await;
let action_notification = Notification::oldest_unread_with_action(db, user.id).await;
Self { Self {
on_water: user.on_water(db).await, on_water: user.on_water(db).await,
roles: user.roles(db).await, roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await, amount_unread_notifications: user.amount_unread_notifications(db).await,
allowed_to_steer, allowed_to_steer,
action_notification,
user, user,
} }
} }

View File

@@ -53,6 +53,21 @@
{% include "includes/footer" %} {% include "includes/footer" %}
{% endif %} {% endif %}
{% include "dynamics/sidebar" %} {% include "dynamics/sidebar" %}
{% if loggedin_user and loggedin_user.action_notification %}
<dialog id="action-notification-modal" class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md">
<div class="p-4">
<small class="text-gray-600 dark:text-gray-100">
<strong>{{ loggedin_user.action_notification.category }}</strong>
</small>
<div class="my-4">{{ loggedin_user.action_notification.message }}</div>
<a href="/notification/{{ loggedin_user.action_notification.id }}/read" class="btn btn-dark w-full mt-3">
&#10003;
<span class="sr-only">Notification gelesen</span>
</a>
</div>
</dialog>
<script>document.getElementById('action-notification-modal').showModal();</script>
{% endif %}
<script src="/public/main.js"></script> <script src="/public/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -78,11 +78,12 @@
var queryParams = new URLSearchParams(window.location.search); var queryParams = new URLSearchParams(window.location.search);
return queryParams.get('year'); return queryParams.get('year');
} }
function populateYears() { function populateYears() {
var select = document.getElementById('yearSelect'); var select = document.getElementById('yearSelect');
var currentYear = new Date().getFullYear(); var currentYear = new Date().getFullYear();
var selectedYear = getYearFromURL() || currentYear; var selectedYear = getYearFromURL() || currentYear;
for (var year = 1977; year <= currentYear; year++) { for (var year = 1977; year <= currentYear; year++) {
var option = document.createElement('option'); var option = document.createElement('option');
option.value = option.textContent = year; option.value = option.textContent = year;
@@ -91,13 +92,21 @@
} }
select.appendChild(option); select.appendChild(option);
} }
var gesamtOption = document.createElement('option');
gesamtOption.value = 0;
gesamtOption.textContent = 'GESAMT';
if (selectedYear == 0) {
gesamtOption.selected = true;
}
select.appendChild(gesamtOption);
} }
function changeYear() { function changeYear() {
var selectedYear = document.getElementById('yearSelect').value; var selectedYear = document.getElementById('yearSelect').value;
window.location.href = '?year=' + selectedYear; window.location.href = '?year=' + selectedYear;
} }
populateYears(); populateYears();
</script> </script>
{% endblock content %} {% endblock content %}