diff --git a/config.py b/config.py index 892794d..942b7c5 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.7" +APP_VERSION = "2.5.9" def load_local_env(env_path: Path) -> None: diff --git a/league_repositories.py b/league_repositories.py index 117f4fc..0665a67 100644 --- a/league_repositories.py +++ b/league_repositories.py @@ -348,3 +348,30 @@ def set_league_member_role(league_id: str, user_id: str, role: str) -> LeagueMem membership.role = role return membership + + +def transfer_league_ownership( + league: League, + current_owner_user_id: str, + new_owner_user_id: str, +) -> tuple[LeagueMembership, LeagueMembership] | None: + if current_owner_user_id == new_owner_user_id: + return None + + current_owner = LeagueMembership.query.filter_by( + league_id=league.id, + user_id=current_owner_user_id, + disabled_at=None, + ).one_or_none() + new_owner = LeagueMembership.query.filter_by( + league_id=league.id, + user_id=new_owner_user_id, + disabled_at=None, + ).one_or_none() + if current_owner is None or current_owner.role != "owner" or new_owner is None: + return None + + current_owner.role = "manager" + new_owner.role = "owner" + league.created_by_user_id = new_owner_user_id + return current_owner, new_owner diff --git a/routes/leagues.py b/routes/leagues.py index 5e85dae..adc2fba 100644 --- a/routes/leagues.py +++ b/routes/leagues.py @@ -1295,6 +1295,34 @@ def update_member_role(league_ref: str, user_id: str): return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) +@leagues_bp.post("/l//settings/transfer-ownership") +@login_required +def transfer_ownership(league_ref: str): + if not db_ready(): + flash("League database is not available.", "error") + return redirect(url_for("public.home")) + + from league_repositories import transfer_league_ownership + + league = require_league(league_ref, {"owner"}) + new_owner_user_id = request.form.get("new_owner_user_id", "").strip() + current_owner_user_id = current_user_id() or "" + + if not new_owner_user_id: + flash("Choose a member to transfer ownership to.", "error") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + transfer = transfer_league_ownership(league, current_owner_user_id, new_owner_user_id) + if transfer is None: + flash("Ownership can only be transferred to an active member.", "error") + return redirect(url_for("leagues.league_settings", league_ref=league.url_ref)) + + _old_owner, _new_owner = transfer + db.session.commit() + flash("League ownership transferred. Your role is now manager.", "success") + return redirect(url_for("leagues.dashboard", league_ref=league.url_ref)) + + @leagues_bp.post("/l//archive") @login_required def archive_league(league_ref: str): diff --git a/static/css/style.css b/static/css/style.css index 29bc89c..5ce2dd9 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1372,7 +1372,19 @@ td a:hover { color: var(--accent); } .btn--ghost:hover { border-color: var(--border-hi); color: var(--text-strong); } .btn--outline { background: var(--accent-tint); color: var(--accent); border: 1px solid var(--accent-chip-bd); } .btn--outline:hover { background: var(--accent-chip); } +.btn--warning { + background: var(--warn-tint); + color: var(--warn); + border: 1px solid var(--warn-tint-bd); + font-weight: 700; +} +.btn--warning:hover { + background: color-mix(in srgb, var(--warn) 18%, transparent); + border-color: color-mix(in srgb, var(--warn) 54%, transparent); + color: var(--warn); +} .btn--sm { padding: 7px 13px; font-size: 13px; } +.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; } /* legacy button classes mapped to new */ .primary-button { @@ -2300,12 +2312,26 @@ select.control { cursor: pointer; } justify-content: space-between; gap: 20px; flex-wrap: wrap; + padding: 4px 0 18px; + border-bottom: 1px solid var(--border); +} +.danger-row + .danger-row { + padding-top: 18px; +} +.danger-row:last-child { + padding-bottom: 0; + border-bottom: none; } .danger-row__title { display: block; color: var(--text-strong); margin-bottom: 4px; } +.danger-row__btn { + width: 168px; + justify-content: center; + text-align: center; +} /* Danger button */ .btn--danger { @@ -5199,4 +5225,5 @@ select.control { cursor: pointer; } font-weight: 600; color: var(--accent); transition: gap .15s; + } diff --git a/templates/league_settings.html b/templates/league_settings.html index 8a30362..738bcbd 100644 --- a/templates/league_settings.html +++ b/templates/league_settings.html @@ -117,18 +117,55 @@
+
+
+ Transfer ownership +

Choose an existing member to become the league owner. Your role will change to manager.

+
+ +
Archive league

Removes this league from your dashboard. All players, sessions, and ledger data are preserved.

- +
+ +