draw log final implementation

fix scheduler + unavailability stuff
add team resting view
paca_championship
Raz 1 year ago
parent 9291d63d05
commit 77b3f27685
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 47
      PadelClub/Data/DrawLog.swift
  3. 9
      PadelClub/Data/Match.swift
  4. 19
      PadelClub/Data/MatchScheduler.swift
  5. 38
      PadelClub/Data/TeamRegistration.swift
  6. 107
      PadelClub/Data/Tournament.swift
  7. 2
      PadelClub/Data/TournamentStore.swift
  8. 12
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  9. 30
      PadelClub/Views/Round/DrawLogsView.swift
  10. 107
      PadelClub/Views/Round/PreviewBracketPositionView.swift
  11. 76
      PadelClub/Views/Round/RoundSettingsView.swift
  12. 145
      PadelClub/Views/Round/RoundView.swift
  13. 101
      PadelClub/Views/Team/TeamRestingView.swift
  14. 138
      PadelClub/Views/Team/TeamRowView.swift
  15. 18
      PadelClubTests/ServerDataTests.swift

@ -430,6 +430,9 @@
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -1074,6 +1077,7 @@
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; };
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; }; FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; };
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; }; FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; };
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -1881,6 +1885,7 @@
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */, FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2454,6 +2459,7 @@
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
@ -2730,6 +2736,7 @@
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
@ -2985,6 +2992,7 @@
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,

@ -19,29 +19,40 @@ final class DrawLog: ModelObject, Storable {
var id: String = Store.randomId() var id: String = Store.randomId()
var tournament: String var tournament: String
var drawDate: Date = Date() var drawDate: Date = Date()
var drawSeed: Int? var drawSeed: Int
var drawPosition: Int var drawMatchIndex: Int
var drawTeamPosition: TeamPosition var drawTeamPosition: TeamPosition
internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int?, drawPosition: Int, drawTeamPosition: TeamPosition) { internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) {
self.id = id self.id = id
self.tournament = tournament self.tournament = tournament
self.drawDate = drawDate self.drawDate = drawDate
self.drawSeed = drawSeed self.drawSeed = drawSeed
self.drawPosition = drawPosition self.drawMatchIndex = drawMatchIndex
self.drawTeamPosition = drawTeamPosition self.drawTeamPosition = drawTeamPosition
} }
func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
func computedBracketPosition() -> Int {
drawMatchIndex * 2 + drawTeamPosition.rawValue
}
func updateTeamBracketPosition(_ team: TeamRegistration) {
guard let match = drawMatch() else { return }
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition)
team.bracketPosition = seedPosition
tournamentObject()?.updateTeamScores(in: seedPosition)
}
func exportedDrawLog() -> String { func exportedDrawLog() -> String {
[drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ") [drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ")
} }
func localizedDrawSeedLabel() -> String { func localizedDrawSeedLabel() -> String {
if let drawSeed { return "Tête de série #\(drawSeed + 1)"
return "Tête de série #\(drawSeed + 1)"
} else {
return "Tête de série non trouvé"
}
} }
func localizedDrawLogLabel() -> String { func localizedDrawLogLabel() -> String {
@ -52,9 +63,21 @@ final class DrawLog: ModelObject, Storable {
drawTeamPosition.localizedBranchLabel() drawTeamPosition.localizedBranchLabel()
} }
func drawMatch() -> Match? {
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex)
return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex })
}
func positionLabel() -> String { func positionLabel() -> String {
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawPosition) return drawMatch()?.roundAndMatchTitle() ?? ""
return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawPosition })?.roundAndMatchTitle() ?? "" }
func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
} }
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
@ -69,7 +92,7 @@ final class DrawLog: ModelObject, Storable {
case _tournament = "tournament" case _tournament = "tournament"
case _drawDate = "drawDate" case _drawDate = "drawDate"
case _drawSeed = "drawSeed" case _drawSeed = "drawSeed"
case _drawPosition = "drawPosition" case _drawMatchIndex = "drawMatchIndex"
case _drawTeamPosition = "drawTeamPosition" case _drawTeamPosition = "drawTeamPosition"
} }

@ -233,6 +233,7 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState() currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
} }
func resetScores() { func resetScores() {
@ -534,7 +535,7 @@ defer {
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
} }
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
@ -557,6 +558,8 @@ defer {
teamOne?.hasArrived() teamOne?.hasArrived()
teamTwo?.hasArrived() teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id losingTeamId = teamTwo?.id
@ -923,6 +926,10 @@ defer {
(teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
} }
func isValidSpot() -> Bool {
previousMatches().allSatisfy({ $0.isSeeded() == false })
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _round = "round" case _round = "round"

@ -493,8 +493,16 @@ final class MatchScheduler : ModelObject, Storable {
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts") print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
if let courtsUnavailability, previousEndDate != nil {
previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
if let courtsUnavailability, previousEndDateNoBreak != nil {
previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak }
@ -651,7 +659,12 @@ final class MatchScheduler : ModelObject, Storable {
} }
if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
print("All courts in rotation \(rotationIndex) are free") print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)")
}
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability)
return computedStartDateAndCourts.earliestFreeDate
} }
return minimumTargetedEndDate return minimumTargetedEndDate

@ -139,11 +139,13 @@ final class TeamRegistration: ModelObject, Storable {
qualified = true qualified = true
} }
if let tournament = tournamentObject() { if let tournament = tournamentObject() {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index(in: tournament.selectedSortedTeams()), drawPosition: match.index, drawTeamPosition: teamPosition) if let index = index(in: tournament.selectedSortedTeams()) {
do { let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition)
try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) do {
} catch { try tournamentStore.drawLogs.addOrUpdate(instance: drawLog)
Logger.error(error) } catch {
Logger.error(error)
}
} }
tournament.updateTeamScores(in: bracketPosition) tournament.updateTeamScores(in: bracketPosition)
} }
@ -556,8 +558,21 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
var _cachedRestingTime: (Bool, Date?)?
func restingTime() -> Date? { func restingTime() -> Date? {
matches().sorted(by: \.computedEndDateForSorting).last?.endDate if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
_cachedRestingTime = (true, restingTime)
return restingTime
}
func resetRestingTime() {
_cachedRestingTime = nil
}
var restingTimeForSorting: Date {
restingTime()!
} }
func teamNameLabel() -> String { func teamNameLabel() -> String {
@ -568,6 +583,17 @@ final class TeamRegistration: ModelObject, Storable {
} }
} }
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if let bracketPosition {
return true
} else if let drawMatchIndex {
return true
}
return false
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _tournament = "tournament" case _tournament = "tournament"

@ -1,5 +1,5 @@
// //
// Tournament.swift // swift
// PadelClub // PadelClub
// //
// Created by Laurent Morvillier on 02/02/2024. // Created by Laurent Morvillier on 02/02/2024.
@ -558,7 +558,7 @@ defer {
return endDate != nil return endDate != nil
} }
func state() -> Tournament.State { func state() -> State {
if self.isCanceled == true { if self.isCanceled == true {
return .canceled return .canceled
} }
@ -2275,6 +2275,109 @@ defer {
self.tournamentStore.drawLogs.sorted(by: \.drawDate) self.tournamentStore.drawLogs.sorted(by: \.drawDate)
} }
func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
func updateSeedsBracketPosition() async {
await removeAllSeeds()
let drawLogs = drawLogs().reversed()
let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
allRounds().forEach({ round in
round.enableRound()
})
}
func addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
func exportedDrawLogs() -> String { func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"] var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))

@ -52,7 +52,7 @@ class TournamentStore: Store, ObservableObject {
self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed)
self.drawLogs = self.registerCollection(synchronized: false, indexed: indexed) self.drawLogs = self.registerCollection(synchronized: synchronized, indexed: indexed)
self.loadCollectionsFromServerIfNoFile() self.loadCollectionsFromServerIfNoFile()

@ -78,16 +78,8 @@ struct PlayerBlockView: View {
} }
} }
if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { if displayRestingTime, let team {
if restingTime > -300 { TeamRowView.TeamRestingView(team: team)
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
.bold(hasWon) .bold(hasWon)

@ -18,12 +18,20 @@ struct DrawLogsView: View {
var body: some View { var body: some View {
List { List {
ForEach(drawLogs) { drawLog in ForEach(drawLogs) { drawLog in
LabeledContent { HStack {
Text(drawLog.localizedDrawBranch()) VStack(alignment: .leading) {
} label: { Text(drawLog.localizedDrawSeedLabel())
Text(drawLog.localizedDrawSeedLabel()) Text(drawLog.drawDate.localizedDate())
Text(drawLog.positionLabel()) .font(.footnote)
Text(drawLog.drawDate.localizedDate()) .foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
.font(.footnote)
.foregroundStyle(.secondary)
}
} }
} }
} }
@ -39,6 +47,16 @@ struct DrawLogsView: View {
Label("Partager les tirages", systemImage: "square.and.arrow.up") Label("Partager les tirages", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon) .labelStyle(.titleAndIcon)
} }
Divider()
Button("Tout effacer", role: .destructive) {
do {
try tournament.tournamentStore.drawLogs.deleteAll()
} catch {
Logger.error(error)
}
}
} label: { } label: {
LabelOptions() LabelOptions()
} }

@ -0,0 +1,107 @@
//
// PreviewBracketPositionView.swift
// PadelClub
//
// Created by razmig on 23/10/2024.
//
import SwiftUI
struct PreviewBracketPositionView: View {
let seeds: [TeamRegistration]
let drawLogs: [DrawLog]
@State private var filterOption: PreviewBracketPositionFilterOption = .difference
enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case all
case difference
case summon
func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool {
switch self {
case .all:
true
case .difference:
team.isDifferentPosition(drawLog?.computedBracketPosition()) == true
case .summon:
team.callDate != drawLog?.drawMatch()?.startDate
}
}
}
var body: some View {
List {
Section {
ForEach(seeds.indices, id: \.self) { seedIndex in
let seed = seeds[seedIndex]
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
if filterOption.isDisplayable(seed, drawLog: drawLog) {
HStack {
VStack(alignment: .leading) {
Text("Tête de série #\(seedIndex + 1)").font(.caption)
TeamRowView.TeamView(team: seed)
TeamRowView.TeamCallDateView(team: seed)
}
Spacer()
if let drawLog {
VStack(alignment: .trailing) {
Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption)
Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle)
Text(drawLog.localizedDrawBranch())
if let expectedDate = drawLog.drawMatch()?.startDate {
Text(expectedDate.localizedDate())
.font(.caption)
} else {
Text("Aucun horaire")
.font(.caption)
}
}
}
}
.listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true)
}
}
} header: {
Picker(selection: $filterOption) {
Text("Tous").tag(PreviewBracketPositionFilterOption.all)
Text("Changements").tag(PreviewBracketPositionFilterOption.difference)
Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon)
} label: {
Text("Filter")
}
.labelsHidden()
.pickerStyle(.segmented)
.textCase(nil)
}
}
.overlay(content: {
if seeds.isEmpty {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi."))
} else if filterOption == .difference, noSeedHasDifferentPlace() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
} else if filterOption == .summon, noSeedHasDifferentSummon() {
ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau."))
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Aperçu du tableau tiré")
}
func noSeedHasDifferentPlace() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false
})
}
func noSeedHasDifferentSummon() -> Bool {
seeds.enumerated().allSatisfy({ seedIndex, seed in
let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex })
return seed.callDate == drawLog?.drawMatch()?.startDate
})
}
}

@ -82,19 +82,19 @@ struct RoundSettingsView: View {
Text("Gestionnaire des tirages au sort") Text("Gestionnaire des tirages au sort")
} }
if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil { if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil, tournament.seedSpotsLeft() {
NavigationLink { NavigationLink {
PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs())
} label: { } label: {
Text("Aperçu du décalage") Text("Aperçu du repositionnement")
} }
RowButtonView("Décaler les têtes de série", role: .destructive) { RowButtonView("Replacer toutes les têtes de série", role: .destructive) {
await tournament.updateSeedsBracketPosition()
} }
} }
} footer: { } footer: {
Text("Vous avez une place libre dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.")
} }
// Section { // Section {
@ -150,72 +150,12 @@ struct RoundSettingsView: View {
} }
private func _removeAllSeeds() async { private func _removeAllSeeds() async {
tournament.unsortedTeams().forEach({ team in await tournament.removeAllSeeds()
team.bracketPosition = nil
})
let ts = tournament.allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
tournament.allRounds().forEach({ round in
round.enableRound()
})
self.isEditingTournamentSeed.wrappedValue = true self.isEditingTournamentSeed.wrappedValue = true
} }
private func _addNewRound(_ roundIndex: Int) async { private func _addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) await tournament.addNewRound(roundIndex)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournament.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
} }
private func _removeRound(_ lastRound: Round) async { private func _removeRound(_ lastRound: Round) async {

@ -37,14 +37,14 @@ struct RoundView: View {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 1 match.teamScores.count == 1
} }.filter({ $0.isValidSpot() })
} }
private var seedSpaceLeft: [Match] { private var seedSpaceLeft: [Match] {
let displayableMatches: [Match] = self.upperRound.round.playedMatches() let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in return displayableMatches.filter { match in
match.teamScores.count == 0 match.teamScores.count == 0
} }.filter({ $0.isValidSpot() })
} }
private var availableSeedGroup: SeedInterval? { private var availableSeedGroup: SeedInterval? {
@ -121,6 +121,7 @@ struct RoundView: View {
} }
} }
} else { } else {
let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index)
let availableSeeds = tournament.availableSeeds() let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -152,87 +153,46 @@ struct RoundView: View {
if availableQualifiedTeams.isEmpty == false { if availableQualifiedTeams.isEmpty == false {
let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false
let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft
if availableSeedSpot.isEmpty == false { Section {
Section { DisclosureGroup {
DisclosureGroup { ForEach(availableQualifiedTeams) { team in
ForEach(availableQualifiedTeams) { team in NavigationLink {
NavigationLink {
SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in
SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in Task {
Task { results.forEach { drawResult in
results.forEach { drawResult in if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot {
if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false)
team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match {
} else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true)
team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true)
}
} }
_save(seeds: [team])
} }
_save(seeds: [team])
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: { .disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false)
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
} }
} header: { } label: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline) Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
}
} header: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
} footer: {
if availableSeedSpot.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible !")
.foregroundStyle(.red)
} }
} }
} }
if availableSeeds.isEmpty == false { if availableSeeds.isEmpty == false {
if seedSpaceLeft.isEmpty == false { let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft
Section { let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true
DisclosureGroup { _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding)
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
} else if spaceLeft.isEmpty == false {
Section {
DisclosureGroup {
ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spaceLeft) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
}
}
} }
} }
@ -259,11 +219,11 @@ struct RoundView: View {
} }
} }
#if DEBUG #if DEBUG
Spacer() Spacer()
Text(match.index.formatted() + " " + match.teamScores.count.formatted()) Text(match.index.formatted() + " " + match.teamScores.count.formatted())
#endif #endif
} }
} footer: { } footer: {
if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true { if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true {
@ -399,12 +359,39 @@ struct RoundView: View {
} }
} }
} }
}
//#Preview { private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View {
// RoundView(round: Round.mock()) Section {
// .environment(Tournament.mock()) DisclosureGroup {
//} ForEach(availableSeeds) { team in
NavigationLink {
SpinDrawView(drawees: [team], segments: spots) { results in
Task {
results.forEach { drawResult in
team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding)
}
_save(seeds: [team])
}
}
} label: {
TeamRowView(team: team, displayCallDate: false)
}
.disabled(spots.isEmpty || isRoundValidForSeeding == false)
}
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
}
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
} footer: {
if spots.isEmpty || isRoundValidForSeeding == false {
Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.")
.foregroundStyle(.red)
}
}
}
}
struct MatchSpot: SpinDrawable { struct MatchSpot: SpinDrawable {
let match: Match let match: Match

@ -9,111 +9,81 @@ import SwiftUI
struct TeamRestingView: View { struct TeamRestingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var sortingMode: SortingMode = .restingTime @State private var displayMode: DisplayMode = .teams
@State private var selectedCourt: Int? @State private var selectedCourt: Int?
@State private var readyMatches: [Match] = [] @State private var readyMatches: [Match] = []
@State private var matchesLeft: [Match] = [] @State private var matchesLeft: [Match] = []
@State private var teams: [TeamRegistration] = []
enum SortingMode: Int, Identifiable, CaseIterable { enum DisplayMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
case index case teams
case restingTime case restingTime
case court
func localizedSortingModeLabel() -> String { func localizedSortingModeLabel() -> String {
switch self { switch self {
case .index: case .teams:
return "Ordre" return "Équipes"
case .court:
return "Terrain"
case .restingTime: case .restingTime:
return "Repos" return "Matchs"
} }
} }
} }
var sortingModeCases: [SortingMode] {
var sortingModes = [SortingMode]()
sortingModes.append(.index)
sortingModes.append(.restingTime)
sortingModes.append(.court)
return sortingModes
}
func contentUnavailableDescriptionLabel() -> String { func contentUnavailableDescriptionLabel() -> String {
switch sortingMode { switch displayMode {
case .index:
return "Ce tournoi n'a aucun match prêt à démarrer"
case .restingTime: case .restingTime:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucun match prêt à démarrer"
case .court: case .teams:
return "Ce tournoi n'a aucun match prêt à démarrer" return "Ce tournoi n'a aucune équipe ayant déjà terminé un match."
} }
} }
var sortedMatches: [Match] { var sortedMatches: [Match] {
switch sortingMode { return readyMatches.sorted(by: \.restingTimeForSorting)
case .index: }
return readyMatches
case .restingTime: var sortedTeams: [TeamRegistration] {
return readyMatches.sorted(by: \.restingTimeForSorting) return teams
case .court:
return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending)
}
} }
var body: some View { var body: some View {
List { List {
Section { Section {
Picker(selection: $selectedCourt) { switch displayMode {
Text("Aucun").tag(nil as Int?) case .teams:
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in if sortedTeams.isEmpty == false {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?) ForEach(sortedTeams) { team in
TeamRowView(team: team, displayRestingTime: true)
}
} else {
ContentUnavailableView("Aucune équipe en repos", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
} }
} label: { case .restingTime:
Text("Sur le terrain") if sortedMatches.isEmpty == false {
} ForEach(sortedMatches) { match in
// MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
// Toggle(isOn: $checkCanPlay) { }
// if isFree { } else {
// Text("Vérifier le paiement ou la présence") ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
// } else {
// Text("Vérifier la présence")
// }
// }
// } footer: {
// if isFree {
// Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
// } else {
// Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// }
}
Section {
if sortedMatches.isEmpty == false {
ForEach(sortedMatches) { match in
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
} }
} else {
ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
} }
} header: { } header: {
Picker(selection: $sortingMode) { Picker(selection: $displayMode) {
ForEach(sortingModeCases) { sortingMode in ForEach(DisplayMode.allCases) { sortingMode in
Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode)
} }
} label: { } label: {
Text("Méthode de tri") Text("Affichage")
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.segmented) .pickerStyle(.segmented)
} }
.headerProminence(.increased)
.textCase(nil) .textCase(nil)
} }
.toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Temps de repos")
.navigationTitle("Match à suivre")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.onAppear { .onAppear {
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let matchesLeft = tournament.matchesLeft(allMatches) let matchesLeft = tournament.matchesLeft(allMatches)
@ -121,6 +91,7 @@ struct TeamRestingView: View {
let readyMatches = tournament.readyMatches(allMatches) let readyMatches = tournament.readyMatches(allMatches)
self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)
} }
} }
} }

@ -12,66 +12,114 @@ struct TeamRowView: View {
var team: TeamRegistration var team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { TeamHeadlineView(team: team)
if let groupStage = team.groupStageObject() { TeamView(team: team)
HStack { }
Text(groupStage.groupStageTitle(.title)) if displayCallDate {
if let finalPosition = groupStage.finalPosition(ofTeam: team) { TeamCallDateView(team: team)
Text((finalPosition + 1).ordinalFormatted()) }
} if displayRestingTime {
} TeamRestingView(team: team)
} else if let round = team.initialRound() { }
Text(round.roundTitle(.wide)) }
} }
if let wildcardLabel = team.wildcardLabel() { struct TeamRestingView: View {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) let team: TeamRegistration
}
@ViewBuilder
var body: some View {
if let restingTime = team.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) {
if restingTime > -300 {
Text("vient de finir")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
Text("en repos depuis " + value)
.font(.footnote)
.foregroundStyle(.secondary)
} }
}
}
}
if let name = team.name, name.isEmpty == false { struct TeamView: View {
Text(name).foregroundStyle(.secondary).font(.footnote) let team: TeamRegistration
if team.players().isEmpty {
Text("Aucun joueur") var body: some View {
} else { if let name = team.name, name.isEmpty == false {
ForEach(team.players()) { player in Text(name).foregroundStyle(.secondary).font(.footnote)
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) if team.players().isEmpty {
} Text("Aucun joueur")
}
} else { } else {
if team.players().isEmpty == false { CompactTeamView(team: team)
ForEach(team.players()) { player in }
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) } else {
if team.players().isEmpty == false {
CompactTeamView(team: team)
} else {
Text("Place réservée")
Text("Place réservée")
}
}
}
}
struct TeamHeadlineView: View {
let team: TeamRegistration
var body: some View {
HStack {
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle(.title))
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
} }
} else {
Text("Place réservée")
Text("Place réservée")
} }
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
} }
}
if displayCallDate { if let wildcardLabel = team.wildcardLabel() {
if let callDate = team.callDate { Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption)
Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.logoRed)
.italic()
.font(.caption)
} else {
Text("Pas encore convoquée")
.foregroundStyle(.logoRed)
.italic()
.font(.caption)
} }
} }
} }
} }
}
//#Preview { struct TeamCallDateView: View {
// TeamRowView(team: TeamRegistration.mock()) let team: TeamRegistration
//}
var body: some View {
if let callDate = team.callDate {
Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.logoRed)
.italic()
.font(.caption)
} else {
Text("Pas encore convoquée")
.foregroundStyle(.logoRed)
.italic()
.font(.caption)
}
}
}
struct CompactTeamView: View {
let team: TeamRegistration
var body: some View {
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
}
}
}
}

@ -369,4 +369,22 @@ final class ServerDataTests: XCTestCase {
} }
func testDrawLog() async throws {
let tournament: [Tournament] = try await StoreCenter.main.service().get()
guard let tournamentId = tournament.first?.id else {
assertionFailure("missing tournament in database")
return
}
let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two)
let d: DrawLog = try await StoreCenter.main.service().post(drawLog)
assert(d.tournament == drawLog.tournament)
assert(d.drawDate.formatted() == drawLog.drawDate.formatted())
assert(d.drawSeed == drawLog.drawSeed)
assert(d.drawTeamPosition == drawLog.drawTeamPosition)
assert(d.drawMatchIndex == drawLog.drawMatchIndex)
}
} }

Loading…
Cancel
Save