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:
2026-05-29 07:23:13 +02:00
parent 4083bc5dee
commit f4b7be0f80
156 changed files with 5033 additions and 2033 deletions

View File

@@ -0,0 +1,100 @@
<% team = local_assigns[:team] %>
<% can_manage = local_assigns[:can_manage] %>
<% owner_membership = local_assigns[:owner_membership] %>
<% staff_memberships = local_assigns[:staff_memberships] %>
<% pending_invitations = local_assigns[:pending_invitations] %>
<% recordings = local_assigns[:recordings] %>
<% entitlements = local_assigns[:entitlements] %>
<section class="team-streaming-staff">
<% if can_manage %>
<% if owner_membership&.staff_kind.blank? %>
<div class="card team-streaming-staff__self">
<h2>Il tuo account (<%= current_user.email %>)</h2>
<p class="muted">
Puoi gestire la trasmissione dallapp. Per il punteggio da un altro telefono, condividi il
<strong>link regia</strong> durante la diretta (non serve un secondo account).
</p>
<div class="team-details-actions">
<%= button_to "Sono io — responsabile trasmissione", public_team_assign_self_staff_path(team), method: :post, params: { staff_kind: "transmission" }, class: "btn btn-primary" %>
</div>
</div>
<% end %>
<% end %>
<h2>Responsabili trasmissione (app)</h2>
<div class="card">
<% if staff_memberships.any? %>
<table class="data">
<thead><tr><th>Nome</th><th>Email</th><th>Ruolo</th><% if can_manage %><th></th><% end %></tr></thead>
<tbody>
<% staff_memberships.each do |ut| %>
<tr>
<td>
<%= ut.user.name %>
<% if ut.user_id == current_user.id %><span class="muted"> (tu)</span><% end %>
</td>
<td><%= ut.user.email %></td>
<td>Trasmissione</td>
<% if can_manage %>
<td>
<% if ut.user_id == current_user.id && team.club.owned_by?(current_user) %>
<%= button_to "Rimuovi ruolo staff", public_team_clear_self_staff_path(team), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: "Rimuovere il ruolo staff dal tuo account?" } } %>
<% elsif ut.role == "member" %>
<%= button_to "Revoca", public_team_remove_member_path(team, ut.user), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: "Revocare l'accesso a #{ut.user.name}?" } } %>
<% end %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="muted">
Nessun responsabile trasmissione.
<% if can_manage %><%= link_to "Invita qualcuno", public_team_invite_path(team) %>.<% end %>
</p>
<% end %>
</div>
<% if can_manage %>
<h2>Inviti in attesa</h2>
<div class="card">
<% if pending_invitations.any? %>
<table class="data">
<thead><tr><th>Email</th><th>Scade</th><th></th></tr></thead>
<tbody>
<% pending_invitations.each do |inv| %>
<tr>
<td><%= inv.email %></td>
<td><%= l inv.expires_at, format: :short %></td>
<td>
<%= button_to "Annulla", public_team_destroy_invitation_path(team, inv), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="muted">Nessuna richiesta in sospeso.</p>
<% end %>
</div>
<% end %>
<% if entitlements.can_access_recordings? && recordings.any? %>
<h2>Archivio gare</h2>
<div class="card">
<table class="data">
<thead><tr><th>Partita</th><th>Replay</th></tr></thead>
<tbody>
<% recordings.each do |rec| %>
<tr>
<td><%= rec.stream_session.match.team.name %> vs <%= rec.stream_session.match.opponent_name %></td>
<td><%= link_to "Guarda", rec.replay_url %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</section>

View File

@@ -20,7 +20,6 @@
<% end %>
<ul>
<li>Staff trasmissione: <%= plan.max_staff_transmission || "illimitato" %> / anno</li>
<li>Staff regia: <%= plan.max_staff_regia || "illimitato" %> / anno</li>
<li>Partite: <%= plan.concurrent_streams_limit || "illimitate" %> in contemporanea</li>
<li>Replay: <%= plan.recordings_enabled? ? "#{plan.recording_days} giorni" : "no" %></li>
<li>YouTube: <% if plan.slug == "premium_light" %>Match TV Light<% elsif plan.youtube_enabled? %>canale società<% else %>no<% end %></li>
@@ -46,5 +45,5 @@
<p style="margin-top:20px"><%= button_to "Gestisci su Stripe", public_team_portal_path(@team), class: "btn btn-secondary" %></p>
<% end %>
<p><%= link_to "← Dashboard", public_team_dashboard_path(@team) %></p>
<p><%= link_to "← Dettagli squadra", public_team_details_path(@team) %></p>
</div>

View File

@@ -1,117 +0,0 @@
<% content_for :title, "#{@team.name} — Dashboard" %>
<% content_for :meta_description, "Dashboard squadra #{@team.name} su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<p style="color:#888;margin:0 0 8px"><%= link_to @club.name, public_club_path(@club) %> · <%= link_to "Modifica squadra", public_edit_team_path(@team) %></p>
<h1 style="display:flex;align-items:center;gap:12px">
<% if @team.effective_logo_url.present? %>
<%= image_tag @team.effective_logo_url, alt: "", width: 40, height: 40, style: "border-radius:8px" %>
<% end %>
<%= @team.name %>
</h1>
<p>
Piano: <strong><%= @entitlements.plan.name %></strong> (società)
· Staff trasmissione: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
· Staff regia: <strong><%= @entitlements.staff_count_for("regia") %> / <%= @entitlements.max_staff_regia || "∞" %></strong>
· Partite attive: <strong><%= @entitlements.concurrent_streams_used %><% if @entitlements.concurrent_streams_limit %> / <%= @entitlements.concurrent_streams_limit %><% else %> (illimitate)<% end %></strong>
</p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin:20px 0">
<%= link_to "Abbonamento società", public_club_billing_path(@club), class: "btn btn-primary" %>
<%= link_to "Aggiungi staff", public_team_invite_path(@team), class: "btn btn-secondary" %>
<%= link_to "Dirette live", public_live_index_path, class: "btn btn-secondary" %>
</div>
<% owner_staff = @owner_membership&.staff_kind %>
<% can_self_transmission = owner_staff != "transmission" && owner_staff != "regia" %>
<% can_self_regia = owner_staff != "regia" && owner_staff != "transmission" %>
<% if can_self_transmission || can_self_regia %>
<div class="card" style="margin-bottom:20px">
<h2 style="margin-top:0;font-size:1.1rem">Il tuo account (<%= current_user.email %>)</h2>
<p style="color:#aaa;font-size:0.92rem;margin-bottom:12px">
Puoi usare le stesse credenziali nellapp mobile. Trasmissione e regia devono essere <strong>email diverse</strong> — non puoi coprire entrambi i ruoli con questo account.
</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<% if can_self_transmission %>
<%= button_to "Sono io — staff trasmissione", public_team_assign_self_staff_path(@team), method: :post, params: { staff_kind: "transmission" }, class: "btn btn-primary" %>
<% end %>
<% if can_self_regia %>
<%= button_to "Sono io — staff regia", public_team_assign_self_staff_path(@team), method: :post, params: { staff_kind: "regia" }, class: "btn btn-secondary" %>
<% end %>
</div>
</div>
<% end %>
<h2>Staff attivo</h2>
<div class="card">
<% if @staff_memberships.any? %>
<table class="data">
<thead><tr><th>Nome</th><th>Email</th><th>Ruolo</th><th></th></tr></thead>
<tbody>
<% @staff_memberships.each do |ut| %>
<tr>
<td>
<%= ut.user.name %>
<% if ut.role == "owner" %><span style="color:#888;font-size:0.85rem"> (tu)</span><% end %>
</td>
<td><%= ut.user.email %></td>
<td><%= ut.staff_kind == "regia" ? "Regia" : "Trasmissione" %></td>
<td>
<% if ut.role == "owner" %>
<%= button_to "Rimuovi ruolo staff", public_team_clear_self_staff_path(@team), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: "Rimuovere il ruolo staff dal tuo account?" } } %>
<% else %>
<%= button_to "Revoca", public_team_remove_member_path(@team, ut.user), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: "Revocare l'accesso a #{ut.user.name}?" } } %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p style="color:#888">Nessun membro staff. Usa i pulsanti sopra per il tuo account o <%= link_to "invita qualcuno", public_team_invite_path(@team) %>.</p>
<% end %>
</div>
<h2>Staff in attesa</h2>
<div class="card">
<% if @pending_invitations.any? %>
<table class="data">
<thead><tr><th>Email</th><th>Ruolo</th><th>Scade</th><th></th></tr></thead>
<tbody>
<% @pending_invitations.each do |inv| %>
<tr>
<td><%= inv.email %></td>
<td><%= inv.staff_kind == "regia" ? "Regia" : "Trasmissione" %></td>
<td><%= l inv.expires_at, format: :short %></td>
<td>
<%= button_to "Annulla", public_team_destroy_invitation_path(@team, inv), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p style="color:#888">Nessuna richiesta in sospeso.</p>
<% end %>
</div>
<% if @entitlements.can_access_recordings? && @recordings.any? %>
<h2>Archivio gare</h2>
<div class="card">
<table class="data">
<thead><tr><th>Partita</th><th>Replay</th></tr></thead>
<tbody>
<% @recordings.each do |rec| %>
<tr>
<td><%= rec.stream_session.match.team.name %> vs <%= rec.stream_session.match.opponent_name %></td>
<td><%= link_to "Guarda", rec.replay_url %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<p style="color:#888;margin-top:24px">Usa l'app mobile con le stesse credenziali per avviare partite e dirette.</p>
</div>

View File

@@ -0,0 +1,101 @@
<% content_for :title, "Dettagli — #{@team.name}" %>
<% content_for :meta_description, "Dettagli squadra #{@team.name} su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<% total_people = @roster_by_category.values.sum(&:size) %>
<div class="wrap team-roster-page">
<nav class="team-dashboard-nav" aria-label="Percorso squadra">
<% if @can_manage %>
<%= link_to "← #{@club.name}", public_club_path(@club), class: "team-dashboard-nav__club" %>
<span class="team-dashboard-nav__sep">·</span>
<%= link_to "Modifica squadra", public_edit_team_path(@team) %>
<% else %>
<span class="team-dashboard-nav__club"><%= @club.name %></span>
<% end %>
</nav>
<header class="roster-hero card">
<div class="roster-hero__intro">
<p class="roster-hero__club"><%= @club.name %></p>
<h1 class="roster-hero__title"><%= @team.name %></h1>
<p class="team-details-meta">
Piano: <strong><%= @entitlements.plan.name %></strong> (società)
· Responsabili trasmissione: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
· Partite attive: <strong><%= @entitlements.concurrent_streams_used %><% if @entitlements.concurrent_streams_limit %> / <%= @entitlements.concurrent_streams_limit %><% else %> (illimitate)<% end %></strong>
</p>
<% if @team.description.present? %>
<div class="roster-hero__desc"><%= simple_format @team.description %></div>
<% elsif @can_manage %>
<p class="roster-hero__desc roster-hero__desc--muted">
Nessuna descrizione.
<%= link_to "Aggiungila dalla modifica squadra", public_edit_team_path(@team) %>.
</p>
<% end %>
<div class="roster-stats">
<span class="roster-stat"><strong><%= total_people %></strong> in organico</span>
<% TeamRosterMember::DISPLAY_ORDER.each do |cat| %>
<% count = @roster_by_category[cat].size %>
<% next if count.zero? %>
<span class="roster-stat"><%= count %> <%= TeamRosterMember::CATEGORY_LABELS[cat].downcase %></span>
<% end %>
</div>
</div>
<div class="roster-hero__media">
<% if @team.team_photo_url.present? %>
<%= image_tag @team.team_photo_url, alt: @team.name, class: "roster-hero__photo" %>
<% elsif @can_manage %>
<div class="roster-hero__photo roster-hero__photo--empty">
<span>Foto squadra non ancora caricata</span>
<%= link_to "Aggiungi dalla modifica squadra", public_edit_team_path(@team), class: "btn btn-secondary", style: "margin-top:8px;padding:8px 14px;font-size:0.85rem" %>
</div>
<% end %>
</div>
</header>
<div class="team-details-actions">
<% if @can_manage %>
<%= link_to "← Società", public_club_path(@club), class: "btn btn-secondary" %>
<%= link_to "Responsabili trasmissione", public_team_invite_path(@team), class: "btn btn-secondary" %>
<% end %>
<% if current_user.can_stream_for?(@team) %>
<%= link_to "Programma partite", public_team_matches_path(@team), class: "btn btn-primary" %>
<% end %>
<%= link_to "Dirette live", public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
</div>
<div class="roster-layout<%= " roster-layout--readonly" unless @can_manage %>">
<section class="roster-main" aria-label="Organico">
<% TeamRosterMember::DISPLAY_ORDER.each do |category| %>
<% members = @roster_by_category[category] %>
<section class="roster-section card">
<header class="roster-section__head">
<h2 class="roster-section__title"><%= TeamRosterMember::CATEGORY_LABELS[category] %></h2>
<span class="roster-section__count"><%= members.size %></span>
</header>
<% if members.any? %>
<div class="roster-list">
<% members.each do |person| %>
<%= render "shared/roster_person_card", person: person, team: @team, editable: @can_manage %>
<% end %>
</div>
<% else %>
<p class="roster-section__empty">Nessuna persona in questa categoria.</p>
<% end %>
</section>
<% end %>
</section>
<% if @can_manage %>
<aside class="roster-sidebar" aria-label="Aggiungi persona">
<%= render "shared/roster_member_form", member: @roster_member, team: @team %>
</aside>
<% end %>
</div>
<%= render "public/teams/streaming_staff", team: @team, club: @club, can_manage: @can_manage,
entitlements: @entitlements, owner_membership: @owner_membership,
staff_memberships: @staff_memberships, pending_invitations: @pending_invitations,
recordings: @recordings %>
<p class="team-details-footer">Usa l'app mobile con le stesse credenziali per avviare partite e dirette.</p>
</div>

View File

@@ -10,6 +10,20 @@
<%= 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) %>
<%= label_tag "team[description]", "Descrizione squadra" %>
<%= text_area_tag "team[description]", @team.description, rows: 5, placeholder: "Presentazione della squadra, palmarès, obiettivi della stagione…" %>
<div class="branding-logo-block">
<%= label_tag "team[photo_file]", "Foto di squadra (gruppo o stemma)" %>
<% if @team.team_photo_url.present? %>
<div class="branding-logo-preview">
<%= image_tag @team.team_photo_url, alt: "Foto squadra", class: "team-profile-photo-preview", width: 200 %>
</div>
<% end %>
<%= file_field_tag "team[photo_file]", accept: "image/png,image/jpeg,image/webp", class: "branding-file-input" %>
</div>
<%= render "shared/branding_fields", record: @team, show_inherit_hint: true, legend: "Branding squadra (override)" %>
<p class="branding-hint">
Colori società:
@@ -19,5 +33,5 @@
<%= submit_tag "Salva squadra", class: "btn btn-primary" %>
<% end %>
</div>
<p><%= link_to "← Dashboard squadra", public_team_dashboard_path(@team) %></p>
<p><%= link_to "← Dettagli squadra", public_team_details_path(@team) %></p>
</div>

View File

@@ -1,43 +1,40 @@
<% content_for :title, "Aggiungi staff — #{@team.name}" %>
<% content_for :title, "Staff trasmissione — #{@team.name}" %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<h1>Aggiungi staff</h1>
<h1>Responsabili trasmissione</h1>
<p>
Trasmissione: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
· Regia: <strong><%= @entitlements.staff_count_for("regia") %> / <%= @entitlements.max_staff_regia || "∞" %></strong>
Assegnati: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
(per squadra, rinnovo annuale)
</p>
<p class="muted" style="max-width:640px">
Il responsabile trasmissione avvia la diretta dallapp (camera sul cavalletto).
Per il punteggio da un secondo telefono non serve un account: condivide il <strong>link regia</strong> dalla schermata di trasmissione
(WhatsApp, email, SMS, ecc.).
</p>
<% owner_staff = current_user.user_teams.find_by(team: @team, role: "owner")&.staff_kind %>
<% unless owner_staff.present? %>
<% owner_membership = current_user.user_teams.find_by(team: @team) %>
<% if owner_membership&.staff_kind.blank? %>
<div class="card" style="max-width:520px;margin-bottom:16px">
<p style="margin:0 0 12px;color:#aaa;font-size:0.92rem">
<strong><%= current_user.email %></strong> — aggiungi il tuo account senza invito (non potrai essere sia trasmissione sia regia).
<strong><%= current_user.email %></strong> — puoi gestire tu camera e punteggio, oppure invitare altri per la trasmissione.
</p>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<%= button_to "Sono io — trasmissione", public_team_assign_self_staff_path(@team), method: :post, params: { staff_kind: "transmission" }, class: "btn btn-primary" %>
<%= button_to "Sono io — regia", public_team_assign_self_staff_path(@team), method: :post, params: { staff_kind: "regia" }, class: "btn btn-secondary" %>
</div>
<%= button_to "Sono io — responsabile trasmissione", public_team_assign_self_staff_path(@team), method: :post, params: { staff_kind: "transmission" }, class: "btn btn-primary" %>
</div>
<% end %>
<div class="card" style="max-width:520px">
<h2 style="margin-top:0;font-size:1.05rem">Invita unaltra email</h2>
<%= form_with url: public_team_invite_path(@team), method: :post do %>
<%= label_tag :staff_kind, "Ruolo" %>
<%= select_tag :staff_kind, options_for_select([
["Trasmissione (camera / streaming)", "transmission"],
["Regia (punteggio)", "regia"]
], params[:staff_kind] || "transmission") %>
<%= label_tag :email, "Email del membro staff" %>
<%= hidden_field_tag :staff_kind, "transmission" %>
<%= label_tag :email, "Email del responsabile trasmissione" %>
<%= email_field_tag :email, params[:email], required: true %>
<%= submit_tag "Genera link di accesso", class: "btn btn-primary" %>
<% end %>
<% if defined?(@invite_url) && @invite_url.present? %>
<p style="margin-top:16px">Condividi (valido 7 giorni):</p>
<code style="word-break:break-all;display:block;background:#0a0a0e;padding:12px;border-radius:8px"><%= @invite_url %></code>
<p style="color:#888;font-size:0.88rem;margin-top:8px">Il membro dello staff si registra con la stessa email e accetta l'accesso. L'email non può essere già usata per l'altro ruolo (trasmissione vs regia).</p>
<% end %>
</div>
<p><%= link_to "← Dashboard", public_team_dashboard_path(@team) %></p>
<p><%= link_to "← Dettagli squadra", public_team_details_path(@team) %></p>
</div>