Compare commits

..
1 Commits
Author SHA1 Message Date
Emiliano FrascaroandCursor 26d36213e8 Prepara iOS 2.0.13 build 37 per lo Store con icona chiara e test del calendario.
Apple ha chiuso il treno 2.0.12: servono versione nuova, monogramma visibile sul nero e copertura dei casi dopo l'orario previsto.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 21:49:08 +02:00
57 changed files with 29 additions and 1409 deletions
@@ -6,8 +6,7 @@ module Admin
@filters = { @filters = {
from: parse_date(params[:from]) || 7.days.ago.to_date, from: parse_date(params[:from]) || 7.days.ago.to_date,
to: parse_date(params[:to]) || Time.zone.today, to: parse_date(params[:to]) || Time.zone.today,
device: params[:device].presence, device: params[:device].presence
chart_path: params[:chart_path].presence
} }
@analytics_preview_active = analytics_preview_active? @analytics_preview_active = analytics_preview_active?
@@ -37,24 +36,6 @@ module Admin
max_scroll: max_scroll_by_path[path].to_i max_scroll: max_scroll_by_path[path].to_i
} }
end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] } end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] }
@chart_path_options = @pages.map { |row| row[:page_path] }
if @filters[:chart_path].present? && @chart_path_options.exclude?(@filters[:chart_path])
@filters[:chart_path] = nil
end
trend_scope = scope
trend_scope = trend_scope.where(page_path: @filters[:chart_path]) if @filters[:chart_path].present?
pageviews_by_day = trend_scope.group(:day).sum(:pageview_count)
@trend = (@filters[:from]..@filters[:to]).map do |day|
{
day: day.iso8601,
pageviews: pageviews_by_day[day].to_i
}
end
@trend_totals = {
pageviews: @trend.sum { |r| r[:pageviews] }
}
end end
def show def show
@@ -1,34 +1,18 @@
module Admin module Admin
class ClubsController < BaseController class ClubsController < BaseController
before_action :set_club, only: %i[show edit update grant_comped revoke_comped set_quote revoke_quote] before_action :set_club, only: %i[show grant_comped revoke_comped set_quote revoke_quote]
def index def index
@clubs = Club.includes(:teams, :billing_quote, { club_memberships: :user }, subscription: %i[plan admin_comped_by]) @clubs = Club.includes(:teams, :billing_quote, subscription: %i[plan admin_comped_by])
.order(:name) .order(:name)
end end
def show def show
load_show_context @subscription = @club.subscription || @club.build_subscription(plan: Plan["free"], status: "active")
end @plans = Plan.ordered.reject { |p| p.slug == "free" }
@teams = @club.teams.order(:name)
def edit @quote = @club.active_billing_quote
load_edit_context @concurrency_violations = StreamConcurrencyViolation.for_club(@club.id).recent.limit(20)
end
def update
Admin::UpdateClubData.call(
club: @club,
club_attrs: club_update_params,
owner_attrs: owner_params,
staff_attrs: staff_params,
invitation_attrs: invitation_params,
team_attrs: team_params
)
redirect_to admin_club_path(@club), notice: t("admin.flash.club_updated", club: @club.name)
rescue Admin::UpdateClubData::Error, ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.message
load_edit_context
render :edit, status: :unprocessable_entity
end end
def grant_comped def grant_comped
@@ -79,76 +63,7 @@ module Admin
private private
def set_club def set_club
@club = Club.includes(club_memberships: :user).find(params[:id]) @club = Club.find(params[:id])
end
def load_show_context
@subscription = @club.subscription || @club.build_subscription(plan: Plan["free"], status: "active")
@plans = Plan.ordered.reject { |p| p.slug == "free" }
@teams = @club.teams.order(:name)
@quote = @club.active_billing_quote
@concurrency_violations = StreamConcurrencyViolation.for_club(@club.id).recent.limit(20)
end
def load_edit_context
@teams = @club.teams.includes(:user_teams, :team_invitations).order(:name)
@owner = @club.owner
@staff_users = User.joins(:user_teams)
.where(user_teams: { team_id: @club.teams.select(:id) })
.distinct
.order(:email)
.to_a
@pending_invitations = TeamInvitation.pending
.where(team_id: @club.teams.select(:id))
.includes(:team)
.order(:email)
@sport_options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] }
end
def club_update_params
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
)
end
def owner_params
params.fetch(:owner, {}).permit(:name, :email).to_h.symbolize_keys
end
def staff_params
raw = params[:staff_users]
return {} if raw.blank?
raw.permit!.to_h.each_with_object({}) do |(user_id, attrs), acc|
next unless attrs.is_a?(Hash)
acc[user_id] = attrs.slice("name", "email").symbolize_keys
end
end
def invitation_params
raw = params[:invitations]
return {} if raw.blank?
raw.permit!.to_h.each_with_object({}) do |(invitation_id, attrs), acc|
next unless attrs.is_a?(Hash)
acc[invitation_id] = attrs.slice("email").symbolize_keys
end
end
def team_params
raw = params[:teams]
return {} if raw.blank?
raw.permit!.to_h.each_with_object({}) do |(team_id, attrs), acc|
next unless attrs.is_a?(Hash)
acc[team_id] = attrs.slice("name", "sport").symbolize_keys
end
end end
def redirect_back_or_club(notice: nil, alert: nil) def redirect_back_or_club(notice: nil, alert: nil)
+1 -8
View File
@@ -21,14 +21,7 @@ class Club < ApplicationRecord
validates :secondary_color, presence: true validates :secondary_color, presence: true
def owner def owner
memberships = club_memberships club_memberships.find_by(role: "owner")&.user
membership =
if memberships.loaded?
memberships.detect { |m| m.role == "owner" }
else
memberships.find_by(role: "owner")
end
membership&.user
end end
def owned_by?(user) def owned_by?(user)
@@ -59,43 +59,18 @@ module ClubBillingProfile
def billing_profile_invoice_lines def billing_profile_invoice_lines
lines = [] lines = []
lines << ["Tipo", self.class.billing_entity_types[billing_entity_type]] if billing_entity_type.present? lines << ["Tipo", self.class.billing_entity_types[billing_entity_type]] if billing_entity_type.present?
lines << ["Intestatario", billing_legal_name] if billing_legal_name.present? lines << ["Intestatario", billing_legal_name]
lines << ["P.IVA", billing_vat_number] if billing_vat_number.present? lines << ["P.IVA", billing_vat_number] if billing_vat_number.present?
lines << ["Codice fiscale", billing_fiscal_code] if billing_fiscal_code.present? lines << ["Codice fiscale", billing_fiscal_code] if billing_fiscal_code.present?
lines << ["Email fatturazione", billing_email] if billing_email.present? lines << ["Email fatturazione", billing_email]
lines << ["Telefono", billing_phone] if billing_phone.present? lines << ["Telefono", billing_phone] if billing_phone.present?
core_address = [billing_address_line, billing_postal_code, billing_city, billing_province].compact_blank addr = [billing_address_line, billing_postal_code, billing_city, billing_province, billing_country].compact.join(", ")
if core_address.any? lines << ["Indirizzo", addr] if addr.present?
core_address << billing_country if billing_country.present?
lines << ["Indirizzo", core_address.join(", ")]
end
lines << ["SDI", billing_recipient_code] if billing_recipient_code.present? lines << ["SDI", billing_recipient_code] if billing_recipient_code.present?
lines << ["PEC", billing_pec] if billing_pec.present? lines << ["PEC", billing_pec] if billing_pec.present?
lines lines
end end
# Stato profilo fiscale per badge admin: :complete | :incomplete | :absent
def billing_profile_admin_status
return :complete if billing_profile_complete?
return :absent unless billing_profile_started?
:incomplete
end
def billing_profile_started?
billing_legal_name.present? ||
billing_vat_number.present? ||
billing_fiscal_code.present? ||
billing_email.present? ||
billing_phone.present? ||
billing_address_line.present? ||
billing_city.present? ||
billing_postal_code.present? ||
billing_province.present? ||
billing_recipient_code.present? ||
billing_pec.present?
end
private private
def billing_profile_for_invoicing def billing_profile_for_invoicing
@@ -1,156 +0,0 @@
# frozen_string_literal: true
module Admin
class UpdateClubData
class Error < StandardError; end
def self.call(**kwargs)
new(**kwargs).call
end
def initialize(club:, club_attrs:, owner_attrs: {}, staff_attrs: {}, invitation_attrs: {}, team_attrs: {})
@club = club
@club_attrs = club_attrs.to_h
@owner_attrs = owner_attrs.to_h
@staff_attrs = staff_attrs.to_h
@invitation_attrs = invitation_attrs.to_h
@team_attrs = team_attrs.to_h
end
def call
ActiveRecord::Base.transaction do
update_club!
update_owner!
update_staff!
update_invitations!
update_teams!
end
@club.reload
end
private
def update_club!
attrs = @club_attrs.dup
if attrs[:sport].present?
attrs[:sport] = Sports::Catalog.normalize_key(attrs[:sport])
end
%i[primary_color secondary_color].each do |key|
next unless attrs.key?(key)
attrs[key] = normalize_hex(attrs[key], key == :primary_color ? "#e53935" : "#ffffff")
end
%i[
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 logo_url
].each do |key|
next unless attrs.key?(key)
attrs[key] = attrs[key].to_s.strip.presence
end
if attrs[:billing_province].present?
attrs[:billing_province] = attrs[:billing_province].to_s.upcase
end
if attrs[:billing_country].present?
attrs[:billing_country] = attrs[:billing_country].to_s.upcase
end
if attrs[:billing_recipient_code].present?
attrs[:billing_recipient_code] = attrs[:billing_recipient_code].to_s.upcase
end
@club.assign_attributes(attrs)
@club.save!
rescue ActiveRecord::RecordInvalid => e
raise Error, e.record.errors.full_messages.join(", ")
end
def update_owner!
return if @owner_attrs.blank?
owner = @club.owner
raise Error, I18n.t("admin.clubs.edit.errors.owner_missing") if owner.blank?
update_user!(owner, @owner_attrs)
end
def update_staff!
return if @staff_attrs.blank?
allowed_ids = staff_users.index_by(&:id)
@staff_attrs.each do |user_id, attrs|
user = allowed_ids[user_id.to_s] || allowed_ids[user_id]
next unless user
update_user!(user, attrs)
end
end
def update_invitations!
return if @invitation_attrs.blank?
pending = TeamInvitation.pending.where(team_id: @club.teams.select(:id)).index_by { |inv| inv.id.to_s }
@invitation_attrs.each do |invitation_id, attrs|
invitation = pending[invitation_id.to_s]
next unless invitation
email = attrs[:email].to_s.strip.presence
next if email.blank?
invitation.update!(email: email)
rescue ActiveRecord::RecordInvalid => e
raise Error, e.record.errors.full_messages.join(", ")
end
end
def update_teams!
return if @team_attrs.blank?
teams = @club.teams.index_by { |t| t.id.to_s }
@team_attrs.each do |team_id, attrs|
team = teams[team_id.to_s]
next unless team
updates = {}
updates[:name] = attrs[:name].to_s.strip if attrs.key?(:name) && attrs[:name].present?
if attrs[:sport].present?
updates[:sport] = Sports::Catalog.normalize_key(attrs[:sport])
end
next if updates.empty?
team.update!(updates)
rescue ActiveRecord::RecordInvalid => e
raise Error, e.record.errors.full_messages.join(", ")
end
end
def update_user!(user, attrs)
updates = {}
updates[:name] = attrs[:name].to_s.strip if attrs.key?(:name) && attrs[:name].present?
if attrs.key?(:email)
email = attrs[:email].to_s.strip.presence
updates[:email] = email if email.present?
end
return if updates.empty?
user.update!(updates)
rescue ActiveRecord::RecordInvalid => e
raise Error, e.record.errors.full_messages.join(", ")
rescue ActiveRecord::RecordNotUnique
raise Error, I18n.t("admin.clubs.edit.errors.email_taken", email: updates[:email])
end
def staff_users
User.joins(:user_teams).where(user_teams: { team_id: @club.teams.select(:id) }).distinct.to_a
end
def normalize_hex(value, fallback)
raw = value.to_s.strip
return fallback if raw.blank?
return raw.downcase if raw.match?(/\A#[0-9a-fA-F]{6}\z/)
return "##{raw.downcase}" if raw.match?(/\A[0-9a-fA-F]{6}\z/)
fallback
end
end
end
@@ -50,14 +50,6 @@
@filters[:device] @filters[:device]
) %> ) %>
</label> </label>
<label class="admin-filter-field">
<span><%= t("admin.analytics.filters.chart_path") %></span>
<%= select_tag :chart_path,
options_for_select(
[[t("admin.analytics.filters.chart_path_all"), ""]] + @chart_path_options.map { |p| [p, p] },
@filters[:chart_path]
) %>
</label>
</div> </div>
<div class="admin-filter-actions"> <div class="admin-filter-actions">
<%= submit_tag t("admin.analytics.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %> <%= submit_tag t("admin.analytics.filters.apply"), class: "admin-btn admin-btn--primary admin-btn--sm" %>
@@ -66,66 +58,8 @@
<% end %> <% end %>
</div> </div>
<% if @trend.any? && @trend_totals[:pageviews].positive? %>
<section class="panel admin-analytics-trend" id="admin-analytics-trend">
<div class="admin-analytics-trend__head">
<div>
<h3><%= t("admin.analytics.index.trend_title") %></h3>
<p class="muted admin-table-sub">
<% if @filters[:chart_path].present? %>
<%= t("admin.analytics.index.trend_lead_path", path: @filters[:chart_path]) %>
<% else %>
<%= t("admin.analytics.index.trend_lead") %>
<% end %>
</p>
</div>
<button type="button"
id="admin-analytics-trend-toggle"
class="admin-btn admin-btn--outline admin-btn--sm"
aria-controls="admin-analytics-trend-body"
aria-expanded="true"><%= t("admin.analytics.index.hide_chart") %></button>
</div>
<div id="admin-analytics-trend-body">
<div class="kpi-grid admin-analytics-trend__kpi">
<div class="kpi">
<div class="kpi-label">
<% if @filters[:chart_path].present? %>
<%= t("admin.analytics.index.trend_total_label_path") %>
<% else %>
<%= t("admin.analytics.index.trend_total_label") %>
<% end %>
</div>
<div class="kpi-value"><%= @trend_totals[:pageviews] %></div>
<div class="kpi-sub muted">
<% if @filters[:chart_path].present? %>
<code class="admin-mono"><%= @filters[:chart_path] %></code>
·
<% end %>
<%= t("admin.analytics.index.trend_total_hint", from: l(@filters[:from]), to: l(@filters[:to])) %>
</div>
</div>
</div>
<div class="chart-wrap chart-wrap--analytics">
<canvas id="chart-analytics-trend" aria-label="<%= t("admin.analytics.index.trend_title") %>"></canvas>
</div>
</div>
</section>
<script>
window.adminAnalyticsTrend = <%= raw @trend.to_json %>;
window.adminAnalyticsI18n = {
pageviews: <%= raw t("admin.analytics.index.table.pageviews").to_json %>,
hideChart: <%= raw t("admin.analytics.index.hide_chart").to_json %>,
showChart: <%= raw t("admin.analytics.index.show_chart").to_json %>
};
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" crossorigin="anonymous"></script>
<script src="/admin-analytics.js?v=3" defer></script>
<% end %>
<div class="panel"> <div class="panel">
<% if @pages.any? %> <% if @pages.any? %>
<h3 class="admin-analytics-pages-title"><%= t("admin.analytics.index.pages_title") %></h3>
<p class="muted admin-table-sub"><%= t("admin.analytics.index.pages_lead") %></p>
<div class="admin-table-wrap"> <div class="admin-table-wrap">
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
@@ -142,18 +76,14 @@
<tbody> <tbody>
<% @pages.each do |row| %> <% @pages.each do |row| %>
<% avg = row[:scroll_samples].positive? ? (row[:scroll_sum].to_f / row[:scroll_samples]).round : 0 %> <% avg = row[:scroll_samples].positive? ? (row[:scroll_sum].to_f / row[:scroll_samples]).round : 0 %>
<% chart_params = { from: @filters[:from], to: @filters[:to], chart_path: row[:page_path] } %> <tr>
<% chart_params[:device] = @filters[:device] if @filters[:device].present? %>
<tr class="<%= "is-chart-focus" if @filters[:chart_path] == row[:page_path] %>">
<td><code class="admin-mono"><%= row[:page_path] %></code></td> <td><code class="admin-mono"><%= row[:page_path] %></code></td>
<td><%= row[:pageviews] %></td> <td><%= row[:pageviews] %></td>
<td><%= row[:moves] %></td> <td><%= row[:moves] %></td>
<td><%= row[:clicks] %></td> <td><%= row[:clicks] %></td>
<td class="muted"><%= avg %>%</td> <td class="muted"><%= avg %>%</td>
<td class="muted"><%= row[:max_scroll] %>%</td> <td class="muted"><%= row[:max_scroll] %>%</td>
<td class="admin-analytics-row-actions"> <td>
<%= link_to t("admin.analytics.index.chart_for_path"), admin_analytics_path(chart_params) %>
·
<% heatmap_params = { page_path: row[:page_path], from: @filters[:from], to: @filters[:to] } %> <% heatmap_params = { page_path: row[:page_path], from: @filters[:from], to: @filters[:to] } %>
<% heatmap_params[:device] = @filters[:device] if @filters[:device].present? %> <% heatmap_params[:device] = @filters[:device] if @filters[:device].present? %>
<%= link_to t("admin.analytics.index.heatmap"), admin_analytics_page_path(heatmap_params) %> <%= link_to t("admin.analytics.index.heatmap"), admin_analytics_page_path(heatmap_params) %>
@@ -1,46 +0,0 @@
<% owner = club.owner %>
<% status = club.billing_profile_admin_status %>
<% lines = club.billing_profile_invoice_lines.select { |_label, value| value.present? } %>
<section class="panel admin-club-profile" style="margin:16px 0 24px">
<h3 style="font-size:1rem;margin:0 0 12px"><%= t("admin.clubs.show.profile_title") %></h3>
<h4 style="font-size:0.9rem;margin:0 0 8px;color:var(--muted)"><%= t("admin.clubs.show.owner_title") %></h4>
<% if owner %>
<dl class="billing-profile-dl" style="margin-bottom:16px">
<dt><%= t("admin.clubs.show.owner_name") %></dt>
<dd><%= owner.name %></dd>
<dt><%= t("admin.clubs.show.owner_email") %></dt>
<dd><%= mail_to owner.email %></dd>
</dl>
<% else %>
<p class="muted" style="margin:0 0 16px"><%= t("admin.clubs.show.owner_none") %></p>
<% end %>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px">
<h4 style="font-size:0.9rem;margin:0;color:var(--muted)"><%= t("admin.clubs.show.billing_title") %></h4>
<span class="admin-billing-status admin-billing-status--<%= status %>">
<%= t("admin.clubs.billing_status.#{status}") %>
</span>
<%= link_to t("admin.clubs.show.edit_link"), edit_admin_club_path(club), class: "admin-btn admin-btn--sm admin-btn--secondary" %>
</div>
<% if status == :absent || lines.empty? %>
<p class="muted" style="margin:0"><%= t("admin.clubs.show.billing_none") %></p>
<% else %>
<dl class="billing-profile-dl">
<% lines.each do |label, value| %>
<dt><%= label %></dt>
<dd><%= value %></dd>
<% end %>
</dl>
<% end %>
<% if status == :incomplete %>
<ul class="muted" style="margin:12px 0 0;padding-left:1.2rem;font-size:0.88rem">
<% club.billing_profile_errors.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
</section>
-139
View File
@@ -1,139 +0,0 @@
<p style="margin-bottom:16px">
<%= link_to t("admin.clubs.edit.back"), admin_club_path(@club) %>
</p>
<h2><%= t("admin.clubs.edit.title", club: @club.name) %></h2>
<p class="muted" style="margin-bottom:20px"><%= t("admin.clubs.edit.lead") %></p>
<% if flash.now[:alert].present? %>
<p class="admin-flash" style="background:#3d1b1b;border-color:#c62828"><%= flash.now[:alert] %></p>
<% end %>
<%= form_with url: admin_club_path(@club), method: :patch, local: true, class: "admin-form admin-form--wide" do %>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.club") %></h3>
<div class="admin-form-row">
<div>
<label for="club_name"><%= t("admin.clubs.edit.fields.name") %></label>
<input type="text" name="club[name]" id="club_name" value="<%= @club.name %>" required>
</div>
<div>
<label for="club_sport"><%= t("admin.clubs.edit.fields.sport") %></label>
<select name="club[sport]" id="club_sport" required>
<% @sport_options.each do |label, key| %>
<option value="<%= key %>" <%= "selected" if @club.sport == key %>><%= label %></option>
<% end %>
</select>
</div>
</div>
<div class="admin-form-row">
<div>
<label for="club_primary_color"><%= t("admin.clubs.edit.fields.primary_color") %></label>
<input type="text" name="club[primary_color]" id="club_primary_color" value="<%= @club.primary_color %>" maxlength="7">
</div>
<div>
<label for="club_secondary_color"><%= t("admin.clubs.edit.fields.secondary_color") %></label>
<input type="text" name="club[secondary_color]" id="club_secondary_color" value="<%= @club.secondary_color %>" maxlength="7">
</div>
</div>
<div>
<label for="club_logo_url"><%= t("admin.clubs.edit.fields.logo_url") %></label>
<input type="url" name="club[logo_url]" id="club_logo_url" value="<%= @club.logo_url %>">
</div>
</section>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.billing") %></h3>
<p class="muted admin-form-hint"><%= t("admin.clubs.edit.billing_hint") %></p>
<%= render "shared/billing_profile_fields", record: @club, show_legend: false %>
</section>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.owner") %></h3>
<% if @owner %>
<div class="admin-form-row">
<div>
<label for="owner_name"><%= t("admin.clubs.edit.fields.owner_name") %></label>
<input type="text" name="owner[name]" id="owner_name" value="<%= @owner.name %>" required>
</div>
<div>
<label for="owner_email"><%= t("admin.clubs.edit.fields.owner_email") %></label>
<input type="email" name="owner[email]" id="owner_email" value="<%= @owner.email %>" required>
</div>
</div>
<% else %>
<p class="muted"><%= t("admin.clubs.show.owner_none") %></p>
<% end %>
</section>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.staff") %></h3>
<% if @staff_users.any? %>
<p class="muted admin-form-hint"><%= t("admin.clubs.edit.staff_hint") %></p>
<% @staff_users.each do |user| %>
<div class="admin-form-row" style="margin-bottom:0.75rem">
<div>
<label><%= t("admin.clubs.edit.fields.staff_name") %></label>
<input type="text" name="staff_users[<%= user.id %>][name]" value="<%= user.name %>">
</div>
<div>
<label><%= t("admin.clubs.edit.fields.staff_email") %></label>
<input type="email" name="staff_users[<%= user.id %>][email]" value="<%= user.email %>">
</div>
</div>
<% end %>
<% else %>
<p class="muted"><%= t("admin.clubs.edit.staff_none") %></p>
<% end %>
</section>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.invitations") %></h3>
<% if @pending_invitations.any? %>
<p class="muted admin-form-hint"><%= t("admin.clubs.edit.invitations_hint") %></p>
<% @pending_invitations.each do |invitation| %>
<div class="admin-form-row" style="margin-bottom:0.75rem">
<div>
<label><%= t("admin.clubs.edit.fields.invitation_team") %></label>
<input type="text" value="<%= invitation.team.name %>" disabled>
</div>
<div>
<label><%= t("admin.clubs.edit.fields.invitation_email") %></label>
<input type="email" name="invitations[<%= invitation.id %>][email]" value="<%= invitation.email %>" required>
</div>
</div>
<% end %>
<% else %>
<p class="muted"><%= t("admin.clubs.edit.invitations_none") %></p>
<% end %>
</section>
<section class="panel">
<h3><%= t("admin.clubs.edit.sections.teams") %></h3>
<% if @teams.any? %>
<% @teams.each do |team| %>
<div class="admin-form-row" style="margin-bottom:0.75rem">
<div>
<label><%= t("admin.clubs.edit.fields.team_name") %></label>
<input type="text" name="teams[<%= team.id %>][name]" value="<%= team.name %>" required>
</div>
<div>
<label><%= t("admin.clubs.edit.fields.team_sport") %></label>
<select name="teams[<%= team.id %>][sport]">
<% @sport_options.each do |label, key| %>
<option value="<%= key %>" <%= "selected" if team.sport == key %>><%= label %></option>
<% end %>
</select>
</div>
</div>
<% end %>
<% else %>
<p class="muted"><%= t("admin.clubs.show.no_teams") %></p>
<% end %>
</section>
<div class="admin-form-actions">
<%= link_to t("admin.clubs.edit.cancel"), admin_club_path(@club), class: "admin-btn admin-btn--secondary" %>
<button type="submit" class="admin-btn admin-btn--primary"><%= t("admin.clubs.edit.submit") %></button>
</div>
<% end %>
@@ -9,7 +9,6 @@
<th><%= t("admin.clubs.index.table.club") %></th> <th><%= t("admin.clubs.index.table.club") %></th>
<th><%= t("admin.clubs.index.table.plan") %></th> <th><%= t("admin.clubs.index.table.plan") %></th>
<th><%= t("admin.clubs.index.table.teams") %></th> <th><%= t("admin.clubs.index.table.teams") %></th>
<th><%= t("admin.clubs.index.table.billing_profile") %></th>
<th><%= t("admin.clubs.index.table.comped") %></th> <th><%= t("admin.clubs.index.table.comped") %></th>
<th><%= t("admin.clubs.index.table.stripe") %></th> <th><%= t("admin.clubs.index.table.stripe") %></th>
<th><%= t("admin.clubs.index.table.quote") %></th> <th><%= t("admin.clubs.index.table.quote") %></th>
@@ -19,16 +18,10 @@
<tbody> <tbody>
<% @clubs.each do |club| %> <% @clubs.each do |club| %>
<% sub = club.subscription %> <% sub = club.subscription %>
<% billing_status = club.billing_profile_admin_status %>
<tr> <tr>
<td><strong><%= club.name %></strong></td> <td><strong><%= club.name %></strong></td>
<td><%= sub&.plan&.name || t("admin.common.free_plan") %></td> <td><%= sub&.plan&.name || t("admin.common.free_plan") %></td>
<td><%= club.teams.size %></td> <td><%= club.teams.size %></td>
<td>
<span class="admin-billing-status admin-billing-status--<%= billing_status %>">
<%= t("admin.clubs.billing_status.#{billing_status}") %>
</span>
</td>
<td> <td>
<% if sub&.admin_comped? %> <% if sub&.admin_comped? %>
<span style="color:#ffb74d"><%= t("admin.common.yes") %></span> <span style="color:#ffb74d"><%= t("admin.common.yes") %></span>
@@ -6,12 +6,8 @@
· <%= link_to t("admin.clubs.show.recordings_archive"), admin_club_recordings_path(@club) %> · <%= link_to t("admin.clubs.show.recordings_archive"), admin_club_recordings_path(@club) %>
· <%= link_to t("admin.clubs.show.billing_link"), admin_billing_path(club_id: @club.id) %> · <%= link_to t("admin.clubs.show.billing_link"), admin_billing_path(club_id: @club.id) %>
· <%= link_to t("admin.clubs.show.youtube_platform_link"), admin_youtube_platform_path %> · <%= link_to t("admin.clubs.show.youtube_platform_link"), admin_youtube_platform_path %>
· <%= link_to t("admin.clubs.show.edit_link"), edit_admin_club_path(@club) %>
</p> </p>
<%= render "admin/clubs/billing_profile", club: @club %>
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %> <%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
<%= render "admin/clubs/quote_form", club: @club, quote: @quote, return_to: admin_club_path(@club) %> <%= render "admin/clubs/quote_form", club: @club, quote: @quote, return_to: admin_club_path(@club) %>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<link rel="stylesheet" href="/admin.css?v=21"> <link rel="stylesheet" href="/admin.css?v=16">
<%= yield :head %> <%= yield :head %>
<% if content_for?(:replay_archive_styles) %> <% if content_for?(:replay_archive_styles) %>
<link rel="stylesheet" href="/marketing.css?v=42"> <link rel="stylesheet" href="/marketing.css?v=42">
-63
View File
@@ -48,7 +48,6 @@ de:
invoice_issued: "Rechnung %{number} ausgestellt und an %{email} gesendet." invoice_issued: "Rechnung %{number} ausgestellt und an %{email} gesendet."
invoice_updated: "Rechnung %{number} aktualisiert." invoice_updated: "Rechnung %{number} aktualisiert."
invoice_uploaded: "Rechnung hochgeladen und an %{email} gesendet." invoice_uploaded: "Rechnung hochgeladen und an %{email} gesendet."
club_updated: "Daten für %{club} aktualisiert."
youtube_not_configured: Konfiguriere YOUTUBE_CLIENT_ID und YOUTUBE_CLIENT_SECRET in .env youtube_not_configured: Konfiguriere YOUTUBE_CLIENT_ID und YOUTUBE_CLIENT_SECRET in .env
announcement_created: Hinweis gespeichert announcement_created: Hinweis gespeichert
announcement_updated: Hinweis aktualisiert announcement_updated: Hinweis aktualisiert
@@ -240,10 +239,8 @@ de:
club: Verein club: Verein
plan: Plan plan: Plan
teams: Teams teams: Teams
billing_profile: Rechnungsdaten
comped: Kostenlos comped: Kostenlos
stripe: Stripe stripe: Stripe
quote: Vereinbart
manage: Verwalten manage: Verwalten
invoices: Rechnungen invoices: Rechnungen
show: show:
@@ -257,13 +254,6 @@ de:
youtube_premium_full_not_connected: Premium Full — Vereinskanal nicht verbunden (nutzt Match Live TV) youtube_premium_full_not_connected: Premium Full — Vereinskanal nicht verbunden (nutzt Match Live TV)
no_channel_hint_html: "Ohne verbundenen Kanal nutzt die App den Match-Live-TV-Kanal. %{link}." no_channel_hint_html: "Ohne verbundenen Kanal nutzt die App den Match-Live-TV-Kanal. %{link}."
link_channel: Kanal verbinden (Vereinsseite) link_channel: Kanal verbinden (Vereinsseite)
profile_title: Kontaktdaten & Rechnungsdaten
owner_title: Kontoinhaber
owner_name: Name
owner_email: E-Mail
owner_none: Kein Inhaber zugeordnet.
billing_title: Rechnungsdaten
billing_none: Keine Rechnungsdaten hinterlegt.
teams_title: Teams teams_title: Teams
no_teams: Keine Teams registriert. no_teams: Keine Teams registriert.
table: table:
@@ -275,46 +265,6 @@ de:
concurrency_lead: Dasselbe Konto hat versucht, eine weitere Direktübertragung zu starten, während bereits eine lief. concurrency_lead: Dasselbe Konto hat versucht, eine weitere Direktübertragung zu starten, während bereits eine lief.
concurrency_none: Keine Versuche für diesen Verein erfasst. concurrency_none: Keine Versuche für diesen Verein erfasst.
concurrency_all: Alle Konto-Missbräuche ansehen concurrency_all: Alle Konto-Missbräuche ansehen
edit_link: Daten bearbeiten
edit:
back: "← Zurück zum Verein"
title: "%{club} bearbeiten"
lead: "Kundendaten überschreiben: Profil, Abrechnung, Inhaber, Staff, Einladungen und Teams."
cancel: Abbrechen
submit: Änderungen speichern
billing_hint: Du kannst ein unvollständiges Profil speichern; der Badge auf der Vereinsseite zeigt den Status.
staff_hint: Bereits mit den Teams des Vereins verknüpfte Benutzer.
staff_none: Kein Staff mit Teams verknüpft.
invitations_hint: Nur noch ausstehende Einladungen.
invitations_none: Keine ausstehenden Einladungen.
sections:
club: Vereinsprofil
billing: Rechnungsdaten
owner: Kontoinhaber
staff: Angenommenes Staff
invitations: Ausstehende Einladungen
teams: Teams
fields:
name: Vereinsname
sport: Sportart
primary_color: Primärfarbe
secondary_color: Sekundärfarbe
logo_url: Logo-URL
owner_name: Name des Inhabers
owner_email: E-Mail des Inhabers
staff_name: Name
staff_email: E-Mail
invitation_team: Team
invitation_email: Einladungs-E-Mail
team_name: Teamname
team_sport: Teamsport
errors:
owner_missing: Kein Inhaber mit diesem Verein verknüpft.
email_taken: "E-Mail bereits vergeben: %{email}"
billing_status:
complete: Vollständig
incomplete: Unvollständig
absent: Fehlend
comped: comped:
title: Kostenloses Abonnement title: Kostenloses Abonnement
description: "Sponsor oder Aktion: Vergib Premium Light/Full ohne Stripe-Zahlung. Jederzeit widerrufbar." description: "Sponsor oder Aktion: Vergib Premium Light/Full ohne Stripe-Zahlung. Jederzeit widerrufbar."
@@ -465,8 +415,6 @@ de:
any: Alle any: Alle
apply: Filtern apply: Filtern
reset: Zurücksetzen reset: Zurücksetzen
chart_path: Seite (Diagramm)
chart_path_all: Alle Seiten
layer: Ebene layer: Ebene
layer_move: Mausbewegungen layer_move: Mausbewegungen
layer_click: Klicks layer_click: Klicks
@@ -479,17 +427,6 @@ de:
lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten. lead: Aggregierte First-Party-Heatmaps (Bewegung/Klick) und Scrolltiefe, nur mit Statistik-Einwilligung. Keine personenbezogenen Daten.
none: Keine Daten im gewählten Zeitraum. none: Keine Daten im gewählten Zeitraum.
heatmap: Heatmap heatmap: Heatmap
chart_for_path: Diagramm
trend_title: Pageview-Verlauf
trend_lead: Gesamte Pageviews aller Seiten, Tag für Tag. Dieselben Filter Von / Bis / Gerät. Seite im Filter wählen oder «Diagramm» in der Tabelle.
trend_lead_path: "Tägliche Pageviews nur für %{path} (gleiche Filter Von / Bis / Gerät)."
trend_total_label: Pageviews gesamt (alle Seiten)
trend_total_label_path: Pageviews der gewählten Seite
trend_total_hint: "%{from} → %{to}"
hide_chart: Diagramm ausblenden
show_chart: Diagramm anzeigen
pages_title: Aufschlüsselung nach Seite
pages_lead: Pageviews und Engagement im gefilterten Zeitraum, nach Pfad. «Diagramm» zeigt den Verlauf einer Seite.
table: table:
path: Seite path: Seite
pageviews: Pageviews pageviews: Pageviews
-63
View File
@@ -48,7 +48,6 @@ en:
invoice_issued: "Invoice %{number} issued and sent to %{email}." invoice_issued: "Invoice %{number} issued and sent to %{email}."
invoice_updated: "Invoice %{number} updated." invoice_updated: "Invoice %{number} updated."
invoice_uploaded: "Invoice uploaded and sent to %{email}." invoice_uploaded: "Invoice uploaded and sent to %{email}."
club_updated: "Data updated for %{club}."
youtube_not_configured: Configure YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET in .env youtube_not_configured: Configure YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET in .env
announcement_created: Notice saved announcement_created: Notice saved
announcement_updated: Notice updated announcement_updated: Notice updated
@@ -240,10 +239,8 @@ en:
club: Club club: Club
plan: Plan plan: Plan
teams: Teams teams: Teams
billing_profile: Billing data
comped: Comp comped: Comp
stripe: Stripe stripe: Stripe
quote: Quoted
manage: Manage manage: Manage
invoices: Invoices invoices: Invoices
show: show:
@@ -257,13 +254,6 @@ en:
youtube_premium_full_not_connected: Premium Full — club channel not connected (using Match Live TV) youtube_premium_full_not_connected: Premium Full — club channel not connected (using Match Live TV)
no_channel_hint_html: "Without a connected channel, the app uses the Match Live TV channel. %{link}." no_channel_hint_html: "Without a connected channel, the app uses the Match Live TV channel. %{link}."
link_channel: Connect channel (club page) link_channel: Connect channel (club page)
profile_title: Contact & billing details
owner_title: Account owner
owner_name: Name
owner_email: Email
owner_none: No owner associated.
billing_title: Billing details
billing_none: No billing details provided.
teams_title: Teams teams_title: Teams
no_teams: No teams registered. no_teams: No teams registered.
table: table:
@@ -275,46 +265,6 @@ en:
concurrency_lead: Same account tried to start another live while one was already running. concurrency_lead: Same account tried to start another live while one was already running.
concurrency_none: No attempts recorded for this club. concurrency_none: No attempts recorded for this club.
concurrency_all: View all account abuse concurrency_all: View all account abuse
edit_link: Edit data
edit:
back: "← Back to club"
title: "Edit %{club}"
lead: "Override client-entered data: profile, billing, owner, staff, invitations and teams."
cancel: Cancel
submit: Save changes
billing_hint: You can save an incomplete profile; the badge on the club page will reflect the status.
staff_hint: Users already linked to this club's teams (including owners if also staff).
staff_none: No staff linked to teams.
invitations_hint: Only invitations still awaiting acceptance.
invitations_none: No pending invitations.
sections:
club: Club profile
billing: Billing details
owner: Account owner
staff: Accepted staff
invitations: Pending invitations
teams: Teams
fields:
name: Club name
sport: Sport
primary_color: Primary colour
secondary_color: Secondary colour
logo_url: Logo URL
owner_name: Owner name
owner_email: Owner email
staff_name: Name
staff_email: Email
invitation_team: Team
invitation_email: Invitation email
team_name: Team name
team_sport: Team sport
errors:
owner_missing: No owner associated with this club.
email_taken: "Email already in use: %{email}"
billing_status:
complete: Complete
incomplete: Incomplete
absent: Missing
comped: comped:
title: Complimentary subscription title: Complimentary subscription
description: "Sponsor or promotion: grant Premium Light/Full without a Stripe payment. Revocable at any time." description: "Sponsor or promotion: grant Premium Light/Full without a Stripe payment. Revocable at any time."
@@ -465,8 +415,6 @@ en:
any: All any: All
apply: Filter apply: Filter
reset: Reset reset: Reset
chart_path: Page (chart)
chart_path_all: All pages
layer: Layer layer: Layer
layer_move: Mouse moves layer_move: Mouse moves
layer_click: Clicks layer_click: Clicks
@@ -479,17 +427,6 @@ en:
lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data. lead: Aggregated first-party move/click heatmaps and scroll depth, only with analytics consent. No personal data.
none: No data in the selected period. none: No data in the selected period.
heatmap: Heatmap heatmap: Heatmap
chart_for_path: Chart
trend_title: Pageview trend
trend_lead: Total pageviews across all pages, day by day. Uses the same From / To / Device filters. Pick a page in the filter or click «Chart» in the table.
trend_lead_path: "Day-by-day pageviews for %{path} only (same From / To / Device filters)."
trend_total_label: Total pageviews (all pages)
trend_total_label_path: Pageviews for selected page
trend_total_hint: "%{from} → %{to}"
hide_chart: Hide chart
show_chart: Show chart
pages_title: Breakdown by page
pages_lead: Pageviews and engagement for the filtered period, split by path. Use «Chart» to see one page over time.
table: table:
path: Page path: Page
pageviews: Pageviews pageviews: Pageviews
-63
View File
@@ -48,7 +48,6 @@ es:
invoice_issued: "Factura %{number} emitida y enviada a %{email}." invoice_issued: "Factura %{number} emitida y enviada a %{email}."
invoice_updated: "Factura %{number} actualizada." invoice_updated: "Factura %{number} actualizada."
invoice_uploaded: "Factura subida y enviada a %{email}." invoice_uploaded: "Factura subida y enviada a %{email}."
club_updated: "Datos actualizados para %{club}."
youtube_not_configured: Configura YOUTUBE_CLIENT_ID y YOUTUBE_CLIENT_SECRET en .env youtube_not_configured: Configura YOUTUBE_CLIENT_ID y YOUTUBE_CLIENT_SECRET en .env
announcement_created: Aviso guardado announcement_created: Aviso guardado
announcement_updated: Aviso actualizado announcement_updated: Aviso actualizado
@@ -240,10 +239,8 @@ es:
club: Club club: Club
plan: Plan plan: Plan
teams: Equipos teams: Equipos
billing_profile: Datos fiscales
comped: Cortesía comped: Cortesía
stripe: Stripe stripe: Stripe
quote: Acordado
manage: Gestionar manage: Gestionar
invoices: Facturas invoices: Facturas
show: show:
@@ -257,13 +254,6 @@ es:
youtube_premium_full_not_connected: Premium Full — canal del club no conectado (usa Match Live TV) youtube_premium_full_not_connected: Premium Full — canal del club no conectado (usa Match Live TV)
no_channel_hint_html: "Sin un canal conectado, la app usa el canal de Match Live TV. %{link}." no_channel_hint_html: "Sin un canal conectado, la app usa el canal de Match Live TV. %{link}."
link_channel: Conectar canal (página del club) link_channel: Conectar canal (página del club)
profile_title: Contacto y datos fiscales
owner_title: Titular de la cuenta
owner_name: Nombre
owner_email: Correo
owner_none: No hay titular asociado.
billing_title: Datos de facturación
billing_none: No hay datos de facturación.
teams_title: Equipos teams_title: Equipos
no_teams: No hay equipos registrados. no_teams: No hay equipos registrados.
table: table:
@@ -275,46 +265,6 @@ es:
concurrency_lead: La misma cuenta intentó iniciar otro directo mientras ya había uno en curso. concurrency_lead: La misma cuenta intentó iniciar otro directo mientras ya había uno en curso.
concurrency_none: No hay intentos registrados para este club. concurrency_none: No hay intentos registrados para este club.
concurrency_all: Ver todos los abusos de cuenta concurrency_all: Ver todos los abusos de cuenta
edit_link: Editar datos
edit:
back: "← Volver al club"
title: "Editar %{club}"
lead: "Fuerza los datos introducidos por el cliente: perfil, facturación, titular, staff, invitaciones y equipos."
cancel: Cancelar
submit: Guardar cambios
billing_hint: Puedes guardar un perfil incompleto; el badge de la ficha reflejará el estado.
staff_hint: Usuarios ya vinculados a los equipos del club.
staff_none: No hay staff vinculado a los equipos.
invitations_hint: Solo invitaciones pendientes de aceptación.
invitations_none: No hay invitaciones pendientes.
sections:
club: Perfil del club
billing: Datos de facturación
owner: Titular de la cuenta
staff: Staff aceptado
invitations: Invitaciones pendientes
teams: Equipos
fields:
name: Nombre del club
sport: Deporte
primary_color: Color primario
secondary_color: Color secundario
logo_url: URL del logo
owner_name: Nombre del titular
owner_email: Correo del titular
staff_name: Nombre
staff_email: Correo
invitation_team: Equipo
invitation_email: Correo de invitación
team_name: Nombre del equipo
team_sport: Deporte del equipo
errors:
owner_missing: No hay titular asociado a este club.
email_taken: "Correo ya en uso: %{email}"
billing_status:
complete: Completo
incomplete: Incompleto
absent: Ausentes
comped: comped:
title: Suscripción de cortesía title: Suscripción de cortesía
description: "Patrocinador o promoción: concede Premium Light/Full sin pago en Stripe. Revocable en cualquier momento." description: "Patrocinador o promoción: concede Premium Light/Full sin pago en Stripe. Revocable en cualquier momento."
@@ -465,8 +415,6 @@ es:
any: Todos any: Todos
apply: Filtrar apply: Filtrar
reset: Restablecer reset: Restablecer
chart_path: Página (gráfico)
chart_path_all: Todas las páginas
layer: Capa layer: Capa
layer_move: Movimientos del ratón layer_move: Movimientos del ratón
layer_click: Clics layer_click: Clics
@@ -479,17 +427,6 @@ es:
lead: Heatmaps de movimientos/clics y scroll agregados (first-party), solo con consentimiento estadístico. Sin datos personales. lead: Heatmaps de movimientos/clics y scroll agregados (first-party), solo con consentimiento estadístico. Sin datos personales.
none: No hay datos en el periodo seleccionado. none: No hay datos en el periodo seleccionado.
heatmap: Heatmap heatmap: Heatmap
chart_for_path: Gráfico
trend_title: Evolución de pageviews
trend_lead: Total de pageviews de todas las páginas, día a día. Usa los mismos filtros Desde / Hasta / Device. Elige una página en el filtro o pulsa «Gráfico» en la tabla.
trend_lead_path: "Pageviews día a día solo para %{path} (mismos filtros Desde / Hasta / Device)."
trend_total_label: Pageviews totales (todas las páginas)
trend_total_label_path: Pageviews de la página seleccionada
trend_total_hint: "%{from} → %{to}"
hide_chart: Ocultar gráfico
show_chart: Mostrar gráfico
pages_title: Detalle por página
pages_lead: Pageviews y engagement en el periodo filtrado, por path. Usa «Gráfico» para ver una sola página en el tiempo.
table: table:
path: Página path: Página
pageviews: Pageviews pageviews: Pageviews
-63
View File
@@ -48,7 +48,6 @@ fr:
invoice_issued: "Facture %{number} émise et envoyée à %{email}." invoice_issued: "Facture %{number} émise et envoyée à %{email}."
invoice_updated: "Facture %{number} mise à jour." invoice_updated: "Facture %{number} mise à jour."
invoice_uploaded: "Facture chargée et envoyée à %{email}." invoice_uploaded: "Facture chargée et envoyée à %{email}."
club_updated: "Données mises à jour pour %{club}."
youtube_not_configured: Configurez YOUTUBE_CLIENT_ID et YOUTUBE_CLIENT_SECRET dans .env youtube_not_configured: Configurez YOUTUBE_CLIENT_ID et YOUTUBE_CLIENT_SECRET dans .env
announcement_created: Alerte enregistrée announcement_created: Alerte enregistrée
announcement_updated: Alerte mise à jour announcement_updated: Alerte mise à jour
@@ -240,10 +239,8 @@ fr:
club: Club club: Club
plan: Forfait plan: Forfait
teams: Équipes teams: Équipes
billing_profile: Données fiscales
comped: Offert comped: Offert
stripe: Stripe stripe: Stripe
quote: Convenu
manage: Gérer manage: Gérer
invoices: Factures invoices: Factures
show: show:
@@ -257,13 +254,6 @@ fr:
youtube_premium_full_not_connected: Premium Full — chaîne du club non connectée (utilise Match Live TV) youtube_premium_full_not_connected: Premium Full — chaîne du club non connectée (utilise Match Live TV)
no_channel_hint_html: "Sans chaîne connectée, l'application utilise la chaîne Match Live TV. %{link}." no_channel_hint_html: "Sans chaîne connectée, l'application utilise la chaîne Match Live TV. %{link}."
link_channel: Connecter la chaîne (page du club) link_channel: Connecter la chaîne (page du club)
profile_title: Coordonnées et données fiscales
owner_title: Titulaire du compte
owner_name: Nom
owner_email: E-mail
owner_none: Aucun titulaire associé.
billing_title: Données de facturation
billing_none: Aucune donnée de facturation renseignée.
teams_title: Équipes teams_title: Équipes
no_teams: Aucune équipe enregistrée. no_teams: Aucune équipe enregistrée.
table: table:
@@ -275,46 +265,6 @@ fr:
concurrency_lead: Le même compte a tenté de démarrer un autre direct alors quun était déjà en cours. concurrency_lead: Le même compte a tenté de démarrer un autre direct alors quun était déjà en cours.
concurrency_none: Aucune tentative enregistrée pour ce club. concurrency_none: Aucune tentative enregistrée pour ce club.
concurrency_all: Voir tous les abus de compte concurrency_all: Voir tous les abus de compte
edit_link: Modifier les données
edit:
back: "← Retour au club"
title: "Modifier %{club}"
lead: "Forcez les données saisies par le client : profil, facturation, titulaire, staff, invitations et équipes."
cancel: Annuler
submit: Enregistrer
billing_hint: Vous pouvez enregistrer un profil incomplet ; le badge sur la fiche reflète l'état.
staff_hint: Utilisateurs déjà liés aux équipes du club.
staff_none: Aucun staff lié aux équipes.
invitations_hint: Uniquement les invitations encore en attente d'acceptation.
invitations_none: Aucune invitation en attente.
sections:
club: Profil du club
billing: Données de facturation
owner: Titulaire du compte
staff: Staff accepté
invitations: Invitations en attente
teams: Équipes
fields:
name: Nom du club
sport: Sport
primary_color: Couleur primaire
secondary_color: Couleur secondaire
logo_url: URL du logo
owner_name: Nom du titulaire
owner_email: E-mail du titulaire
staff_name: Nom
staff_email: E-mail
invitation_team: Équipe
invitation_email: E-mail d'invitation
team_name: Nom de l'équipe
team_sport: Sport de l'équipe
errors:
owner_missing: Aucun titulaire associé à ce club.
email_taken: "E-mail déjà utilisé : %{email}"
billing_status:
complete: Complet
incomplete: Incomplet
absent: Absentes
comped: comped:
title: Abonnement offert title: Abonnement offert
description: "Sponsor ou promotion : accordez Premium Light/Full sans paiement Stripe. Révocable à tout moment." description: "Sponsor ou promotion : accordez Premium Light/Full sans paiement Stripe. Révocable à tout moment."
@@ -465,8 +415,6 @@ fr:
any: Tous any: Tous
apply: Filtrer apply: Filtrer
reset: Réinitialiser reset: Réinitialiser
chart_path: Page (graphique)
chart_path_all: Toutes les pages
layer: Couche layer: Couche
layer_move: Mouvements souris layer_move: Mouvements souris
layer_click: Clics layer_click: Clics
@@ -479,17 +427,6 @@ fr:
lead: Heatmaps mouvements/clics et scroll agrégés (first-party), uniquement avec consentement statistiques. Aucune donnée personnelle. lead: Heatmaps mouvements/clics et scroll agrégés (first-party), uniquement avec consentement statistiques. Aucune donnée personnelle.
none: Aucune donnée sur la période sélectionnée. none: Aucune donnée sur la période sélectionnée.
heatmap: Heatmap heatmap: Heatmap
chart_for_path: Graphique
trend_title: Évolution des pages vues
trend_lead: Total des pages vues sur toutes les pages, jour par jour. Mêmes filtres De / À / Appareil. Choisissez une page dans le filtre ou cliquez « Graphique » dans le tableau.
trend_lead_path: "Pages vues jour par jour pour %{path} uniquement (mêmes filtres De / À / Appareil)."
trend_total_label: Pages vues totales (toutes les pages)
trend_total_label_path: Pages vues de la page sélectionnée
trend_total_hint: "%{from} → %{to}"
hide_chart: Masquer le graphique
show_chart: Afficher le graphique
pages_title: Détail par page
pages_lead: Pages vues et engagement sur la période filtrée, par chemin. « Graphique » montre l’évolution dune page.
table: table:
path: Page path: Page
pageviews: Pages vues pageviews: Pages vues
-62
View File
@@ -52,7 +52,6 @@ it:
quote_revoked: "Prezzo concordato revocato per %{club}." quote_revoked: "Prezzo concordato revocato per %{club}."
transfer_confirmed: "Bonifico confermato: piano %{plan} attivo per %{club}." transfer_confirmed: "Bonifico confermato: piano %{plan} attivo per %{club}."
transfer_cancelled: Bonifico in attesa annullato. transfer_cancelled: Bonifico in attesa annullato.
club_updated: "Dati aggiornati per %{club}."
youtube_not_configured: Configura YOUTUBE_CLIENT_ID e YOUTUBE_CLIENT_SECRET in .env youtube_not_configured: Configura YOUTUBE_CLIENT_ID e YOUTUBE_CLIENT_SECRET in .env
announcement_created: Avviso salvato announcement_created: Avviso salvato
announcement_updated: Avviso aggiornato announcement_updated: Avviso aggiornato
@@ -244,7 +243,6 @@ it:
club: Società club: Società
plan: Piano plan: Piano
teams: Squadre teams: Squadre
billing_profile: Dati fiscali
comped: Omaggio comped: Omaggio
stripe: Stripe stripe: Stripe
quote: Concordato quote: Concordato
@@ -261,13 +259,6 @@ it:
youtube_premium_full_not_connected: Premium Full — canale società non collegato (usa Match Live TV) youtube_premium_full_not_connected: Premium Full — canale società non collegato (usa Match Live TV)
no_channel_hint_html: "Senza canale collegato, lapp usa il canale Match Live TV. %{link}." no_channel_hint_html: "Senza canale collegato, lapp usa il canale Match Live TV. %{link}."
link_channel: Collega canale (pagina società) link_channel: Collega canale (pagina società)
profile_title: Anagrafica e dati fiscali
owner_title: Titolare account
owner_name: Nome
owner_email: Email
owner_none: Nessun titolare associato.
billing_title: Dati di fatturazione
billing_none: Nessun dato di fatturazione compilato.
teams_title: Squadre teams_title: Squadre
no_teams: Nessuna squadra registrata. no_teams: Nessuna squadra registrata.
table: table:
@@ -279,46 +270,6 @@ it:
concurrency_lead: Stesso account che ha provato ad avviare unaltra diretta mentre ne era già in corso una. concurrency_lead: Stesso account che ha provato ad avviare unaltra diretta mentre ne era già in corso una.
concurrency_none: Nessun tentativo registrato per questa società. concurrency_none: Nessun tentativo registrato per questa società.
concurrency_all: Vedi tutti gli abusi account concurrency_all: Vedi tutti gli abusi account
edit_link: Modifica dati
edit:
back: "← Torna alla società"
title: "Modifica %{club}"
lead: "Forza i dati inseriti dal cliente: anagrafica, fatturazione, titolare, staff, inviti e squadre."
cancel: Annulla
submit: Salva modifiche
billing_hint: Puoi salvare anche un profilo incompleto; il badge in scheda rifletterà lo stato.
staff_hint: Utenti già collegati alle squadre della società (inclusi eventuali titolari).
staff_none: Nessuno staff collegato alle squadre.
invitations_hint: Solo inviti ancora in attesa di accettazione.
invitations_none: Nessun invito in attesa.
sections:
club: Anagrafica società
billing: Dati di fatturazione
owner: Titolare account
staff: Staff e invitati accettati
invitations: Inviti in attesa
teams: Squadre
fields:
name: Nome società
sport: Sport
primary_color: Colore primario
secondary_color: Colore secondario
logo_url: URL logo
owner_name: Nome titolare
owner_email: Email titolare
staff_name: Nome
staff_email: Email
invitation_team: Squadra
invitation_email: Email invito
team_name: Nome squadra
team_sport: Sport squadra
errors:
owner_missing: Nessun titolare associato a questa società.
email_taken: "Email già in uso: %{email}"
billing_status:
complete: Completo
incomplete: Incompleto
absent: Assenti
comped: comped:
title: Abbonamento omaggio title: Abbonamento omaggio
description: "Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento." description: "Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento."
@@ -485,8 +436,6 @@ it:
any: Tutti any: Tutti
apply: Filtra apply: Filtra
reset: Azzera reset: Azzera
chart_path: Pagina (grafico)
chart_path_all: Tutte le pagine
layer: Livello layer: Livello
layer_move: Movimenti mouse layer_move: Movimenti mouse
layer_click: Click layer_click: Click
@@ -499,17 +448,6 @@ it:
lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale. lead: Heatmap movimenti/click e scroll aggregati (first-party), solo con consenso statistico. Nessun dato personale.
none: Nessun dato nel periodo selezionato. none: Nessun dato nel periodo selezionato.
heatmap: Heatmap heatmap: Heatmap
chart_for_path: Grafico
trend_title: Andamento pageview
trend_lead: Totale pageview di tutte le pagine, giorno per giorno. Usa gli stessi filtri Da / A / Device. Scegli una pagina nel filtro oppure clicca «Grafico» in tabella.
trend_lead_path: "Pageview giorno per giorno solo per %{path} (stessi filtri Da / A / Device)."
trend_total_label: Pageview totali (tutte le pagine)
trend_total_label_path: Pageview della pagina selezionata
trend_total_hint: "%{from} → %{to}"
hide_chart: Nascondi grafico
show_chart: Mostra grafico
pages_title: Dettaglio per pagina
pages_lead: Pageview e engagement nel periodo filtrato, spezzati per path. Usa «Grafico» per vedere landamento di una sola pagina.
table: table:
path: Pagina path: Pagina
pageviews: Pageview pageviews: Pageview
+1 -1
View File
@@ -101,7 +101,7 @@ Rails.application.routes.draw do
post "billing/transfers/:id/confirm", to: "billing#confirm_transfer", as: :billing_transfer_confirm post "billing/transfers/:id/confirm", to: "billing#confirm_transfer", as: :billing_transfer_confirm
post "billing/transfers/:id/cancel", to: "billing#cancel_transfer", as: :billing_transfer_cancel post "billing/transfers/:id/cancel", to: "billing#cancel_transfer", as: :billing_transfer_cancel
resources :teams, only: %i[show] resources :teams, only: %i[show]
resources :clubs, only: %i[index show edit update] do resources :clubs, only: %i[index show] do
member do member do
post :grant_comped post :grant_comped
delete :revoke_comped delete :revoke_comped
-89
View File
@@ -1,89 +0,0 @@
(function () {
var STORAGE_KEY = "mltv_admin_analytics_chart_hidden";
var trend = window.adminAnalyticsTrend || [];
var i18n = window.adminAnalyticsI18n || {};
var panel = document.getElementById("admin-analytics-trend");
var canvas = document.getElementById("chart-analytics-trend");
var toggle = document.getElementById("admin-analytics-trend-toggle");
var body = document.getElementById("admin-analytics-trend-body");
function setHidden(hidden) {
if (!panel || !body || !toggle) return;
body.hidden = hidden;
panel.classList.toggle("is-collapsed", hidden);
toggle.setAttribute("aria-expanded", hidden ? "false" : "true");
toggle.textContent = hidden
? (i18n.showChart || "Mostra grafico")
: (i18n.hideChart || "Nascondi grafico");
try {
localStorage.setItem(STORAGE_KEY, hidden ? "1" : "0");
} catch (e) { /* ignore */ }
}
function initiallyHidden() {
try {
return localStorage.getItem(STORAGE_KEY) === "1";
} catch (e) {
return false;
}
}
if (toggle && body) {
setHidden(initiallyHidden());
toggle.addEventListener("click", function () {
setHidden(!body.hidden);
});
}
if (!canvas || !trend.length || typeof Chart === "undefined") return;
function dayLabel(dayStr) {
var parts = dayStr.split("-");
if (parts.length < 3) return dayStr;
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
return d.toLocaleDateString([], { day: "2-digit", month: "short" });
}
new Chart(canvas, {
type: "line",
data: {
labels: trend.map(function (row) { return dayLabel(row.day); }),
datasets: [
{
label: i18n.pageviews || "Pageviews",
data: trend.map(function (r) { return r.pageviews || 0; }),
borderColor: "rgba(229, 57, 53, 0.95)",
backgroundColor: "rgba(229, 57, 53, 0.18)",
fill: true,
tension: 0.25,
pointRadius: 3,
pointHoverRadius: 5
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
animation: { duration: 280 },
scales: {
x: {
ticks: { maxTicksLimit: 14, color: "#9a9aad", font: { size: 10 } },
grid: { color: "rgba(255,255,255,0.06)" }
},
y: {
beginAtZero: true,
ticks: {
color: "#9a9aad",
font: { size: 10 },
precision: 0
},
grid: { color: "rgba(255,255,255,0.06)" }
}
},
plugins: {
legend: { display: false }
}
}
});
})();
-80
View File
@@ -173,44 +173,6 @@ body.admin-body {
height: 200px; height: 200px;
} }
.chart-wrap--analytics {
height: 260px;
margin-top: 0.75rem;
}
.admin-analytics-trend__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.admin-analytics-trend__head h3 {
margin: 0 0 0.35rem;
}
.admin-analytics-trend__kpi {
margin: 0.75rem 0 0;
}
.admin-analytics-trend.is-collapsed .admin-analytics-trend__head {
margin-bottom: 0;
}
.admin-analytics-pages-title {
margin: 0 0 0.35rem;
font-size: 1rem;
}
.admin-table tr.is-chart-focus td {
background: rgba(229, 57, 53, 0.12);
}
.admin-analytics-row-actions {
white-space: nowrap;
}
.admin-panels { .admin-panels {
display: grid; display: grid;
grid-template-columns: 1.4fr 1fr; grid-template-columns: 1.4fr 1fr;
@@ -622,35 +584,6 @@ body.admin-body {
word-break: break-word; word-break: break-word;
} }
.admin-billing-status {
display: inline-block;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
padding: 0.15rem 0.5rem;
border-radius: 999px;
border: 1px solid #3a3a45;
color: #ccc;
}
.admin-billing-status--complete {
border-color: #2e7d32;
color: #a5f0b8;
background: #1b3d1b;
}
.admin-billing-status--incomplete {
border-color: #f9a825;
color: #ffe082;
background: #3d3210;
}
.admin-billing-status--absent {
border-color: #555;
color: #999;
background: #1a1a22;
}
.billing-upload-form { .billing-upload-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -681,19 +614,6 @@ body.admin-body {
max-width: 720px; max-width: 720px;
} }
.admin-form--wide {
max-width: 920px;
}
.admin-form .panel {
max-width: none;
}
.admin-form .panel h3 {
margin: 0 0 0.85rem;
font-size: 1rem;
}
.admin-form-row { .admin-form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -41,15 +41,4 @@ RSpec.describe ClubBillingProfile do
club.billing_fiscal_code = "RSSMRA80A01H501U" club.billing_fiscal_code = "RSSMRA80A01H501U"
expect(club.billing_profile_complete?).to be true expect(club.billing_profile_complete?).to be true
end end
it "classifies admin billing status" do
expect(club.billing_profile_admin_status).to eq(:complete)
club.billing_recipient_code = nil
club.billing_pec = nil
expect(club.billing_profile_admin_status).to eq(:incomplete)
empty = Club.create!(name: "Empty", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff")
expect(empty.billing_profile_admin_status).to eq(:absent)
end
end end
@@ -7,8 +7,6 @@ RSpec.describe "Admin analytics", type: :request do
before do before do
post admin_login_path, params: { username: admin.username, password: "Password123" } post admin_login_path, params: { username: admin.username, password: "Password123" }
AnalyticsPageStat.delete_all
AnalyticsPageCell.delete_all
AnalyticsPageStat.create!( AnalyticsPageStat.create!(
day: Time.zone.today, day: Time.zone.today,
page_path: "/prezzi", page_path: "/prezzi",
@@ -35,62 +33,8 @@ RSpec.describe "Admin analytics", type: :request do
expect(response.body).to include("/prezzi") expect(response.body).to include("/prezzi")
expect(response.body).to include("12") expect(response.body).to include("12")
expect(response.body).to include("40") expect(response.body).to include("40")
expect(response.body).to include("chart-analytics-trend")
expect(response.body).to include("adminAnalyticsTrend")
expect(response.body).to include(I18n.t("admin.analytics.index.hide_chart"))
expect(response.body).to include("hideChart")
expect(response.body).not_to include("clicks:")
end end
it "il grafico rispetta i filtri data e aggrega pageview di tutte le pagine" do
AnalyticsPageStat.create!(
day: 1.day.ago.to_date,
page_path: "/prezzi",
device: "desktop",
pageview_count: 5,
scroll_samples: 0,
scroll_sum_pct: 0,
max_scroll_pct: 0
)
AnalyticsPageStat.create!(
day: Time.zone.today,
page_path: "/altro",
device: "desktop",
pageview_count: 7,
scroll_samples: 0,
scroll_sum_pct: 0,
max_scroll_pct: 0
)
get admin_analytics_path, params: { from: 2.days.ago.to_date, to: Time.zone.today }
expect(response).to have_http_status(:ok)
expect(response.body).to include(I18n.t("admin.analytics.index.trend_total_label"))
expect(response.body).to include(">24<")
expect(response.body).to include("\"pageviews\":5")
expect(response.body).to include("\"pageviews\":19")
end
it "filtra il grafico su una sola pagina" do
AnalyticsPageStat.create!(
day: Time.zone.today,
page_path: "/altro",
device: "desktop",
pageview_count: 7,
scroll_samples: 0,
scroll_sum_pct: 0,
max_scroll_pct: 0
)
get admin_analytics_path, params: { chart_path: "/prezzi" }
expect(response).to have_http_status(:ok)
expect(response.body).to include(I18n.t("admin.analytics.index.trend_total_label_path"))
expect(response.body).to include(">12<")
expect(response.body).to include(I18n.t("admin.analytics.index.chart_for_path"))
expect(response.body).to include("is-chart-focus")
expect(response.body).not_to include(">19<")
end
it "mostra la heatmap di una pagina con tab dispositivo" do it "mostra la heatmap di una pagina con tab dispositivo" do
get admin_analytics_page_path, params: { page_path: "/prezzi" } get admin_analytics_page_path, params: { page_path: "/prezzi" }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -1,95 +0,0 @@
require "rails_helper"
RSpec.describe "Admin clubs billing profile", type: :request do
let!(:admin) { AdminAccount.create!(username: "ops-clubs-billing", password: "Password123") }
let!(:complete_club) do
Club.create!(
name: "Club Completo",
sport: "volleyball",
primary_color: "#e53935",
secondary_color: "#ffffff",
billing_entity_type: "company",
billing_legal_name: "ASD Completo",
billing_email: "fatture@completo.it",
billing_address_line: "Via Roma 1",
billing_city: "Milano",
billing_province: "MI",
billing_postal_code: "20100",
billing_country: "IT",
billing_vat_number: "12345678901",
billing_recipient_code: "ABCDEFG"
)
end
let!(:incomplete_club) do
Club.create!(
name: "Club Incompleto",
sport: "volleyball",
primary_color: "#e53935",
secondary_color: "#ffffff",
billing_entity_type: "company",
billing_legal_name: "ASD Incompleto",
billing_vat_number: "10987654321"
)
end
let!(:absent_club) do
Club.create!(
name: "Club Assente",
sport: "volleyball",
primary_color: "#e53935",
secondary_color: "#ffffff"
)
end
let!(:owner) do
user = User.create!(email: "owner-completo@test.it", name: "Mario Rossi", password: "Password123", role: "coach")
ClubMembership.create!(user: user, club: complete_club, role: "owner")
user
end
before do
post admin_login_path, params: { username: "ops-clubs-billing", password: "Password123" }
end
it "mostra i dati fiscali e il titolare nella scheda società" do
get admin_club_path(complete_club)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Anagrafica e dati fiscali")
expect(response.body).to include("Mario Rossi")
expect(response.body).to include("owner-completo@test.it")
expect(response.body).to include("ASD Completo")
expect(response.body).to include("12345678901")
expect(response.body).to include("ABCDEFG")
expect(response.body).to include("Completo")
end
it "mostra lo stato incompleto con errori mancanti" do
get admin_club_path(incomplete_club)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Incompleto")
expect(response.body).to include("ASD Incompleto")
expect(response.body).to include("10987654321")
end
it "mostra lo stato assente quando non ci sono dati fiscali" do
get admin_club_path(absent_club)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Assenti")
expect(response.body).to include("Nessun dato di fatturazione compilato.")
end
it "elenca lo stato del profilo fiscale nella lista società" do
get admin_clubs_path
expect(response).to have_http_status(:ok)
expect(response.body).to include("Dati fiscali")
expect(response.body).to include("Completo")
expect(response.body).to include("Incompleto")
expect(response.body).to include("Assenti")
end
end
@@ -1,104 +0,0 @@
require "rails_helper"
RSpec.describe "Admin club data edit", type: :request do
let!(:admin) { AdminAccount.create!(username: "ops-club-edit", password: "Password123") }
let!(:club) do
Club.create!(
name: "Edit Club",
sport: "pallavolo",
primary_color: "#e53935",
secondary_color: "#ffffff",
billing_entity_type: "company",
billing_legal_name: "ASD Edit",
billing_email: "old-bill@test.it",
billing_address_line: "Via 1",
billing_city: "Milano",
billing_province: "MI",
billing_postal_code: "20100",
billing_country: "IT",
billing_vat_number: "12345678901",
billing_recipient_code: "ABCDEFG"
)
end
let!(:owner) do
user = User.create!(email: "owner-typo@test.it", name: "Owner Old", password: "Password123", role: "coach")
ClubMembership.create!(user: user, club: club, role: "owner")
user
end
let!(:team) { club.teams.create!(name: "U14", sport: "pallavolo") }
let!(:staff) do
user = User.create!(email: "staff@test.it", name: "Staff Old", password: "Password123", role: "coach")
UserTeam.create!(user: user, team: team, role: "member", staff_kind: "transmission")
user
end
let!(:invitation) do
token = TeamInvitation.generate_token
TeamInvitation.create!(
team: team,
email: "invite-old@test.it",
token_digest: Digest::SHA256.hexdigest(token),
role: "member",
staff_kind: "transmission",
expires_at: 3.days.from_now
)
end
before do
post admin_login_path, params: { username: "ops-club-edit", password: "Password123" }
end
it "mostra il form di modifica" do
get edit_admin_club_path(club)
expect(response).to have_http_status(:ok)
expect(response.body).to include("Modifica Edit Club")
expect(response.body).to include("owner-typo@test.it")
expect(response.body).to include("invite-old@test.it")
end
it "forza anagrafica, billing, owner, staff, invito e squadra" do
patch admin_club_path(club), params: {
club: {
name: "Edit Club Nuovo",
sport: "pallavolo",
primary_color: "#112233",
secondary_color: "#ffffff",
billing_entity_type: "company",
billing_legal_name: "ASD Edit Nuova",
billing_email: "amministrazione@test.it",
billing_vat_number: "12345678901",
billing_address_line: "Via Roma 10",
billing_city: "Milano",
billing_province: "mi",
billing_postal_code: "20121",
billing_country: "it",
billing_recipient_code: "ABCDEFG"
},
owner: { name: "Owner Fixed", email: "amministrazione@test.it" },
staff_users: {
staff.id => { name: "Staff Fixed", email: "staff-fixed@test.it" }
},
invitations: {
invitation.id => { email: "invite-fixed@test.it" }
},
teams: {
team.id => { name: "U15", sport: "pallavolo" }
}
}
expect(response).to redirect_to(admin_club_path(club))
follow_redirect!
expect(response.body).to include("Dati aggiornati per Edit Club Nuovo")
club.reload
expect(club.name).to eq("Edit Club Nuovo")
expect(club.billing_email).to eq("amministrazione@test.it")
expect(club.billing_province).to eq("MI")
expect(club.billing_legal_name).to eq("ASD Edit Nuova")
expect(owner.reload.email).to eq("amministrazione@test.it")
expect(owner.name).to eq("Owner Fixed")
expect(staff.reload.email).to eq("staff-fixed@test.it")
expect(invitation.reload.email).to eq("invite-fixed@test.it")
expect(team.reload.name).to eq("U15")
end
end
@@ -1,46 +0,0 @@
require "rails_helper"
RSpec.describe Admin::UpdateClubData do
let!(:club) do
Club.create!(
name: "Svc Club",
sport: "pallavolo",
primary_color: "#e53935",
secondary_color: "#ffffff"
)
end
let!(:owner) do
user = User.create!(email: "svc-owner@test.it", name: "Owner", password: "Password123", role: "coach")
ClubMembership.create!(user: user, club: club, role: "owner")
user
end
it "updates club and owner email" do
described_class.call(
club: club,
club_attrs: {
name: "Svc Club Updated",
billing_email: "bill@test.it",
billing_province: "bz"
},
owner_attrs: { email: "owner-fixed@test.it", name: "Owner Fixed" }
)
expect(club.reload.name).to eq("Svc Club Updated")
expect(club.billing_email).to eq("bill@test.it")
expect(club.billing_province).to eq("BZ")
expect(owner.reload.email).to eq("owner-fixed@test.it")
end
it "raises when email is already taken" do
User.create!(email: "taken@test.it", name: "Other", password: "Password123", role: "coach")
expect do
described_class.call(
club: club,
club_attrs: {},
owner_attrs: { email: "taken@test.it" }
)
end.to raise_error(Admin::UpdateClubData::Error)
end
end
+2 -2
View File
@@ -24,8 +24,8 @@ android {
applicationId = "com.matchlivetv.match_live_tv" applicationId = "com.matchlivetv.match_live_tv"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 34 versionCode = 33
versionName = "2.0.13-native" versionName = "2.0.12-native"
val apiBaseUrl = project.findProperty("API_BASE_URL") as String? val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
?: "https://www.matchlivetv.it" ?: "https://www.matchlivetv.it"
@@ -677,7 +677,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)"; INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -698,7 +698,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S8Q9TWBRG5; DEVELOPMENT_TEAM = S8Q9TWBRG5;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
@@ -727,7 +727,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S8Q9TWBRG5; DEVELOPMENT_TEAM = S8Q9TWBRG5;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
@@ -756,7 +756,7 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)"; INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -775,7 +775,7 @@
buildSettings = { buildSettings = {
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)"; INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
@@ -794,7 +794,7 @@
buildSettings = { buildSettings = {
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)"; INFOPLIST_KEY_API_BASE_URL = "$(API_BASE_URL)";
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 16.6;
+1 -1
View File
@@ -32,7 +32,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>38</string> <string>37</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
+2 -2
View File
@@ -96,7 +96,7 @@ lines += [
app_settings = """ app_settings = """
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@@ -122,7 +122,7 @@ app_debug_settings = app_settings + """
test_settings = """ test_settings = """
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 37;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 2.0.13; MARKETING_VERSION = 2.0.13;