Billing Stripe, link regia mobile e staff solo trasmissione.
Aggiunge fatturazione club, pagina regia condivisibile senza account, roster squadre e rimuove la modalità controller dall'app (v1.1.0). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
50
backend/app/controllers/admin/billing_invoices_controller.rb
Normal file
50
backend/app/controllers/admin/billing_invoices_controller.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
module Admin
|
||||
class BillingInvoicesController < BaseController
|
||||
before_action :set_club
|
||||
|
||||
def index
|
||||
@invoices = @club.billing_invoices.recent
|
||||
end
|
||||
|
||||
def new
|
||||
@invoice = @club.billing_invoices.build(issued_on: Date.current, currency: "eur", source: "aruba")
|
||||
end
|
||||
|
||||
def create
|
||||
@invoice = @club.billing_invoices.build(invoice_params)
|
||||
@invoice.source = "aruba"
|
||||
|
||||
if @invoice.save
|
||||
redirect_to admin_club_billing_invoices_path(@club), notice: "Fattura #{@invoice.number} registrata."
|
||||
else
|
||||
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_club
|
||||
@club = Club.find(params[:club_id])
|
||||
end
|
||||
|
||||
def invoice_params
|
||||
params.require(:billing_invoice).permit(
|
||||
:number,
|
||||
:issued_on,
|
||||
:amount_cents,
|
||||
:amount_euros,
|
||||
:currency,
|
||||
:status,
|
||||
:aruba_document_id,
|
||||
:billing_payment_id,
|
||||
:notes,
|
||||
:pdf
|
||||
).tap do |p|
|
||||
if p[:amount_euros].present?
|
||||
p[:amount_cents] = (p.delete(:amount_euros).to_f * 100).round
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,5 +8,19 @@ module Admin
|
||||
@session = StreamSession.find(params[:id])
|
||||
@events = @session.stream_events.recent.limit(50)
|
||||
end
|
||||
|
||||
def stop
|
||||
@session = StreamSession.find(params[:id])
|
||||
if @session.terminal?
|
||||
redirect_to admin_session_path(@session), alert: "Sessione già terminata (#{@session.status})."
|
||||
return
|
||||
end
|
||||
|
||||
Sessions::Stop.new(@session).call
|
||||
redirect_to admin_session_path(@session), notice: "Sessione terminata."
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("[admin] stop session #{@session.id}: #{e.class} #{e.message}")
|
||||
redirect_to admin_session_path(@session), alert: "Errore durante la chiusura: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,11 +40,13 @@ module Api
|
||||
private
|
||||
|
||||
def set_team
|
||||
@team = current_user.manageable_teams.find(params[:team_id])
|
||||
@team = current_user.streamable_teams.find { |t| t.id == params[:team_id] }
|
||||
raise ActiveRecord::RecordNotFound unless @team
|
||||
end
|
||||
|
||||
def set_match
|
||||
@match = Match.joins(:team).merge(current_user.manageable_teams).find(params[:id])
|
||||
team_ids = current_user.streamable_teams.map(&:id)
|
||||
@match = Match.where(team_id: team_ids).find(params[:id])
|
||||
end
|
||||
|
||||
def match_params
|
||||
|
||||
@@ -4,7 +4,8 @@ module Api
|
||||
before_action :set_session, except: :create
|
||||
|
||||
def create
|
||||
match = Match.joins(:team).merge(current_user.manageable_teams).find(params[:match_id])
|
||||
team_ids = current_user.streamable_teams.map(&:id)
|
||||
match = Match.where(team_id: team_ids).find(params[:match_id])
|
||||
session = Sessions::Create.new(user: current_user, match: match, params: session_params).call
|
||||
render json: session_json(session), status: :created
|
||||
end
|
||||
@@ -66,6 +67,15 @@ module Api
|
||||
}
|
||||
end
|
||||
|
||||
def regia_link
|
||||
access = Sessions::RegiaAccess.new(@session)
|
||||
token = access.issue_token!
|
||||
render json: {
|
||||
regia_url: access.url(token),
|
||||
expires_at: @session.regia_token_expires_at
|
||||
}
|
||||
end
|
||||
|
||||
def claim_pairing
|
||||
token = params.require(:pairing_token)
|
||||
digest = Digest::SHA256.hexdigest(token)
|
||||
@@ -101,8 +111,9 @@ module Api
|
||||
private
|
||||
|
||||
def set_session
|
||||
@session = StreamSession.joins(match: :team)
|
||||
.merge(current_user.manageable_teams)
|
||||
team_ids = current_user.streamable_teams.map(&:id)
|
||||
@session = StreamSession.joins(:match)
|
||||
.where(matches: { team_id: team_ids })
|
||||
.find(params[:id])
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ module Api
|
||||
module V1
|
||||
class TeamsController < BaseController
|
||||
def index
|
||||
teams = current_user.manageable_teams.includes(:club, :matches)
|
||||
teams = current_user.streamable_teams
|
||||
render json: teams.map { |t| team_json(t) }
|
||||
end
|
||||
|
||||
@@ -27,7 +27,8 @@ module Api
|
||||
end
|
||||
|
||||
def recordings
|
||||
team = current_user.manageable_teams.find(params[:id])
|
||||
team = current_user.streamable_teams.find { |t| t.id.to_s == params[:id].to_s }
|
||||
raise ActiveRecord::RecordNotFound unless team
|
||||
unless team.entitlements.can_access_recordings?
|
||||
return render json: {
|
||||
error: "Archivio gare disponibile con Premium Light o Full",
|
||||
@@ -89,16 +90,16 @@ module Api
|
||||
staff_manage_url: ent.staff_manage_url,
|
||||
max_staff: ent.max_staff,
|
||||
max_staff_transmission: ent.max_staff_transmission,
|
||||
max_staff_regia: ent.max_staff_regia,
|
||||
staff_used: ent.staff_count,
|
||||
staff_transmission_used: ent.staff_count_for("transmission"),
|
||||
staff_regia_used: ent.staff_count_for("regia"),
|
||||
concurrent_streams_used: ent.concurrent_streams_used,
|
||||
concurrent_streams_limit: ent.concurrent_streams_limit,
|
||||
recordings_enabled: ent.can_access_recordings?,
|
||||
phone_download_enabled: ent.phone_download_enabled?,
|
||||
youtube_enabled: ent.youtube_enabled?,
|
||||
youtube_mode: ent.plan.youtube_mode
|
||||
youtube_mode: ent.plan.youtube_mode,
|
||||
staff_role: current_user.staff_role_for(team),
|
||||
can_stream: current_user.can_stream_for?(team)
|
||||
}
|
||||
if detail
|
||||
data[:members] = team.user_teams.includes(:user).where.not(role: "owner").map do |ut|
|
||||
|
||||
70
backend/app/controllers/public/club_billing_controller.rb
Normal file
70
backend/app/controllers/public/club_billing_controller.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
module Public
|
||||
class ClubBillingController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_club
|
||||
before_action :require_club_owner_for_club!
|
||||
|
||||
def profile
|
||||
end
|
||||
|
||||
def update_profile
|
||||
@club.assign_attributes(billing_profile_params)
|
||||
if @club.save(context: :billing_profile)
|
||||
redirect_to public_club_billing_path(@club), notice: "Dati di fatturazione aggiornati."
|
||||
else
|
||||
flash.now[:alert] = @club.errors.full_messages.join(", ")
|
||||
render :profile, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def sync_payments
|
||||
unless MatchLiveTv.stripe_enabled?
|
||||
redirect_to public_club_billing_path(@club), alert: "Stripe non configurato."
|
||||
return
|
||||
end
|
||||
|
||||
count = Billing::Stripe::SyncPayments.call(club: @club)
|
||||
redirect_to public_club_billing_path(@club), notice: "Sincronizzati #{count} pagamenti da Stripe."
|
||||
rescue ::Stripe::StripeError => e
|
||||
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
|
||||
end
|
||||
|
||||
def download_invoice
|
||||
invoice = @club.billing_invoices.find(params[:invoice_id])
|
||||
unless invoice.pdf.attached?
|
||||
redirect_to public_club_billing_path(@club), alert: "PDF fattura non disponibile."
|
||||
return
|
||||
end
|
||||
|
||||
redirect_to rails_blob_path(invoice.pdf, disposition: "attachment"), allow_other_host: true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_club
|
||||
@club = current_user.owned_clubs.find(params[:id])
|
||||
end
|
||||
|
||||
def require_club_owner_for_club!
|
||||
require_club_owner!(@club)
|
||||
end
|
||||
|
||||
def billing_profile_params
|
||||
params.require(:club).permit(
|
||||
:billing_entity_type,
|
||||
:billing_legal_name,
|
||||
:billing_vat_number,
|
||||
:billing_fiscal_code,
|
||||
:billing_email,
|
||||
:billing_phone,
|
||||
:billing_address_line,
|
||||
:billing_city,
|
||||
:billing_province,
|
||||
:billing_postal_code,
|
||||
:billing_country,
|
||||
:billing_recipient_code,
|
||||
:billing_pec
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
85
backend/app/controllers/public/club_matches_controller.rb
Normal file
85
backend/app/controllers/public/club_matches_controller.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
module Public
|
||||
class ClubMatchesController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_club
|
||||
before_action :load_schedulable_teams
|
||||
before_action :require_any_schedulable_team!
|
||||
|
||||
def new
|
||||
@match = default_match_for_form
|
||||
@selected_team = resolve_selected_team
|
||||
end
|
||||
|
||||
def create
|
||||
@selected_team = @schedulable_teams.find { |t| t.id.to_s == params[:team_id].to_s }
|
||||
unless @selected_team
|
||||
@match = default_match_for_form
|
||||
flash.now[:alert] = "Seleziona una squadra valida."
|
||||
return render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@match = @selected_team.matches.create!(
|
||||
match_params.merge(sport: @selected_team.sport, sets_to_win: 3)
|
||||
)
|
||||
redirect_to public_live_index_path(club_id: @club.id),
|
||||
notice: "Partita programmata: #{@selected_team.name} vs #{@match.opponent_name}."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@match = @selected_team.matches.build(match_params)
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_club
|
||||
@club = Club.find(params[:club_id])
|
||||
end
|
||||
|
||||
def load_schedulable_teams
|
||||
@schedulable_teams = current_user.schedulable_teams_for(@club)
|
||||
@club_admin = @club.owned_by?(current_user)
|
||||
end
|
||||
|
||||
def require_any_schedulable_team!
|
||||
return if @schedulable_teams.any?
|
||||
|
||||
redirect_to public_live_index_path(club_id: @club.id),
|
||||
alert: "Non hai permessi per programmare partite in questa società."
|
||||
end
|
||||
|
||||
def resolve_selected_team
|
||||
if params[:team_id].present?
|
||||
@schedulable_teams.find { |t| t.id.to_s == params[:team_id].to_s }
|
||||
end || @schedulable_teams.first
|
||||
end
|
||||
|
||||
def default_match_for_form
|
||||
team = resolve_selected_team
|
||||
Match.new(
|
||||
sport: team&.sport || @club.sport,
|
||||
sets_to_win: 3,
|
||||
scheduled_at: default_scheduled_at
|
||||
)
|
||||
end
|
||||
|
||||
def match_params
|
||||
p = params.require(:match).permit(:opponent_name, :location, :scheduled_at)
|
||||
p[:scheduled_at] = parse_scheduled_at(p[:scheduled_at]) if p[:scheduled_at].present?
|
||||
p
|
||||
end
|
||||
|
||||
def parse_scheduled_at(value)
|
||||
raw = value.to_s.strip
|
||||
return nil if raw.blank?
|
||||
|
||||
Time.zone.strptime(raw, "%Y-%m-%dT%H:%M")
|
||||
rescue ArgumentError
|
||||
Time.zone.parse(raw)
|
||||
end
|
||||
|
||||
def default_scheduled_at
|
||||
t = Time.current + 2.hours
|
||||
Time.zone.local(t.year, t.month, t.day, t.hour, 0, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ module Public
|
||||
|
||||
before_action :require_login!
|
||||
before_action :set_club, only: %i[show edit update billing checkout portal]
|
||||
before_action :require_billing_profile_for_premium_checkout!, only: :checkout
|
||||
|
||||
def new
|
||||
redirect_to public_club_path(current_user.primary_club) if current_user.primary_club
|
||||
@@ -42,6 +43,7 @@ module Public
|
||||
|
||||
def show
|
||||
require_club_owner!(@club)
|
||||
apply_checkout_flash!
|
||||
@entitlements_team = @club.teams.first
|
||||
@entitlements = @entitlements_team&.entitlements
|
||||
@teams = @club.teams.order(:name)
|
||||
@@ -64,9 +66,13 @@ module Public
|
||||
|
||||
def billing
|
||||
require_club_owner!(@club)
|
||||
apply_checkout_flash!
|
||||
@team = @club.teams.first!
|
||||
@entitlements = @team.entitlements
|
||||
@subscription = @club.subscription
|
||||
@plans = Plan.ordered
|
||||
@payments = @club.billing_payments.recent.limit(50)
|
||||
@invoices = @club.billing_invoices.recent.limit(50)
|
||||
end
|
||||
|
||||
def checkout
|
||||
@@ -77,8 +83,20 @@ module Public
|
||||
end
|
||||
|
||||
plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) || "premium_light"
|
||||
url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user, plan_slug: plan_slug).url
|
||||
redirect_to url, allow_other_host: true
|
||||
target_plan = Plan[plan_slug]
|
||||
sub = @club.subscription
|
||||
|
||||
if sub&.stripe_subscription_id.present? && sub.active? && sub.plan.slug != plan_slug
|
||||
Billing::Stripe::ChangePlan.call(club: @club, plan_slug: plan_slug)
|
||||
redirect_to public_club_billing_path(@club),
|
||||
notice: "Piano aggiornato a #{target_plan.name}. L'eventuale differenza verrà addebitata o accreditata da Stripe."
|
||||
else
|
||||
url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user, plan_slug: plan_slug).url
|
||||
redirect_to url, allow_other_host: true
|
||||
end
|
||||
rescue ::Stripe::StripeError => e
|
||||
Rails.logger.warn("[Stripe checkout] #{e.message}")
|
||||
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
|
||||
end
|
||||
|
||||
def portal
|
||||
@@ -99,10 +117,34 @@ module Public
|
||||
end
|
||||
|
||||
def club_params
|
||||
p = params.require(:club).permit(:name, :sport, :logo_url, :primary_color, :secondary_color)
|
||||
p = params.require(:club).permit(
|
||||
:name, :sport, :logo_url, :primary_color, :secondary_color,
|
||||
:billing_entity_type, :billing_legal_name, :billing_vat_number, :billing_fiscal_code,
|
||||
:billing_email, :billing_phone, :billing_address_line, :billing_city, :billing_province,
|
||||
:billing_postal_code, :billing_country, :billing_recipient_code, :billing_pec
|
||||
)
|
||||
p[:primary_color] = normalize_hex_color(p[:primary_color], "#e53935")
|
||||
p[:secondary_color] = normalize_hex_color(p[:secondary_color], "#ffffff")
|
||||
p
|
||||
end
|
||||
|
||||
def require_billing_profile_for_premium_checkout!
|
||||
require_club_owner!(@club)
|
||||
plan_slug = params[:plan].presence_in(%w[premium_light premium_full])
|
||||
return if plan_slug.blank?
|
||||
return if @club.billing_profile_complete?
|
||||
|
||||
redirect_to public_club_billing_profile_path(@club),
|
||||
alert: "Completa i dati di fatturazione prima di attivare un piano premium."
|
||||
end
|
||||
|
||||
def apply_checkout_flash!
|
||||
case params[:checkout]
|
||||
when "success"
|
||||
flash.now[:notice] = "Pagamento completato! Il piano premium è attivo per la società."
|
||||
when "canceled"
|
||||
flash.now[:alert] = "Pagamento annullato. Nessun addebito è stato effettuato."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module Public
|
||||
return
|
||||
end
|
||||
invitation.accept!(current_user)
|
||||
redirect_to public_team_dashboard_path(invitation.team), notice: "Sei entrato nella squadra!"
|
||||
redirect_to public_team_details_path(invitation.team), notice: "Sei entrato nella squadra!"
|
||||
else
|
||||
session[:pending_invite_token] = params[:token]
|
||||
redirect_to public_signup_path, notice: "Registrati con #{invitation.email} per accettare l'invito"
|
||||
|
||||
@@ -6,33 +6,46 @@ module Public
|
||||
|
||||
def index
|
||||
@query = params[:q].to_s.strip
|
||||
@sessions = StreamSession
|
||||
@club = Club.find_by(id: params[:club_id]) if params[:club_id].present?
|
||||
team_ids = @club&.teams&.pluck(:id)
|
||||
|
||||
sessions = StreamSession
|
||||
.broadcasting
|
||||
.includes(:score_state, match: :team)
|
||||
.includes(:score_state, match: { team: :club })
|
||||
sessions = sessions.joins(:match).where(matches: { team_id: team_ids }) if team_ids.present?
|
||||
|
||||
@sessions = sessions
|
||||
.search_by_team_or_opponent(@query)
|
||||
.order(Arel.sql("started_at DESC NULLS LAST"), created_at: :desc)
|
||||
|
||||
busy_match_ids = StreamSession.where.not(status: %w[ended error]).select(:match_id)
|
||||
@upcoming_matches = Match
|
||||
.scheduled_upcoming
|
||||
.includes(:team)
|
||||
# Solo dirette già in onda: le partite «programmate» restano visibili anche se c’è sessione idle.
|
||||
broadcasting_match_ids = StreamSession.broadcasting.select(:match_id)
|
||||
upcoming = Match.scheduled_for_live.includes(team: :club)
|
||||
upcoming = upcoming.where(team_id: team_ids) if team_ids.present?
|
||||
|
||||
@upcoming_matches = upcoming
|
||||
.search_teams_or_opponents(@query)
|
||||
.where.not(id: busy_match_ids)
|
||||
.where.not(id: broadcasting_match_ids)
|
||||
.order(scheduled_at: :asc)
|
||||
.limit(30)
|
||||
.limit(50)
|
||||
|
||||
@online_paths = Mediamtx::Client.new.online_path_names
|
||||
|
||||
return unless logged_in? && @club
|
||||
|
||||
@schedulable_teams = current_user.schedulable_teams_for(@club)
|
||||
@can_schedule_match = @schedulable_teams.any?
|
||||
end
|
||||
|
||||
def show
|
||||
@session = StreamSession.includes(match: :team).find(params[:id])
|
||||
@session = StreamSession.includes(match: { team: :club }).find(params[:id])
|
||||
@match = @session.match
|
||||
@stream_closed = @session.terminal?
|
||||
@on_air = !@stream_closed && mediamtx_online?(@session)
|
||||
end
|
||||
|
||||
def status
|
||||
session = StreamSession.includes(match: :team).find(params[:id])
|
||||
session = StreamSession.includes(match: { team: :club }).find(params[:id])
|
||||
closed = session.terminal?
|
||||
render json: {
|
||||
status: session.status,
|
||||
|
||||
102
backend/app/controllers/public/matches_controller.rb
Normal file
102
backend/app/controllers/public/matches_controller.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
module Public
|
||||
class MatchesController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_team
|
||||
before_action :require_can_schedule!
|
||||
before_action :set_match, only: %i[edit update destroy]
|
||||
|
||||
def index
|
||||
@matches = @team.matches
|
||||
.includes(:stream_sessions)
|
||||
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc)
|
||||
@can_manage = @team.club.owned_by?(current_user)
|
||||
end
|
||||
|
||||
def new
|
||||
@match = @team.matches.build(
|
||||
sport: @team.sport,
|
||||
sets_to_win: 3,
|
||||
scheduled_at: default_scheduled_at
|
||||
)
|
||||
end
|
||||
|
||||
def create
|
||||
@match = @team.matches.create!(
|
||||
match_params.merge(sport: @team.sport, sets_to_win: match_params[:sets_to_win].presence || 3)
|
||||
)
|
||||
redirect_to public_team_matches_path(@team),
|
||||
notice: "Partita programmata: #{@match.opponent_name}. Avvia lo streaming dall’app quando sei in palestra."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@match = @team.matches.build(match_params)
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@match.update!(match_params)
|
||||
redirect_to public_team_matches_path(@team), notice: "Partita aggiornata."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
active = active_session_for(@match)
|
||||
if active&.resumable?
|
||||
redirect_to public_team_matches_path(@team),
|
||||
alert: "Chiudi la diretta prima di eliminare questa partita."
|
||||
return
|
||||
end
|
||||
|
||||
@match.destroy!
|
||||
redirect_to public_team_matches_path(@team), notice: "Partita eliminata."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_team
|
||||
@team = current_user.manageable_teams.find(params[:team_id])
|
||||
@club = @team.club
|
||||
end
|
||||
|
||||
def require_can_schedule!
|
||||
return if current_user.can_schedule_for?(@team)
|
||||
|
||||
redirect_to public_team_details_path(@team),
|
||||
alert: "Solo un responsabile trasmissione o il titolare della società possono programmare le partite."
|
||||
end
|
||||
|
||||
def set_match
|
||||
@match = @team.matches.find(params[:id])
|
||||
end
|
||||
|
||||
def match_params
|
||||
p = params.require(:match).permit(:opponent_name, :location, :scheduled_at, :sport, :sets_to_win)
|
||||
p[:scheduled_at] = parse_scheduled_at(p[:scheduled_at]) if p[:scheduled_at].present?
|
||||
p
|
||||
end
|
||||
|
||||
def parse_scheduled_at(value)
|
||||
raw = value.to_s.strip
|
||||
return nil if raw.blank?
|
||||
|
||||
Time.zone.strptime(raw, "%Y-%m-%dT%H:%M")
|
||||
rescue ArgumentError
|
||||
Time.zone.parse(raw)
|
||||
end
|
||||
|
||||
def default_scheduled_at
|
||||
t = Time.current + 2.hours
|
||||
Time.zone.local(t.year, t.month, t.day, t.hour, 0, 0)
|
||||
end
|
||||
|
||||
def active_session_for(match)
|
||||
match.stream_sessions
|
||||
.order(created_at: :desc)
|
||||
.find { |s| !%w[ended error].include?(s.status) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,6 +8,20 @@ module Public
|
||||
|
||||
def pricing
|
||||
@plans = Plan.ordered
|
||||
load_club_billing_context_for_pricing
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_club_billing_context_for_pricing
|
||||
return unless logged_in?
|
||||
|
||||
@club = current_user.primary_club
|
||||
return unless @club
|
||||
|
||||
@subscription = @club.subscription
|
||||
@team = @club.teams.order(:name).first
|
||||
@entitlements = @team&.entitlements
|
||||
end
|
||||
|
||||
def privacy
|
||||
|
||||
82
backend/app/controllers/public/regia_controller.rb
Normal file
82
backend/app/controllers/public/regia_controller.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
module Public
|
||||
class RegiaController < SiteBaseController
|
||||
include Public::LiveHelper
|
||||
|
||||
layout "regia"
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
skip_before_action :verify_authenticity_token, only: %i[score pause stop]
|
||||
|
||||
before_action :set_session_from_token
|
||||
|
||||
def show
|
||||
@match = @session.match
|
||||
@team = @match.team
|
||||
@score = @session.score_state || @session.create_score_state!(
|
||||
home_sets: 0, away_sets: 0, home_points: 0, away_points: 0, current_set: 1, set_partials: []
|
||||
)
|
||||
@rules = Scoring::Rules.from_match(@match)
|
||||
@stream_closed = @session.terminal?
|
||||
@on_air = !@stream_closed && mediamtx_online?(@session)
|
||||
@share_url = request.original_url
|
||||
@share_title = "Regia — #{@team.name} vs #{@match.opponent_name}"
|
||||
end
|
||||
|
||||
def status
|
||||
render json: status_payload
|
||||
end
|
||||
|
||||
def score
|
||||
cmd = params[:cmd].presence || params[:score_action].presence
|
||||
result = Scoring::ApplyAction.new(session: @session, action: cmd).call
|
||||
render json: status_payload.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 pause
|
||||
Sessions::Pause.new(@session).call
|
||||
render json: status_payload
|
||||
end
|
||||
|
||||
def stop
|
||||
Sessions::Stop.new(@session).call
|
||||
render json: status_payload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_session_from_token
|
||||
@session = Sessions::RegiaAccess.find_session_by_token(params[:token])
|
||||
return if @session
|
||||
|
||||
render file: Rails.root.join("public/404.html"), status: :not_found, layout: false
|
||||
throw :abort
|
||||
end
|
||||
|
||||
def status_payload
|
||||
score = @session.score_state
|
||||
closed = @session.terminal?
|
||||
{
|
||||
status: @session.status,
|
||||
stream_closed: closed,
|
||||
live: !closed && (@session.live? || mediamtx_online?(@session)),
|
||||
on_air: !closed && mediamtx_online?(@session),
|
||||
ended_at: @session.ended_at,
|
||||
home_name: @session.match.team.name,
|
||||
away_name: @session.match.opponent_name,
|
||||
points_target: Scoring::Rules.from_match(@session.match).points_target(score&.current_set || 1),
|
||||
score: score&.as_cable_payload
|
||||
}
|
||||
end
|
||||
|
||||
def mediamtx_online?(session)
|
||||
@online_paths_cache ||= Mediamtx::Client.new.online_path_names
|
||||
@online_paths_cache.include?(session.mediamtx_path_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,7 +20,7 @@ module Public
|
||||
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token))
|
||||
if invitation && invitation.email.downcase == @user.email.downcase
|
||||
invitation.accept!(@user)
|
||||
return redirect_to public_team_dashboard_path(invitation.team), notice: "Benvenuto nella squadra!"
|
||||
return redirect_to public_team_details_path(invitation.team), notice: "Benvenuto nella squadra!"
|
||||
end
|
||||
end
|
||||
redirect_to public_new_club_path, notice: "Account creato. Ora registra la tua società."
|
||||
|
||||
@@ -13,7 +13,7 @@ module Public
|
||||
dest = if user.primary_club
|
||||
public_club_path(user.primary_club)
|
||||
elsif user.manageable_teams.first
|
||||
public_team_dashboard_path(user.manageable_teams.first)
|
||||
public_team_details_path(user.manageable_teams.first)
|
||||
else
|
||||
public_new_club_path
|
||||
end
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
module Public
|
||||
class TeamRosterMembersController < WebBaseController
|
||||
before_action :require_login!
|
||||
before_action :set_team
|
||||
before_action :require_team_owner!
|
||||
before_action :set_member, only: %i[edit update destroy]
|
||||
|
||||
def create
|
||||
member = @team.roster_members.build(member_params)
|
||||
attach_photo(member)
|
||||
member.save!
|
||||
redirect_to public_team_details_path(@team), notice: "#{member.full_name} aggiunto all'organico."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
redirect_to public_team_details_path(@team), alert: e.record.errors.full_messages.join(", ")
|
||||
end
|
||||
|
||||
def edit
|
||||
@club = @team.club
|
||||
end
|
||||
|
||||
def update
|
||||
@member.assign_attributes(member_params)
|
||||
attach_photo(@member)
|
||||
@member.save!
|
||||
redirect_to public_team_details_path(@team), notice: "Scheda aggiornata."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@club = @team.club
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
name = @member.full_name
|
||||
@member.destroy!
|
||||
redirect_to public_team_details_path(@team), notice: "#{name} rimosso dall'organico."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_team
|
||||
@team = current_user.manageable_teams.find(params[:team_id])
|
||||
end
|
||||
|
||||
def require_team_owner!
|
||||
require_club_owner_for_team!(@team)
|
||||
end
|
||||
|
||||
def set_member
|
||||
@member = @team.roster_members.find(params[:id])
|
||||
end
|
||||
|
||||
def member_params
|
||||
params.require(:team_roster_member).permit(
|
||||
:category, :full_name, :role_label, :jersey_number, :bio, :position
|
||||
)
|
||||
end
|
||||
|
||||
def attach_photo(member)
|
||||
file = params.dig(:team_roster_member, :photo_file)
|
||||
member.photo_file.attach(file) if file.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,7 @@ module Public
|
||||
before_action :require_login!
|
||||
before_action :set_club, only: %i[new create]
|
||||
before_action :set_team, only: %i[
|
||||
dashboard edit update invite create_invitation
|
||||
details dashboard roster edit update invite create_invitation
|
||||
assign_self_staff clear_self_staff remove_member destroy_invitation
|
||||
]
|
||||
|
||||
@@ -24,15 +24,17 @@ module Public
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def details
|
||||
load_team_details!
|
||||
render :details
|
||||
end
|
||||
|
||||
def dashboard
|
||||
require_club_owner_for_team!(@team)
|
||||
@club = @team.club
|
||||
@entitlements = @team.entitlements
|
||||
@recordings = @team.recordings.ready.includes(stream_session: :match).order(created_at: :desc).limit(10)
|
||||
@owner_membership = current_user.user_teams.find_by(team: @team)
|
||||
@staff_memberships = @team.user_teams.includes(:user)
|
||||
.where("user_teams.role = 'member' OR (user_teams.staff_kind IS NOT NULL)")
|
||||
@pending_invitations = @team.team_invitations.pending.order(created_at: :desc)
|
||||
redirect_to public_team_details_path(params[:id])
|
||||
end
|
||||
|
||||
def roster
|
||||
redirect_to public_team_details_path(params[:id])
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -44,8 +46,9 @@ module Public
|
||||
require_club_owner_for_team!(@team)
|
||||
@team.assign_attributes(team_params)
|
||||
attach_branding_logo(@team)
|
||||
attach_team_photo(@team)
|
||||
@team.save!
|
||||
redirect_to public_team_dashboard_path(@team), notice: "Squadra aggiornata."
|
||||
redirect_to public_team_details_path(@team), notice: "Squadra aggiornata."
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@club = @team.club
|
||||
flash.now[:alert] = e.record.errors.full_messages.join(", ")
|
||||
@@ -59,27 +62,25 @@ module Public
|
||||
|
||||
def assign_self_staff
|
||||
require_club_owner_for_team!(@team)
|
||||
staff_kind = params[:staff_kind].presence_in(TeamInvitation::STAFF_KINDS) || "transmission"
|
||||
membership = current_user.user_teams.find_or_initialize_by(team: @team)
|
||||
membership.role = "member" if membership.new_record?
|
||||
Teams::StaffAssignment.call(team: @team, user: current_user, staff_kind: staff_kind, membership: membership)
|
||||
label = staff_kind == "regia" ? "Regia" : "Trasmissione"
|
||||
redirect_to public_team_dashboard_path(@team), notice: "Il tuo account è ora staff #{label}."
|
||||
Teams::StaffAssignment.call(team: @team, user: current_user, membership: membership)
|
||||
redirect_to public_team_details_path(@team), notice: "Il tuo account è ora responsabile trasmissione."
|
||||
rescue Teams::StaffAssignmentError, Teams::EntitlementError => e
|
||||
redirect_to public_team_dashboard_path(@team), alert: e.message
|
||||
redirect_to public_team_details_path(@team), alert: e.message
|
||||
end
|
||||
|
||||
def clear_self_staff
|
||||
require_club_owner_for_team!(@team)
|
||||
membership = current_user.user_teams.find_by(team: @team)
|
||||
membership&.update!(staff_kind: nil)
|
||||
redirect_to public_team_dashboard_path(@team), notice: "Ruolo staff rimosso dal tuo account."
|
||||
redirect_to public_team_details_path(@team), notice: "Ruolo staff rimosso dal tuo account."
|
||||
end
|
||||
|
||||
def create_invitation
|
||||
require_club_owner_for_team!(@team)
|
||||
@entitlements = @team.entitlements
|
||||
staff_kind = params[:staff_kind].presence_in(TeamInvitation::STAFF_KINDS) || "transmission"
|
||||
staff_kind = "transmission"
|
||||
@entitlements.assert_can_invite!(staff_kind: staff_kind)
|
||||
|
||||
email = params[:email]&.downcase&.strip
|
||||
@@ -103,18 +104,35 @@ module Public
|
||||
require_club_owner_for_team!(@team)
|
||||
ut = @team.user_teams.find_by!(user_id: params[:user_id], role: "member")
|
||||
ut.destroy!
|
||||
redirect_to public_team_dashboard_path(@team), notice: "Accesso revocato"
|
||||
redirect_to public_team_details_path(@team), notice: "Accesso revocato"
|
||||
end
|
||||
|
||||
def destroy_invitation
|
||||
require_club_owner_for_team!(@team)
|
||||
inv = @team.team_invitations.pending.find(params[:invitation_id])
|
||||
inv.destroy!
|
||||
redirect_to public_team_dashboard_path(@team), notice: "Invito annullato"
|
||||
redirect_to public_team_details_path(@team), notice: "Invito annullato"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_team_details!
|
||||
@club = @team.club
|
||||
@can_manage = @club.owned_by?(current_user)
|
||||
@entitlements = @team.entitlements
|
||||
@roster_by_category = @team.roster_by_category
|
||||
if @can_manage
|
||||
@roster_member = @team.roster_members.build(
|
||||
category: params[:category].presence_in(TeamRosterMember::CATEGORIES) || "player"
|
||||
)
|
||||
end
|
||||
@recordings = @team.recordings.ready.includes(stream_session: :match).order(created_at: :desc).limit(10)
|
||||
@owner_membership = current_user.user_teams.find_by(team: @team)
|
||||
@staff_memberships = @team.user_teams.includes(:user)
|
||||
.where("user_teams.role = 'member' OR (user_teams.staff_kind IS NOT NULL)")
|
||||
@pending_invitations = @team.team_invitations.pending.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def set_club
|
||||
@club = current_user.owned_clubs.find(params[:club_id])
|
||||
end
|
||||
@@ -123,8 +141,13 @@ module Public
|
||||
@team = current_user.manageable_teams.find(params[:id])
|
||||
end
|
||||
|
||||
def attach_team_photo(team)
|
||||
file = params.dig(:team, :photo_file)
|
||||
team.photo_file.attach(file) if file.present?
|
||||
end
|
||||
|
||||
def team_params
|
||||
p = params.require(:team).permit(:name, :sport, :logo_url, :primary_color, :secondary_color)
|
||||
p = params.require(:team).permit(:name, :sport, :description, :logo_url, :primary_color, :secondary_color)
|
||||
p[:primary_color] = p[:primary_color].presence
|
||||
p[:secondary_color] = p[:secondary_color].presence
|
||||
if p[:primary_color].present?
|
||||
|
||||
Reference in New Issue
Block a user