Lascia trasmissibili le partite anche dopo l'orario previsto.

Se l'operatore è in ritardo deve ancora trovare la gara nell'app: restano in hub per tutta la giornata e, nei tornei aperti, fino alla fine dell'evento.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-06 10:07:23 +02:00
co-authored by Cursor
parent c3ef8e91a6
commit 24c6ce4da0
10 changed files with 91 additions and 14 deletions
@@ -10,7 +10,7 @@ module Api
def index def index
matches = @team.matches matches = @team.matches
.includes(:team, :stream_sessions, :home_participant, :away_participant) .includes(:team, :stream_sessions, :home_participant, :away_participant, :tournament)
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc) .order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc)
unless current_user.club_admin?(@team.club) unless current_user.club_admin?(@team.club)
if @team.tournament_broadcast? if @team.tournament_broadcast?
+10 -2
View File
@@ -98,10 +98,18 @@ class Match < ApplicationRecord
active = active_stream_session active = active_stream_session
return true if active&.resumable? return true if active&.resumable?
return true if active&.idle? return true if active&.idle?
return false if stream_completed? return false if stream_completed?
return true if scheduled_on_calendar?
return true if tournament_open_for_late_stream?
scheduled_upcoming? false
end
def tournament_open_for_late_stream?
return false unless tournament_match?
event = tournament
event.present? && !event.archived? && event.ends_on >= Date.current
end end
def effective_board_type def effective_board_type
+38 -2
View File
@@ -83,15 +83,51 @@ 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 "nasconde partita programmata nel passato mai trasmessa" do it "mostra partita di oggi già passata d'orario se non è stata trasmessa" do
match = team.matches.create!( match = team.matches.create!(
opponent_name: "Missed", opponent_name: "Late",
scheduled_at: 2.hours.ago, scheduled_at: 2.hours.ago,
sets_to_win: 3 sets_to_win: 3
) )
expect(match.coach_hub_visible?).to be(true)
end
it "nasconde partita di un giorno precedente mai trasmessa" do
match = team.matches.create!(
opponent_name: "Yesterday",
scheduled_at: 1.day.ago.change(hour: 10),
sets_to_win: 3
)
expect(match.coach_hub_visible?).to be(false) expect(match.coach_hub_visible?).to be(false)
end end
it "mostra gara di torneo in corso anche se l'orario era ieri" do
broadcast = club.teams.create!(
name: "Torneo Late",
sport_key: "pallavolo",
internal_kind: Tournament::INTERNAL_TEAM_KIND
)
tournament = Tournament.create!(
club: club,
name: "Torneo Late",
sport_key: "pallavolo",
starts_on: Date.yesterday,
ends_on: Date.current,
format_kind: "free",
status: "published",
broadcast_team: broadcast
)
match = broadcast.matches.create!(
opponent_name: "TBD",
scheduled_at: 1.day.ago.change(hour: 10),
sets_to_win: 3,
tournament: tournament
)
expect(match.coach_hub_visible?).to be(true)
end
end end
describe "scoring_rules" do describe "scoring_rules" do
@@ -51,3 +51,11 @@ private fun parseInstantOrNull(value: String): Instant? {
} }
fun Instant.isScheduledFuture(now: Instant = Instant.now()): Boolean = isAfter(now) fun Instant.isScheduledFuture(now: Instant = Instant.now()): Boolean = isAfter(now)
fun Instant.isScheduledOnCalendar(
now: Instant = Instant.now(),
zone: ZoneId = ZoneId.of("Europe/Rome"),
): Boolean {
val startOfToday = now.atZone(zone).toLocalDate().atStartOfDay(zone).toInstant()
return !isBefore(startOfToday)
}
@@ -1,10 +1,10 @@
package com.matchlivetv.match_live_tv.domain package com.matchlivetv.match_live_tv.domain
import com.matchlivetv.match_live_tv.core.isScheduledFuture import com.matchlivetv.match_live_tv.core.isScheduledOnCalendar
import com.matchlivetv.match_live_tv.core.parseApiInstant import com.matchlivetv.match_live_tv.core.parseApiInstant
import java.time.Instant import java.time.Instant
/** Partite visibili nell'hub coach: dirette attive, wizard in corso, programmate future. */ /** Partite visibili nell'hub coach: dirette attive, wizard in corso, in calendario oggi o future. */
fun Match.isCoachHubVisible(now: Instant = Instant.now()): Boolean { fun Match.isCoachHubVisible(now: Instant = Instant.now()): Boolean {
coachHubVisible?.let { return it } coachHubVisible?.let { return it }
@@ -15,7 +15,7 @@ fun Match.isCoachHubVisible(now: Instant = Instant.now()): Boolean {
if (streamCompleted) return false if (streamCompleted) return false
val scheduled = parseApiInstant(scheduledAt) val scheduled = parseApiInstant(scheduledAt)
return scheduled != null && scheduled.isScheduledFuture(now) return scheduled != null && scheduled.isScheduledOnCalendar(now)
} }
fun List<Match>.coachHubVisible(now: Instant = Instant.now()): List<Match> = fun List<Match>.coachHubVisible(now: Instant = Instant.now()): List<Match> =
@@ -47,8 +47,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.R
import com.matchlivetv.match_live_tv.core.isScheduledFuture
import com.matchlivetv.match_live_tv.core.parseApiInstant
import com.matchlivetv.match_live_tv.data.AppContainer import com.matchlivetv.match_live_tv.data.AppContainer
import com.matchlivetv.match_live_tv.data.repository.MatchSessionLauncher import com.matchlivetv.match_live_tv.data.repository.MatchSessionLauncher
import com.matchlivetv.match_live_tv.domain.Match import com.matchlivetv.match_live_tv.domain.Match
@@ -161,7 +159,7 @@ fun MatchesScreen(
(match.hasActiveSession && match.activeSessionStatus == "idle") (match.hasActiveSession && match.activeSessionStatus == "idle")
} }
val scheduledMatches = matches.filter { match -> val scheduledMatches = matches.filter { match ->
!match.hasActiveSession && parseApiInstant(match.scheduledAt)?.isScheduledFuture() == true !match.hasActiveSession && !match.scheduledAt.isNullOrBlank()
} }
val draftMatches = matches.filter { !it.hasActiveSession && it.scheduledAt == null } val draftMatches = matches.filter { !it.hasActiveSession && it.scheduledAt == null }
val calendarMatches = (scheduledMatches + draftMatches).distinctBy { it.id } val calendarMatches = (scheduledMatches + draftMatches).distinctBy { it.id }
@@ -10,7 +10,22 @@ class ApiInstantTest {
fun parseApiInstant_railsOffsetWithMillis() { fun parseApiInstant_railsOffsetWithMillis() {
val instant = parseApiInstant("2026-06-06T20:00:00.000+02:00") val instant = parseApiInstant("2026-06-06T20:00:00.000+02:00")
assertNotNull(instant) assertNotNull(instant)
assertTrue(instant!!.isScheduledFuture(Instant.parse("2026-06-06T16:00:00Z"))) assertTrue(instant!!.isScheduledOnCalendar(Instant.parse("2026-06-06T16:00:00Z")))
}
@Test
fun isScheduledOnCalendar_keepsThisMorningAfterKickoff() {
val kickoff = Instant.parse("2026-09-06T07:00:00Z")
val now = Instant.parse("2026-09-06T08:30:00Z")
assertTrue(kickoff.isScheduledOnCalendar(now))
assertTrue(!kickoff.isScheduledFuture(now))
}
@Test
fun isScheduledOnCalendar_hidesYesterday() {
val yesterday = Instant.parse("2026-09-05T10:00:00Z")
val now = Instant.parse("2026-09-06T08:30:00Z")
assertTrue(!yesterday.isScheduledOnCalendar(now))
} }
@Test @Test
@@ -19,6 +19,14 @@ enum ApiInstant {
return date > now return date > now
} }
static func isScheduledOnCalendar(_ raw: String?, now: Date = Date()) -> Bool {
guard let date = parse(raw) else { return false }
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/Rome") ?? .current
let startOfToday = calendar.startOfDay(for: now)
return date >= startOfToday
}
static func formatMatchDate(_ raw: String?) -> String? { static func formatMatchDate(_ raw: String?) -> String? {
guard let date = parse(raw) else { return nil } guard let date = parse(raw) else { return nil }
let f = DateFormatter() let f = DateFormatter()
@@ -12,13 +12,17 @@ enum MatchHubFilter {
} }
if match.streamCompleted { return false } if match.streamCompleted { return false }
return ApiInstant.isScheduledFuture(match.scheduledAt, now: now) return ApiInstant.isScheduledOnCalendar(match.scheduledAt, now: now)
} }
static func isScheduledFuture(_ match: Match, now: Date = Date()) -> Bool { static func isScheduledFuture(_ match: Match, now: Date = Date()) -> Bool {
ApiInstant.isScheduledFuture(match.scheduledAt, now: now) ApiInstant.isScheduledFuture(match.scheduledAt, now: now)
} }
static func isOnCalendar(_ match: Match, now: Date = Date()) -> Bool {
ApiInstant.isScheduledOnCalendar(match.scheduledAt, now: now)
}
static func isDraft(_ match: Match) -> Bool { static func isDraft(_ match: Match) -> Bool {
!match.hasActiveSession && (match.scheduledAt == nil || match.scheduledAt?.isEmpty == true) !match.hasActiveSession && (match.scheduledAt == nil || match.scheduledAt?.isEmpty == true)
} }
@@ -289,7 +289,7 @@ struct MatchesScreen: View {
} }
private var scheduledMatches: [Match] { private var scheduledMatches: [Match] {
matches.filter { !$0.hasActiveSession && MatchHubFilter.isScheduledFuture($0) } matches.filter { !$0.hasActiveSession && !($0.scheduledAt ?? "").isEmpty }
} }
private var calendarMatches: [Match] { private var calendarMatches: [Match] {