Fix sport squadra su partite basket e ruoli organico per board.
Le partite ereditano lo sport_key della squadra alla creazione invece del default pallavolo; l'organico mostra ruoli per board (basket, volley, timed) e il wizard Android usa lo sport del team per overlay e icona. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,7 @@ module Api
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
attrs = match_params.to_h
|
attrs = match_params.to_h
|
||||||
attrs["sport_key"] = @team.sport_key if attrs["sport_key"].blank?
|
attrs["sport_key"] = @team.sport_key
|
||||||
normalize_scoring_rules!(attrs)
|
normalize_scoring_rules!(attrs)
|
||||||
match = @team.matches.create!(attrs)
|
match = @team.matches.create!(attrs)
|
||||||
attach_opponent_logo(match)
|
attach_opponent_logo(match)
|
||||||
@@ -70,6 +70,7 @@ module Api
|
|||||||
if p[:sport].present? && p[:sport_key].blank?
|
if p[:sport].present? && p[:sport_key].blank?
|
||||||
p[:sport_key] = p.delete(:sport)
|
p[:sport_key] = p.delete(:sport)
|
||||||
end
|
end
|
||||||
|
p[:overlay_kind] = nil if p.key?(:overlay_kind) && p[:overlay_kind].blank?
|
||||||
p
|
p
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
25
backend/app/domain/sports/player_roles.rb
Normal file
25
backend/app/domain/sports/player_roles.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sports
|
||||||
|
class PlayerRoles
|
||||||
|
BY_BOARD = {
|
||||||
|
"volley" => %w[Schiacciatore Opposto Centrale Palleggiatore Libero Universale],
|
||||||
|
"basket" => ["Playmaker", "Guardia", "Ala", "Ala forte", "Centro"],
|
||||||
|
"racket" => %w[Giocatore],
|
||||||
|
"timed" => %w[Portiere Difensore Centrocampista Attaccante],
|
||||||
|
"generic" => %w[Giocatore],
|
||||||
|
"timer" => %w[Giocatore]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def for_team(team)
|
||||||
|
board = team.effective_board_type
|
||||||
|
BY_BOARD[board] || BY_BOARD["generic"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_board(board)
|
||||||
|
BY_BOARD[board.to_s] || BY_BOARD["generic"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -23,9 +23,10 @@ module RosterHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def roster_player_role_options(selected = nil)
|
def roster_player_role_options(team, selected = nil)
|
||||||
|
roles = Sports::PlayerRoles.for_team(team)
|
||||||
options_for_select(
|
options_for_select(
|
||||||
[["— Seleziona ruolo —", ""]] + TeamRosterMember::VOLLEYBALL_PLAYER_ROLES.map { |r| [r, r] },
|
[["— Seleziona ruolo —", ""]] + roles.map { |r| [r, r] },
|
||||||
selected
|
selected
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -129,7 +129,10 @@ class Match < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def inherit_sport_from_team
|
def inherit_sport_from_team
|
||||||
self.sport_key = team.sport_key if sport_key.blank? && team.present?
|
return unless team.present?
|
||||||
|
|
||||||
|
# Il default DB (pallavolo) non deve prevalere sullo sport della squadra alla creazione.
|
||||||
|
self.sport_key = team.sport_key if new_record? || sport_key.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def sport_key_known
|
def sport_key_known
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ class TeamRosterMember < ApplicationRecord
|
|||||||
"player" => "Giocatori"
|
"player" => "Giocatori"
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
VOLLEYBALL_PLAYER_ROLES = %w[
|
VOLLEYBALL_PLAYER_ROLES = Sports::PlayerRoles::BY_BOARD["volley"].freeze
|
||||||
Schiacciatore
|
|
||||||
Opposto
|
|
||||||
Centrale
|
|
||||||
Palleggiatore
|
|
||||||
Libero
|
|
||||||
Universale
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
belongs_to :team
|
belongs_to :team
|
||||||
|
|
||||||
@@ -74,9 +67,10 @@ class TeamRosterMember < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def player_role_label_valid
|
def player_role_label_valid
|
||||||
return if role_label.in?(VOLLEYBALL_PLAYER_ROLES)
|
allowed = Sports::PlayerRoles.for_team(team)
|
||||||
|
return if role_label.in?(allowed)
|
||||||
|
|
||||||
errors.add(:role_label, "non è un ruolo pallavolo valido")
|
errors.add(:role_label, "non è un ruolo valido per questo sport")
|
||||||
end
|
end
|
||||||
|
|
||||||
def photo_file_type
|
def photo_file_type
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<% form_url = member.persisted? ? public_team_roster_member_path(team, member) : public_team_roster_members_path(team) %>
|
<% form_url = member.persisted? ? public_team_roster_member_path(team, member) : public_team_roster_members_path(team) %>
|
||||||
<% form_method = member.persisted? ? :patch : :post %>
|
<% form_method = member.persisted? ? :patch : :post %>
|
||||||
<% is_player = member.category == "player" %>
|
<% is_player = member.category == "player" %>
|
||||||
<% player_role = member.role_label if member.role_label.in?(TeamRosterMember::VOLLEYBALL_PLAYER_ROLES) %>
|
<% player_roles = Sports::PlayerRoles.for_team(team) %>
|
||||||
|
<% player_role = member.role_label if member.role_label.in?(player_roles) %>
|
||||||
|
|
||||||
<%= form_with url: form_url, method: form_method, multipart: true, class: "roster-member-form card roster-sidebar__form", data: { roster_form: true } do %>
|
<%= form_with url: form_url, method: form_method, multipart: true, class: "roster-member-form card roster-sidebar__form", data: { roster_form: true } do %>
|
||||||
<h3 class="roster-sidebar__title"><%= member.persisted? ? "Modifica scheda" : "Aggiungi persona" %></h3>
|
<h3 class="roster-sidebar__title"><%= member.persisted? ? "Modifica scheda" : "Aggiungi persona" %></h3>
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
<div data-roster-role-fields>
|
<div data-roster-role-fields>
|
||||||
<div data-roster-role-player <%= "hidden" unless is_player %>>
|
<div data-roster-role-player <%= "hidden" unless is_player %>>
|
||||||
<%= label_tag "team_roster_member[role_label]", "Ruolo in campo" %>
|
<%= label_tag "team_roster_member[role_label]", "Ruolo in campo" %>
|
||||||
<%= select_tag "team_roster_member[role_label]", roster_player_role_options(player_role),
|
<%= select_tag "team_roster_member[role_label]", roster_player_role_options(team, player_role),
|
||||||
data: { roster_role_player: true }, disabled: !is_player %>
|
data: { roster_role_player: true }, disabled: !is_player %>
|
||||||
</div>
|
</div>
|
||||||
<div data-roster-role-other <%= "hidden" if is_player %>>
|
<div data-roster-role-other <%= "hidden" if is_player %>>
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ RSpec.describe Match, "scheduling scopes" do
|
|||||||
expect(match.coach_hub_visible?).to be(false)
|
expect(match.coach_hub_visible?).to be(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "eredita lo sport della squadra alla creazione (non il default pallavolo)" do
|
||||||
|
basket_team = club.teams.create!(name: "U13 Basket", sport_key: "basket")
|
||||||
|
match = basket_team.matches.create!(opponent_name: "Ospiti", sets_to_win: 3)
|
||||||
|
|
||||||
|
expect(match.sport_key).to eq("basket")
|
||||||
|
expect(match.effective_board_type).to eq("basket")
|
||||||
|
expect(match.effective_overlay_kind).to eq("basket")
|
||||||
|
end
|
||||||
|
|
||||||
it "nasconde partita con diretta terminata anche se resta una sessione idle vecchia" do
|
it "nasconde partita con diretta terminata anche se resta una sessione idle vecchia" do
|
||||||
match = team.matches.create!(opponent_name: "Avversario", sets_to_win: 3)
|
match = team.matches.create!(opponent_name: "Avversario", sets_to_win: 3)
|
||||||
StreamSession.create!(
|
StreamSession.create!(
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ private const val DEFAULT_PERIOD_DURATION_SECS = 600
|
|||||||
private const val DEFAULT_OVERTIME_DURATION_SECS = 300
|
private const val DEFAULT_OVERTIME_DURATION_SECS = 300
|
||||||
private const val DEFAULT_OPPONENT_COLOR = "#1E3A8A"
|
private const val DEFAULT_OPPONENT_COLOR = "#1E3A8A"
|
||||||
|
|
||||||
|
private fun overlayLabel(key: String): String =
|
||||||
|
OverlayKind.fromApi(key).name.lowercase().replaceFirstChar { it.uppercase() }
|
||||||
|
|
||||||
|
/** True se la partita ha un overlay diverso dal default dello sport. */
|
||||||
|
private fun matchHasCustomOverlay(match: Match, sportDefaultOverlay: String?): Boolean {
|
||||||
|
val default = sportDefaultOverlay ?: return false
|
||||||
|
val current = match.overlayKind ?: return false
|
||||||
|
return current != default
|
||||||
|
}
|
||||||
|
|
||||||
/** True se la partita ha regole non standard rispetto ai default del board. */
|
/** True se la partita ha regole non standard rispetto ai default del board. */
|
||||||
private fun matchHasCustomScoring(match: Match, boardType: String): Boolean {
|
private fun matchHasCustomScoring(match: Match, boardType: String): Boolean {
|
||||||
val rules = match.scoringRules ?: return false
|
val rules = match.scoringRules ?: return false
|
||||||
@@ -136,22 +146,41 @@ fun StepMatchScreen(
|
|||||||
var saving by remember { mutableStateOf(false) }
|
var saving by remember { mutableStateOf(false) }
|
||||||
var sports by remember { mutableStateOf<List<SportOption>>(emptyList()) }
|
var sports by remember { mutableStateOf<List<SportOption>>(emptyList()) }
|
||||||
var selectedSportKey by remember { mutableStateOf(match.sportKey) }
|
var selectedSportKey by remember { mutableStateOf(match.sportKey) }
|
||||||
var selectedOverlay by remember { mutableStateOf(match.overlayKind ?: match.effectiveOverlayKind) }
|
var customOverlay by remember { mutableStateOf(false) }
|
||||||
|
var selectedOverlay by remember { mutableStateOf(match.effectiveOverlayKind) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
runCatching { container.matchRepository.fetchSports() }
|
runCatching { container.matchRepository.fetchSports() }
|
||||||
.onSuccess { sports = it }
|
.onSuccess { sports = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentSport = sports.find { it.key == selectedSportKey }
|
val teamSportKey = homeTeam?.sportKey?.takeIf { it.isNotBlank() }
|
||||||
?: sports.find { it.key == homeTeam?.sportKey }
|
val effectiveSportKey = teamSportKey ?: selectedSportKey
|
||||||
|
val currentSport = sports.find { it.key == effectiveSportKey }
|
||||||
val allowedOverlays = currentSport?.allowedOverlays.orEmpty()
|
val allowedOverlays = currentSport?.allowedOverlays.orEmpty()
|
||||||
|
val defaultOverlay = currentSport?.overlay ?: match.effectiveOverlayKind
|
||||||
val boardType = currentSport?.board ?: homeTeam?.boardType ?: match.boardType
|
val boardType = currentSport?.board ?: homeTeam?.boardType ?: match.boardType
|
||||||
|
|
||||||
LaunchedEffect(boardType, match.id) {
|
LaunchedEffect(boardType, match.id) {
|
||||||
customRules = matchHasCustomScoring(match, boardType)
|
customRules = matchHasCustomScoring(match, boardType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(currentSport?.overlay, match.id) {
|
||||||
|
customOverlay = matchHasCustomOverlay(match, currentSport?.overlay)
|
||||||
|
selectedOverlay = when {
|
||||||
|
customOverlay -> match.overlayKind ?: match.effectiveOverlayKind
|
||||||
|
else -> defaultOverlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(customOverlay, defaultOverlay) {
|
||||||
|
if (!customOverlay) {
|
||||||
|
selectedOverlay = defaultOverlay
|
||||||
|
} else if (selectedOverlay !in allowedOverlays) {
|
||||||
|
selectedOverlay = defaultOverlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
@@ -214,19 +243,21 @@ fun StepMatchScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
WizardReadOnlyField(
|
|
||||||
label = "Sport",
|
|
||||||
value = currentSport?.label ?: match.sportLabel ?: selectedSportKey,
|
|
||||||
)
|
|
||||||
if (allowedOverlays.isNotEmpty()) {
|
if (allowedOverlays.isNotEmpty()) {
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text("Overlay video personalizzato", modifier = Modifier.weight(1f))
|
||||||
|
Switch(checked = customOverlay, onCheckedChange = { customOverlay = it })
|
||||||
|
}
|
||||||
|
if (customOverlay) {
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Text("Overlay video", style = MaterialTheme.typography.bodyMedium)
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
allowedOverlays.forEach { overlayKey ->
|
allowedOverlays.forEach { overlayKey ->
|
||||||
val selected = selectedOverlay == overlayKey
|
val selected = selectedOverlay == overlayKey
|
||||||
val label = OverlayKind.fromApi(overlayKey).name.lowercase()
|
val label = overlayLabel(overlayKey)
|
||||||
.replaceFirstChar { it.uppercase() }
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
MatchPrimaryButton(
|
MatchPrimaryButton(
|
||||||
label = label,
|
label = label,
|
||||||
@@ -242,6 +273,7 @@ fun StepMatchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Row(
|
Row(
|
||||||
@@ -440,9 +472,11 @@ fun StepMatchScreen(
|
|||||||
category = campionato.trim(),
|
category = campionato.trim(),
|
||||||
opponentPrimaryColor = opponentPrimaryColor,
|
opponentPrimaryColor = opponentPrimaryColor,
|
||||||
scoringRules = resolvedRules,
|
scoringRules = resolvedRules,
|
||||||
sportKey = selectedSportKey,
|
sportKey = effectiveSportKey,
|
||||||
overlayKind = selectedOverlay.takeIf {
|
overlayKind = if (customOverlay && selectedOverlay != defaultOverlay) {
|
||||||
it != currentSport?.overlay
|
selectedOverlay
|
||||||
|
} else {
|
||||||
|
""
|
||||||
},
|
},
|
||||||
opponentLogoUri = opponentLogoUri,
|
opponentLogoUri = opponentLogoUri,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.matchlivetv.match_live_tv.ui.wizard
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -25,9 +26,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||||
import com.matchlivetv.match_live_tv.domain.Match
|
import com.matchlivetv.match_live_tv.domain.Match
|
||||||
import com.matchlivetv.match_live_tv.domain.StreamSession
|
import com.matchlivetv.match_live_tv.domain.StreamSession
|
||||||
|
import com.matchlivetv.match_live_tv.domain.Team
|
||||||
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
||||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -46,6 +49,7 @@ fun WizardShellScreen(
|
|||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
var match by remember { mutableStateOf<Match?>(null) }
|
var match by remember { mutableStateOf<Match?>(null) }
|
||||||
|
var homeTeam by remember { mutableStateOf<Team?>(null) }
|
||||||
val currentStep = step.coerceIn(1, 3)
|
val currentStep = step.coerceIn(1, 3)
|
||||||
|
|
||||||
fun showError(message: String) {
|
fun showError(message: String) {
|
||||||
@@ -55,9 +59,11 @@ fun WizardShellScreen(
|
|||||||
LaunchedEffect(matchId) {
|
LaunchedEffect(matchId) {
|
||||||
loading = true
|
loading = true
|
||||||
runCatching { container.matchRepository.fetchMatch(matchId) }
|
runCatching { container.matchRepository.fetchMatch(matchId) }
|
||||||
.onSuccess {
|
.onSuccess { loaded ->
|
||||||
match = it
|
match = loaded
|
||||||
container.wizardSession.match = it
|
container.wizardSession.match = loaded
|
||||||
|
runCatching { container.matchRepository.fetchTeam(loaded.teamId) }
|
||||||
|
.onSuccess { homeTeam = it }
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,19 @@ fun WizardShellScreen(
|
|||||||
Icon(Icons.Default.Close, contentDescription = "Chiudi", tint = Color.White)
|
Icon(Icons.Default.Close, contentDescription = "Chiudi", tint = Color.White)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
actions = {
|
||||||
|
if (currentStep == 1) {
|
||||||
|
val badgeSportKey = homeTeam?.sportKey ?: match?.sportKey.orEmpty()
|
||||||
|
if (badgeSportKey.isNotBlank()) {
|
||||||
|
SportWizardBadge(
|
||||||
|
sportKey = badgeSportKey,
|
||||||
|
boardType = homeTeam?.boardType ?: match?.boardType ?: "volley",
|
||||||
|
sportLabel = match?.sportLabel,
|
||||||
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MatchColors.Background,
|
containerColor = MatchColors.Background,
|
||||||
titleContentColor = Color.White,
|
titleContentColor = Color.White,
|
||||||
|
|||||||
Reference in New Issue
Block a user