Files
MatchLiveTv/backend/app/models/match.rb
Emiliano Frascaro 34c97f4447 Fix sport squadra su partite basket e ruoli organico per board.
Le partite ereditano lo sport_key della squadra alla creazione invece del default pallavolo; l'organico mostra ruoli per board (basket, volley, timed) e il wizard Android usa lo sport del team per overlay e icona.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 22:32:36 +02:00

177 lines
4.8 KiB
Ruby

class Match < ApplicationRecord
belongs_to :team
has_many :stream_sessions, dependent: :destroy
has_one_attached :opponent_logo_file
ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze
DEFAULT_OPPONENT_COLOR = "#1E3A8A"
validates :opponent_name, presence: true
validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 }
validates :sport_key, presence: true
validates :opponent_primary_color, format: { with: Brandable::HEX_COLOR }, allow_blank: true
validate :sport_key_known
validate :overlay_kind_allowed
validate :scoring_rules_shape
validate :opponent_logo_file_type, if: -> { opponent_logo_file.attached? }
before_validation :normalize_sport_key
before_validation :inherit_sport_from_team, on: :create
scope :scheduled_for_live, -> {
where.not(scheduled_at: nil).where("scheduled_at >= ?", Time.zone.now)
}
scope :scheduled_upcoming, -> { scheduled_for_live }
def scheduled_upcoming?
scheduled_at.present? && scheduled_at >= Time.zone.now
end
def scheduled_on_calendar?
scheduled_at.present? && scheduled_at >= Time.zone.now.beginning_of_day
end
def active_stream_session
latest = stream_sessions.max_by(&:created_at)
return nil if latest.nil? || latest.status.in?(%w[ended error])
latest
end
def deletable?
session = active_stream_session
session.nil? || !session.resumable?
end
def public_status_label
active = active_stream_session
return "In corso / da riprendere" if active
return "Programmata" if scheduled_upcoming?
return "Pronta" if scheduled_at.present?
"Senza orario"
end
scope :search_teams_or_opponents, lambda { |query|
q = query.to_s.strip
return all if q.blank?
term = "%#{sanitize_sql_like(q)}%"
joins(team: :club).where(
"teams.name ILIKE :term OR matches.opponent_name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term",
term: term
)
}
def broadcast_active?
stream_sessions.where(status: ACTIVE_SESSION_STATUSES).exists?
end
def stream_completed?
stream_sessions.where(status: %w[ended error]).exists?
end
def opponent_logo_url
return unless opponent_logo_file.attached?
Rails.application.routes.url_helpers.rails_blob_path(opponent_logo_file, only_path: true)
end
def effective_opponent_primary_color
opponent_primary_color.presence || DEFAULT_OPPONENT_COLOR
end
def coach_hub_visible?
active = active_stream_session
return true if active&.resumable?
return true if active&.idle?
return false if stream_completed?
scheduled_upcoming?
end
def effective_board_type
Sports::Catalog.board_for(sport_key)
end
def effective_overlay_kind
kind = overlay_kind.presence || Sports::Catalog.overlay_for(sport_key)
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
allowed.include?(kind) ? kind : Sports::Catalog.overlay_for(sport_key)
end
def effective_scoring_rules
merged = Sports::Catalog.default_rules_for(sport_key).deep_merge(normalized_scoring_rules)
merged["sets_to_win"] = sets_to_win if merged["sets_to_win"].blank? && sets_to_win.present?
merged
end
def sport_label
Sports::Catalog.find_optional(sport_key)&.dig(:label) || sport_key.to_s.humanize
end
# Alias legacy API/spec (colonna rinominata in sport_key).
def sport
sport_key
end
def sport=(value)
self.sport_key = Sports::Catalog.normalize_key(value)
end
private
def normalize_sport_key
self.sport_key = Sports::Catalog.normalize_key(sport_key)
end
def inherit_sport_from_team
return unless team.present?
# Il default DB (pallavolo) non deve prevalere sullo sport della squadra alla creazione.
self.sport_key = team.sport_key if new_record? || sport_key.blank?
end
def sport_key_known
return if sport_key.blank?
return if Sports::Catalog.find_optional(sport_key)
errors.add(:sport_key, "non valido")
end
def overlay_kind_allowed
return if overlay_kind.blank?
unless Sports::OverlayKind.valid?(overlay_kind)
errors.add(:overlay_kind, "non valido")
return
end
allowed = Sports::Catalog.allowed_overlays_for(sport_key)
errors.add(:overlay_kind, "non consentito per questo sport") unless allowed.include?(overlay_kind)
end
def normalized_scoring_rules
return {} unless scoring_rules.is_a?(Hash)
scoring_rules.each_with_object({}) do |(k, v), h|
h[k.to_s] = v
end
end
def opponent_logo_file_type
return if opponent_logo_file.content_type.in?(%w[image/png image/jpeg image/webp])
errors.add(:opponent_logo_file, "deve essere PNG, JPEG o WebP")
end
def scoring_rules_shape
Sports::RulesSchema.validate!(effective_board_type, normalized_scoring_rules)
rescue ArgumentError => e
errors.add(:scoring_rules, e.message)
end
end