chore: save current Ruby implementation state before Next.js rewrite
This commit is contained in:
@@ -1,83 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Actions
|
||||
module Admin
|
||||
module Matches
|
||||
class ProcessUpload < EuchreCamp::Action
|
||||
include Deps[repo: 'repositories.matches']
|
||||
include Deps[events_repo: 'repositories.events']
|
||||
|
||||
def handle(request, response)
|
||||
# In Hanami, file uploads are in request.params
|
||||
file = request.params[:csv_file]
|
||||
event_id = request.params[:event_id]&.to_i
|
||||
|
||||
unless file && file[:tempfile]
|
||||
response.flash[:error] = "No file uploaded"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
matches_created = 0
|
||||
errors = []
|
||||
|
||||
# Parse CSV
|
||||
CSV.foreach(file[:tempfile].path, headers: true, header_converters: :symbol) do |row|
|
||||
begin
|
||||
# Map CSV columns to our database structure
|
||||
# Expected columns: played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
|
||||
|
||||
# Find or create players by name
|
||||
player_1 = find_or_create_player(row[:team_1_p1_name])
|
||||
player_2 = find_or_create_player(row[:team_1_p2_name])
|
||||
player_3 = find_or_create_player(row[:team_2_p1_name])
|
||||
player_4 = find_or_create_player(row[:team_2_p2_name])
|
||||
|
||||
played_at = begin
|
||||
Time.parse(row[:played_at])
|
||||
rescue
|
||||
Time.now
|
||||
end
|
||||
|
||||
match_data = {
|
||||
played_at: played_at,
|
||||
team_1_p1_id: player_1[:id],
|
||||
team_1_p2_id: player_2[:id],
|
||||
team_1_score: row[:team_1_score].to_i,
|
||||
team_2_p1_id: player_3[:id],
|
||||
team_2_p2_id: player_4[:id],
|
||||
team_2_score: row[:team_2_score].to_i,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
match = repo.create(match_data)
|
||||
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
|
||||
matches_created += 1
|
||||
|
||||
rescue => e
|
||||
errors << "Row #{row}: #{e.message}"
|
||||
end
|
||||
unless event_id
|
||||
response.flash[:error] = "Please select a tournament"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
response.flash[:message] = "Created #{matches_created} matches.#{' Errors: ' + errors.join(', ') if errors.any?}"
|
||||
response.redirect_to("/admin/matches")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_create_player(name)
|
||||
rom = EuchreCamp::App["persistence.rom"]
|
||||
return nil unless name
|
||||
|
||||
name = name.strip
|
||||
player = rom.relations[:players].where(name: name).one
|
||||
|
||||
if player
|
||||
player
|
||||
else
|
||||
rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
|
||||
rom.relations[:players].where(name: name).one
|
||||
# Verify event exists
|
||||
begin
|
||||
events_repo.by_id(event_id)
|
||||
rescue
|
||||
response.flash[:error] = "Invalid tournament ID"
|
||||
response.redirect_to("/admin/matches/upload")
|
||||
return
|
||||
end
|
||||
|
||||
# Use the specialized importer for Euchre tournament results
|
||||
importer = EuchreCamp::Services::TournamentCsvImporter.new(event_id: event_id)
|
||||
results = importer.import(file[:tempfile].path)
|
||||
|
||||
message = "Created #{results[:matches_created]} matches, #{results[:teams_created]} teams"
|
||||
message += ". Errors: #{results[:errors].join(', ')}" if results[:errors].any?
|
||||
|
||||
response.flash[:message] = message
|
||||
response.redirect_to("/admin/tournaments/#{event_id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Actions
|
||||
module Admin
|
||||
module Matches
|
||||
class Upload < EuchreCamp::Action
|
||||
include Deps[events_repo: 'repositories.events']
|
||||
|
||||
def handle(request, response)
|
||||
response.render(view)
|
||||
tournaments = events_repo.all
|
||||
response.render(view, tournaments: tournaments)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "csv"
|
||||
|
||||
module EuchreCamp
|
||||
module Services
|
||||
class TournamentCsvImporter
|
||||
# Map table names to numeric IDs
|
||||
TABLE_NAME_MAP = {
|
||||
"Clubs" => 1,
|
||||
"Hearts" => 2,
|
||||
"Diamonds" => 3,
|
||||
"Spades" => 4,
|
||||
"Stars" => 5
|
||||
}.freeze
|
||||
|
||||
def initialize(event_id:)
|
||||
@event_id = event_id
|
||||
@rom = EuchreCamp::App["persistence.rom"]
|
||||
@matches_repo = EuchreCamp::App["repositories.matches"]
|
||||
@teams_repo = EuchreCamp::App["repositories.teams"]
|
||||
@events_repo = EuchreCamp::App["repositories.events"]
|
||||
@rounds_repo = EuchreCamp::App["repositories.tournament_rounds"]
|
||||
@bracket_matchups_repo = EuchreCamp::App["repositories.bracket_matchups"]
|
||||
end
|
||||
|
||||
def import(csv_file_path)
|
||||
results = {
|
||||
matches_created: 0,
|
||||
teams_created: 0,
|
||||
rounds_created: 0,
|
||||
errors: []
|
||||
}
|
||||
|
||||
begin
|
||||
event = @events_repo.by_id(@event_id)
|
||||
|
||||
CSV.foreach(csv_file_path, headers: true, header_converters: :symbol) do |row|
|
||||
begin
|
||||
process_row(row, event, results)
|
||||
rescue => e
|
||||
results[:errors] << "Row #{row}: #{e.message}"
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
results[:errors] << "Fatal error: #{e.message}"
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_row(row, event, results)
|
||||
# Extract data from CSV row
|
||||
round_number = row[:round].to_i
|
||||
table_name = row[:table]
|
||||
seat_1_name = row[:seat_1]&.strip
|
||||
seat_3_name = row[:seat_3]&.strip
|
||||
odds_points = row[:odds_points].to_i
|
||||
seat_2_name = row[:seat_2]&.strip
|
||||
seat_4_name = row[:seat_4]&.strip
|
||||
evens_points = row[:evens_points].to_i
|
||||
winner = row[:winner]&.strip&.downcase
|
||||
|
||||
# Validate required fields
|
||||
unless seat_1_name && seat_3_name && seat_2_name && seat_4_name
|
||||
raise "Missing player names in row"
|
||||
end
|
||||
|
||||
# Find or create players
|
||||
player_1 = find_or_create_player(seat_1_name)
|
||||
player_2 = find_or_create_player(seat_3_name)
|
||||
player_3 = find_or_create_player(seat_2_name)
|
||||
player_4 = find_or_create_player(seat_4_name)
|
||||
|
||||
# Find or create teams
|
||||
team_1 = @teams_repo.find_or_create(@event_id, player_1[:id], player_2[:id], "#{seat_1_name} + #{seat_3_name}")
|
||||
team_2 = @teams_repo.find_or_create(@event_id, player_3[:id], player_4[:id], "#{seat_2_name} + #{seat_4_name}")
|
||||
|
||||
results[:teams_created] += 1 if team_1[:created_at] == team_2[:created_at] # Rough check for new teams
|
||||
|
||||
# Find or create round
|
||||
round = find_or_create_round(round_number)
|
||||
|
||||
# Map table name to number
|
||||
table_number = TABLE_NAME_MAP[table_name] || table_name.to_i
|
||||
if table_number == 0
|
||||
raise "Unknown table name: #{table_name}"
|
||||
end
|
||||
|
||||
# Determine winner based on scores or explicit winner column
|
||||
team_1_score = odds_points
|
||||
team_2_score = evens_points
|
||||
|
||||
# Verify winner matches scores if explicitly stated
|
||||
if winner
|
||||
if winner == "odds" && team_1_score <= team_2_score
|
||||
results[:errors] << "Warning: Row claims Odds won but score is #{team_1_score}-#{team_2_score}"
|
||||
elsif winner == "evens" && team_2_score <= team_1_score
|
||||
results[:errors] << "Warning: Row claims Evens won but score is #{team_1_score}-#{team_2_score}"
|
||||
end
|
||||
end
|
||||
|
||||
# Create match
|
||||
match_data = {
|
||||
team_1_p1_id: player_1[:id],
|
||||
team_1_p2_id: player_2[:id],
|
||||
team_1_score: team_1_score,
|
||||
team_2_p1_id: player_3[:id],
|
||||
team_2_p2_id: player_4[:id],
|
||||
team_2_score: team_2_score,
|
||||
played_at: Time.now,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
match = @matches_repo.create(match_data)
|
||||
results[:matches_created] += 1
|
||||
|
||||
# Create bracket matchup entry
|
||||
bracket_data = {
|
||||
round_id: round[:id],
|
||||
event_id: @event_id,
|
||||
team_1_id: team_1[:id],
|
||||
team_2_id: team_2[:id],
|
||||
table_number: table_number,
|
||||
bracket_position: table_number,
|
||||
status: 'completed'
|
||||
}
|
||||
|
||||
@bracket_matchups_repo.create(bracket_data)
|
||||
|
||||
# Queue Elo calculation
|
||||
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
|
||||
|
||||
rescue => e
|
||||
raise "Error processing row: #{e.message}"
|
||||
end
|
||||
|
||||
def find_or_create_player(name)
|
||||
return nil unless name
|
||||
|
||||
player = @rom.relations[:players].where(name: name).one
|
||||
|
||||
if player
|
||||
player
|
||||
else
|
||||
# Create player with default rating
|
||||
@rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
|
||||
@rom.relations[:players].where(name: name).one
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_round(round_number)
|
||||
round = @rom.relations[:tournament_rounds].where(event_id: @event_id, round_number: round_number).one
|
||||
|
||||
if round
|
||||
round
|
||||
else
|
||||
@rounds_repo.create(
|
||||
event_id: @event_id,
|
||||
round_number: round_number,
|
||||
status: 'completed'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +1,110 @@
|
||||
<h1>Upload Matches (CSV)</h1>
|
||||
<h1>Upload Tournament Results (CSV)</h1>
|
||||
|
||||
<p>Upload a CSV file with the following columns:</p>
|
||||
<pre>
|
||||
played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
|
||||
</pre>
|
||||
<p>Upload a CSV file with Euchre tournament results. The format supports standard Euchre scoring with Odds/Evens teams.</p>
|
||||
|
||||
<form action="/admin/matches/process_upload" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
|
||||
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="event_id">Tournament:</label>
|
||||
<select id="event_id" name="event_id" required>
|
||||
<option value="">Select a tournament...</option>
|
||||
<% if defined?(tournaments) && tournaments %>
|
||||
<% tournaments.each do |tournament| %>
|
||||
<option value="<%= tournament.id %>"><%= tournament.name %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="csv_file">CSV File:</label>
|
||||
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
</div>
|
||||
|
||||
<button type="submit">Upload</button>
|
||||
<button type="submit" class="btn-primary">Upload Results</button>
|
||||
</form>
|
||||
|
||||
<p><a href="/admin/matches">Back to Matches</a></p>
|
||||
|
||||
<h2>Euchre Tournament CSV Format</h2>
|
||||
|
||||
<p>The CSV should have the following columns:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Event #</td>
|
||||
<td>Tournament ID (can be omitted if selected above)</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Round</td>
|
||||
<td>Round number</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Table</td>
|
||||
<td>Table name (Clubs, Hearts, Diamonds, Spades, Stars)</td>
|
||||
<td>Clubs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 1</td>
|
||||
<td>Player name (Odds team, position 1)</td>
|
||||
<td>Derrick</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 3</td>
|
||||
<td>Player name (Odds team, position 2)</td>
|
||||
<td>Jesse C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Odds Points</td>
|
||||
<td>Score for Odds team</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 2</td>
|
||||
<td>Player name (Evens team, position 1)</td>
|
||||
<td>Emma</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Seat 4</td>
|
||||
<td>Player name (Evens team, position 2)</td>
|
||||
<td>Alissa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Evens Points</td>
|
||||
<td>Score for Evens team</td>
|
||||
<td>10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Winner</td>
|
||||
<td>"Odds" or "Evens" (optional, for validation)</td>
|
||||
<td>Evens</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Example CSV:</h2>
|
||||
<pre>
|
||||
played_at,team_1_p1_name,team_1_p2_name,team_1_score,team_2_p1_name,team_2_p2_name,team_2_score
|
||||
2026-03-26 10:00,Alice,Bob,10,Charlie,David,7
|
||||
2026-03-26 10:30,Alice,Bob,10,Charlie,David,7
|
||||
<pre style="background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto;">
|
||||
Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner
|
||||
1,1,Clubs,Derrick,Jesse C,5,Emma,Alissa,10,Evens
|
||||
1,1,Hearts,Kevin,Andy,8,Ellie,Jesse,6,Odds
|
||||
1,1,Diamonds,Sara M,Amelia,10,Mike G,AJ,4,Odds
|
||||
</pre>
|
||||
|
||||
<h3>Notes:</h3>
|
||||
<ul>
|
||||
<li>Player names must match exactly (including middle initials if present)</li>
|
||||
<li>Table names are mapped to numeric IDs (Clubs=1, Hearts=2, etc.)</li>
|
||||
<li>New players will be created automatically if they don't exist</li>
|
||||
<li>Teams are automatically created for each player pair</li>
|
||||
<li>Elo ratings are calculated after import</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user