Unifica admin società/squadre e YouTube Full con fallback MLTV.
Premium Full senza canale società usa il canale piattaforma come Light; in app restano solo Match Live TV e YouTube Live. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,13 +3,14 @@ module Admin
|
||||
before_action :set_club, only: %i[show grant_comped revoke_comped]
|
||||
|
||||
def index
|
||||
@clubs = Club.includes(:subscription, subscription: :plan, subscription: :admin_comped_by)
|
||||
@clubs = Club.includes(:subscription, :teams, subscription: :plan, subscription: :admin_comped_by)
|
||||
.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
@subscription = @club.subscription || @club.build_subscription(plan: Plan["free"], status: "active")
|
||||
@plans = Plan.ordered.reject { |p| p.slug == "free" }
|
||||
@teams = @club.teams.includes(:youtube_credential).order(:name)
|
||||
end
|
||||
|
||||
def grant_comped
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Admin
|
||||
class TeamsController < Admin::BaseController
|
||||
def index
|
||||
@teams = Team.all.order(:name)
|
||||
redirect_to admin_clubs_path, status: :moved_permanently
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@@ -6,19 +6,20 @@ module Youtube
|
||||
|
||||
def initialize(team)
|
||||
@team = team
|
||||
@status = TeamStatus.new(team)
|
||||
@mode = team.entitlements.plan.youtube_mode
|
||||
end
|
||||
|
||||
def resolve
|
||||
if @mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||
if @status.uses_platform_channel?
|
||||
PlatformCredential.new
|
||||
else
|
||||
elsif @mode == "team"
|
||||
@team.youtube_credential
|
||||
end
|
||||
end
|
||||
|
||||
def uses_platform_channel?
|
||||
@mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||
@status.uses_platform_channel?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ module Youtube
|
||||
when "matchlivetv_light"
|
||||
PlatformCredential.configured?
|
||||
when "team"
|
||||
@team.youtube_credential.present?
|
||||
@team.youtube_credential.present? || PlatformCredential.configured?
|
||||
else
|
||||
false
|
||||
end
|
||||
@@ -26,7 +26,15 @@ module Youtube
|
||||
end
|
||||
|
||||
def uses_platform_channel?
|
||||
@mode == "matchlivetv_light" && PlatformCredential.configured?
|
||||
return false unless @ent.youtube_enabled?
|
||||
|
||||
if @mode == "matchlivetv_light"
|
||||
PlatformCredential.configured?
|
||||
elsif @mode == "team"
|
||||
@team.youtube_credential.blank? && PlatformCredential.configured?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def channel_title
|
||||
@@ -38,7 +46,7 @@ module Youtube
|
||||
end
|
||||
|
||||
def needs_team_oauth?
|
||||
@mode == "team" && @ent.premium_full?
|
||||
@mode == "team" && @ent.premium_full? && @team.youtube_credential.blank? && !PlatformCredential.configured?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<h2>Società</h2>
|
||||
<h2>Società e squadre</h2>
|
||||
<p style="color:#888;margin-bottom:16px">
|
||||
Gestisci abbonamenti omaggio (sponsor / promozioni). Per pagamenti Stripe usa <%= link_to "Fatturazione", admin_billing_path %>.
|
||||
Abbonamenti omaggio (sponsor / promozioni) e stato YouTube per squadra.
|
||||
Pagamenti Stripe: <%= link_to "Fatturazione", admin_billing_path %>.
|
||||
</p>
|
||||
|
||||
<table class="admin-table">
|
||||
@@ -8,6 +9,7 @@
|
||||
<tr>
|
||||
<th>Società</th>
|
||||
<th>Piano</th>
|
||||
<th>Squadre</th>
|
||||
<th>Omaggio</th>
|
||||
<th>Stripe</th>
|
||||
<th></th>
|
||||
@@ -19,6 +21,7 @@
|
||||
<tr>
|
||||
<td><strong><%= club.name %></strong></td>
|
||||
<td><%= sub&.plan&.name || "Free" %></td>
|
||||
<td><%= club.teams.size %></td>
|
||||
<td>
|
||||
<% if sub&.admin_comped? %>
|
||||
<span style="color:#ffb74d">Sì</span>
|
||||
|
||||
@@ -1,16 +1,52 @@
|
||||
<p style="margin-bottom:16px"><%= link_to "← Tutte le società", admin_clubs_path %></p>
|
||||
<p style="margin-bottom:16px"><%= link_to "← Società e squadre", admin_clubs_path %></p>
|
||||
|
||||
<h2><%= @club.name %></h2>
|
||||
<p style="color:#888">
|
||||
Sport: <%= @club.sport %>
|
||||
· <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @club.id) %>
|
||||
· <%= link_to "Canale YouTube piattaforma", admin_youtube_platform_path %>
|
||||
</p>
|
||||
|
||||
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
|
||||
|
||||
<h3 style="font-size:1rem;margin-top:24px">Squadre</h3>
|
||||
<ul>
|
||||
<% @club.teams.order(:name).each do |team| %>
|
||||
<li><%= link_to team.name, admin_team_path(team) %></li>
|
||||
<h3 style="font-size:1rem;margin-top:28px">Squadre</h3>
|
||||
<% if @teams.empty? %>
|
||||
<p class="muted">Nessuna squadra registrata.</p>
|
||||
<% else %>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Squadra</th>
|
||||
<th>Sport</th>
|
||||
<th>YouTube</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @teams.each do |team| %>
|
||||
<% yt = Youtube::TeamStatus.new(team) %>
|
||||
<tr>
|
||||
<td><strong><%= team.name %></strong></td>
|
||||
<td><%= team.sport %></td>
|
||||
<td>
|
||||
<% if yt.selectable? %>
|
||||
<% if yt.uses_platform_channel? %>
|
||||
<span style="color:#81c784">Match Live TV</span>
|
||||
<% if team.youtube_credential.blank? && team.entitlements.premium_full? %>
|
||||
<span class="muted"> (default Full)</span>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<span style="color:#81c784">✓ <%= yt.channel_title.presence || "Canale società" %></span>
|
||||
<% end %>
|
||||
<% elsif team.entitlements.youtube_enabled? %>
|
||||
<span class="muted">In configurazione</span>
|
||||
<% else %>
|
||||
—
|
||||
<% end %>
|
||||
</td>
|
||||
<td><%= link_to "Partite e dettagli", admin_team_path(team) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if @stats[:teams_count] > @teams.size %>
|
||||
<p class="kpi-sub" style="margin-top:0.75rem"><%= link_to "Vedi tutte (#{@stats[:teams_count]})", admin_teams_path %></p>
|
||||
<p class="kpi-sub" style="margin-top:0.75rem"><%= link_to "Vedi società (#{@stats[:teams_count]} squadre)", admin_clubs_path %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<h2>Teams</h2>
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>Nome</th><th>Società</th><th>Sport</th><th>YouTube</th><th>Fatture</th></tr></thead>
|
||||
<tbody>
|
||||
<% @teams.each do |t| %>
|
||||
<tr>
|
||||
<td><%= link_to t.name, admin_team_path(t) %></td>
|
||||
<td><%= t.club&.name || "—" %></td>
|
||||
<td><%= t.sport %></td>
|
||||
<td><%= t.youtube_credential.present? ? "✓" : "—" %></td>
|
||||
<td>
|
||||
<% if t.club %>
|
||||
<%= link_to "Fatturazione", admin_billing_path(club_id: t.club.id) %>
|
||||
<% else %>
|
||||
—
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,14 +1,32 @@
|
||||
<% if @team.club %>
|
||||
<p style="margin-bottom:16px"><%= link_to "← #{@team.club.name}", admin_club_path(@team.club) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2><%= @team.name %></h2>
|
||||
<p>Sport: <%= @team.sport %></p>
|
||||
<% if @team.club %>
|
||||
<p>Società: <%= @team.club.name %> · <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @team.club.id) %></p>
|
||||
<% end %>
|
||||
<% yt = Youtube::TeamStatus.new(@team) %>
|
||||
<p>YouTube: <%= yt.channel_title || "—" %> · <%= yt.selectable? ? "pronto" : "non pronto" %></p>
|
||||
<% if @team.entitlements.plan.youtube_mode == "team" && @team.youtube_credential.blank? %>
|
||||
<p><%= link_to "Collega (da account titolare sul sito)", public_team_details_path(@team) %></p>
|
||||
<p>
|
||||
YouTube:
|
||||
<% if yt.selectable? %>
|
||||
<strong><%= yt.channel_title || "—" %></strong>
|
||||
<% if yt.uses_platform_channel? %>
|
||||
(canale piattaforma<%= @team.entitlements.premium_full? && @team.youtube_credential.blank? ? ", default senza OAuth società" : "" %>)
|
||||
<% else %>
|
||||
(canale società)
|
||||
<% end %>
|
||||
<% else %>
|
||||
non pronto
|
||||
<% end %>
|
||||
</p>
|
||||
<% if @team.entitlements.premium_full? && @team.youtube_credential.blank? %>
|
||||
<p class="muted">
|
||||
Senza canale collegato, l’app usa il canale Match Live TV.
|
||||
<%= link_to "Collega canale (sito pubblico)", public_team_details_path(@team) %>.
|
||||
</p>
|
||||
<% end %>
|
||||
<p><%= link_to "Token canale Match Live TV (Light)", admin_youtube_platform_path %></p>
|
||||
|
||||
<h3>Partite</h3>
|
||||
<ul>
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
<nav class="admin-nav">
|
||||
<% if admin_logged_in? %>
|
||||
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
|
||||
<%= link_to "Teams", admin_teams_path, class: ("active" if controller_name == "teams") %>
|
||||
<%= link_to "Società", admin_clubs_path, class: ("active" if controller_name == "clubs") %>
|
||||
<%= link_to "Società e squadre", admin_clubs_path, class: ("active" if controller_name.in?(%w[clubs teams])) %>
|
||||
<%= link_to "Fatturazione", admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
|
||||
<%= link_to "YouTube", admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
|
||||
<%= link_to "Sessions", admin_sessions_path, class: ("active" if controller_name == "sessions") %>
|
||||
|
||||
@@ -28,13 +28,18 @@
|
||||
<%= button_to "Scollega canale", public_team_youtube_disconnect_path(team),
|
||||
method: :delete,
|
||||
class: "btn btn-secondary",
|
||||
form: { data: { turbo_confirm: "Scollegare il canale YouTube? Le dirette future richiederanno un nuovo collegamento." } } %>
|
||||
form: { data: { turbo_confirm: "Scollegare il canale YouTube? Le dirette useranno il canale Match Live TV finché non ricolleghi il tuo." } } %>
|
||||
<% else %>
|
||||
<p class="muted">
|
||||
Collega il canale YouTube della società per trasmettere dall’app con la piattaforma «YouTube Live».
|
||||
<p>
|
||||
Senza canale collegato, le dirette YouTube dall’app vanno sul canale ufficiale
|
||||
<strong>Match Live TV</strong>.
|
||||
Puoi collegare il canale della società quando vuoi per trasmettere sul tuo profilo.
|
||||
</p>
|
||||
<% if yt.selectable? %>
|
||||
<p class="muted">YouTube pronto in app (canale Match Live TV).</p>
|
||||
<% end %>
|
||||
<% if ENV["YOUTUBE_CLIENT_ID"].present? %>
|
||||
<%= link_to "Collega canale YouTube", public_team_youtube_connect_path(team), class: "btn btn-primary" %>
|
||||
<%= link_to "Collega il tuo canale YouTube", public_team_youtube_connect_path(team), class: "btn btn-primary" %>
|
||||
<% else %>
|
||||
<p class="muted">OAuth YouTube non ancora configurato sul server.</p>
|
||||
<% end %>
|
||||
|
||||
@@ -67,7 +67,7 @@ Rails.application.routes.draw do
|
||||
get "metrics", to: "dashboard#metrics"
|
||||
get "billing", to: "billing#index", as: :billing
|
||||
post "billing/payments/:payment_id/attach_pdf", to: "billing#attach_pdf", as: :billing_payment_attach_pdf
|
||||
resources :teams, only: %i[index show]
|
||||
resources :teams, only: %i[show]
|
||||
resources :clubs, only: %i[index show] do
|
||||
member do
|
||||
post :grant_comped
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
# Diretta YouTube dall’app (invito staff)
|
||||
|
||||
Percorso per il **responsabile trasmissione** invitato via email: accede in app, collega **il proprio** canale YouTube e avvia la live.
|
||||
Percorso per il **responsabile trasmissione** invitato via email: accede in app e avvia la live su YouTube.
|
||||
|
||||
## Prerequisiti (società)
|
||||
|
||||
- Piano **Premium Full** attivo
|
||||
- Piano **Premium Light** o **Premium Full** attivo
|
||||
- `YOUTUBE_CLIENT_ID` / `YOUTUBE_CLIENT_SECRET` configurati sul server
|
||||
- Partita programmata per la squadra
|
||||
|
||||
### Canale usato
|
||||
|
||||
| Piano | Canale YouTube in diretta |
|
||||
|-------|--------------------------|
|
||||
| Premium Light | Sempre **Match Live TV** (nessun OAuth società) |
|
||||
| Premium Full, canale collegato | Canale **della società** |
|
||||
| Premium Full, canale non collegato | **Match Live TV** (default, come Light) |
|
||||
|
||||
Il collegamento del canale società è **opzionale** su Full: si fa dal sito (dettaglio squadra) o in futuro da app; finché non c’è OAuth, l’app trasmette sul canale piattaforma.
|
||||
|
||||
## 1. Invito
|
||||
|
||||
1. Il titolare società → **Responsabili trasmissione** → invita `tua@email.it`
|
||||
@@ -17,15 +27,12 @@ Percorso per il **responsabile trasmissione** invitato via email: accede in app,
|
||||
|
||||
API: `GET /api/v1/invitations/:token`, `POST /api/v1/invitations/:token/accept`
|
||||
|
||||
## 2. Collega canale YouTube (tuo)
|
||||
## 2. Collega canale YouTube (opzionale, Premium Full)
|
||||
|
||||
1. Nell’app: nuova diretta → passo **Piattaforma**
|
||||
2. Tocca **YouTube Live** → si apre Google OAuth
|
||||
3. **Avanzate** → continua (se app OAuth in verifica)
|
||||
4. Scegli l’account del **tuo** canale → Consenti
|
||||
5. Torna all’app → messaggio «Canale collegato»
|
||||
Solo se la società vuole il **proprio** canale invece di Match Live TV:
|
||||
|
||||
Il token OAuth è salvato sulla **squadra**; chi collega deve essere `can_stream` (responsabile trasmissione o titolare).
|
||||
1. Sul sito: dettaglio squadra → **Collega il tuo canale YouTube**
|
||||
2. Oppure (se abilitato) OAuth da app al tocco YouTube quando non c’è ancora canale piattaforma
|
||||
|
||||
## 3. Scegli la partita
|
||||
|
||||
@@ -38,9 +45,9 @@ Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmiss
|
||||
|
||||
## 4. Avvia diretta
|
||||
|
||||
1. Nel wizard, seleziona **YouTube Live** (dopo il collegamento canale)
|
||||
1. Nel wizard, seleziona **YouTube Live** (se il piano lo consente e il canale è pronto)
|
||||
2. Completa i passi → **Camera**
|
||||
3. Il telefono invia RTMP a MediaMTX; il server inolvia su YouTube con la stream key della live creata via API
|
||||
3. Il telefono invia RTMP a MediaMTX; il server inoltra su YouTube
|
||||
|
||||
## Sviluppo locale
|
||||
|
||||
@@ -53,7 +60,7 @@ Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmiss
|
||||
| Problema | Soluzione |
|
||||
|----------|-----------|
|
||||
| Nessuna squadra in app | Accetta invito con email corretta |
|
||||
| YouTube disabilitato | Premium Full + collegamento canale |
|
||||
| «Collega canale» | Tocca YouTube Live per OAuth da app |
|
||||
| YouTube disabilitato | Premium Light o Full |
|
||||
| YouTube non selezionabile | Token canale Match Live TV sul server (`admin` → YouTube piattaforma) |
|
||||
| OAuth «app non verificata» | Utente di test Google + Avanzate |
|
||||
| Live non su YouTube | Verifica relay ffmpeg / `stream_key` in sessione |
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../app/theme.dart';
|
||||
import '../../providers/session_provider.dart';
|
||||
import '../../shared/api_client.dart';
|
||||
@@ -75,62 +73,17 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connectYoutube(Team team) async {
|
||||
try {
|
||||
final url = await ref.read(apiClientProvider).youtubeAuthorizeUrl(team.id);
|
||||
final uri = Uri.parse(url);
|
||||
if (!await canLaunchUrl(uri)) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossibile aprire il browser per Google')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Completa il consenso Google, poi torna qui e seleziona YouTube Live.',
|
||||
),
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
final apiErr = ApiException.fromDio(e);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(apiErr?.message ?? 'Errore collegamento YouTube')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onYoutubeTap(Team? team) {
|
||||
if (team == null) return;
|
||||
if (team.isYoutubeReady) {
|
||||
setState(() => _platform = 'youtube');
|
||||
return;
|
||||
}
|
||||
if (team.canUseYoutube && team.youtubeMode == 'team' && team.canConnectYoutube) {
|
||||
_connectYoutube(team);
|
||||
return;
|
||||
}
|
||||
if (team.canUseYoutube && team.youtubeMode == 'team') {
|
||||
showPremiumRequiredDialog(
|
||||
context,
|
||||
message: 'Collega il canale YouTube (Premium Full) per trasmettere sul tuo canale.',
|
||||
billingUrl: team.staffManageUrl ?? team.billingUrl,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (team.canUseYoutube && team.youtubeMode == 'matchlivetv_light') {
|
||||
if (team.canUseYoutube) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'YouTube sul canale Match Live TV non è ancora attivo. Riprova più tardi o usa Match Live TV.',
|
||||
'YouTube non è ancora disponibile. Riprova più tardi o usa Match Live TV sul sito.',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -152,23 +105,12 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
||||
if (team.youtubeUsesPlatformChannel) {
|
||||
return label != null ? 'Canale $label' : 'Canale Match Live TV';
|
||||
}
|
||||
return label != null ? 'Canale $label' : 'Canale collegato';
|
||||
return label != null ? 'Canale $label' : 'Canale società collegato';
|
||||
}
|
||||
if (team.youtubeMode == 'matchlivetv_light') {
|
||||
if (team.youtubeMode == 'matchlivetv_light' || team.youtubeMode == 'team') {
|
||||
return 'Canale Match Live TV (in attivazione)';
|
||||
}
|
||||
if (team.canConnectYoutube) {
|
||||
return 'Tocca per collegare il tuo canale YouTube';
|
||||
}
|
||||
return 'Collega canale (Premium Full)';
|
||||
}
|
||||
|
||||
void _onExternalPremiumTap(String platform, Team? team) {
|
||||
showPremiumRequiredDialog(
|
||||
context,
|
||||
message: '$platform è disponibile con Premium (in arrivo).',
|
||||
billingUrl: team?.billingUrl,
|
||||
);
|
||||
return 'In attivazione';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -221,24 +163,6 @@ class _StepTransmissionState extends ConsumerState<StepTransmission> {
|
||||
badge: team?.canUseYoutube == true ? null : 'Premium',
|
||||
onTap: () => _onYoutubeTap(team),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_PlatformCard(
|
||||
title: 'Facebook Live',
|
||||
subtitle: 'Premium — presto disponibile',
|
||||
selected: false,
|
||||
enabled: false,
|
||||
badge: 'Premium',
|
||||
onTap: () => _onExternalPremiumTap('Facebook Live', team),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_PlatformCard(
|
||||
title: 'Twitch',
|
||||
subtitle: 'Premium — presto disponibile',
|
||||
selected: false,
|
||||
enabled: false,
|
||||
badge: 'Premium',
|
||||
onTap: () => _onExternalPremiumTap('Twitch', team),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('Privacy', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Reference in New Issue
Block a user