Completa multi-sport su web, API score_action e controlli Android board-aware.

Allinea i form società/squadra al catalogo sport, espone score_action per basket/timed e corregge bug namespace Sports e Result nel period engine; l'app nativa gestisce overlay e punteggio per board con wizard regole esteso.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 21:48:29 +02:00
parent c0ede48091
commit ad9d67ddb6
25 changed files with 513 additions and 116 deletions

View File

@@ -2,7 +2,7 @@ module Api
module V1
class SportsController < BaseController
def index
render json: Sports::Catalog.as_api_list
render json: ::Sports::Catalog.as_api_list
end
end
end

View File

@@ -39,6 +39,20 @@ module Api
render json: session_json(@session, detail: true)
end
def score_action
action = params[:score_action].presence
raise ArgumentError, "Azione mancante" if action.blank?
result = Scoring::ApplyAction.new(session: @session, action: action).call
render json: session_json(@session, detail: true).merge(
set_won: result.set_won,
match_won: result.match_won,
winner: result.winner&.to_s
)
rescue ArgumentError => e
render json: { error: e.message }, status: :unprocessable_entity
end
def events
events = @session.stream_events.recent.limit(100)
render json: events.map { |e| event_json(e) }

View File

@@ -21,7 +21,7 @@ module Api
}, status: :unprocessable_entity
end
sport_key = Sports::Catalog.normalize_key(team_params[:sport_key] || team_params[:sport] || "pallavolo")
sport_key = ::Sports::Catalog.normalize_key(team_params[:sport_key] || team_params[:sport] || "pallavolo")
club = Club.create!(name: team_params[:name], sport: sport_key,
primary_color: "#e53935", secondary_color: "#ffffff")
ClubMembership.create!(user: current_user, club: club, role: "owner")

View File

@@ -190,6 +190,7 @@ module Public
)
p[:primary_color] = normalize_hex_color(p[:primary_color], "#e53935")
p[:secondary_color] = normalize_hex_color(p[:secondary_color], "#ffffff")
p[:sport] = Sports::Catalog.normalize_key(p[:sport]) if p[:sport].present?
p
end

View File

@@ -165,7 +165,12 @@ module Public
end
def team_params
p = params.require(:team).permit(:name, :sport, :description, :logo_url, :primary_color, :secondary_color)
p = params.require(:team).permit(:name, :sport, :sport_key, :description, :logo_url, :primary_color, :secondary_color)
if p[:sport].present? && p[:sport_key].blank?
p[:sport_key] = p.delete(:sport)
elsif p[:sport_key].present?
p.delete(:sport)
end
p[:primary_color] = p[:primary_color].presence
p[:secondary_color] = p[:secondary_color].presence
if p[:primary_color].present?

View File

@@ -16,4 +16,9 @@ module ApplicationHelper
value = date_or_time.respond_to?(:in_time_zone) ? date_or_time.in_time_zone : date_or_time
I18n.with_locale(:it) { I18n.l(value, format: format) }
end
def sport_catalog_options(selected = nil)
options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] }
options_for_select(options, Sports::Catalog.normalize_key(selected))
end
end

View File

@@ -1,6 +1,8 @@
module Scoring
module Engines
module PeriodEngineMixin
Result = Scoring::ApplyAction::Result
private
def period_rules

View File

@@ -8,7 +8,7 @@
<%= label_tag "club[name]", "Nome società" %>
<%= text_field_tag "club[name]", @club.name, required: true %>
<%= label_tag "club[sport]", "Sport principale" %>
<%= select_tag "club[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @club.sport) %>
<%= select_tag "club[sport]", sport_catalog_options(@club.sport) %>
<%= render "shared/branding_fields", record: @club, legend: "Logo e colori societari" %>
<%= render "shared/billing_profile_fields", record: @club %>
<%= submit_tag "Salva", class: "btn btn-primary" %>

View File

@@ -11,7 +11,7 @@
<%= label_tag "club[name]", "Nome società / club" %>
<%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: "es. Crazy Volley Rozzano" %>
<%= label_tag "club[sport]", "Sport principale" %>
<%= select_tag "club[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], params.dig(:club, :sport) || "volleyball") %>
<%= select_tag "club[sport]", sport_catalog_options(params.dig(:club, :sport) || "pallavolo") %>
<%= render "shared/branding_fields", record: Club.new(primary_color: "#e53935", secondary_color: "#ffffff"), legend: "Logo e colori societari" %>
<%= render "shared/billing_profile_fields", record: Club.new(billing_country: "IT") %>

View File

@@ -9,7 +9,7 @@
<%= label_tag "team[name]", "Nome squadra" %>
<%= text_field_tag "team[name]", @team.name, required: true %>
<%= label_tag "team[sport]", "Sport" %>
<%= select_tag "team[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @team.sport) %>
<%= select_tag "team[sport]", sport_catalog_options(@team.sport_key) %>
<%= label_tag "team[description]", "Descrizione squadra" %>
<%= text_area_tag "team[description]", @team.description, rows: 5, placeholder: "Presentazione della squadra, palmarès, obiettivi della stagione…" %>

View File

@@ -9,7 +9,7 @@
<%= label_tag "team[name]", "Nome squadra" %>
<%= text_field_tag "team[name]", nil, required: true, placeholder: "es. Under 13, Serie C" %>
<%= label_tag "team[sport]", "Sport" %>
<%= select_tag "team[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @club.sport) %>
<%= select_tag "team[sport]", sport_catalog_options(@club.sport) %>
<%= render "shared/branding_fields", record: Team.new(club: @club), show_inherit_hint: true, legend: "Override branding (opzionale)" %>
<%= submit_tag "Aggiungi squadra", class: "btn btn-primary" %>
<% end %>

View File

@@ -43,6 +43,7 @@ Rails.application.routes.draw do
patch :pause
patch :resume
patch :score
post :score_action
get :events
post :telemetry
post :pairing_token

10
backend/db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
ActiveRecord::Schema[7.2].define(version: 2026_06_09_100000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -147,7 +147,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
t.string "opponent_name", null: false
t.string "location"
t.datetime "scheduled_at"
t.string "sport", default: "volleyball"
t.string "sport_key", default: "pallavolo"
t.integer "sets_to_win", default: 3
t.jsonb "roster_numbers", default: []
t.string "category"
@@ -155,6 +155,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.jsonb "scoring_rules", default: {}, null: false
t.string "opponent_primary_color"
t.string "overlay_kind"
t.index ["team_id"], name: "index_matches_on_team_id"
end
@@ -217,6 +219,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.jsonb "set_partials", default: [], null: false
t.string "board_type", default: "volley", null: false
t.jsonb "data", default: {}, null: false
t.index ["stream_session_id"], name: "index_score_states_on_stream_session_id", unique: true
end
@@ -319,7 +323,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
create_table "teams", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "sport", default: "volleyball"
t.string "sport_key", default: "pallavolo"
t.string "logo_url"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false

View File

@@ -8,7 +8,7 @@ RSpec.describe Sports::Catalog do
entry = described_class.find("pallavolo")
expect(entry[:board]).to eq("volley")
expect(entry[:overlay]).to eq("volley")
expect(entry[:default_rules]["sets_to_win"]).to eq(3)
expect(entry[:default_rules][:sets_to_win]).to eq(3)
end
it "mappa legacy volleyball" do