xcode16
Raz 1 year ago
parent a442cd934d
commit 9a2d293b41
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 5
      PadelClub/Data/GroupStage.swift
  3. 45
      PadelClub/Data/Match.swift
  4. 45
      PadelClub/Data/Round.swift
  5. 36
      PadelClub/Data/Tournament.swift
  6. 29
      PadelClub/Extensions/Array+Extensions.swift
  7. 7
      PadelClub/Utils/PadelRule.swift
  8. 4
      PadelClub/ViewModel/SeedInterval.swift
  9. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  10. 50
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  11. 19
      PadelClub/Views/GroupStage/GroupStagesView.swift
  12. 122
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  13. 85
      PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift
  14. 4
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  15. 61
      PadelClub/Views/Match/MatchSetupView.swift
  16. 27
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  17. 8
      PadelClub/Views/Round/LoserRoundView.swift
  18. 4
      PadelClub/Views/Round/LoserRoundsView.swift
  19. 14
      PadelClub/Views/Round/RoundSettingsView.swift
  20. 66
      PadelClub/Views/Team/TeamPickerView.swift
  21. 11
      PadelClub/Views/Team/TeamRowView.swift
  22. 39
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift

@ -79,7 +79,6 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; };
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -147,6 +146,7 @@
FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */; }; FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */; };
FF6087EA2BE25EF1004E1E47 /* TournamentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */; }; FF6087EA2BE25EF1004E1E47 /* TournamentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */; };
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; };
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; };
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.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 */; };
@ -425,7 +425,6 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserGroupStageSettingsView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -493,6 +492,7 @@
FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSeedEditing.swift; sourceTree = "<group>"; }; FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSeedEditing.swift; sourceTree = "<group>"; };
FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatusView.swift; sourceTree = "<group>"; }; FF6087E92BE25EF1004E1E47 /* TournamentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatusView.swift; sourceTree = "<group>"; };
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; }; FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; };
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@ -1196,7 +1196,7 @@
FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */,
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */, FF5DA18E2BB9268800A33061 /* GroupStagesSettingsView.swift */,
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */, FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */,
FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3932BE3625D00C2E883 /* Components */,
FF9AC3922BE3625200C2E883 /* Shared */, FF9AC3922BE3625200C2E883 /* Shared */,
); );
@ -1594,7 +1594,6 @@
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */,
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */, FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */,
FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */,
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */,
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */,
C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */, C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */,
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */, FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */,
@ -1689,6 +1688,7 @@
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */,
FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */, FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,

@ -384,6 +384,11 @@ final class GroupStage: ModelObject, Storable {
return data.joined(separator: "\n") return data.joined(separator: "\n")
} }
func finalPosition(ofTeam team: TeamRegistration) -> Int? {
guard hasEnded() else { return nil }
return teams(true).firstIndex(of: team)
}
override func deleteDependencies() throws { override func deleteDependencies() throws {
let matches = self._matches() let matches = self._matches()
for match in matches { for match in matches {

@ -121,6 +121,10 @@ defer {
print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
if roundObject?.groupStageLoserBracket == true {
return "\(index)\(index.ordinalFormattedSuffix()) place"
}
if let groupStageObject { if let groupStageObject {
return groupStageObject.localizedMatchUpLabel(for: index) return groupStageObject.localizedMatchUpLabel(for: index)
} }
@ -361,9 +365,17 @@ defer {
return false return false
} }
func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) { func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
//if disabled == state { return } //if disabled == state { return }
disabled = state disabled = state
if disabled {
do {
try self.tournamentStore.teamScores.delete(contentOfs: teamScores)
} catch {
Logger.error(error)
}
}
if state == true { if state == true {
let teams = teams() let teams = teams()
for team in teams { for team in teams {
@ -384,12 +396,14 @@ defer {
Logger.error(error) Logger.error(error)
} }
_toggleLoserMatchDisableState(state) if single == false {
if forward { _toggleLoserMatchDisableState(state)
_toggleForwardMatchDisableState(state) if forward {
} else { _toggleForwardMatchDisableState(state)
topPreviousRoundMatch()?._toggleMatchDisableState(state) } else {
bottomPreviousRoundMatch()?._toggleMatchDisableState(state) topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
}
} }
} }
@ -837,7 +851,22 @@ defer {
} }
var isLoserBracket: Bool { var isLoserBracket: Bool {
return roundObject?.parent != nil if let roundObject {
if roundObject.parent != nil || roundObject.groupStageLoserBracket {
return true
}
}
return false
}
var matchType: MatchType {
if isLoserBracket {
return .loserBracket
} else if isGroupStage() {
return .groupStage
} else {
return .bracket
}
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {

@ -22,13 +22,15 @@ final class Round: ModelObject, Storable {
var parent: String? var parent: String?
private(set) var format: MatchFormat? private(set) var format: MatchFormat?
var startDate: Date? var startDate: Date?
var groupStageLoserBracket: Bool = false
internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil) { internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false) {
self.tournament = tournament self.tournament = tournament
self.index = index self.index = index
self.parent = parent self.parent = parent
self.format = matchFormat self.format = matchFormat
self.startDate = startDate self.startDate = startDate
self.groupStageLoserBracket = groupStageLoserBracket
} }
// MARK: - Computed dependencies // MARK: - Computed dependencies
@ -67,7 +69,7 @@ final class Round: ModelObject, Storable {
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
if parent == nil { if isUpperBracket() {
return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false
} else { } else {
return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false
@ -184,13 +186,13 @@ defer {
case .two: case .two:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
return luckyLoser.team return luckyLoser.team
} else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { } else if groupStageLoserBracket == false, let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) {
if let teamId = previousMatch.winningTeamId { if let teamId = previousMatch.winningTeamId {
return self.tournamentStore.teamRegistrations.findById(teamId) return self.tournamentStore.teamRegistrations.findById(teamId)
} else if previousMatch.disabled { } else if previousMatch.disabled {
return previousMatch.teams().first return previousMatch.teams().first
} }
} else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { } else if groupStageLoserBracket == false, let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId {
return tournamentStore.findById(parent) return tournamentStore.findById(parent)
} }
} }
@ -292,7 +294,7 @@ defer {
// } // }
func playedMatches() -> [Match] { func playedMatches() -> [Match] {
if parent == nil { if isUpperBracket() {
return enabledMatches() return enabledMatches()
} else { } else {
return _matches() return _matches()
@ -477,15 +479,19 @@ defer {
} }
#endif #endif
if parent == nil { if isUpperBracket() {
if index == 0 { return SeedInterval(first: 1, last: 2) } if index == 0 { return SeedInterval(first: 1, last: 2) }
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter {
$0.bracketPosition != nil $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
} }
let playedMatches = playedMatches()
let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) let playedMatches = playedMatches().count
let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2
//print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count)
let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count)
//print(seedInterval.localizedLabel())
return seedInterval return seedInterval
} }
@ -496,7 +502,7 @@ defer {
return previousRound.seedInterval(initialMode: initialMode) return previousRound.seedInterval(initialMode: initialMode)
} }
} else if let parentRound { } else if let parentRound {
if parentRound.parent == nil { if parentRound.isUpperBracket() {
return parentRound.seedInterval(initialMode: initialMode) return parentRound.seedInterval(initialMode: initialMode)
} }
return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last
@ -506,6 +512,10 @@ defer {
} }
func roundTitle(_ displayStyle: DisplayStyle = .wide, initialMode: Bool = false) -> String { func roundTitle(_ displayStyle: DisplayStyle = .wide, initialMode: Bool = false) -> String {
if groupStageLoserBracket {
return "Classement Poules"
}
if parent != nil { if parent != nil {
if let seedInterval = seedInterval(initialMode: initialMode) { if let seedInterval = seedInterval(initialMode: initialMode) {
return seedInterval.localizedLabel(displayStyle) return seedInterval.localizedLabel(displayStyle)
@ -557,11 +567,11 @@ defer {
} }
func isUpperBracket() -> Bool { func isUpperBracket() -> Bool {
return parent == nil return parent == nil && groupStageLoserBracket == false
} }
func isLoserBracket() -> Bool { func isLoserBracket() -> Bool {
return parent != nil return parent != nil || groupStageLoserBracket
} }
func deleteLoserBracket() { func deleteLoserBracket() {
@ -670,6 +680,18 @@ defer {
case _parent = "parent" case _parent = "parent"
case _format = "format" case _format = "format"
case _startDate = "startDate" case _startDate = "startDate"
case _groupStageLoserBracket = "groupStageLoserBracket"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: ._id)
tournament = try container.decode(String.self, forKey: ._tournament)
index = try container.decode(Int.self, forKey: ._index)
parent = try container.decodeIfPresent(String.self, forKey: ._parent)
format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -678,6 +700,7 @@ defer {
try container.encode(id, forKey: ._id) try container.encode(id, forKey: ._id)
try container.encode(tournament, forKey: ._tournament) try container.encode(tournament, forKey: ._tournament)
try container.encode(index, forKey: ._index) try container.encode(index, forKey: ._index)
try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket)
if let parent = parent { if let parent = parent {
try container.encode(parent, forKey: ._parent) try container.encode(parent, forKey: ._parent)

@ -809,7 +809,7 @@ defer {
} }
func rounds() -> [Round] { func rounds() -> [Round] {
let rounds: [Round] = self.tournamentStore.rounds.filter { $0.parent == nil } let rounds: [Round] = self.tournamentStore.rounds.filter { $0.isUpperBracket() }
return rounds.sorted(by: \.index).reversed() return rounds.sorted(by: \.index).reversed()
} }
@ -1224,7 +1224,7 @@ defer {
_removeStrings(from: &teams, stringsToRemove: disabledIds) _removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.last] = disabledIds teams[interval.last] = disabledIds
let teamNames : [String] = disabledIds.compactMap { let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0) let t : TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
return t return t
}.map { $0.canonicalName } }.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.last) : ", teamNames) print("winners.isEmpty", "\(interval.last) : ", teamNames)
@ -1237,7 +1237,7 @@ defer {
_removeStrings(from: &teams, stringsToRemove: winners) _removeStrings(from: &teams, stringsToRemove: winners)
teams[interval.first + winners.count - 1] = winners teams[interval.first + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap { let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0) let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
return t return t
}.map { $0.canonicalName } }.map { $0.canonicalName }
print("winners", "\(interval.last + winners.count - 1) : ", teamNames) print("winners", "\(interval.last + winners.count - 1) : ", teamNames)
@ -1246,25 +1246,32 @@ defer {
if losers.isEmpty == false { if losers.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: losers) _removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.last] = losers teams[interval.first + winners.count] = losers
let loserTeamNames : [String] = losers.compactMap { let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0) let t: TeamRegistration? = tournamentStore.teamRegistrations.findById($0)
return t return t
}.map { $0.canonicalName } }.map { $0.canonicalName }
print("losers", "\(interval.last) : ", loserTeamNames) print("losers", "\(interval.first + winners.count) : ", loserTeamNames)
losers.forEach { ids.insert($0) } losers.forEach { ids.insert($0) }
} }
} }
} }
} }
groupStageLoserBracket()?.playedMatches().forEach({ match in
if match.hasEnded() {
teams.setOrAppend(match.winningTeamId, at: match.index)
teams.setOrAppend(match.losingTeamId, at: match.index + 1)
}
})
let groupStages = groupStages() let groupStages = groupStages()
let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 }))
groupStages.forEach { groupStage in groupStages.forEach { groupStage in
let groupStageTeams = groupStage.teams(true) let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() { for (index, team) in groupStageTeams.enumerated() {
if team.qualified == false { if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1 let _index = baseRank + groupStageWidth + 1
@ -1934,10 +1941,7 @@ defer {
} }
func tournamentWinner() -> TeamRegistration? { func tournamentWinner() -> TeamRegistration? {
let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.parent == nil }) let finals: Round? = self.tournamentStore.rounds.first(where: { $0.index == 0 && $0.isUpperBracket() })
// let rounds: [Round] = Store.main.filter(isIncluded: { $0.index == 0 && $0.tournament == id && $0.parent == nil })
// let final: Round? = .first
return finals?.playedMatches().first?.winner() return finals?.playedMatches().first?.winner()
} }
@ -1998,6 +2002,14 @@ defer {
return (Array(shouldBeInIt), Array(shouldNotBeInIt)) return (Array(shouldBeInIt), Array(shouldNotBeInIt))
} }
func groupStageLoserBracket() -> Round? {
tournamentStore.rounds.first(where: { $0.groupStageLoserBracket })
}
func groupStageLoserBracketsInitialPlace() -> Int {
selectedSortedTeams().filter({ $0.qualified || $0.bracketPosition != nil }).count + 1
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {

@ -18,6 +18,16 @@ extension Array {
return first(where: { p($0) }) != nil return first(where: { p($0) }) != nil
//return !self.allSatisfy { !p($0) } //return !self.allSatisfy { !p($0) }
} }
// Check if the number of elements in the sequence is even
var isEven: Bool {
return self.count % 2 == 0
}
// Check if the number of elements in the sequence is odd
var isOdd: Bool {
return self.count % 2 != 0
}
} }
extension Array where Element: Equatable { extension Array where Element: Equatable {
@ -49,3 +59,22 @@ extension Array where Element: CustomStringConvertible {
} }
} }
extension Dictionary where Key == Int, Value == [String] {
mutating func setOrAppend(_ element: String?, at key: Int) {
// Check if the element is nil; do nothing if it is
guard let element = element else {
return
}
// Check if the key exists in the dictionary
if var array = self[key] {
// If it exists, append the element to the array
array.append(element)
self[key] = array
} else {
// If it doesn't exist, create a new array with the element
self[key] = [element]
}
}
}

@ -291,6 +291,13 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
self.init(rawValue: value) self.init(rawValue: value)
} }
func pointsRange(first: Int, last: Int, teamsCount: Int) -> String {
let range = [points(for: last - 1, count: teamsCount),
points(for: first - 1, count: teamsCount)]
return range.map { $0.formatted(.number.sign(strategy: .always())) }.joined(separator: " / ") + " pts"
}
func hideWeight() -> Bool { func hideWeight() -> Bool {
switch self { switch self {
case .unlisted: case .unlisted:

@ -12,9 +12,7 @@ struct SeedInterval: Hashable, Comparable {
let last: Int let last: Int
func pointsRange(tournamentLevel: TournamentLevel, teamsCount: Int) -> String { func pointsRange(tournamentLevel: TournamentLevel, teamsCount: Int) -> String {
let range = [tournamentLevel.points(for: last - 1, count: teamsCount), tournamentLevel.pointsRange(first: first, last: last, teamsCount: teamsCount)
tournamentLevel.points(for: first - 1, count: teamsCount)]
return range.map { $0.formatted(.number.sign(strategy: .always())) }.joined(separator: " / ") + " pts"
} }
static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool { static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool {

@ -202,7 +202,7 @@ struct GroupStageView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("#\(index + 1)") Text("#\(index + 1)")
.font(.caption) .font(.caption)
TeamPickerView(groupStagePosition: index, teamPicked: { team in TeamPickerView(groupStagePosition: index, matchTypeContext: .groupStage, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
team.groupStage = groupStage.id team.groupStage = groupStage.id
team.groupStagePosition = index team.groupStagePosition = index

@ -73,6 +73,30 @@ struct GroupStagesSettingsView: View {
} }
} }
Section {
if tournament.groupStageLoserBracket() == nil {
RowButtonView("Ajouter des matchs de classements", role: .destructive) {
let round = Round(tournament: tournament.id, index: 0, matchFormat: tournament.loserRoundFormat, groupStageLoserBracket: true)
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
}
} else if let groupStageLoserBracket = tournament.groupStageLoserBracket() {
RowButtonView("Supprimer les matchs de classements", role: .destructive) {
do {
try groupStageLoserBracket.deleteDependencies()
try tournamentStore.rounds.delete(instance: groupStageLoserBracket)
} catch {
Logger.error(error)
}
}
}
}
#if DEBUG #if DEBUG
Section { Section {
RowButtonView("delete all group stages") { RowButtonView("delete all group stages") {
@ -81,12 +105,6 @@ struct GroupStagesSettingsView: View {
} }
#endif #endif
// NavigationLink {
// LoserGroupStageSettingsView(tournament: tournament)
// } label: {
// Text("Match de perdant de poules")
// }
Section { Section {
RowButtonView("Retirer tous les horaires", role: .destructive) { RowButtonView("Retirer tous les horaires", role: .destructive) {
let matches = tournament.groupStages().flatMap({ $0._matches() }) let matches = tournament.groupStages().flatMap({ $0._matches() })
@ -156,6 +174,26 @@ struct GroupStagesSettingsView: View {
} }
} }
Section {
RowButtonView("Retirer tout le monde", role: .destructive) {
tournament.groupStages().forEach { groupStage in
let teams = groupStage.teams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
groupStage._matches().forEach({ $0.updateTeamScores() })
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
}
}
} footer: {
Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.")
}
} }
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
if generationDone { if generationDone {

@ -18,12 +18,15 @@ struct GroupStagesView: View {
} }
case all(Tournament) case all(Tournament)
case loserBracket(Round)
case groupStage(GroupStage) case groupStage(GroupStage)
var id: String { var id: String {
switch self { switch self {
case .all: case .all:
return "all-group-stage" return "all-group-stage"
case .loserBracket(let loserBracket):
return loserBracket.id
case .groupStage(let groupStage): case .groupStage(let groupStage):
return groupStage.id return groupStage.id
} }
@ -33,6 +36,8 @@ struct GroupStagesView: View {
switch self { switch self {
case .all: case .all:
return "Tout" return "Tout"
case .loserBracket:
return "Classement"
case .groupStage(let groupStage): case .groupStage(let groupStage):
return groupStage.groupStageTitle() return groupStage.groupStageTitle()
} }
@ -42,6 +47,8 @@ struct GroupStagesView: View {
switch self { switch self {
case .all: case .all:
return nil return nil
case .loserBracket(let loserBracket):
return loserBracket.badgeValue()
case .groupStage(let groupStage): case .groupStage(let groupStage):
return groupStage.badgeValue() return groupStage.badgeValue()
} }
@ -59,6 +66,8 @@ struct GroupStagesView: View {
} else { } else {
return nil return nil
} }
case .loserBracket(let loserBracket):
return loserBracket.badgeImage()
case .groupStage(let groupStage): case .groupStage(let groupStage):
return groupStage.badgeImage() return groupStage.badgeImage()
} }
@ -69,9 +78,10 @@ struct GroupStagesView: View {
tournament.groupStagesMatches() tournament.groupStagesMatches()
} }
@State private var isEditingLoserBracketGroupStage: Bool
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
if tournament.shouldVerifyGroupStage { if tournament.shouldVerifyGroupStage {
_selectedDestination = State(wrappedValue: nil) _selectedDestination = State(wrappedValue: nil)
} else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty { } else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty {
@ -82,11 +92,15 @@ struct GroupStagesView: View {
_selectedDestination = State(wrappedValue: .groupStage(gs)) _selectedDestination = State(wrappedValue: .groupStage(gs))
} }
} }
_isEditingLoserBracketGroupStage = .init(wrappedValue: tournament.groupStageLoserBracket()?._matches().isEmpty ?? false)
} }
func allDestinations() -> [GroupStageDestination] { func allDestinations() -> [GroupStageDestination] {
var allDestinations : [GroupStageDestination] = [.all(tournament)] var allDestinations : [GroupStageDestination] = [.all(tournament)]
let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) } let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) }
if let loserBracket = tournament.groupStageLoserBracket() {
allDestinations.insert(.loserBracket(loserBracket), at: 0)
}
allDestinations.append(contentsOf: groupStageDestinations) allDestinations.append(contentsOf: groupStageDestinations)
return allDestinations return allDestinations
} }
@ -142,6 +156,9 @@ struct GroupStagesView: View {
.navigationTitle("Toutes les poules") .navigationTitle("Toutes les poules")
case .groupStage(let groupStage): case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage).id(groupStage.id) GroupStageView(groupStage: groupStage).id(groupStage.id)
case .loserBracket(let loserBracket):
LoserBracketFromGroupStageView(loserBracket: loserBracket).id(loserBracket.id)
.environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage)
case nil: case nil:
GroupStagesSettingsView() GroupStagesSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")

@ -0,0 +1,122 @@
//
// LoserBracketFromGroupStageView.swift
// PadelClub
//
// Created by razmig on 07/09/2024.
//
import SwiftUI
import LeStorage
struct LoserBracketFromGroupStageView: View {
@Environment(\.isEditingTournamentSeed) var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State var loserBracket: Round
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
var body: some View {
List {
let displayableMatches = loserBracket.playedMatches().sorted(by: \.index)
if isEditingTournamentSeed.wrappedValue == true {
Section {
RowButtonView("Ajouter un match", role: .destructive) {
let placeCount = tournament.groupStageLoserBracketsInitialPlace() + displayableMatches.count * 2
let match = Match(round: loserBracket.id, index: placeCount, matchFormat: loserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place"
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
}
}
}
ForEach(displayableMatches) { match in
Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} header: {
let tournamentTeamCount = tournament.teamCount
let seedIntervalPointRange = tournament.tournamentLevel.pointsRange(first: match.index, last: match.index + 1, teamsCount: tournamentTeamCount)
HStack {
Text(match.matchTitle(.wide))
Spacer()
Text(seedIntervalPointRange)
.font(.caption)
}
} footer: {
if isEditingTournamentSeed.wrappedValue == true {
HStack {
if match.index > tournament.groupStageLoserBracketsInitialPlace() {
FooterButtonView("même place qu'au-dessus") {
match.index -= 2
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
}
Spacer()
}
FooterButtonView("effacer", role: .destructive) {
do {
try match.deleteDependencies()
try tournamentStore.matches.delete(instance: match)
} catch {
Logger.error(error)
}
}
}
}
}
}
Section {
if displayableMatches.isEmpty == false && isEditingTournamentSeed.wrappedValue == true {
Section {
RowButtonView("Effacer tous les matchs", role: .destructive) {
_deleteAllMatches()
}
} footer: {
Text("Efface tous les matchs de classement de poules ci-dessus.")
}
}
}
}
.headerProminence(.increased)
.navigationTitle("Classement de poules")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
if isEditingTournamentSeed.wrappedValue == true {
isEditingTournamentSeed.wrappedValue = false
} else {
isEditingTournamentSeed.wrappedValue = true
}
}
}
}
}
private func _deleteAllMatches() {
let displayableMatches = loserBracket.playedMatches().sorted(by: \.index)
do {
for match in displayableMatches {
try match.deleteDependencies()
}
try tournamentStore.matches.delete(contentOfs: displayableMatches)
} catch {
Logger.error(error)
}
}
}

@ -1,85 +0,0 @@
//
// LoserGroupStageSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/06/2024.
//
import SwiftUI
extension Round {
var isGroupStageLoserBracket: Bool {
return false
}
}
extension Tournament {
func groupStageLoserBrackets() -> [Round] {
[]
}
func removeGroupStageLoserBrackets() {
}
}
struct LoserGroupStageSettingsView: View {
var tournament: Tournament
@State private var loserGroupStageBracketType: Int? = nil
@State private var losers : Set<TeamRegistration> = Set()
@Environment(\.editMode) private var editMode
var body: some View {
List(selection: $losers) {
if tournament.groupStageLoserBrackets().isEmpty == false {
//for each all rounds without parent and loserGroupStage, ability to delete them
Section {
RowButtonView("Effacer", role: .destructive) {
tournament.removeGroupStageLoserBrackets()
}
}
}
if self.editMode?.wrappedValue == .active {
Section {
//rajouter + toolbar valider / cancel
ForEach(tournament.groupStageTeams().filter({ $0.qualified == false })) { team in
TeamRowView(team: team).tag(team)
}
} header: {
Text("Sélection des perdants de poules")
}
} else {
Section {
RowButtonView("Ajouter un match de perdant") {
self.editMode?.wrappedValue = .active
}
} footer: {
Text("Permet d'ajouter un match de perdant de poules.")
}
}
}
.toolbar {
if self.editMode?.wrappedValue == .active {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
self.editMode?.wrappedValue = .inactive
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Valider") {
self.editMode?.wrappedValue = .inactive
//tournament.createGroupStageLoserBracket()
}
}
}
}
.navigationTitle("Match de perdant de poules")
.navigationBarBackButtonHidden(self.editMode?.wrappedValue == .active)
.navigationBarTitleDisplayMode(.inline)
.toolbar(.visible, for: .navigationBar)
.headerProminence(.increased)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct PlayerBlockView: View { struct PlayerBlockView: View {
var match: Match @State var match: Match
let teamPosition: TeamPosition let teamPosition: TeamPosition
let team: TeamRegistration? let team: TeamRegistration?
let color: Color let color: Color
@ -52,7 +52,7 @@ struct PlayerBlockView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let names { if let names {
if let teamScore, teamScore.luckyLoser != nil { if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false {
Text("Repêchée").italic().font(.caption) Text("Repêchée").italic().font(.caption)
} }

@ -13,13 +13,17 @@ struct MatchSetupView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var match: Match @State var match: Match
@State private var seedGroup: SeedInterval? @State private var seedGroup: SeedInterval?
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return match.tournamentStore return match.tournamentStore
} }
var matchTypeContext: MatchType {
match.matchType
}
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
ForEach(TeamPosition.allCases) { teamPosition in ForEach(TeamPosition.allCases) { teamPosition in
@ -38,7 +42,7 @@ struct MatchSetupView: View {
if let team, teamScore?.walkOut == nil { if let team, teamScore?.walkOut == nil {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if let teamScore, teamScore.luckyLoser != nil { if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false {
Text("Repêchée").italic().font(.caption) Text("Repêchée").italic().font(.caption)
} }
Menu { Menu {
@ -59,9 +63,9 @@ struct MatchSetupView: View {
} }
HStack { HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil { if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition) match.setLuckyLoser(team: team, teamPosition: teamPosition)
do { do {
try tournamentStore.matches.addOrUpdate(instance: match) try tournamentStore.matches.addOrUpdate(instance: match)
@ -82,7 +86,7 @@ struct MatchSetupView: View {
} }
} }
}) })
if let tournament = match.currentTournament() { if matchTypeContext == .bracket, let tournament = match.currentTournament() {
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
let availableSeedGroups = tournament.availableSeedGroups() let availableSeedGroups = tournament.availableSeedGroups()
Text("ou") Text("ou")
@ -146,30 +150,33 @@ struct MatchSetupView: View {
.underline() .underline()
} }
.disabled(availableSeedGroups.isEmpty && walkOutSpot == false && availableQualifiedTeams.isEmpty) .disabled(availableSeedGroups.isEmpty && walkOutSpot == false && availableQualifiedTeams.isEmpty)
Spacer()
if match.isSeedLocked(atTeamPosition: teamPosition) { if matchTypeContext == .bracket {
Button { Spacer()
match.unlockSeedPosition(atTeamPosition: teamPosition) if match.isSeedLocked(atTeamPosition: teamPosition) {
do { Button {
try tournamentStore.matches.addOrUpdate(instance: match) match.unlockSeedPosition(atTeamPosition: teamPosition)
} catch { do {
Logger.error(error) try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
} label: {
Text("Libérer")
.underline()
} }
} label: { } else {
Text("Libérer") ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) {
.underline() _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
} do {
} else { try tournamentStore.matches.addOrUpdate(instance: match)
ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { } catch {
_ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) Logger.error(error)
do { }
try tournamentStore.matches.addOrUpdate(instance: match) } label: {
} catch { Text("Réserver")
Logger.error(error) .underline()
} }
} label: {
Text("Réserver")
.underline()
} }
} }
} }

@ -16,11 +16,31 @@ struct LoserRoundSettingsView: View {
var body: some View { var body: some View {
List { List {
Section {
RowButtonView(isEditingTournamentSeed.wrappedValue == true ? "Terminer l'édition" : "Éditer les tours joués") {
isEditingTournamentSeed.wrappedValue.toggle()
}
}
Section {
RowButtonView("Synchroniser les noms des matchs") {
let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches
})
allRoundMatches.forEach({ $0.name = $0.roundTitle() })
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch {
Logger.error(error)
}
}
}
Section { Section {
RowButtonView("Effacer les matchs de classements", role: .destructive) { RowButtonView("Effacer les matchs de classements", role: .destructive) {
upperBracketRound.round.deleteLoserBracket() upperBracketRound.round.deleteLoserBracket()
} }
} }
.disabled(upperBracketRound.round.loserRounds().isEmpty)
Section { Section {
RowButtonView("Créer les matchs de classements", role: .destructive) { RowButtonView("Créer les matchs de classements", role: .destructive) {
@ -30,12 +50,7 @@ struct LoserRoundSettingsView: View {
} }
} }
} }
.disabled(upperBracketRound.round.loserRounds().isEmpty == false)
Section {
RowButtonView(isEditingTournamentSeed.wrappedValue == true ? "Terminer l'édition" : "Éditer les tours joués") {
isEditingTournamentSeed.wrappedValue.toggle()
}
}
//todo proposer ici l'impression des matchs de classements peut-être? //todo proposer ici l'impression des matchs de classements peut-être?
} }

@ -53,13 +53,13 @@ struct LoserRoundView: View {
if isEditingTournamentSeed.wrappedValue == true { if isEditingTournamentSeed.wrappedValue == true {
RowButtonView(match.disabled ? "Jouer ce match" : "Ne pas jouer ce match", role: .destructive) { RowButtonView(match.disabled ? "Jouer ce match" : "Ne pas jouer ce match", role: .destructive) {
match._toggleMatchDisableState(!match.disabled) match._toggleMatchDisableState(!match.disabled, single: true)
} }
} }
} }
} header: { } header: {
HStack { HStack {
if let seedInterval = loserRound.seedInterval() { if let seedInterval = loserRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) {
Text(seedInterval.localizedLabel(.wide)) Text(seedInterval.localizedLabel(.wide))
let seedIntervalPointRange = seedInterval.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournament.teamCount) let seedIntervalPointRange = seedInterval.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournament.teamCount)
Spacer() Spacer()
@ -67,13 +67,13 @@ struct LoserRoundView: View {
.font(.caption) .font(.caption)
} else { } else {
if let previousRound = loserRound.previousRound() { if let previousRound = loserRound.previousRound() {
if let seedInterval = previousRound.seedInterval() { if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) {
Text(seedInterval.localizedLabel()) Text(seedInterval.localizedLabel())
} else { } else {
Text("no previous round") Text("no previous round")
} }
} else if let parentRound = loserRound.parentRound { } else if let parentRound = loserRound.parentRound {
if let seedInterval = parentRound.seedInterval() { if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) {
Text(seedInterval.localizedLabel()) Text(seedInterval.localizedLabel())
} else { } else {
Text("no parent round") Text("no parent round")

@ -167,6 +167,10 @@ struct LoserRoundsView: View {
init(upperBracketRound: UpperRound) { init(upperBracketRound: UpperRound) {
self.upperBracketRound = upperBracketRound self.upperBracketRound = upperBracketRound
_selectedRound = State(wrappedValue: upperBracketRound.loserRounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? upperBracketRound.loserRounds.first(where: { $0.shouldBeDisplayed })) _selectedRound = State(wrappedValue: upperBracketRound.loserRounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? upperBracketRound.loserRounds.first(where: { $0.shouldBeDisplayed }))
if upperBracketRound.loserRounds.allSatisfy({ $0.shouldBeDisplayed == false }) {
_isEditingTournamentSeed = .init(wrappedValue: true)
}
} }
var destinations: [LoserRound] { var destinations: [LoserRound] {

@ -92,7 +92,7 @@ struct RoundSettingsView: View {
Section { Section {
let roundIndex = tournament.rounds().count let roundIndex = tournament.rounds().count
RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) { RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex), role: .destructive) {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
@ -153,6 +153,18 @@ struct RoundSettingsView: View {
} }
} }
} }
Section {
RowButtonView("Synchroniser les noms des matchs") {
let allRoundMatches = tournament.allRoundMatches()
allRoundMatches.forEach({ $0.name = $0.roundTitle() })
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch {
Logger.error(error)
}
}
}
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {

@ -14,8 +14,11 @@ struct TeamPickerView: View {
@State private var confirmTeam: TeamRegistration? @State private var confirmTeam: TeamRegistration?
@State private var presentTeamPickerView: Bool = false @State private var presentTeamPickerView: Bool = false
@State private var searchField: String = "" @State private var searchField: String = ""
@State private var sortOrder: SortOrder = .ascending
var shouldConfirm: Bool = false var shouldConfirm: Bool = false
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var matchTypeContext: MatchType = .bracket
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
@ -43,40 +46,20 @@ struct TeamPickerView: View {
Text("Même ligne en poule") Text("Même ligne en poule")
} }
} }
let teams = tournament.selectedSortedTeams() _sectionView(luckyLosers.sorted(by: \.weight, order: sortOrder), title: "Repêchage")
if luckyLosers.isEmpty == false {
Section {
_teamListView(luckyLosers.sorted(by: \.weight))
} header: {
Text("Repêchage")
}
}
let qualified = tournament.availableQualifiedTeams() let qualified = tournament.availableQualifiedTeams()
if qualified.isEmpty == false { _sectionView(qualified.sorted(by: \.weight, order: sortOrder), title: "Qualifiées entrants")
Section {
_teamListView(qualified.sorted(by: \.weight))
} header: {
Text("Qualifiées entrants")
}
}
Section {
_teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) let teams = tournament.selectedSortedTeams()
} header: { if matchTypeContext == .loserBracket {
Text("Disponible") _sectionView(teams.filter({ $0.inGroupStage() && $0.qualified == false }).sorted(by: \.weight, order: sortOrder), title: "Non qualifié de poules")
}
Section {
_teamListView(teams.filter({ $0.inGroupStage() }).sorted(by: \.groupStagePosition!).reversed())
} header: {
Text("Déjà placée en poule")
}
Section {
_teamListView(teams.filter({ $0.inRound() }).sorted(by: \.bracketPosition!).reversed())
} header: {
Text("Déjà placée dans le tableau")
} }
_sectionView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight, order: sortOrder), title: "Disponible")
_sectionView(teams.filter({ $0.inGroupStage() }).sorted(by: \.weight, order: sortOrder), title: "Déjà placée en poule")
_sectionView(teams.filter({ $0.inRound() }).sorted(by: \.weight, order: sortOrder), title: "Déjà placée dans le tableau")
} }
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always)) .searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always))
.keyboardType(.alphabet) .keyboardType(.alphabet)
@ -84,11 +67,36 @@ struct TeamPickerView: View {
.navigationTitle("Choisir une équipe") .navigationTitle("Choisir une équipe")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Picker(selection: $sortOrder) {
Label("Poids", systemImage: "arrow.up").tag(SortOrder.ascending)
.labelStyle(.titleAndIcon)
Label("Poids", systemImage: "arrow.down").tag(SortOrder.descending)
.labelStyle(.titleAndIcon)
} label: {
Label("Trier", systemImage: "arrow.up.arrow.down")
}
.pickerStyle(.menu)
}
}
.headerProminence(.increased)
} }
.tint(.master) .tint(.master)
} }
} }
@ViewBuilder
private func _sectionView(_ teams: [TeamRegistration], title: String) -> some View {
if teams.isEmpty == false {
Section {
_teamListView(teams)
} header: {
Text(title)
}
}
}
private func _teamListView(_ teams: [TeamRegistration]) -> some View { private func _teamListView(_ teams: [TeamRegistration]) -> some View {
ForEach(teams) { team in ForEach(teams) { team in
if searchField.isEmpty || team.contains(searchField) { if searchField.isEmpty || team.contains(searchField) {

@ -21,6 +21,17 @@ struct TeamRowView: View {
Text(name).foregroundStyle(.secondary) Text(name).foregroundStyle(.secondary)
} }
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle())
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
}
}
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
}
if team.players().isEmpty == false { if team.players().isEmpty == false {
ForEach(team.players()) { player in ForEach(team.players()) { player in
Text(player.playerLabel()) Text(player.playerLabel())

@ -192,26 +192,37 @@ struct TournamentRankView: View {
.fontWeight(.bold) .fontWeight(.bold)
Text(key.ordinalFormattedSuffix()).font(.caption) Text(key.ordinalFormattedSuffix()).font(.caption)
} }
if let index = tournament.indexOf(team: team) { if let index = tournament.indexOf(team: team) {
let rankingDifference = index - (key - 1) ZStack {
if rankingDifference > 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.up.fill")
.imageScale(.small)
}
.foregroundColor(.green)
} else if rankingDifference < 0 {
HStack(spacing: 0.0) { HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always()))) Text(tournament.teamCount.formatted(.number.sign(strategy: .always())))
.monospacedDigit() .monospacedDigit()
Image(systemName: "arrowtriangle.down.fill") Image(systemName: "arrowtriangle.down.fill")
.imageScale(.small) .imageScale(.small)
} }
.foregroundColor(.logoRed) .opacity(0)
} else {
Text("--") let rankingDifference = index - (key - 1)
if rankingDifference > 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.up.fill")
.imageScale(.small)
}
.foregroundColor(.green)
} else if rankingDifference < 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.down.fill")
.imageScale(.small)
}
.foregroundColor(.logoRed)
} else {
Text("--")
}
} }
} }
} }

Loading…
Cancel
Save