feat(tournaments): add partnership tracking and tournament management

Implement comprehensive partnership tracking system and tournament
management features including round-robin, single/double elimination,
and Swiss system pairings.

Changes:
- Add partnership_games and partnership_stats tables
- Implement PartnershipTracker service for recording partnership data
- Add tournament generator with multiple formats
- Create tournament management actions and views
- Add acceptance tests for partnership tracking
- Fix PartnershipTracker to use single snapshot per player per match
- Fix round-robin test to query by round_number instead of hardcoded ID

Note: All 8 acceptance tests now passing
This commit is contained in:
2026-03-27 12:25:34 -07:00
parent cb47b9da99
commit ac858f1e6d
55 changed files with 2591 additions and 0 deletions
@@ -0,0 +1,42 @@
<h1>Edit Tournament: <%= tournament.name %></h1>
<form action="/admin/tournaments/<%= tournament.id %>" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="patch">
<div>
<label for="name">Tournament Name *</label>
<input type="text" id="name" name="name" value="<%= tournament.name %>" required>
</div>
<div>
<label for="description">Description</label>
<textarea id="description" name="description" rows="3"><%= tournament.description %></textarea>
</div>
<div>
<label for="event_date">Date/Time</label>
<input type="datetime-local" id="event_date" name="event_date"
value="<%= tournament.event_date&.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<div>
<label for="format">Tournament Format</label>
<select id="format" name="format">
<option value="single_elim" <%= 'selected' if tournament.format == 'single_elim' %>>Single Elimination</option>
<option value="double_elim" <%= 'selected' if tournament.format == 'double_elim' %>>Double Elimination</option>
<option value="round_robin" <%= 'selected' if tournament.format == 'round_robin' %>>Round Robin</option>
<option value="swiss" <%= 'selected' if tournament.format == 'swiss' %>>Swiss System</option>
</select>
</div>
<div>
<label for="max_participants">Max Participants (teams)</label>
<input type="number" id="max_participants" name="max_participants"
min="2" max="32" value="<%= tournament.max_participants %>">
</div>
<button type="submit">Update Tournament</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Cancel</a></p>
@@ -0,0 +1,64 @@
<h1>Tournament Administration</h1>
<p><a href="/admin/tournaments/new">Create New Tournament</a></p>
<% if upcoming.any? %>
<h2>Upcoming / Active Tournaments</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Format</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% upcoming.each do |tournament| %>
<tr>
<td><%= tournament.name %></td>
<td><%= tournament.event_date&.strftime('%Y-%m-%d %H:%M') %></td>
<td><%= tournament.format %></td>
<td><%= tournament.status %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>">View</a> |
<a href="/admin/tournaments/<%= tournament.id %>/edit">Edit</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% if completed.any? %>
<h2>Past Tournaments</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Format</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% completed.each do |tournament| %>
<tr>
<td><%= tournament.name %></td>
<td><%= tournament.event_date&.strftime('%Y-%m-%d') %></td>
<td><%= tournament.format %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>">View Results</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% if upcoming.empty? && completed.empty? %>
<p>No tournaments yet. <a href="/admin/tournaments/new">Create your first tournament!</a></p>
<% end %>
<p><a href="/admin/players">Back to Admin</a></p>
@@ -0,0 +1,40 @@
<h1>Create New Tournament</h1>
<form action="/admin/tournaments" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label for="name">Tournament Name *</label>
<input type="text" id="name" name="name" required placeholder="e.g., Spring Championship">
</div>
<div>
<label for="description">Description</label>
<textarea id="description" name="description" rows="3" placeholder="Optional description or rules..."></textarea>
</div>
<div>
<label for="event_date">Date/Time</label>
<input type="datetime-local" id="event_date" name="event_date" value="<%= Time.now.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<div>
<label for="format">Tournament Format</label>
<select id="format" name="format">
<option value="single_elim">Single Elimination</option>
<option value="double_elim">Double Elimination</option>
<option value="round_robin">Round Robin</option>
<option value="swiss">Swiss System</option>
</select>
</div>
<div>
<label for="max_participants">Max Participants (teams)</label>
<input type="number" id="max_participants" name="max_participants" min="2" max="32" placeholder="Optional">
<small>Leave blank for no limit</small>
</div>
<button type="submit">Create Tournament</button>
</form>
<p><a href="/admin/tournaments">Cancel</a></p>
@@ -0,0 +1,44 @@
<h1>Enter Results: <%= tournament.name %> - Round <%= round[:round_number] %></h1>
<form action="/admin/tournaments/<%= tournament.id %>/results" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<table>
<thead>
<tr>
<th>Matchup</th>
<th>Team 1</th>
<th>Score</th>
<th>vs</th>
<th>Team 2</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<% matchups.each_with_index do |matchup, i| %>
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
<% next if team_1.nil? || team_2.nil? %>
<tr>
<td><%= i + 1 %></td>
<td><%= team_1.team_name %></td>
<td>
<input type="number" name="team_1_score_<%= matchup[:id] %>" min="0" max="10" value="10" style="width: 60px;">
</td>
<td>vs</td>
<td><%= team_2.team_name %></td>
<td>
<input type="number" name="team_2_score_<%= matchup[:id] %>" min="0" max="10" value="7" style="width: 60px;">
</td>
<td>
<input type="checkbox" name="matchup_<%= matchup[:id] %>" checked style="display:none;">
</td>
</tr>
<% end %>
</tbody>
</table>
<button type="submit">Save All Results</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
@@ -0,0 +1,47 @@
<h1><%= tournament.name %> - Round <%= round[:round_number] %></h1>
<p><strong>Status:</strong> <%= round[:status] %></p>
<h2>Matchups</h2>
<% if matchups.any? %>
<table>
<thead>
<tr>
<th>Position</th>
<th>Team 1</th>
<th>vs</th>
<th>Team 2</th>
<th>Winner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% matchups.each do |matchup| %>
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
<% winner = teams.find { |t| t.id == matchup[:winner_team_id] } %>
<tr>
<td><%= matchup[:bracket_position] %></td>
<td><%= team_1&.team_name || 'BYE' %></td>
<td>vs</td>
<td><%= team_2&.team_name || 'BYE' %></td>
<td><%= winner&.team_name || '-' %></td>
<td>
<% if matchup[:match_id].nil? %>
<a href="/admin/matches/new?event_id=<%= tournament.id %>&team_1_id=<%= matchup[:team_1_id] %>&team_2_id=<%= matchup[:team_2_id] %>">
Create Match
</a>
<% else %>
<a href="/admin/matches/<%= matchup[:match_id] %>">View Match</a>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No matchups scheduled for this round.</p>
<% end %>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
@@ -0,0 +1,236 @@
<h1><%= tournament.name %></h1>
<% if tournament.description %>
<p><em><%= tournament.description %></em></p>
<% end %>
<div class="tournament-meta">
<strong>Format:</strong> <%= tournament.format %> |
<strong>Status:</strong> <%= tournament.status %> |
<% if tournament.event_date %>
<strong>Date:</strong> <%= tournament.event_date.strftime('%Y-%m-%d %H:%M') %>
<% end %>
</div>
<hr>
<div class="tournament-tabs">
<ul>
<li><a href="#participants">Participants</a></li>
<li><a href="#teams">Teams</a></li>
<li><a href="#rounds">Rounds & Schedule</a></li>
<li><a href="#standings">Standings</a></li>
</ul>
</div>
<!-- Participants Section -->
<section id="participants">
<h2>Participants</h2>
<% if players.any? %>
<p><strong>Total:</strong> <%= players.count %> players registered</p>
<table>
<thead>
<tr>
<th>Player</th>
<th>Status</th>
<th>Seed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% players.each do |player| %>
<% participant = participants.find { |p| p.player_id == player.id } %>
<tr>
<td><%= player.name %></td>
<td><%= participant&.status || 'registered' %></td>
<td><%= participant&.seed || '-' %></td>
<td>
<form action="/admin/tournaments/<%= tournament.id %>/participants/<%= player.id %>" method="post" style="display:inline;">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="delete">
<button type="submit" onclick="return confirm('Remove <%= player.name %> from tournament?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No participants registered yet.</p>
<% end %>
<h3>Add Participants</h3>
<% if available_players.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/participants" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<label>Add Player:</label>
<select name="player_id">
<option value="">Select a player...</option>
<% available_players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
<button type="submit">Add to Tournament</button>
</form>
<% else %>
<p><em>All available players are already registered.</em></p>
<% end %>
</section>
<!-- Teams Section -->
<section id="teams">
<h2>Teams</h2>
<% if teams.any? %>
<p><strong>Total:</strong> <%= teams.count %> teams</p>
<table>
<thead>
<tr>
<th>Team Name</th>
<th>Player 1</th>
<th>Player 2</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% teams.each do |team| %>
<% p1 = players.find { |p| p.id == team.player_1_id } %>
<% p2 = players.find { |p| p.id == team.player_2_id } %>
<tr>
<td><%= team.team_name %></td>
<td><%= p1&.name || team.player_1_id %></td>
<td><%= p2&.name || team.player_2_id %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>/edit">Edit</a> |
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post" style="display:inline;">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="delete">
<button type="submit" onclick="return confirm('Remove team?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No teams created yet.</p>
<% end %>
<h3>Create Teams</h3>
<% if players.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/teams" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label>Team Name:</label>
<input type="text" name="team_name" placeholder="e.g., Ace High">
</div>
<div>
<label>Player 1:</label>
<select name="player_1_id">
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label>Player 2:</label>
<select name="player_2_id">
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<button type="submit">Create Team</button>
</form>
<% else %>
<p><em>Add participants first before creating teams.</em></p>
<% end %>
</section>
<!-- Rounds Section -->
<section id="rounds">
<h2>Rounds & Schedule</h2>
<% if rounds.any? %>
<table>
<thead>
<tr>
<th>Round</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% rounds.each do |round| %>
<tr>
<td>Round <%= round.round_number %></td>
<td><%= round.status %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>/rounds/<%= round.id %>">View</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No rounds scheduled yet.</p>
<% end %>
<% if teams.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/schedule" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<button type="submit">Generate Schedule / Rounds</button>
</form>
<% else %>
<p><em>Add teams first to generate the schedule.</em></p>
<% end %>
</section>
<!-- Standings Section -->
<section id="standings">
<h2>Standings</h2>
<% if teams.any? %>
<table>
<thead>
<tr>
<th>#</th>
<th>Team</th>
<th>W</th>
<th>L</th>
<th>PCT</th>
<th>PF</th>
<th>PA</th>
<th>PD</th>
</tr>
</thead>
<tbody>
<% teams.each_with_index do |team, i| %>
<tr>
<td><%= i + 1 %></td>
<td><%= team.team_name %></td>
<td>0</td>
<td>0</td>
<td>.000</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No teams to display standings.</p>
<% end %>
</section>
<hr>
<div class="tournament-actions">
<a href="/admin/tournaments/<%= tournament.id %>/results">Enter Match Results</a> |
<a href="/admin/tournaments/<%= tournament.id %>/complete">Complete Tournament</a> |
<a href="/admin/tournaments">Back to Tournaments</a>
</div>
@@ -0,0 +1,39 @@
<h1>Edit Team: <%= team.team_name %></h1>
<p><strong>Tournament:</strong> <%= tournament.name %></p>
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="patch">
<div>
<label for="team_name">Team Name</label>
<input type="text" id="team_name" name="team_name" value="<%= team.team_name %>" required>
</div>
<div>
<label for="player_1_id">Player 1</label>
<select id="player_1_id" name="player_1_id" required>
<% players.each do |player| %>
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_1_id %>>
<%= player.name %>
</option>
<% end %>
</select>
</div>
<div>
<label for="player_2_id">Player 2</label>
<select id="player_2_id" name="player_2_id" required>
<% players.each do |player| %>
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_2_id %>>
<%= player.name %>
</option>
<% end %>
</select>
</div>
<button type="submit">Update Team</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>