actually add seasons
This commit is contained in:
1 parent
cae60c9305
commit
48ad7fe1a6
11 files changed
+700
-10
No files matched your search
@@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv"
|
|||||||
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
||||||
|
|
||||||
ELIGIBLE_MIN_SESSIONS = 3
|
ELIGIBLE_MIN_SESSIONS = 3
|
||||||
APP_VERSION = "2.5.17"
|
APP_VERSION = "2.5.18"
|
||||||
|
|
||||||
|
|
||||||
def load_local_env(env_path: Path) -> None:
|
def load_local_env(env_path: Path) -> None:
|
||||||
|
|||||||
@@ -375,3 +375,76 @@ def transfer_league_ownership(
|
|||||||
new_owner.role = "owner"
|
new_owner.role = "owner"
|
||||||
league.created_by_user_id = new_owner_user_id
|
league.created_by_user_id = new_owner_user_id
|
||||||
return current_owner, new_owner
|
return current_owner, new_owner
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Seasons
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def list_seasons_for_league(league_id: str, include_archived: bool = False) -> list[Season]:
|
||||||
|
q = Season.query.filter_by(league_id=league_id)
|
||||||
|
if not include_archived:
|
||||||
|
q = q.filter(Season.archived_at.is_(None))
|
||||||
|
return q.order_by(Season.sort_order.asc(), Season.created_at.asc()).all()
|
||||||
|
|
||||||
|
|
||||||
|
def find_season(league_id: str, season_id: str) -> Season | None:
|
||||||
|
return Season.query.filter_by(league_id=league_id, id=season_id).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def update_season(
|
||||||
|
season: Season,
|
||||||
|
name: str,
|
||||||
|
start_date: date | None = None,
|
||||||
|
end_date: date | None = None,
|
||||||
|
) -> Season:
|
||||||
|
season.name = name.strip()
|
||||||
|
season.start_date = start_date
|
||||||
|
season.end_date = end_date
|
||||||
|
return season
|
||||||
|
|
||||||
|
|
||||||
|
def archive_season(season: Season) -> Season:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
season.archived_at = datetime.now(timezone.utc)
|
||||||
|
return season
|
||||||
|
|
||||||
|
|
||||||
|
def unarchive_season(season: Season) -> Season:
|
||||||
|
season.archived_at = None
|
||||||
|
return season
|
||||||
|
|
||||||
|
|
||||||
|
def delete_season(season: Season) -> None:
|
||||||
|
PokerSession.query.filter_by(season_id=season.id).update({"season_id": None})
|
||||||
|
db.session.delete(season)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_assign_sessions_to_seasons(league_id: str) -> int:
|
||||||
|
"""Assign unassigned sessions to seasons based on date ranges.
|
||||||
|
|
||||||
|
Only seasons with both start_date and end_date set are considered.
|
||||||
|
Sessions with exactly one matching season are assigned; sessions that
|
||||||
|
match zero or multiple seasons are left untouched (caller decides).
|
||||||
|
Returns the number of sessions assigned.
|
||||||
|
"""
|
||||||
|
eligible_seasons = [
|
||||||
|
s for s in list_seasons_for_league(league_id, include_archived=False)
|
||||||
|
if s.start_date is not None and s.end_date is not None
|
||||||
|
]
|
||||||
|
if not eligible_seasons:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
unassigned = PokerSession.query.filter_by(
|
||||||
|
league_id=league_id, season_id=None
|
||||||
|
).all()
|
||||||
|
assigned = 0
|
||||||
|
for session in unassigned:
|
||||||
|
matches = [
|
||||||
|
s for s in eligible_seasons
|
||||||
|
if s.start_date <= session.session_date <= s.end_date
|
||||||
|
]
|
||||||
|
if len(matches) == 1:
|
||||||
|
session.season_id = matches[0].id
|
||||||
|
assigned += 1
|
||||||
|
return assigned
|
||||||
+263
-5
@@ -307,7 +307,11 @@ def leaderboard(league_ref: str):
|
|||||||
return redirect(url_for("public.home"))
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
from ledger_repositories import list_event_rows_for_league
|
from ledger_repositories import list_event_rows_for_league
|
||||||
from league_repositories import list_players_for_league, user_has_league_role
|
from league_repositories import (
|
||||||
|
list_players_for_league,
|
||||||
|
list_seasons_for_league,
|
||||||
|
user_has_league_role,
|
||||||
|
)
|
||||||
|
|
||||||
league, resp = get_league_with_visibility_gate(league_ref)
|
league, resp = get_league_with_visibility_gate(league_ref)
|
||||||
if resp:
|
if resp:
|
||||||
@@ -319,6 +323,25 @@ def leaderboard(league_ref: str):
|
|||||||
all_sessions = build_session_summaries(list_event_rows_for_league(league.id))
|
all_sessions = build_session_summaries(list_event_rows_for_league(league.id))
|
||||||
ordered_sessions = sorted(all_sessions, key=session_sort_key)
|
ordered_sessions = sorted(all_sessions, key=session_sort_key)
|
||||||
|
|
||||||
|
# Season filter: restrict to sessions belonging to a specific season.
|
||||||
|
seasons = list_seasons_for_league(league.id)
|
||||||
|
selected_season_id = request.args.get("season", "").strip()
|
||||||
|
selected_season = None
|
||||||
|
if selected_season_id:
|
||||||
|
from league_repositories import find_season, list_sessions_for_league
|
||||||
|
selected_season = find_season(league.id, selected_season_id)
|
||||||
|
if selected_season:
|
||||||
|
from ledger_repositories import session_event_ref
|
||||||
|
season_db_sessions = list_sessions_for_league(league.id)
|
||||||
|
season_refs = {
|
||||||
|
session_event_ref(s)
|
||||||
|
for s in season_db_sessions
|
||||||
|
if s.season_id == selected_season_id
|
||||||
|
}
|
||||||
|
ordered_sessions = [s for s in ordered_sessions if s.session_id in season_refs]
|
||||||
|
else:
|
||||||
|
selected_season_id = ""
|
||||||
|
|
||||||
session_ids = [session.session_id for session in ordered_sessions]
|
session_ids = [session.session_id for session in ordered_sessions]
|
||||||
selected_session_id = request.args.get("through_session", "").strip()
|
selected_session_id = request.args.get("through_session", "").strip()
|
||||||
mode = request.args.get("mode", "eligible").strip()
|
mode = request.args.get("mode", "eligible").strip()
|
||||||
@@ -390,7 +413,7 @@ def leaderboard(league_ref: str):
|
|||||||
total_session_count=len(all_sessions),
|
total_session_count=len(all_sessions),
|
||||||
cash_paid_out_cents=cash_paid_out_cents,
|
cash_paid_out_cents=cash_paid_out_cents,
|
||||||
chart_data=chart_data,
|
chart_data=chart_data,
|
||||||
available_sessions=all_sessions,
|
available_sessions=ordered_sessions,
|
||||||
selected_session_id=selected_session_id,
|
selected_session_id=selected_session_id,
|
||||||
selected_session_label=(label if selected_session_id else "Latest session"),
|
selected_session_label=(label if selected_session_id else "Latest session"),
|
||||||
selected_session_date=(
|
selected_session_date=(
|
||||||
@@ -406,6 +429,9 @@ def leaderboard(league_ref: str):
|
|||||||
},
|
},
|
||||||
can_manage=can_manage,
|
can_manage=can_manage,
|
||||||
is_owner=is_owner,
|
is_owner=is_owner,
|
||||||
|
seasons=seasons,
|
||||||
|
selected_season=selected_season,
|
||||||
|
selected_season_id=selected_season_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -759,6 +785,7 @@ def sessions(league_ref: str):
|
|||||||
from ledger_repositories import append_ledger_event
|
from ledger_repositories import append_ledger_event
|
||||||
from league_repositories import (
|
from league_repositories import (
|
||||||
create_poker_session,
|
create_poker_session,
|
||||||
|
list_seasons_for_league,
|
||||||
list_sessions_for_league,
|
list_sessions_for_league,
|
||||||
user_has_league_role,
|
user_has_league_role,
|
||||||
)
|
)
|
||||||
@@ -777,6 +804,7 @@ def sessions(league_ref: str):
|
|||||||
"label": request.form.get("label", "").strip(),
|
"label": request.form.get("label", "").strip(),
|
||||||
"notes": request.form.get("notes", "").strip(),
|
"notes": request.form.get("notes", "").strip(),
|
||||||
"status": request.form.get("status", "open").strip(),
|
"status": request.form.get("status", "open").strip(),
|
||||||
|
"season_id": request.form.get("season_id", "").strip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -786,9 +814,23 @@ def sessions(league_ref: str):
|
|||||||
flash("Session date must be a valid date.", "error")
|
flash("Session date must be a valid date.", "error")
|
||||||
else:
|
else:
|
||||||
status = "closed" if form["status"] == "closed" else "open"
|
status = "closed" if form["status"] == "closed" else "open"
|
||||||
|
season_id = form["season_id"] or None
|
||||||
|
if season_id is None:
|
||||||
|
from league_repositories import auto_assign_sessions_to_seasons as _auto
|
||||||
|
# try auto-assign: create the session first, then let the
|
||||||
|
# function match it; we pass a temporary session date check inline
|
||||||
|
from league_repositories import list_seasons_for_league as _ls
|
||||||
|
eligible = [
|
||||||
|
s for s in _ls(league.id, include_archived=False)
|
||||||
|
if s.start_date and s.end_date
|
||||||
|
and s.start_date <= session_date <= s.end_date
|
||||||
|
]
|
||||||
|
if len(eligible) == 1:
|
||||||
|
season_id = eligible[0].id
|
||||||
session = create_poker_session(
|
session = create_poker_session(
|
||||||
league.id,
|
league.id,
|
||||||
session_date,
|
session_date,
|
||||||
|
season_id=season_id,
|
||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
session.label = form["label"] or None
|
session.label = form["label"] or None
|
||||||
@@ -826,6 +868,17 @@ def sessions(league_ref: str):
|
|||||||
for sm in summaries
|
for sm in summaries
|
||||||
if sm.session_id in ref_to_db_id
|
if sm.session_id in ref_to_db_id
|
||||||
}
|
}
|
||||||
|
seasons = list_seasons_for_league(league.id)
|
||||||
|
season_map = {s.id: s for s in seasons}
|
||||||
|
|
||||||
|
selected_season_id = request.args.get("season", "").strip()
|
||||||
|
selected_season = season_map.get(selected_season_id) if selected_season_id else None
|
||||||
|
if selected_season:
|
||||||
|
display_sessions = [s for s in all_sessions if s.season_id == selected_season_id]
|
||||||
|
else:
|
||||||
|
selected_season_id = ""
|
||||||
|
display_sessions = all_sessions
|
||||||
|
|
||||||
empty_count = sum(
|
empty_count = sum(
|
||||||
1 for s in all_sessions
|
1 for s in all_sessions
|
||||||
if s.id not in db_id_to_summary or not db_id_to_summary[s.id].entries
|
if s.id not in db_id_to_summary or not db_id_to_summary[s.id].entries
|
||||||
@@ -834,12 +887,17 @@ def sessions(league_ref: str):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"league_sessions.html",
|
"league_sessions.html",
|
||||||
league=league,
|
league=league,
|
||||||
sessions=all_sessions,
|
sessions=display_sessions,
|
||||||
|
all_session_count=len(all_sessions),
|
||||||
db_id_to_summary=db_id_to_summary,
|
db_id_to_summary=db_id_to_summary,
|
||||||
form=form,
|
form=form,
|
||||||
can_manage=can_manage,
|
can_manage=can_manage,
|
||||||
is_owner=is_owner,
|
is_owner=is_owner,
|
||||||
empty_count=empty_count,
|
empty_count=empty_count,
|
||||||
|
seasons=seasons,
|
||||||
|
season_map=season_map,
|
||||||
|
selected_season=selected_season,
|
||||||
|
selected_season_id=selected_season_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -916,6 +974,7 @@ def edit_session(league_ref: str, session_id: str):
|
|||||||
new_label = request.form.get("label", "").strip() or None
|
new_label = request.form.get("label", "").strip() or None
|
||||||
new_notes = request.form.get("notes", "").strip() or None
|
new_notes = request.form.get("notes", "").strip() or None
|
||||||
new_date_str = request.form.get("session_date", "").strip()
|
new_date_str = request.form.get("session_date", "").strip()
|
||||||
|
new_season_id = request.form.get("season_id", "").strip() or None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_date = date.fromisoformat(new_date_str)
|
new_date = date.fromisoformat(new_date_str)
|
||||||
@@ -933,8 +992,14 @@ def edit_session(league_ref: str, session_id: str):
|
|||||||
session.sequence_on_date = int(max_seq or 0) + 1
|
session.sequence_on_date = int(max_seq or 0) + 1
|
||||||
session.session_date = new_date
|
session.session_date = new_date
|
||||||
|
|
||||||
|
if new_season_id is not None:
|
||||||
|
from league_repositories import find_season
|
||||||
|
valid = find_season(league.id, new_season_id)
|
||||||
|
new_season_id = valid.id if valid else None
|
||||||
|
|
||||||
session.label = new_label
|
session.label = new_label
|
||||||
session.notes = new_notes
|
session.notes = new_notes
|
||||||
|
session.season_id = new_season_id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Session updated.", "success")
|
flash("Session updated.", "success")
|
||||||
return redirect(url_for("leagues.session_detail", league_ref=league.url_ref, session_id=session_id))
|
return redirect(url_for("leagues.session_detail", league_ref=league.url_ref, session_id=session_id))
|
||||||
@@ -951,6 +1016,7 @@ def session_detail(league_ref: str, session_id: str):
|
|||||||
from league_repositories import (
|
from league_repositories import (
|
||||||
find_session_for_league,
|
find_session_for_league,
|
||||||
list_players_for_league,
|
list_players_for_league,
|
||||||
|
list_seasons_for_league,
|
||||||
user_has_league_role,
|
user_has_league_role,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1012,6 +1078,7 @@ def session_detail(league_ref: str, session_id: str):
|
|||||||
summary = summaries[0] if summaries else empty_session_summary(session)
|
summary = summaries[0] if summaries else empty_session_summary(session)
|
||||||
all_rows = list_all_event_rows_for_session(league.id, session.id)
|
all_rows = list_all_event_rows_for_session(league.id, session.id)
|
||||||
|
|
||||||
|
seasons = list_seasons_for_league(league.id)
|
||||||
return render_template(
|
return render_template(
|
||||||
"league_session_detail.html",
|
"league_session_detail.html",
|
||||||
league=league,
|
league=league,
|
||||||
@@ -1024,6 +1091,7 @@ def session_detail(league_ref: str, session_id: str):
|
|||||||
can_manage=can_manage,
|
can_manage=can_manage,
|
||||||
is_owner=is_owner,
|
is_owner=is_owner,
|
||||||
session_label=session_label,
|
session_label=session_label,
|
||||||
|
seasons=seasons,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1035,7 +1103,7 @@ def session_public_view(league_ref: str, session_id: str):
|
|||||||
|
|
||||||
from charts import session_breakdown_series
|
from charts import session_breakdown_series
|
||||||
from ledger_repositories import list_event_rows_for_league, list_event_rows_for_session
|
from ledger_repositories import list_event_rows_for_league, list_event_rows_for_session
|
||||||
from league_repositories import find_league_by_public_key, find_session_for_league, list_sessions_for_league, user_has_league_role
|
from league_repositories import find_league_by_public_key, find_season, find_session_for_league, list_sessions_for_league, user_has_league_role
|
||||||
|
|
||||||
_slug, public_key = split_league_ref(league_ref)
|
_slug, public_key = split_league_ref(league_ref)
|
||||||
league = find_league_by_public_key(public_key)
|
league = find_league_by_public_key(public_key)
|
||||||
@@ -1078,6 +1146,8 @@ def session_public_view(league_ref: str, session_id: str):
|
|||||||
prev_summary = all_sessions[chrono_idx - 1] if chrono_idx > 0 else None
|
prev_summary = all_sessions[chrono_idx - 1] if chrono_idx > 0 else None
|
||||||
next_summary = all_sessions[chrono_idx + 1] if chrono_idx < len(all_sessions) - 1 else None
|
next_summary = all_sessions[chrono_idx + 1] if chrono_idx < len(all_sessions) - 1 else None
|
||||||
|
|
||||||
|
season = find_season(league.id, session.season_id) if session.season_id else None
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"league_session_view.html",
|
"league_session_view.html",
|
||||||
league=league,
|
league=league,
|
||||||
@@ -1091,6 +1161,7 @@ def session_public_view(league_ref: str, session_id: str):
|
|||||||
next_session_id=ref_to_db_id.get(next_summary.session_id) if next_summary else None,
|
next_session_id=ref_to_db_id.get(next_summary.session_id) if next_summary else None,
|
||||||
can_manage=can_manage,
|
can_manage=can_manage,
|
||||||
is_owner=is_owner,
|
is_owner=is_owner,
|
||||||
|
season=season,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1137,6 +1208,193 @@ def update_session_status(league_ref: str, session_id: str, status: str, message
|
|||||||
return redirect(url_for("leagues.sessions", **league_url_values(league)))
|
return redirect(url_for("leagues.sessions", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.route("/l/<league_ref>/seasons", methods=["GET", "POST"])
|
||||||
|
def seasons(league_ref: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import (
|
||||||
|
create_season,
|
||||||
|
list_seasons_for_league,
|
||||||
|
user_has_league_role,
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
else:
|
||||||
|
league, resp = get_league_with_visibility_gate(league_ref)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
user_id = current_user_id() or ""
|
||||||
|
can_manage = user_has_league_role(user_id, league.id, {"owner", "manager"})
|
||||||
|
is_owner = user_has_league_role(user_id, league.id, {"owner"})
|
||||||
|
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
start_raw = request.form.get("start_date", "").strip()
|
||||||
|
end_raw = request.form.get("end_date", "").strip()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if not name:
|
||||||
|
flash("Season name is required.", "error")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
start_date = date.fromisoformat(start_raw) if start_raw else None
|
||||||
|
end_date = date.fromisoformat(end_raw) if end_raw else None
|
||||||
|
except ValueError:
|
||||||
|
flash("Invalid date format.", "error")
|
||||||
|
start_date = end_date = None
|
||||||
|
else:
|
||||||
|
existing = list_seasons_for_league(league.id, include_archived=True)
|
||||||
|
create_season(
|
||||||
|
league.id,
|
||||||
|
name,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
sort_order=len(existing),
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Season \"{name}\" created.", "success")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
active_seasons = list_seasons_for_league(league.id, include_archived=False)
|
||||||
|
archived_seasons = list_seasons_for_league(league.id, include_archived=True)
|
||||||
|
archived_seasons = [s for s in archived_seasons if s.archived_at is not None]
|
||||||
|
return render_template(
|
||||||
|
"league_seasons.html",
|
||||||
|
league=league,
|
||||||
|
active_seasons=active_seasons,
|
||||||
|
archived_seasons=archived_seasons,
|
||||||
|
form={
|
||||||
|
"name": name if request.method == "POST" else "",
|
||||||
|
"start_date": start_raw if request.method == "POST" else "",
|
||||||
|
"end_date": end_raw if request.method == "POST" else "",
|
||||||
|
},
|
||||||
|
can_manage=can_manage,
|
||||||
|
is_owner=is_owner,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/seasons/auto-assign")
|
||||||
|
@login_required
|
||||||
|
def auto_assign_seasons(league_ref: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import auto_assign_sessions_to_seasons
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
count = auto_assign_sessions_to_seasons(league.id)
|
||||||
|
db.session.commit()
|
||||||
|
if count:
|
||||||
|
flash(f"Assigned {count} session{'s' if count != 1 else ''} to seasons.", "success")
|
||||||
|
else:
|
||||||
|
flash("No sessions could be auto-assigned. Check that your seasons have start and end dates set, and that unassigned sessions fall within exactly one season.", "info")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/seasons/<season_id>/update")
|
||||||
|
@login_required
|
||||||
|
def update_season(league_ref: str, season_id: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import find_season, update_season as repo_update_season
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
season = find_season(league.id, season_id)
|
||||||
|
if season is None:
|
||||||
|
flash("Season not found.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
name = request.form.get("name", "").strip()
|
||||||
|
start_raw = request.form.get("start_date", "").strip()
|
||||||
|
end_raw = request.form.get("end_date", "").strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
flash("Season name is required.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_date = date.fromisoformat(start_raw) if start_raw else None
|
||||||
|
end_date = date.fromisoformat(end_raw) if end_raw else None
|
||||||
|
except ValueError:
|
||||||
|
flash("Invalid date format.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
repo_update_season(season, name, start_date=start_date, end_date=end_date)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Season \"{name}\" updated.", "success")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/seasons/<season_id>/archive")
|
||||||
|
@login_required
|
||||||
|
def archive_season(league_ref: str, season_id: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import archive_season as repo_archive, find_season
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
season = find_season(league.id, season_id)
|
||||||
|
if season is None:
|
||||||
|
flash("Season not found.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
repo_archive(season)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Season \"{season.name}\" archived.", "success")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/seasons/<season_id>/unarchive")
|
||||||
|
@login_required
|
||||||
|
def unarchive_season(league_ref: str, season_id: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import find_season, unarchive_season as repo_unarchive
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
season = find_season(league.id, season_id)
|
||||||
|
if season is None:
|
||||||
|
flash("Season not found.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
repo_unarchive(season)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Season \"{season.name}\" restored.", "success")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
|
@leagues_bp.post("/l/<league_ref>/seasons/<season_id>/delete")
|
||||||
|
@login_required
|
||||||
|
def delete_season(league_ref: str, season_id: str):
|
||||||
|
if not db_ready():
|
||||||
|
flash("League database is not available.", "error")
|
||||||
|
return redirect(url_for("public.home"))
|
||||||
|
|
||||||
|
from league_repositories import delete_season as repo_delete, find_season
|
||||||
|
|
||||||
|
league = require_league(league_ref, {"owner", "manager"})
|
||||||
|
season = find_season(league.id, season_id)
|
||||||
|
if season is None:
|
||||||
|
flash("Season not found.", "error")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
name = season.name
|
||||||
|
repo_delete(season)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Season \"{name}\" deleted. Sessions in this season were unassigned.", "success")
|
||||||
|
return redirect(url_for("leagues.seasons", **league_url_values(league)))
|
||||||
|
|
||||||
|
|
||||||
@leagues_bp.route("/l/<league_ref>/settings", methods=["GET", "POST"])
|
@leagues_bp.route("/l/<league_ref>/settings", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def league_settings(league_ref: str):
|
def league_settings(league_ref: str):
|
||||||
@@ -1194,7 +1452,7 @@ def league_settings(league_ref: str):
|
|||||||
from league_repositories import list_members_for_league
|
from league_repositories import list_members_for_league
|
||||||
|
|
||||||
members = list_members_for_league(league.id)
|
members = list_members_for_league(league.id)
|
||||||
return render_template("league_settings.html", league=league, form=form, is_owner=True, members=members)
|
return render_template("league_settings.html", league=league, form=form, is_owner=True, can_manage=True, members=members)
|
||||||
|
|
||||||
|
|
||||||
@leagues_bp.post("/l/<league_ref>/settings/invite")
|
@leagues_bp.post("/l/<league_ref>/settings/invite")
|
||||||
|
|||||||
@@ -2449,6 +2449,21 @@ select.control { cursor: pointer; }
|
|||||||
background: var(--field);
|
background: var(--field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.season-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Session list row with left accent */
|
/* Session list row with left accent */
|
||||||
.session-row {
|
.session-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4814,6 +4829,16 @@ select.control { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.db-nav-tile--ledger:hover { border-left-color: rgba(224,177,92,.5); }
|
.db-nav-tile--ledger:hover { border-left-color: rgba(224,177,92,.5); }
|
||||||
|
|
||||||
|
.db-nav-tile--seasons {
|
||||||
|
border-left: 3px solid rgba(155,140,240,.28);
|
||||||
|
}
|
||||||
|
.db-nav-tile--seasons .db-nav-tile__icon {
|
||||||
|
background: rgba(155,140,240,.1);
|
||||||
|
border: 1px solid rgba(155,140,240,.24);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.db-nav-tile--seasons:hover { border-left-color: rgba(155,140,240,.55); }
|
||||||
|
|
||||||
/* Live tile overrides the sessions color */
|
/* Live tile overrides the sessions color */
|
||||||
.db-nav-tile--sessions.db-nav-tile--live {
|
.db-nav-tile--sessions.db-nav-tile--live {
|
||||||
border-left-color: rgba(111,192,147,.55);
|
border-left-color: rgba(111,192,147,.55);
|
||||||
|
|||||||
@@ -51,6 +51,15 @@
|
|||||||
<span>Leaderboard</span>
|
<span>Leaderboard</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.seasons' }}"
|
||||||
|
href="{{ url_for('leagues.seasons', league_ref=league.url_ref) }}">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.4"/>
|
||||||
|
<path d="M8 4v4l3 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Seasons</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
{% if can_manage is defined and can_manage %}
|
{% if can_manage is defined and can_manage %}
|
||||||
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.ledger' }}"
|
<a class="sidebar__link {{ 'is-active' if request.endpoint == 'leagues.ledger' }}"
|
||||||
href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
|
href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
|
||||||
|
|||||||
@@ -307,6 +307,22 @@
|
|||||||
<span class="db-nav-tile__arrow">→</span>
|
<span class="db-nav-tile__arrow">→</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="db-nav-tile db-nav-tile--seasons" href="{{ url_for('leagues.seasons', league_ref=league.url_ref) }}">
|
||||||
|
<div class="db-nav-tile__icon">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/>
|
||||||
|
<path d="M12 7v5l3.5 3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="db-nav-tile__body">
|
||||||
|
<div class="db-nav-tile__title-row">
|
||||||
|
<strong class="db-nav-tile__title">Seasons</strong>
|
||||||
|
</div>
|
||||||
|
<p class="db-nav-tile__desc">Browse sessions by season, view season standings, and manage date ranges.</p>
|
||||||
|
</div>
|
||||||
|
<span class="db-nav-tile__arrow">→</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
<a class="db-nav-tile db-nav-tile--ledger" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
|
<a class="db-nav-tile db-nav-tile--ledger" href="{{ url_for('leagues.ledger', league_ref=league.url_ref) }}">
|
||||||
<div class="db-nav-tile__icon">
|
<div class="db-nav-tile__icon">
|
||||||
|
|||||||
@@ -64,8 +64,20 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar__right">
|
<div class="toolbar__right">
|
||||||
|
{% if seasons %}
|
||||||
|
<form class="session-filter-form" method="get" style="margin-right:8px;">
|
||||||
|
<input type="hidden" name="mode" value="{{ mode }}">
|
||||||
|
<select class="session-filter-select" name="season" onchange="this.form.submit()">
|
||||||
|
<option value="">All seasons</option>
|
||||||
|
{% for s in seasons %}
|
||||||
|
<option value="{{ s.id }}" {{ 'selected' if selected_season_id == s.id else '' }}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<form class="session-filter-form" method="get">
|
<form class="session-filter-form" method="get">
|
||||||
<input type="hidden" name="mode" value="{{ mode }}">
|
<input type="hidden" name="mode" value="{{ mode }}">
|
||||||
|
{% if selected_season_id %}<input type="hidden" name="season" value="{{ selected_season_id }}">{% endif %}
|
||||||
<select class="session-filter-select" name="through_session" onchange="this.form.submit()">
|
<select class="session-filter-select" name="through_session" onchange="this.form.submit()">
|
||||||
<option value="">Latest session</option>
|
<option value="">Latest session</option>
|
||||||
{% for s in available_sessions %}
|
{% for s in available_sessions %}
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Seasons · {{ league.name }} · myboker.org{% endblock %}
|
||||||
|
{% block sidebar %}{% include "_league_sidebar.html" %}{% endblock %}
|
||||||
|
{% block page_class %}page--app{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="page-header__title">Seasons</h1>
|
||||||
|
<p class="page-header__sub">Organise sessions into named seasons for {{ league.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack">
|
||||||
|
|
||||||
|
{% if can_manage %}
|
||||||
|
{% set has_dated_seasons = active_seasons | selectattr('start_date') | list | length > 0 %}
|
||||||
|
{% if has_dated_seasons %}
|
||||||
|
<div class="panel" style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;">
|
||||||
|
<div>
|
||||||
|
<p class="panel__title" style="margin:0;">Auto-assign sessions</p>
|
||||||
|
<p class="muted-text" style="margin:4px 0 0;font-size:13px;">Assign unassigned sessions to seasons based on their date. Sessions that match zero or multiple seasons are skipped.</p>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('leagues.auto_assign_seasons', league_ref=league.url_ref) }}" style="margin:0;flex-shrink:0;margin-left:20px;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="submit">Auto-assign</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="panel form-card" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Seasons</p>
|
||||||
|
<h2 class="panel__title" style="margin-top:4px;">New season</h2>
|
||||||
|
</div>
|
||||||
|
<div class="session-create-row">
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input type="text" name="name" value="{{ form.name or '' }}" placeholder="e.g. Season 1" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Start date <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<input type="date" name="start_date" value="{{ form.start_date or '' }}">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>End date <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<input type="date" name="end_date" value="{{ form.end_date or '' }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn--primary" type="submit">Create season</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}{# can_manage #}
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__head">
|
||||||
|
<div>
|
||||||
|
<p class="kicker" style="margin:0;">Active</p>
|
||||||
|
<h2 class="panel__title">Current seasons</h2>
|
||||||
|
</div>
|
||||||
|
<span class="panel__tag">{{ active_seasons|length }} season{{ 's' if active_seasons|length != 1 else '' }}</span>
|
||||||
|
</div>
|
||||||
|
{% if active_seasons %}
|
||||||
|
<div>
|
||||||
|
{% for season in active_seasons %}
|
||||||
|
<div class="session-row session-row--closed">
|
||||||
|
<div class="session-row__info">
|
||||||
|
<div class="session-row__text">
|
||||||
|
<div class="session-row__title">
|
||||||
|
<a href="{{ url_for('leagues.sessions', league_ref=league.url_ref, season=season.id) }}">{{ season.name }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="session-row__meta">
|
||||||
|
{% if season.start_date %}
|
||||||
|
<span>{{ season.start_date.strftime('%b %-d, %Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if season.start_date and season.end_date %}
|
||||||
|
<span>—</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if season.end_date %}
|
||||||
|
<span>{{ season.end_date.strftime('%b %-d, %Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if not season.start_date and not season.end_date %}
|
||||||
|
<span class="muted-text">No dates set</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if can_manage %}
|
||||||
|
<div class="session-row__actions">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button"
|
||||||
|
data-modal="modal-edit-season"
|
||||||
|
data-season-id="{{ season.id }}"
|
||||||
|
data-season-name="{{ season.name }}"
|
||||||
|
data-season-start="{{ season.start_date.isoformat() if season.start_date else '' }}"
|
||||||
|
data-season-end="{{ season.end_date.isoformat() if season.end_date else '' }}">Edit</button>
|
||||||
|
<form method="post" action="{{ url_for('leagues.archive_season', league_ref=league.url_ref, season_id=season.id) }}" style="margin:0;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="submit">Archive</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-panel">
|
||||||
|
<p class="muted-text">No active seasons. Create one above.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if archived_seasons %}
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__head">
|
||||||
|
<div>
|
||||||
|
<p class="kicker" style="margin:0;">Archived</p>
|
||||||
|
<h2 class="panel__title">Past seasons</h2>
|
||||||
|
</div>
|
||||||
|
<span class="panel__tag">{{ archived_seasons|length }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for season in archived_seasons %}
|
||||||
|
<div class="session-row session-row--closed" style="opacity:.7;">
|
||||||
|
<div class="session-row__info">
|
||||||
|
<div class="session-row__text">
|
||||||
|
<div class="session-row__title">{{ season.name }}</div>
|
||||||
|
<div class="session-row__meta">
|
||||||
|
{% if season.start_date %}
|
||||||
|
<span>{{ season.start_date.strftime('%b %-d, %Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if season.start_date and season.end_date %}
|
||||||
|
<span>—</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if season.end_date %}
|
||||||
|
<span>{{ season.end_date.strftime('%b %-d, %Y') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if not season.start_date and not season.end_date %}
|
||||||
|
<span class="muted-text">No dates set</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if can_manage %}
|
||||||
|
<div class="session-row__actions">
|
||||||
|
<form method="post" action="{{ url_for('leagues.unarchive_season', league_ref=league.url_ref, season_id=season.id) }}" style="margin:0;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="submit">Restore</button>
|
||||||
|
</form>
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button"
|
||||||
|
data-modal="modal-delete-season"
|
||||||
|
data-season-id="{{ season.id }}"
|
||||||
|
data-season-name="{{ season.name }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if can_manage %}
|
||||||
|
<!-- Edit season modal -->
|
||||||
|
<div class="modal-backdrop" id="modal-edit-season" hidden>
|
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-edit-season-title">
|
||||||
|
<div class="modal-card__head">
|
||||||
|
<h2 class="modal-card__title" id="modal-edit-season-title">Edit season</h2>
|
||||||
|
<button class="modal-card__close" type="button" data-close-modal aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<form method="post" id="form-edit-season" action="">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="modal-card__body">
|
||||||
|
<div class="form-card" style="padding:0;gap:14px;">
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input type="text" name="name" id="edit-season-name" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Start date <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<input type="date" name="start_date" id="edit-season-start">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>End date <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<input type="date" name="end_date" id="edit-season-end">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-card__foot">
|
||||||
|
<button class="btn btn--primary" type="submit">Save changes</button>
|
||||||
|
<button class="btn btn--ghost" type="button" data-close-modal>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete season confirmation modal -->
|
||||||
|
<div class="modal-backdrop" id="modal-delete-season" hidden>
|
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="modal-delete-season-title">
|
||||||
|
<div class="modal-card__head">
|
||||||
|
<h2 class="modal-card__title" id="modal-delete-season-title">Delete season</h2>
|
||||||
|
<button class="modal-card__close" type="button" data-close-modal aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-card__body">
|
||||||
|
<p>Permanently delete <strong id="delete-season-name"></strong>? Sessions assigned to this season will not be deleted — they will simply become unassigned. This cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<form method="post" id="form-delete-season" action="">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="modal-card__foot">
|
||||||
|
<button class="btn btn--destructive" type="submit">Delete season</button>
|
||||||
|
<button class="btn btn--ghost" type="button" data-close-modal>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const leagueRef = {{ league.url_ref | tojson }};
|
||||||
|
|
||||||
|
function openModal(id) {
|
||||||
|
document.getElementById(id).hidden = false;
|
||||||
|
}
|
||||||
|
function closeModal(el) {
|
||||||
|
el.closest('.modal-backdrop').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (e.target.matches('[data-close-modal]') || e.target.closest('[data-close-modal]')) {
|
||||||
|
closeModal(e.target.closest('[data-close-modal]') || e.target);
|
||||||
|
}
|
||||||
|
if (e.target.matches('.modal-backdrop')) {
|
||||||
|
e.target.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-modal="modal-edit-season"]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const id = btn.dataset.seasonId;
|
||||||
|
document.getElementById('edit-season-name').value = btn.dataset.seasonName;
|
||||||
|
document.getElementById('edit-season-start').value = btn.dataset.seasonStart || '';
|
||||||
|
document.getElementById('edit-season-end').value = btn.dataset.seasonEnd || '';
|
||||||
|
document.getElementById('form-edit-season').action = '/l/' + leagueRef + '/seasons/' + id + '/update';
|
||||||
|
openModal('modal-edit-season');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-modal="modal-delete-season"]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const id = btn.dataset.seasonId;
|
||||||
|
document.getElementById('delete-season-name').textContent = btn.dataset.seasonName;
|
||||||
|
document.getElementById('form-delete-season').action = '/l/' + leagueRef + '/seasons/' + id + '/delete';
|
||||||
|
openModal('modal-delete-season');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endif %}{# can_manage #}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -271,6 +271,17 @@
|
|||||||
<span>Date</span>
|
<span>Date</span>
|
||||||
<input type="date" name="session_date" value="{{ session_model.session_date.isoformat() }}" required>
|
<input type="date" name="session_date" value="{{ session_model.session_date.isoformat() }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
{% if seasons %}
|
||||||
|
<label>
|
||||||
|
<span>Season <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<select name="season_id">
|
||||||
|
<option value="">No season</option>
|
||||||
|
{% for s in seasons %}
|
||||||
|
<option value="{{ s.id }}" {{ 'selected' if session_model.season_id == s.id else '' }}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
<label>
|
<label>
|
||||||
<span>Label <span class="form-opt-hint">(optional)</span></span>
|
<span>Label <span class="form-opt-hint">(optional)</span></span>
|
||||||
<input type="text" name="label" value="{{ session_model.label or '' }}" placeholder="e.g. Main table">
|
<input type="text" name="label" value="{{ session_model.label or '' }}" placeholder="e.g. Main table">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="hero" style="padding-top:0;">
|
<div class="hero" style="padding-top:0;">
|
||||||
<p class="hero__kicker session-number">Session #{{ "%03d"|format(session_number) }}</p>
|
<p class="hero__kicker session-number">Session #{{ "%03d"|format(session_number) }}</p>
|
||||||
<h1 class="hero__title">{{ session_model.display_label }}</h1>
|
<h1 class="hero__title">{{ session_model.display_label }}</h1>
|
||||||
<p class="hero__sub">{{ session.entries|length }} player{{ 's' if session.entries|length != 1 else '' }} · <span class="status-badge status-{{ session_model.status }}">{{ session_model.status }}</span></p>
|
<p class="hero__sub">{{ session.entries|length }} player{{ 's' if session.entries|length != 1 else '' }} · <span class="status-badge status-{{ session_model.status }}">{{ session_model.status }}</span>{% if season %} · <a class="season-tag" href="{{ url_for('leagues.sessions', league_ref=league.url_ref, season=session_model.season_id) }}">{{ season.name }}</a>{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recon cards -->
|
<!-- Recon cards -->
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
<span>Date <span class="info-tip" tabindex="0"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M6.5 6v3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="6.5" cy="4" r=".7" fill="currentColor"/></svg><span class="info-tip__bubble">The night the game was played. Same-day sessions automatically become S1, S2, etc.</span></span></span>
|
<span>Date <span class="info-tip" tabindex="0"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M6.5 6v3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="6.5" cy="4" r=".7" fill="currentColor"/></svg><span class="info-tip__bubble">The night the game was played. Same-day sessions automatically become S1, S2, etc.</span></span></span>
|
||||||
<input type="date" name="session_date" value="{{ form.session_date }}" required>
|
<input type="date" name="session_date" value="{{ form.session_date }}" required>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Season <span class="form-opt-hint">(optional)</span></span>
|
||||||
|
<select name="season_id">
|
||||||
|
<option value="">No season</option>
|
||||||
|
{% for s in seasons %}
|
||||||
|
<option value="{{ s.id }}" {{ 'selected' if form.season_id == s.id else '' }}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Label <span class="form-opt-hint">(optional)</span> <span class="info-tip" tabindex="0"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M6.5 6v3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="6.5" cy="4" r=".7" fill="currentColor"/></svg><span class="info-tip__bubble">A short name for this session — 'Main table', 'Side game', etc. Shown in the session list alongside the date.</span></span></span>
|
<span>Label <span class="form-opt-hint">(optional)</span> <span class="info-tip" tabindex="0"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="1.2"/><path d="M6.5 6v3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="6.5" cy="4" r=".7" fill="currentColor"/></svg><span class="info-tip__bubble">A short name for this session — 'Main table', 'Side game', etc. Shown in the session list alongside the date.</span></span></span>
|
||||||
<input type="text" name="label" value="{{ form.label or '' }}" placeholder="e.g. Main table">
|
<input type="text" name="label" value="{{ form.label or '' }}" placeholder="e.g. Main table">
|
||||||
@@ -48,8 +57,19 @@
|
|||||||
<h2 class="panel__title">All sessions</h2>
|
<h2 class="panel__title">All sessions</h2>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:8px;">
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
<span class="panel__tag" id="sess-count-label">{{ sessions|length }} total</span>
|
{% if seasons %}
|
||||||
{% if can_manage and empty_count > 0 %}
|
<select class="session-filter-select" onchange="if(this.value) window.location=this.value;">
|
||||||
|
<option value="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}" {{ 'selected' if not selected_season else '' }}>All seasons</option>
|
||||||
|
{% for s in seasons %}
|
||||||
|
<option value="{{ url_for('leagues.sessions', league_ref=league.url_ref, season=s.id) }}" {{ 'selected' if selected_season_id == s.id else '' }}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% if selected_season %}
|
||||||
|
<a class="btn btn--ghost btn--sm" href="{{ url_for('leagues.sessions', league_ref=league.url_ref) }}">Clear</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="panel__tag" id="sess-count-label">{{ sessions|length }}{% if selected_season %} / {{ all_session_count }}{% endif %} total</span>
|
||||||
|
{% if can_manage and empty_count > 0 and not selected_season %}
|
||||||
<form method="post" action="{{ url_for('leagues.prune_empty_sessions', league_ref=league.url_ref) }}" style="margin:0;">
|
<form method="post" action="{{ url_for('leagues.prune_empty_sessions', league_ref=league.url_ref) }}" style="margin:0;">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button class="btn btn--ghost btn--sm" type="submit">Prune empty ({{ empty_count }})</button>
|
<button class="btn btn--ghost btn--sm" type="submit">Prune empty ({{ empty_count }})</button>
|
||||||
@@ -71,8 +91,9 @@
|
|||||||
<div>
|
<div>
|
||||||
{% for session in sessions %}
|
{% for session in sessions %}
|
||||||
{% set sm = db_id_to_summary.get(session.id) %}
|
{% set sm = db_id_to_summary.get(session.id) %}
|
||||||
|
{% set season_name = season_map[session.season_id].name if session.season_id and season_map.get(session.season_id) else '' %}
|
||||||
<div class="session-row session-row--{{ session.status }}"
|
<div class="session-row session-row--{{ session.status }}"
|
||||||
data-search="{{ (session.display_label ~ ' ' ~ session.session_date.isoformat() ~ ' ' ~ session.session_date.strftime('%B %Y') ~ ' ' ~ (session.notes or '')) | lower }}">
|
data-search="{{ (session.display_label ~ ' ' ~ session.session_date.isoformat() ~ ' ' ~ session.session_date.strftime('%B %Y') ~ ' ' ~ (session.notes or '') ~ ' ' ~ season_name) | lower }}">
|
||||||
<div class="session-row__info">
|
<div class="session-row__info">
|
||||||
<div class="session-date-block">
|
<div class="session-date-block">
|
||||||
<span class="session-date-block__mon">{{ session.session_date.strftime('%b') }}</span>
|
<span class="session-date-block__mon">{{ session.session_date.strftime('%b') }}</span>
|
||||||
@@ -84,6 +105,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="session-row__meta">
|
<div class="session-row__meta">
|
||||||
<span>S{{ session.sequence_on_date }}</span>
|
<span>S{{ session.sequence_on_date }}</span>
|
||||||
|
{% if session.season_id and season_map.get(session.season_id) %}
|
||||||
|
<span class="season-tag">{{ season_map[session.season_id].name }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if sm and sm.entries %}
|
{% if sm and sm.entries %}
|
||||||
<span>· {{ sm.entries|length }} player{{ 's' if sm.entries|length != 1 else '' }}</span>
|
<span>· {{ sm.entries|length }} player{{ 's' if sm.entries|length != 1 else '' }}</span>
|
||||||
<span>· {{ sm.total_invested_cents | money }} pot</span>
|
<span>· {{ sm.total_invested_cents | money }} pot</span>
|
||||||
|
|||||||
Reference in new issue
Block a user