diff --git a/Euchre Tournament Results - Sheet1.csv b/Euchre Tournament Results - Sheet1.csv new file mode 100644 index 0000000..c0a9263 --- /dev/null +++ b/Euchre Tournament Results - Sheet1.csv @@ -0,0 +1,31 @@ +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 +1,1,Spades,John,Dave B,7,Sara R,Emily,9,Evens +1,1,Stars,Linden,Jim,1,David G,Lynn,10,Evens +1,2,Clubs,John,Morgan,7,Alissa,Emma,14,Evens +1,2,Hearts,Lynn,Mike G,6,Kevin,AJ,8,Evens +1,2,Diamonds,Jesse F,Amelia,6,Linden,Jesse C,8,Evens +1,2,Spades,Lucas,Andy,18,Derrick,Ellie,7,Odds +1,2,Stars,Dave B,Emily,9,Jim,Sara R,12,Evens +1,3,Hearts,Lynn,Derrick,10,Alissa,Emma,13,Evens +1,3,Clubs,Kevin,AJ,2,Andy,Jesse C,11,Evens +1,3,Diamonds,Jim,Mike G,8,Amelia,Kristen,4,Odds +1,3,Spades,Ellie,John,11,Sara R,Dave B,12,Evens +1,3,Stars,Lucas,Linden,7,Emily,Sara M,14,Evens +1,4,Diamonds,John,Kevin,10,Alissa,Emma,7,Odds +1,4,Hearts,Emily,Sara M,11,Jim,Ellie,6,Odds +1,4,Clubs,Linden,AJ,2,Amelia,Mike G,8,Evens +1,4,Spades,Andy,Derrick,10,Sara R,Jesse C,5,Odds +1,4,Stars,Dave B,Lucas,9,David G,Lynn,7,Odds +1,5,Clubs,Amelia,Alissa,11,Linden,Ellie,3,Odds +1,5,Hearts,Emily,Sara R,10,Mike G,Andy,9,Odds +1,5,Diamonds,Derrick,Jesse F,5,Sara M,Jim,5,Evens +1,5,Spades,Kevin,Emma,11,Lynn,Dave B,11,Evens +1,5,Stars,John,Lucas,9,AJ,Jesse C,6,Odds +1,6,Diamonds,Alissa,Jim,11,John,Mike G,6,Odds +1,6,Hearts,Kevin,AJ,8,Sara R,Emma,12,Evens +1,6,Clubs,Derrick,Lucas,6,David G,Jesse C,5,Odds +1,6,Spades,Lynn,Emily,9,Amelia,Dave B,7,Odds +1,6,Stars,Sara M,Ellie,5,Linden,Andy,9,Evens \ No newline at end of file diff --git a/USER_STORIES.md b/USER_STORIES.md new file mode 100644 index 0000000..b50c843 --- /dev/null +++ b/USER_STORIES.md @@ -0,0 +1,221 @@ +# EuchreCamp User Stories + +## Epic 1: Authentication & User Management + +### As a new user, I want to register for an account so that I can participate in tournaments and track my games +- Acceptance Criteria: + - Registration form with name, email, password, password confirmation + - Email validation and uniqueness check + - Password strength requirements (8+ chars, uppercase, lowercase, number, special char) + - Account confirmation via email + - Auto-create player profile upon registration + +### As a registered user, I want to log in to my account so that I can access my data +- Acceptance Criteria: + - Login form with email and password + - Session management with secure cookies + - "Remember me" functionality + - Error handling for invalid credentials + - Account lockout after 5 failed attempts + +### As a logged-in user, I want to log out so that I can secure my account +- Acceptance Criteria: + - Logout button in navigation + - Session cleared on logout + - Redirect to home page + +### As a user who forgot my password, I want to reset it so that I can regain access +- Acceptance Criteria: + - "Forgot password" link on login page + - Email input for reset request + - Unique token generation (expires in 1 hour) + - Email with reset link + - Password update form with validation + +## Epic 2: Player Profile & Analytics + +### As a player, I want to view my profile so that I can see my statistics +- Acceptance Criteria: + - Profile header with name and avatar + - Current Elo rating with trend indicator + - Total games played + - Win rate percentage + - Member since date + +### As a player, I want to see my partnership performance so that I can identify my best partners +- Acceptance Criteria: + - Table showing all partners + - Games played with each partner + - Win rate with each partner + - Total Elo change per partnership + - Last played together date + +### As a player, I want to see my recent games so that I can track my performance +- Acceptance Criteria: + - Timeline of recent matches + - Date, opponents, scores, and results + - Partner information for each game + - Pagination for loading more games + +### As a player, I want to see my tournament history so that I can review past performances +- Acceptance Criteria: + - List of tournaments participated in + - Final standings in each tournament + - Performance summary per tournament + +## Epic 3: Rankings & Public Data + +### As a visitor, I want to view player rankings so that I can see top players +- Acceptance Criteria: + - Sortable rankings table + - Columns: Rank, Name, Elo, Win Rate, Games Played + - Search/filter functionality + - Pagination + +### As a visitor, I want to view player profiles so that I can learn about other players +- Acceptance Criteria: + - Public profile pages (read-only for non-admins) + - Basic stats: Elo, games played, win rate + - Partnership performance (if public) + - Recent games (limited) + +## Epic 4: Tournament Management + +### As a tournament admin, I want to create a new tournament so that I can organize events +- Acceptance Criteria: + - Tournament creation form + - Fields: Name, Format (round-robin, single elim, double elim, Swiss), Date, Status + - Default settings for each format + - Validation for required fields + +### As a tournament admin, I want to manage tournament participants so that I can control who plays +- Acceptance Criteria: + - Add participants from player list + - Remove participants + - Bulk import from CSV + - View participant list with status + +### As a tournament admin, I want to create teams so that I can organize players into pairs +- Acceptance Criteria: + - Team creation form + - Select two players per team + - Auto-generate team names + - Edit/delete teams + +### As a tournament admin, I want to generate a schedule so that matches are organized +- Acceptance Criteria: + - Round-robin schedule generation + - Single/double elimination bracket generation + - View schedule by round + - Re-schedule matches if needed + +### As a tournament admin, I want to record match results so that I can track tournament progress +- Acceptance Criteria: + - Match result entry form + - Select teams and enter scores + - Auto-determine winner from scores + - Validation for score formats + +### As a tournament admin, I want to upload results via CSV so that I can batch process matches +- Acceptance Criteria: + - CSV upload form with tournament selection + - Parse CSV with specific format (Event, Round, Table, Seats, Scores, Winner) + - Validate player names exist in system + - Create teams automatically if needed + - Import matches and update Elo ratings + +### As a tournament admin, I want to view tournament analytics so that I can assess performance +- Acceptance Criteria: + - Tournament statistics (total matches, average score, etc.) + - Partnership performance within tournament + - Elo changes for participants + - Visual charts for key metrics + +## Epic 5: Match Recording & CSV Import + +### As a user, I want to record a match result so that I can track my games +- Acceptance Criteria: + - Match creation form + - Select Team 1 and Team 2 (each with 2 players) + - Enter scores for each team + - Auto-calculate winner + - Update Elo ratings for all players + - Update partnership statistics + +### As a user, I want to upload match results via CSV so that I can process multiple matches at once +- Acceptance Criteria: + - CSV upload form + - Support for Euchre format (Event, Round, Table, Seat 1-4, Odds/Evens Points, Winner) + - Map table names (Clubs, Hearts, Diamonds, Spades, Stars) to numeric IDs + - Handle player name variations + - Create teams automatically + - Import matches and calculate Elo + - Show import summary with success/error count + +### As a user, I want to edit match results so that I can correct mistakes +- Acceptance Criteria: + - Edit form for existing matches + - Validation for time limits (5 minutes for players, unlimited for admins) + - Update Elo ratings on change + - Log changes in activity feed + +## Epic 6: Club Administration + +### As a club admin, I want to manage all players so that I can maintain the player database +- Acceptance Criteria: + - Player directory with search and filter + - Create, edit, delete players + - Bulk actions (export, update ratings) + - View player activity + +### As a club admin, I want to manage all tournaments so that I can oversee club events +- Acceptance Criteria: + - Tournament list with filters + - Clone existing tournaments + - Archive completed tournaments + - View tournament analytics + +### As a club admin, I want to configure club settings so that I can customize the system +- Acceptance Criteria: + - Elo rating parameters + - Partnership tracking preferences + - Notification settings + - Default tournament settings + +### As a club admin, I want to view club analytics so that I can assess overall performance +- Acceptance Criteria: + - Rating distribution charts + - Activity heatmaps + - Partnership network graph + - Export reports (PDF/CSV) + +## Epic 7: Mobile Responsiveness + +### As a mobile user, I want to navigate the site so that I can access features on my phone +- Acceptance Criteria: + - Responsive navigation bar + - Bottom navigation for mobile + - Touch-friendly buttons and links + - Optimized layouts for small screens + +### As a mobile user, I want to record match results so that I can log games on the go +- Acceptance Criteria: + - Mobile-optimized forms + - Quick entry for common actions + - Offline support (optional) + +## Epic 8: Data Management & Export + +### As a club admin, I want to export data so that I can backup or analyze externally +- Acceptance Criteria: + - Export players to CSV + - Export tournaments to CSV + - Export matches to CSV + - Export partnership data to CSV + +### As a club admin, I want to import data so that I can migrate from other systems +- Acceptance Criteria: + - Import players from CSV + - Import tournaments from CSV + - Import matches from CSV + - Data validation and error reporting diff --git a/app/actions/admin/matches/process_upload.rb b/app/actions/admin/matches/process_upload.rb index 2d1b2e8..c6517b0 100644 --- a/app/actions/admin/matches/process_upload.rb +++ b/app/actions/admin/matches/process_upload.rb @@ -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 diff --git a/app/actions/admin/matches/upload.rb b/app/actions/admin/matches/upload.rb index 21d5334..2c2e8fe 100644 --- a/app/actions/admin/matches/upload.rb +++ b/app/actions/admin/matches/upload.rb @@ -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 diff --git a/app/services/tournament_csv_importer.rb b/app/services/tournament_csv_importer.rb new file mode 100644 index 0000000..8d7ed92 --- /dev/null +++ b/app/services/tournament_csv_importer.rb @@ -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 diff --git a/app/templates/admin/matches/upload.html.erb b/app/templates/admin/matches/upload.html.erb index 6378429..60d4896 100644 --- a/app/templates/admin/matches/upload.html.erb +++ b/app/templates/admin/matches/upload.html.erb @@ -1,26 +1,110 @@ -
Upload a CSV file with the following 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 -+
Upload a CSV file with Euchre tournament results. The format supports standard Euchre scoring with Odds/Evens teams.