491 lines
14 KiB
Rust
491 lines
14 KiB
Rust
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(())
|
|
}
|
|
}
|