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 { 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(()) } }