Compare commits
16 Commits
f8ea6d5aa5
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
| 45f595e147 | |||
|
|
1beb6ebfe9 | ||
| 55666a6eff | |||
| de357c3db3 | |||
| 0e4c0573d9 | |||
| 6d36d01c2f | |||
| b2157a31c5 | |||
| 2ed22d6440 | |||
| 13de487b10 | |||
| bfb3ae4b6e | |||
| 3fcf24958b | |||
| 88a3e5f2d0 | |||
| 32c250536d | |||
| b9d0e2a2dc | |||
| b597898bdf | |||
| e90555214a |
@@ -133,19 +133,14 @@ WHERE l.distance_in_km IS NOT NULL {year_filter} AND not b.external;
|
||||
let guest_km: i32 = guests.get(0);
|
||||
let guest_amount_trips: i32 = guests.get(1);
|
||||
|
||||
// e.g. scheckbücher
|
||||
// e.g. scheckbücher (users without any role)
|
||||
let guest_user = sqlx::query(&format!(
|
||||
"
|
||||
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
|
||||
WHERE u.id NOT IN (
|
||||
SELECT ur.user_id
|
||||
FROM user_role ur
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'Donau Linz'
|
||||
)
|
||||
WHERE u.id NOT IN (SELECT user_id FROM user_role)
|
||||
AND l.distance_in_km IS NOT NULL
|
||||
{year_filter}
|
||||
AND u.name != 'Externe Steuerperson';
|
||||
@@ -195,14 +190,7 @@ AND u.name != 'Externe Steuerperson';
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
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 (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'Donau Linz'
|
||||
)
|
||||
) u
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL {year_filter} AND u.name != 'Externe Steuerperson'
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(crate) enum Member {
|
||||
Regular(User),
|
||||
Foerdernd(User),
|
||||
Unterstuetzend(User),
|
||||
NoMembership(User),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
@@ -31,7 +32,7 @@ impl Member {
|
||||
} else if user.has_role(db, "Unterstützend").await {
|
||||
Self::Unterstuetzend(user)
|
||||
} else {
|
||||
panic!("User {user} has no membership_type!!");
|
||||
Self::NoMembership(user)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
490
src/model/user/merge.rs
Normal file
490
src/model/user/merge.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{Row, Sqlite, SqlitePool, Transaction};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::model::{activity::ActivityBuilder, stat::Stat};
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct UserWithKm {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub total_km: i32,
|
||||
pub trip_count: i32,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
impl UserWithKm {
|
||||
/// Get all users with their total km stats, sorted by name
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT u.id, u.name, u.deleted,
|
||||
COALESCE(CAST(SUM(l.distance_in_km) AS INTEGER), 0) AS total_km,
|
||||
COUNT(r.logbook_id) AS trip_count
|
||||
FROM user u
|
||||
LEFT JOIN rower r ON u.id = r.rower_id
|
||||
LEFT JOIN logbook l ON r.logbook_id = l.id AND l.distance_in_km IS NOT NULL
|
||||
WHERE u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.id
|
||||
ORDER BY u.name COLLATE NOCASE
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| UserWithKm {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
total_km: row.get("total_km"),
|
||||
trip_count: row.get("trip_count"),
|
||||
deleted: row.get("deleted"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct MergePreview {
|
||||
pub source_user: User,
|
||||
pub target_user: User,
|
||||
pub source_total_km: i32,
|
||||
pub target_total_km: i32,
|
||||
pub source_trip_count: i32,
|
||||
pub target_trip_count: i32,
|
||||
pub rower_entries_to_transfer: i64,
|
||||
pub rower_conflicts: i64,
|
||||
pub role_entries_to_transfer: i64,
|
||||
pub role_conflicts: i64,
|
||||
pub user_trip_entries_to_transfer: i64,
|
||||
pub user_trip_conflicts: i64,
|
||||
pub logbook_shipmaster_entries: i64,
|
||||
pub logbook_steering_entries: i64,
|
||||
pub trip_cox_entries: i64,
|
||||
pub boat_owner_entries: i64,
|
||||
pub boat_damage_entries: i64,
|
||||
pub boat_reservation_entries: i64,
|
||||
pub trailer_reservation_entries: i64,
|
||||
pub notification_entries: i64,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Generate a preview of what would happen if source user is merged into target user.
|
||||
/// Source user will be deleted, target user will receive all references.
|
||||
pub async fn merge_preview(db: &SqlitePool, source: &User, target: &User) -> MergePreview {
|
||||
let source_stats = Stat::total_km(db, source).await;
|
||||
let target_stats = Stat::total_km(db, target).await;
|
||||
|
||||
// Rower entries to transfer (no conflict - source is in logbooks target isn't)
|
||||
let rower_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id NOT IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Rower conflicts (both users in same logbook - will delete source's entry)
|
||||
let rower_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Role entries to transfer (no conflict)
|
||||
let role_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id NOT IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Role conflicts (both have same role - will delete source's entry)
|
||||
let role_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User trip entries to transfer (no conflict)
|
||||
let user_trip_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id NOT IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User trip conflicts
|
||||
let user_trip_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simple counts for other tables
|
||||
let logbook_shipmaster_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM logbook WHERE shipmaster = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let logbook_steering_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM logbook WHERE steering_person = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip_cox_entries =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM trip WHERE cox_id = ?", source.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_owner_entries =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM boat WHERE owner = ?", source.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_damage_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM boat_damage
|
||||
WHERE user_id_created = ? OR user_id_fixed = ? OR user_id_verified = ?",
|
||||
source.id,
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_reservation_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM boat_reservation
|
||||
WHERE user_id_applicant = ? OR user_id_confirmation = ?",
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trailer_reservation_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM trailer_reservation
|
||||
WHERE user_id_applicant = ? OR user_id_confirmation = ?",
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let notification_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notification WHERE user_id = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
MergePreview {
|
||||
source_user: source.clone(),
|
||||
target_user: target.clone(),
|
||||
source_total_km: source_stats.rowed_km,
|
||||
target_total_km: target_stats.rowed_km,
|
||||
source_trip_count: source_stats.amount_trips,
|
||||
target_trip_count: target_stats.amount_trips,
|
||||
rower_entries_to_transfer,
|
||||
rower_conflicts,
|
||||
role_entries_to_transfer,
|
||||
role_conflicts,
|
||||
user_trip_entries_to_transfer,
|
||||
user_trip_conflicts,
|
||||
logbook_shipmaster_entries,
|
||||
logbook_steering_entries,
|
||||
trip_cox_entries,
|
||||
boat_owner_entries,
|
||||
boat_damage_entries,
|
||||
boat_reservation_entries,
|
||||
trailer_reservation_entries,
|
||||
notification_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge source user into target user, then hard delete source.
|
||||
/// All foreign key references are transferred from source to target.
|
||||
/// Returns Ok(()) on success, Err with description on failure.
|
||||
pub async fn merge_into(
|
||||
db: &SqlitePool,
|
||||
source: &User,
|
||||
target: &User,
|
||||
merged_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
// Validation
|
||||
if source.id == target.id {
|
||||
return Err("Kann Benutzer nicht mit sich selbst zusammenführen".into());
|
||||
}
|
||||
|
||||
if source.name == "Externe Steuerperson" {
|
||||
return Err("'Externe Steuerperson' kann nicht zusammengeführt werden".into());
|
||||
}
|
||||
|
||||
if source.on_water(db).await {
|
||||
return Err(format!(
|
||||
"{} ist gerade auf dem Wasser und kann nicht zusammengeführt werden",
|
||||
source.name
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// Execute merge in transaction
|
||||
Self::merge_into_tx(&mut tx, source, target).await?;
|
||||
|
||||
// Log activity
|
||||
ActivityBuilder::new(&format!(
|
||||
"{} hat Benutzer '{}' ({} km, {} Ausfahrten) in '{}' zusammengeführt und gelöscht.",
|
||||
merged_by.name,
|
||||
source.name,
|
||||
Stat::total_km(db, source).await.rowed_km,
|
||||
Stat::total_km(db, source).await.amount_trips,
|
||||
target.name
|
||||
))
|
||||
.user(target)
|
||||
.save_tx(&mut tx)
|
||||
.await;
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn merge_into_tx(
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
source: &User,
|
||||
target: &User,
|
||||
) -> Result<(), String> {
|
||||
// Step 1: DELETE conflicts (where both users have same FK target)
|
||||
|
||||
// Delete rower entries where both users rowed in same logbook
|
||||
sqlx::query!(
|
||||
"DELETE FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete role entries where both users have same role
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete user_trip entries where both users in same trip
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: UPDATE remaining references
|
||||
|
||||
// rower.rower_id
|
||||
sqlx::query!(
|
||||
"UPDATE rower SET rower_id = ? WHERE rower_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// user_role.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE user_role SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// user_trip.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE user_trip SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// logbook.shipmaster
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET shipmaster = ? WHERE shipmaster = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// logbook.steering_person
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET steering_person = ? WHERE steering_person = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// trip.cox_id
|
||||
sqlx::query!(
|
||||
"UPDATE trip SET cox_id = ? WHERE cox_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat.owner
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET owner = ? WHERE owner = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat_damage (3 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_created = ? WHERE user_id_created = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_fixed = ? WHERE user_id_fixed = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_verified = ? WHERE user_id_verified = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat_reservation (2 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET user_id_applicant = ? WHERE user_id_applicant = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET user_id_confirmation = ? WHERE user_id_confirmation = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// trailer_reservation (2 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET user_id_applicant = ? WHERE user_id_applicant = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET user_id_confirmation = ? WHERE user_id_confirmation = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// notification.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE notification SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 3: Hard delete the source user
|
||||
sqlx::query!("DELETE FROM user WHERE id = ?", source.id)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ pub(crate) mod clubmember;
|
||||
mod fee;
|
||||
pub(crate) mod foerdernd;
|
||||
pub(crate) mod member;
|
||||
pub mod merge;
|
||||
pub(crate) mod nomembership;
|
||||
pub(crate) mod regular;
|
||||
pub(crate) mod scheckbuch;
|
||||
pub(crate) mod schnupperant;
|
||||
|
||||
233
src/model/user/nomembership.rs
Normal file
233
src/model/user/nomembership.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::notification::Notification;
|
||||
use chrono::NaiveDate;
|
||||
use rocket::fs::TempFile;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub(crate) struct NoMembershipUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for NoMembershipUser {
|
||||
type Target = User;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NoMembershipUser {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.user.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl NoMembershipUser {
|
||||
pub(crate) async fn new(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
if ScheckbuchUser::new(db, user).await.is_some() {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "schnupper-interessierte").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "schnupperant").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Donau Linz").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Förderndes Mitglied").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Unterstützend").await {
|
||||
return None;
|
||||
}
|
||||
Some(Self { user: user.clone() })
|
||||
}
|
||||
|
||||
async fn set_data_for_clubmember(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.user.update_birthdate(db, changed_by, birthdate).await;
|
||||
self.user
|
||||
.update_member_since(db, changed_by, member_since)
|
||||
.await;
|
||||
self.user.update_phone(db, changed_by, &phone).await;
|
||||
self.user.update_address(db, changed_by, &address).await;
|
||||
self.user
|
||||
.add_membership_pdf(db, changed_by, membership_pdf)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_regular_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
|
||||
let regular = RegularUser::new(db, &self.user).await.unwrap();
|
||||
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues reguläres Mitglied. 🎉",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_unterstuetzend_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
.send_welcome_mail_to_user(db, smtp_pw)
|
||||
.await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues unterstützendes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein unterstützendes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_foerdernd_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let foerdernd = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
self.user.add_role(db, changed_by, &foerdernd).await?;
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues förderndes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ use crate::{
|
||||
role::Role,
|
||||
user::{
|
||||
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
|
||||
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
|
||||
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
|
||||
nomembership::NoMembershipUser, regular::RegularUser, scheckbuch::ScheckbuchUser,
|
||||
schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser,
|
||||
unterstuetzend::UnterstuetzendUser,
|
||||
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
|
||||
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
|
||||
},
|
||||
@@ -64,6 +65,7 @@ async fn index(
|
||||
|
||||
let user: User = user.into_inner();
|
||||
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
|
||||
let is_admin = AdminUser::new(db, &user).await.is_some();
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
let financial = Role::all_cluster(db, "financial").await;
|
||||
@@ -76,6 +78,7 @@ async fn index(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("is_admin", &is_admin);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
@@ -110,6 +113,7 @@ async fn index_admin(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("is_admin", &true);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
@@ -306,6 +310,97 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
|
||||
}
|
||||
}
|
||||
|
||||
use crate::model::user::merge::UserWithKm;
|
||||
|
||||
#[get("/user/merge?<source>&<target>")]
|
||||
async fn merge_page(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
source: Option<i32>,
|
||||
target: Option<i32>,
|
||||
) -> Template {
|
||||
let users_with_km = UserWithKm::all(db).await;
|
||||
|
||||
let admin_user: User = admin.into_inner();
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("users", &users_with_km);
|
||||
|
||||
// If both source and target are selected, show preview
|
||||
if let (Some(source_id), Some(target_id)) = (source, target) {
|
||||
if source_id != target_id {
|
||||
if let (Some(source_user), Some(target_user)) = (
|
||||
User::find_by_id(db, source_id).await,
|
||||
User::find_by_id(db, target_id).await,
|
||||
) {
|
||||
let preview = User::merge_preview(db, &source_user, &target_user).await;
|
||||
context.insert("source_user", &source_user);
|
||||
context.insert("target_user", &target_user);
|
||||
context.insert("preview", &preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("selected_source", &source);
|
||||
context.insert("selected_target", &target);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin_user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/merge", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MergeForm {
|
||||
source_id: i32,
|
||||
target_id: i32,
|
||||
}
|
||||
|
||||
#[post("/user/merge", data = "<data>")]
|
||||
async fn merge_execute(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
data: Form<MergeForm>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(source_user) = User::find_by_id(db, data.source_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/merge"),
|
||||
format!("User mit ID {} existiert nicht", data.source_id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(target_user) = User::find_by_id(db, data.target_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/merge"),
|
||||
format!("Ziel-User mit ID {} existiert nicht", data.target_id),
|
||||
);
|
||||
};
|
||||
|
||||
let source_name = source_user.name.clone();
|
||||
|
||||
match User::merge_into(db, &source_user, &target_user, &admin).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", data.target_id)),
|
||||
format!(
|
||||
"Benutzer '{}' erfolgreich in '{}' zusammengeführt",
|
||||
source_name, target_user.name
|
||||
),
|
||||
),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to(format!(
|
||||
"/admin/user/merge?source={}&target={}",
|
||||
data.source_id, data.target_id
|
||||
)),
|
||||
e,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailUpdateForm {
|
||||
mail: String,
|
||||
@@ -1047,6 +1142,115 @@ async fn scheckbook_to_regular(
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/user/<id>/nomembership-to-regular", data = "<data>")]
|
||||
async fn nomembership_to_regular(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ScheckToRegularForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = NoMembershipUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"User hat keinen fehlenden Mitgliedsstatus",
|
||||
);
|
||||
};
|
||||
|
||||
let Ok(phone) = data.phone.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Telefonnummer",
|
||||
);
|
||||
};
|
||||
let Ok(address) = data.address.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Adresse",
|
||||
);
|
||||
};
|
||||
let response = match &*data.membertype {
|
||||
"regular" => {
|
||||
user.convert_to_regular_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"unterstuetzend" => {
|
||||
user.convert_to_unterstuetzend_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"foerdernd" => {
|
||||
user.convert_to_foerdernd_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Membertype gibts ned",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeMembertypeForm {
|
||||
membertype: String,
|
||||
@@ -1437,6 +1641,9 @@ pub fn routes() -> Vec<Route> {
|
||||
view,
|
||||
resetpw,
|
||||
delete,
|
||||
// Merge
|
||||
merge_page,
|
||||
merge_execute,
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
@@ -1457,6 +1664,7 @@ pub fn routes() -> Vec<Route> {
|
||||
remove_role,
|
||||
// Moves
|
||||
scheckbook_to_regular,
|
||||
nomembership_to_regular,
|
||||
schnupperant_to_regular,
|
||||
schnupperant_to_scheckbook,
|
||||
schnupperinterest_to_schnupperant,
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Users</h1>
|
||||
{% if allowed_to_edit %}
|
||||
{% if is_admin %}
|
||||
<div class="mt-5 flex gap-3">
|
||||
<a href="/admin/user/merge" class="btn btn-dark">Benutzer zusammenführen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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
|
||||
|
||||
141
templates/admin/user/merge.html.tera
Normal file
141
templates/admin/user/merge.html.tera
Normal file
@@ -0,0 +1,141 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-xl w-full">
|
||||
<div class="mb-5 lg:mb-0">
|
||||
<a href="/admin/user" class="link link-primary link-no-underline">← Userverwaltung</a>
|
||||
</div>
|
||||
<h1 class="h1">Benutzer zusammenführen</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Wähle zwei Benutzer aus: Der erste (Quelle) wird gelöscht und alle Daten werden zum zweiten (Ziel) übertragen.
|
||||
</p>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-4">
|
||||
<h2 class="text-lg font-bold mb-3 text-red-600 dark:text-red-400">Quelle (wird gelöscht)</h2>
|
||||
<form method="get" id="source-form">
|
||||
{% if selected_target %}
|
||||
<input type="hidden" name="target" value="{{ selected_target }}" />
|
||||
{% endif %}
|
||||
<select name="source" class="input rounded-md w-full" onchange="this.form.submit()">
|
||||
<option value="">-- Benutzer auswählen --</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if selected_source == user.id %}selected{% endif %}>
|
||||
{{ user.name }}{% if user.deleted %} [gelöscht]{% endif %} ({{ user.total_km }} km)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-4">
|
||||
<h2 class="text-lg font-bold mb-3 text-green-600 dark:text-green-400">Ziel (bleibt erhalten)</h2>
|
||||
<form method="get" id="target-form">
|
||||
{% if selected_source %}
|
||||
<input type="hidden" name="source" value="{{ selected_source }}" />
|
||||
{% endif %}
|
||||
<select name="target" class="input rounded-md w-full" onchange="this.form.submit()">
|
||||
<option value="">-- Benutzer auswählen --</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if selected_target == user.id %}selected{% endif %}>
|
||||
{{ user.name }}{% if user.deleted %} [gelöscht]{% endif %} ({{ user.total_km }} km)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if preview %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-6 mb-6">
|
||||
<h2 class="text-lg font-bold mb-4">Vorschau der Änderungen</h2>
|
||||
|
||||
<div class="grid sm:grid-cols-3 gap-6 mb-6">
|
||||
<div class="border border-red-300 dark:border-red-700 rounded-md p-4 bg-red-50 dark:bg-red-900/20">
|
||||
<h3 class="font-semibold text-red-700 dark:text-red-400 mb-2">
|
||||
{{ source_user.name }}
|
||||
<span class="text-sm font-normal block">(wird gelöscht)</span>
|
||||
</h3>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li><strong>{{ preview.source_total_km }}</strong> km</li>
|
||||
<li><strong>{{ preview.source_trip_count }}</strong> Ausfahrten</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center text-4xl text-gray-400">
|
||||
→
|
||||
</div>
|
||||
|
||||
<div class="border border-green-300 dark:border-green-700 rounded-md p-4 bg-green-50 dark:bg-green-900/20">
|
||||
<h3 class="font-semibold text-green-700 dark:text-green-400 mb-2">
|
||||
{{ target_user.name }}
|
||||
<span class="text-sm font-normal block">(bleibt)</span>
|
||||
</h3>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li><strong>{{ preview.target_total_km }}</strong> km</li>
|
||||
<li><strong>{{ preview.target_trip_count }}</strong> Ausfahrten</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-md p-4 mb-4">
|
||||
<h3 class="font-semibold mb-2">Nach Zusammenführung:</h3>
|
||||
<p class="text-lg">
|
||||
<strong>{{ target_user.name }}</strong> wird haben:
|
||||
<strong>{{ preview.source_total_km + preview.target_total_km }}</strong> km,
|
||||
<strong>{{ preview.source_trip_count + preview.target_trip_count - preview.rower_conflicts }}</strong> Ausfahrten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% set total_to_transfer = preview.rower_entries_to_transfer + preview.role_entries_to_transfer + preview.user_trip_entries_to_transfer + preview.logbook_shipmaster_entries + preview.logbook_steering_entries %}
|
||||
{% if total_to_transfer > 0 %}
|
||||
<div class="mb-4">
|
||||
<h3 class="font-semibold mb-2">Daten die übertragen werden:</h3>
|
||||
<ul class="text-sm list-disc ml-6 space-y-1">
|
||||
{% if preview.rower_entries_to_transfer > 0 %}
|
||||
<li>{{ preview.rower_entries_to_transfer }} Ausfahrten</li>
|
||||
{% endif %}
|
||||
{% if preview.role_entries_to_transfer > 0 %}
|
||||
<li>{{ preview.role_entries_to_transfer }} Rollen</li>
|
||||
{% endif %}
|
||||
{% if preview.logbook_shipmaster_entries > 0 %}
|
||||
<li>{{ preview.logbook_shipmaster_entries }} Logbuch-Einträge (als Schiffsführer)</li>
|
||||
{% endif %}
|
||||
{% if preview.logbook_steering_entries > 0 %}
|
||||
<li>{{ preview.logbook_steering_entries }} Logbuch-Einträge (als Steuerperson)</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set total_conflicts = preview.rower_conflicts + preview.role_conflicts + preview.user_trip_conflicts %}
|
||||
{% if total_conflicts > 0 %}
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-300 dark:border-yellow-700 rounded-md p-3 mb-4">
|
||||
<p class="text-yellow-800 dark:text-yellow-300 font-semibold">
|
||||
{{ total_conflicts }} doppelte Einträge werden entfernt
|
||||
</p>
|
||||
<ul class="text-sm text-yellow-700 dark:text-yellow-400 list-disc ml-6 mt-1">
|
||||
{% if preview.rower_conflicts > 0 %}
|
||||
<li>{{ preview.rower_conflicts }} Ausfahrten (beide waren im selben Boot)</li>
|
||||
{% endif %}
|
||||
{% if preview.role_conflicts > 0 %}
|
||||
<li>{{ preview.role_conflicts }} Rollen (beide haben dieselbe Rolle)</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/admin/user/merge" method="post" class="flex gap-4">
|
||||
<input type="hidden" name="source_id" value="{{ source_user.id }}" />
|
||||
<input type="hidden" name="target_id" value="{{ target_user.id }}" />
|
||||
<a href="/admin/user/merge" class="btn btn-secondary flex-1 text-center">Abbrechen</a>
|
||||
<button type="submit"
|
||||
class="btn btn-alert flex-1"
|
||||
onclick="return confirm('Bist du sicher? {{ source_user.name }} wird unwiderruflich gelöscht und alle Daten zu {{ target_user.name }} übertragen!')">
|
||||
Zusammenführen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -73,6 +73,8 @@
|
||||
Förderndes Vereinsmitglied
|
||||
{% elif "Unterstuetzend" in member %}
|
||||
Unterstützendes Vereinsmitglied
|
||||
{% elif "NoMembership" in member %}
|
||||
⚠️ Kein Mitgliedsstatus!
|
||||
{% endif %}
|
||||
</small>
|
||||
</h2>
|
||||
@@ -228,8 +230,19 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "NoMembership" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member or "NoMembership" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
<button type="button"
|
||||
@@ -257,6 +270,8 @@
|
||||
{% set action = "scheckbook-to-regular" %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% set action = "schnupperant-to-regular" %}
|
||||
{% elif "NoMembership" in member %}
|
||||
{% set action = "nomembership-to-regular" %}
|
||||
{% endif %}
|
||||
<form action="/admin/user/{{ user.id }}/{{ action }}"
|
||||
method="post"
|
||||
|
||||
Reference in New Issue
Block a user