Laurent 1 year ago
commit edc99d1e79
  1. 1176
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub Raw.xcscheme
  3. 16
      PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub TestFlight.xcscheme
  4. 2
      PadelClub.xcodeproj/xcshareddata/xcschemes/PadelClub.xcscheme
  5. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  6. 40
      PadelClub/Data/Federal/FederalTournament.swift
  7. 2
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  8. 5
      PadelClub/Data/GroupStage.swift
  9. 33
      PadelClub/Data/Match.swift
  10. 45
      PadelClub/Data/Round.swift
  11. 4
      PadelClub/Data/TeamRegistration.swift
  12. 74
      PadelClub/Data/Tournament.swift
  13. 7
      PadelClub/Data/User.swift
  14. 45
      PadelClub/Extensions/Array+Extensions.swift
  15. 20
      PadelClub/Extensions/Calendar+Extensions.swift
  16. 4
      PadelClub/Extensions/String+Extensions.swift
  17. 7
      PadelClub/Utils/ContactManager.swift
  18. 60
      PadelClub/Utils/Network/NetworkFederalService.swift
  19. 13
      PadelClub/Utils/PadelRule.swift
  20. 42
      PadelClub/ViewModel/AgendaDestination.swift
  21. 59
      PadelClub/ViewModel/FederalDataViewModel.swift
  22. 4
      PadelClub/ViewModel/SeedInterval.swift
  23. 5
      PadelClub/ViewModel/Selectable.swift
  24. 2
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  25. 2
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  26. 2
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  27. 8
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  28. 4
      PadelClub/Views/GroupStage/GroupStageView.swift
  29. 50
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  30. 21
      PadelClub/Views/GroupStage/GroupStagesView.swift
  31. 206
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  32. 85
      PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift
  33. 4
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  34. 12
      PadelClub/Views/Match/EditSharingView.swift
  35. 3
      PadelClub/Views/Match/MatchRowView.swift
  36. 25
      PadelClub/Views/Match/MatchSetupView.swift
  37. 3
      PadelClub/Views/Match/MatchSummaryView.swift
  38. 162
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  39. 7
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  40. 40
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  41. 483
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  42. 232
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  43. 27
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  44. 8
      PadelClub/Views/Round/LoserRoundView.swift
  45. 4
      PadelClub/Views/Round/LoserRoundsView.swift
  46. 17
      PadelClub/Views/Round/RoundSettingsView.swift
  47. 2
      PadelClub/Views/Round/RoundView.swift
  48. 89
      PadelClub/Views/Shared/TournamentFilterView.swift
  49. 1
      PadelClub/Views/Team/EditingTeamView.swift
  50. 81
      PadelClub/Views/Team/TeamPickerView.swift
  51. 11
      PadelClub/Views/Team/TeamRowView.swift
  52. 14
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  53. 10
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  54. 7
      PadelClub/Views/Tournament/TournamentInitView.swift
  55. 3
      PadelClub/Views/Tournament/TournamentRunningView.swift
  56. 4
      PadelClub/Views/Tournament/TournamentView.swift

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1530" LastUpgradeVersion = "1600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

@ -15,7 +15,7 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "FF8044AE2C90379300A49A52" BlueprintIdentifier = "FF70FABD2C90584900129CC2"
BuildableName = "PadelClub TestFlight.app" BuildableName = "PadelClub TestFlight.app"
BlueprintName = "PadelClub TestFlight" BlueprintName = "PadelClub TestFlight"
ReferencedContainer = "container:PadelClub.xcodeproj"> ReferencedContainer = "container:PadelClub.xcodeproj">
@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES"> shouldAutocreateTestPlan = "YES">
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"
@ -44,7 +44,7 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "FF8044AE2C90379300A49A52" BlueprintIdentifier = "FF70FABD2C90584900129CC2"
BuildableName = "PadelClub TestFlight.app" BuildableName = "PadelClub TestFlight.app"
BlueprintName = "PadelClub TestFlight" BlueprintName = "PadelClub TestFlight"
ReferencedContainer = "container:PadelClub.xcodeproj"> ReferencedContainer = "container:PadelClub.xcodeproj">
@ -57,6 +57,16 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FF70FABD2C90584900129CC2"
BuildableName = "PadelClub TestFlight.app"
BlueprintName = "PadelClub TestFlight"
ReferencedContainer = "container:PadelClub.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1530" LastUpgradeVersion = "1600"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

@ -58,6 +58,10 @@ extension ImportedPlayer: PlayerHolder {
male male
} }
func pasteData() -> String {
return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense].compactMap({ $0 }).joined(separator: " ")
}
func isNotFromCurrentDate() -> Bool { func isNotFromCurrentDate() -> Bool {
if let importDate, importDate != SourceFileManager.shared.lastDataSourceDate() { if let importDate, importDate != SourceFileManager.shared.lastDataSourceDate() {
return true return true

@ -7,10 +7,23 @@ import Foundation
import CoreLocation import CoreLocation
import LeStorage import LeStorage
enum DayPeriod { enum DayPeriod: CaseIterable, Identifiable {
var id: Self { self }
case all case all
case weekend case weekend
case week case week
func localizedDayPeriodLabel() -> String {
switch self {
case .all:
return "n'importe"
case .week:
return "la semaine"
case .weekend:
return "le week-end"
}
}
} }
// MARK: - FederalTournament // MARK: - FederalTournament
@ -126,10 +139,34 @@ struct FederalTournament: Identifiable, Codable {
?? [] ?? []
} }
var federalClub: FederalClub? {
if let codeClub {
return FederalClub(federalClubCode: codeClub, federalClubName: clubLabel())
} else {
return nil
}
}
var shareMessage: String { var shareMessage: String {
[libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n" [libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n"
} }
var japMessage: String {
[nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";")
}
func umpireLabel() -> String {
[jugeArbitre?.nom, jugeArbitre?.prenom].compactMap({$0}).joined(separator: " ")
}
func phoneLabel() -> String {
[installation?.telephone].compactMap({$0}).joined(separator: " ")
}
func mailLabel() -> String {
[courrielEngagement].compactMap({$0}).joined(separator: " ")
}
func validForSearch(_ searchText: String, scope: FederalTournamentSearchScope) -> Bool { func validForSearch(_ searchText: String, scope: FederalTournamentSearchScope) -> Bool {
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")
@ -156,7 +193,6 @@ extension FederalTournament: FederalTournamentHolder {
// var importedId: Int { id } // var importedId: Int { id }
var holderId: String { id.string } var holderId: String { id.string }
func clubLabel() -> String { func clubLabel() -> String {
nomClub ?? villeEngagement ?? installation?.nom ?? "" nomClub ?? villeEngagement ?? installation?.nom ?? ""
} }

@ -11,10 +11,12 @@ protocol FederalTournamentHolder {
var holderId: String { get } var holderId: String { get }
var startDate: Date { get } var startDate: Date { get }
var endDate: Date? { get } var endDate: Date? { get }
var codeClub: String? { get }
var tournaments: [any TournamentBuildHolder] { get } var tournaments: [any TournamentBuildHolder] { get }
func clubLabel() -> String func clubLabel() -> String
func subtitleLabel() -> String func subtitleLabel() -> String
var dayDuration: Int { get } var dayDuration: Int { get }
var dayPeriod: DayPeriod { get }
} }
extension FederalTournamentHolder { extension FederalTournamentHolder {

@ -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,6 +396,7 @@ defer {
Logger.error(error) Logger.error(error)
} }
if single == false {
_toggleLoserMatchDisableState(state) _toggleLoserMatchDisableState(state)
if forward { if forward {
_toggleForwardMatchDisableState(state) _toggleForwardMatchDisableState(state)
@ -392,6 +405,7 @@ defer {
bottomPreviousRoundMatch()?._toggleMatchDisableState(state) bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
} }
} }
}
func next() -> Match? { func next() -> Match? {
let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index } let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index }
@ -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)

@ -503,6 +503,10 @@ final class TeamRegistration: ModelObject, Storable {
else { confirmationDate = nil } else { confirmationDate = nil }
} }
func didConfirmSummon() -> Bool {
confirmationDate != nil
}
func tournamentObject() -> Tournament? { func tournamentObject() -> Tournament? {
return Store.main.findById(tournament) return Store.main.findById(tournament)
} }

@ -348,19 +348,19 @@ final class Tournament : ModelObject, Storable {
override func deleteDependencies() throws { override func deleteDependencies() throws {
let store = self.tournamentStore let store = self.tournamentStore
let teams = self.unsortedTeams() let teams = self.tournamentStore.teamRegistrations
for team in teams { for team in teams {
try team.deleteDependencies() try team.deleteDependencies()
} }
store.teamRegistrations.deleteDependencies(teams) store.teamRegistrations.deleteDependencies(teams)
let groups = self.groupStages() let groups = self.tournamentStore.groupStages
for group in groups { for group in groups {
try group.deleteDependencies() try group.deleteDependencies()
} }
store.groupStages.deleteDependencies(groups) store.groupStages.deleteDependencies(groups)
let rounds = self.rounds() let rounds = self.self.tournamentStore.rounds
for round in rounds { for round in rounds {
try round.deleteDependencies() try round.deleteDependencies()
} }
@ -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()
} }
@ -1151,7 +1151,7 @@ defer {
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting)
} }
func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] { func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1159,8 +1159,11 @@ defer {
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let _limit = limit ?? courtCount if let limit {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(limit))
} else {
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed())
}
} }
func teamsRanked() -> [TeamRegistration] { func teamsRanked() -> [TeamRegistration] {
@ -1224,7 +1227,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 +1240,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 +1249,35 @@ 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) }
} }
} }
} }
} }
if let groupStageLoserBracketPlayedMatches = groupStageLoserBracket()?.playedMatches() {
groupStageLoserBracketPlayedMatches.forEach({ match in
if match.hasEnded() {
let sameMatchIndexCount = groupStageLoserBracketPlayedMatches.filter({ $0.index == match.index }).count
teams.setOrAppend(match.winningTeamId, at: match.index)
teams.setOrAppend(match.losingTeamId, at: match.index + sameMatchIndexCount)
}
})
}
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 +1947,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,19 +2008,27 @@ defer {
return (Array(shouldBeInIt), Array(shouldNotBeInIt)) return (Array(shouldBeInIt), Array(shouldNotBeInIt))
} }
func groupStageLoserBracket() -> Round? {
tournamentStore.rounds.first(where: { $0.groupStageLoserBracket })
}
func groupStageLoserBracketsInitialPlace() -> Int {
return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {
DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self) DataStore.shared.tournaments.writeChangeAndInsertOnServer(instance: self)
for teamRegistration in self.unsortedTeams() { for teamRegistration in self.tournamentStore.teamRegistrations {
teamRegistration.insertOnServer() teamRegistration.insertOnServer()
} }
for groupStage in self.groupStages() { for groupStage in self.tournamentStore.groupStages {
groupStage.insertOnServer() groupStage.insertOnServer()
} }
for round in self.allRounds() { for round in self.tournamentStore.rounds {
round.insertOnServer() round.insertOnServer()
} }
@ -2100,6 +2118,10 @@ extension Tournament: Hashable {
} }
extension Tournament: FederalTournamentHolder { extension Tournament: FederalTournamentHolder {
var codeClub: String? {
club()?.code
}
var holderId: String { id } var holderId: String { id }
func clubLabel() -> String { func clubLabel() -> String {
@ -2115,6 +2137,16 @@ extension Tournament: FederalTournamentHolder {
self self
] ]
} }
var dayPeriod: DayPeriod {
let day = startDate.get(.weekday)
switch day {
case 2...6:
return .week
default:
return .weekend
}
}
} }
extension Tournament: TournamentBuildHolder { extension Tournament: TournamentBuildHolder {

@ -75,6 +75,13 @@ class User: ModelObject, UserBase, Storable {
return "Sportivement,\n\(firstName) \(lastName), votre JAP." return "Sportivement,\n\(firstName) \(lastName), votre JAP."
} }
func fullName() -> String? {
guard firstName.isEmpty == false && lastName.isEmpty == false else {
return nil
}
return "\(firstName) \(lastName)"
}
func hasTenupClubs() -> Bool { func hasTenupClubs() -> Bool {
self.clubsObjects().filter({ $0.code != nil }).isEmpty == false self.clubsObjects().filter({ $0.code != nil }).isEmpty == false
} }

@ -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,38 @@ 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]
}
}
}
extension Array where Element == String {
func formatList(maxDisplay: Int = 2) -> [String] {
// Check if the array has fewer or equal elements than the maximum display limit
if self.count <= maxDisplay {
// Join all elements with commas
return self
} else {
// Join only the first `maxDisplay` elements and add "et plus"
let displayedItems = self.prefix(maxDisplay)
let remainingCount = self.count - maxDisplay
return displayedItems.dropLast() + [displayedItems.last! + " et \(remainingCount) de plus"]
}
}
}

@ -49,3 +49,23 @@ extension Calendar {
return sportYear return sportYear
} }
} }
extension Calendar {
// Add or subtract months from a date
func addMonths(_ months: Int, to date: Date) -> Date {
return self.date(byAdding: .month, value: months, to: date)!
}
// Generate a list of month start dates between two dates
func generateMonthRange(startDate: Date, endDate: Date) -> [Date] {
var dates: [Date] = []
var currentDate = startDate
while currentDate <= endDate {
dates.append(currentDate)
currentDate = self.addMonths(1, to: currentDate)
}
return dates
}
}

@ -17,6 +17,10 @@ extension String {
replaceCharactersFromSet(characterSet: .newlines).trimmingCharacters(in: .whitespacesAndNewlines) replaceCharactersFromSet(characterSet: .newlines).trimmingCharacters(in: .whitespacesAndNewlines)
} }
var trimmedMultiline: String {
self.trimmingCharacters(in: .whitespacesAndNewlines)
}
func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String { func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String {
components(separatedBy: characterSet).joined(separator:replacementString) components(separatedBy: characterSet).joined(separator:replacementString)
} }

@ -30,7 +30,10 @@ enum ContactType: Identifiable {
} }
extension ContactType { extension ContactType {
static let defaultCustomMessage: String = "Il est conseillé de vous présenter 10 minutes avant de jouer.\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire." static let defaultCustomMessage: String =
"""
Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire.
"""
static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces." static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces."
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
@ -77,7 +80,7 @@ extension ContactType {
} }
var computedMessage: String { var computedMessage: String {
[entryFeeMessage, message].compacted().map { $0.trimmed }.joined(separator: "\n\n") [entryFeeMessage, message].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
} }
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"

@ -194,4 +194,64 @@ recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.
print("no data found in html") print("no data found in html")
} }
} }
func getAllFederalTournaments(sortingOption: String, page: Int, startDate: Date, endDate: Date, city: String, distance: Double, categories: [TournamentCategory], levels: [TournamentLevel], lat: String?, lng: String?, ages: [FederalTournamentAge], types: [FederalTournamentType], nationalCup: Bool) async throws -> [HttpCommand] {
var cityParameter = ""
var searchType = "ligue"
if city.trimmed.isEmpty == false {
searchType = "ville"
cityParameter = city
}
var levelsParameter = ""
if levels.isEmpty == false {
levelsParameter = levels.map { "categorie_tournoi[\($0.localizedLabel)]=\($0.localizedLabel)" }.joined(separator: "&") + "&"
}
var categoriesParameter = ""
if categories.isEmpty == false {
categoriesParameter = categories.map { "epreuve[\($0.requestLabel)]=\($0.requestLabel)" }.joined(separator: "&") + "&"
}
var agesParameter = ""
if ages.isEmpty == false {
agesParameter = ages.map { "categorie_age[\($0.rawValue)]=\($0.rawValue)" }.joined(separator: "&") + "&"
}
var typesParameter = ""
if types.isEmpty == false {
typesParameter = types.map { "type[\($0.rawValue.capitalized)]=\($0.rawValue.capitalized)" }.joined(separator: "&") + "&"
}
var npc = ""
if nationalCup {
npc = "&tournoi_npc=1"
}
let parameters = """
recherche_type=\(searchType)&ville%5Bautocomplete%5D%5Bcountry%5D=fr&ville%5Bautocomplete%5D%5Btextfield%5D=&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blat_field%5D=\(lat ?? "")&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blng_field%5D=\(lng ?? "")&ville%5Bdistance%5D%5Bvalue_field%5D=\(Int(distance))&club%5Bautocomplete%5D%5Btextfield%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=&pratique=PADEL&date%5Bstart%5D=\(startDate.twoDigitsYearFormatted)&date%5Bend%5D=\(endDate.twoDigitsYearFormatted)&\(categoriesParameter)\(levelsParameter)\(agesParameter)\(typesParameter)\(npc)&page=\(page)&sort=\(sortingOption)&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language")
request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.addValue("application/x-www-form-urlencoded; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
return try await runTenupTask(request: request)
}
} }

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import LeStorage
enum RankSource: Hashable { enum RankSource: Hashable {
case national case national
@ -33,7 +34,8 @@ protocol TournamentBuildHolder: Identifiable {
} }
struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
var id: String { identifier } var uniqueId: String = Store.randomId()
var id: String { uniqueId }
let category: TournamentCategory let category: TournamentCategory
let level: TournamentLevel let level: TournamentLevel
let age: FederalTournamentAge let age: FederalTournamentAge
@ -42,7 +44,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
// var japLastName: String? = nil // var japLastName: String? = nil
func buildHolderTitle() -> String { func buildHolderTitle() -> String {
localizedLabel() computedLabel
} }
var identifier: String { var identifier: String {
@ -291,6 +293,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: first - 1, count: teamsCount),
points(for: last - 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:

@ -18,11 +18,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
case activity case activity
case history case history
case tenup case tenup
case around
enum ViewStyle {
case list
case calendar
}
var localizedTitleKey: String { var localizedTitleKey: String {
switch self { switch self {
@ -32,6 +28,8 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
return "Terminé" return "Terminé"
case .tenup: case .tenup:
return "Tenup" return "Tenup"
case .around:
return "Autour"
} }
} }
@ -39,14 +37,12 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
localizedTitleKey localizedTitleKey
} }
var systemImage: String { func systemImage() -> String? {
switch self { switch self {
case .activity: case .around:
return "squares.leading.rectangle" return "location.magnifyingglass"
case .history: default:
return "book.closed" return nil
case .tenup:
return "tennisball"
} }
} }
@ -58,6 +54,9 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
case .tenup: case .tenup:
FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+) FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+)
case .around:
nil
} }
} }
@ -84,6 +83,25 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
} else { } else {
return nil return nil
} }
case .around:
return nil
} }
} }
} }
enum ViewStyle {
case list
case calendar
}
struct ViewStyleKey: EnvironmentKey {
static let defaultValue: ViewStyle = .list
}
extension EnvironmentValues {
var viewStyle: ViewStyle {
get { self[ViewStyleKey.self] }
set { self[ViewStyleKey.self] = newValue }
}
}

@ -13,26 +13,44 @@ class FederalDataViewModel {
static let shared = FederalDataViewModel() static let shared = FederalDataViewModel()
var federalTournaments: [FederalTournament] = [] var federalTournaments: [FederalTournament] = []
var searchedFederalTournaments: [FederalTournament] = []
var levels: Set<TournamentLevel> = Set() var levels: Set<TournamentLevel> = Set()
var categories: Set<TournamentCategory> = Set() var categories: Set<TournamentCategory> = Set()
var ageCategories: Set<FederalTournamentAge> = Set() var ageCategories: Set<FederalTournamentAge> = Set()
var selectedClubs: Set<String> = Set() var selectedClubs: Set<String> = Set()
var id: UUID = UUID() var id: UUID = UUID()
var searchAttemptCount: Int = 0
var dayDuration: Int?
var dayPeriod: DayPeriod = .all
func filterStatus() -> String { func filterStatus() -> String {
var labels: [String] = [] var labels: [String] = []
labels.append(contentsOf: levels.map { $0.localizedLabel() }) labels.append(contentsOf: levels.map { $0.localizedLabel() }.formatList())
labels.append(contentsOf: categories.map { $0.localizedLabel() }) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList())
labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }) labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList())
let clubNames = selectedClubs.compactMap { codeClub in let clubNames = selectedClubs.compactMap { codeClub in
let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub }) let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub })
return club?.clubTitle(.short) return club?.clubTitle(.short)
} }
labels.append(contentsOf: clubNames) labels.append(contentsOf: clubNames.formatList())
if dayPeriod != .all {
labels.append(dayPeriod.localizedDayPeriodLabel())
}
if let dayDuration {
labels.append("max " + dayDuration.formatted() + " jour" + dayDuration.pluralSuffix)
}
return labels.joined(separator: ", ") return labels.joined(separator: ", ")
} }
var searchedClubs: [FederalClub] {
searchedFederalTournaments.compactMap { ft in
ft.federalClub
}.uniqued { fc in
fc.federalClubCode
}.sorted(by: \.federalClubName)
}
func selectedClub() -> Club? { func selectedClub() -> Club? {
if selectedClubs.isEmpty == false { if selectedClubs.isEmpty == false {
return DataStore.shared.clubs.first(where: { $0.code == selectedClubs.first! }) return DataStore.shared.clubs.first(where: { $0.code == selectedClubs.first! })
@ -46,15 +64,25 @@ class FederalDataViewModel {
categories.removeAll() categories.removeAll()
ageCategories.removeAll() ageCategories.removeAll()
selectedClubs.removeAll() selectedClubs.removeAll()
dayPeriod = .all
dayDuration = nil
id = UUID() id = UUID()
} }
func areFiltersEnabled() -> Bool { func areFiltersEnabled() -> Bool {
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty) == false (levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false
}
var filteredFederalTournaments: [FederalTournamentHolder] {
filteredFederalTournaments(from: federalTournaments)
}
var filteredSearchedFederalTournaments: [FederalTournamentHolder] {
filteredFederalTournaments(from: searchedFederalTournaments)
} }
var filteredFederalTournaments: [FederalTournament] { func filteredFederalTournaments(from tournaments: [any FederalTournamentHolder]) -> [FederalTournamentHolder] {
federalTournaments.filter({ tournament in tournaments.filter({ tournament in
(levels.isEmpty || tournament.tournaments.anySatisfy({ levels.contains($0.level) })) (levels.isEmpty || tournament.tournaments.anySatisfy({ levels.contains($0.level) }))
&& &&
(categories.isEmpty || tournament.tournaments.anySatisfy({ categories.contains($0.category) })) (categories.isEmpty || tournament.tournaments.anySatisfy({ categories.contains($0.category) }))
@ -62,6 +90,10 @@ class FederalDataViewModel {
(ageCategories.isEmpty || tournament.tournaments.anySatisfy({ ageCategories.contains($0.age) })) (ageCategories.isEmpty || tournament.tournaments.anySatisfy({ ageCategories.contains($0.age) }))
&& &&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) (selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
}) })
} }
@ -72,6 +104,10 @@ class FederalDataViewModel {
(categories.isEmpty || categories.contains(tournament.category)) (categories.isEmpty || categories.contains(tournament.category))
&& &&
(ageCategories.isEmpty || ageCategories.contains(tournament.age)) (ageCategories.isEmpty || ageCategories.contains(tournament.age))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
if let codeClub = tournament.club()?.code { if let codeClub = tournament.club()?.code {
return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub)) return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub))
@ -88,6 +124,10 @@ class FederalDataViewModel {
(ageCategories.isEmpty || ageCategories.contains(build.age)) (ageCategories.isEmpty || ageCategories.contains(build.age))
&& &&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) (selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
} }
func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws { func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws {
@ -103,3 +143,8 @@ class FederalDataViewModel {
} }
} }
struct FederalClub: Identifiable {
var id: String { federalClubCode }
var federalClubCode: String
var federalClubName: String
}

@ -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 {

@ -14,9 +14,14 @@ protocol Selectable {
func badgeImage() -> Badge? func badgeImage() -> Badge?
func badgeValueColor() -> Color? func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool func displayImageIfValueZero() -> Bool
func systemImage() -> String?
} }
extension Selectable { extension Selectable {
func systemImage() -> String? {
return nil
}
func displayImageIfValueZero() -> Bool { func displayImageIfValueZero() -> Bool {
return false return false
} }

@ -43,7 +43,7 @@ struct CallMessageCustomizationView: View {
} }
var computedMessage: String { var computedMessage: String {
[entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmed }.joined(separator: "\n") [entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmedMultiline }.joined(separator: "\n")
} }
var finalMessage: String? { var finalMessage: String? {

@ -64,7 +64,7 @@ struct EventCreationView: View {
} }
} }
TextField("Nom de l'événement", text: $eventName, axis: .vertical) TextField("Description de l'événement", text: $eventName, axis: .vertical)
.lineLimit(2) .lineLimit(2)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)

@ -46,7 +46,7 @@ struct EventSettingsView: View {
var body: some View { var body: some View {
Form { Form {
Section { Section {
TextField("Nom de l'événement", text: $eventName, axis: .vertical) TextField("Description de l'événement", text: $eventName, axis: .vertical)
.lineLimit(2) .lineLimit(2)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)

@ -23,6 +23,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
} label: { } label: {
Image(systemName: "wrench.and.screwdriver") Image(systemName: "wrench.and.screwdriver")
.foregroundColor(selectedDestination == nil ? .white : .black) .foregroundColor(selectedDestination == nil ? .white : .black)
.contentShape(Capsule())
} }
.padding() .padding()
.background { .background {
@ -38,8 +39,15 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
Button { Button {
selectedDestination = destination selectedDestination = destination
} label: { } label: {
if let systemImage = destination.systemImage() {
Image(systemName: systemImage)
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
.contentShape(Capsule())
} else {
Text(destination.selectionLabel(index: index)) Text(destination.selectionLabel(index: index))
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black) .foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
.contentShape(Capsule())
}
} }
.padding() .padding()
.background { .background {

@ -197,12 +197,12 @@ struct GroupStageView: View {
} }
} }
.listRowView(isActive: team.qualified, color: .master) .listRowView(isActive: team.qualified, color: .master)
.listRowView(isActive: team.isHere(), color: .green, hideColorVariation: true) .listRowView(isActive: team.isHere() && groupStage.hasEnded() == false, color: .green, hideColorVariation: true)
} else { } else {
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 {

@ -9,8 +9,9 @@ import SwiftUI
import LeStorage import LeStorage
struct GroupStagesView: View { struct GroupStagesView: View {
var tournament: Tournament @State var tournament: Tournament
@State private var selectedDestination: GroupStageDestination? @State private var selectedDestination: GroupStageDestination?
@EnvironmentObject var dataStore: DataStore
enum GroupStageDestination: Selectable, Identifiable, Equatable { enum GroupStageDestination: Selectable, Identifiable, Equatable {
static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool { static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool {
@ -18,12 +19,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 +37,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 +48,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 +67,9 @@ struct GroupStagesView: View {
} else { } else {
return nil return nil
} }
case .loserBracket(let loserBracket):
if loserBracket._matches().isEmpty { return nil }
return loserBracket.badgeImage()
case .groupStage(let groupStage): case .groupStage(let groupStage):
return groupStage.badgeImage() return groupStage.badgeImage()
} }
@ -71,7 +82,6 @@ struct GroupStagesView: View {
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 {
@ -87,6 +97,9 @@ struct GroupStagesView: View {
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
} }
@ -96,7 +109,7 @@ struct GroupStagesView: View {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination { switch selectedDestination {
case .all: case .all:
let finishedMatches = tournament.finishedMatches(allMatches) let finishedMatches = tournament.finishedMatches(allMatches, limit: nil)
List { List {
if tournament.groupStageAdditionalQualified > 0 { if tournament.groupStageAdditionalQualified > 0 {
@ -142,6 +155,8 @@ 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)
case nil: case nil:
GroupStagesSettingsView() GroupStagesSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")

@ -0,0 +1,206 @@
//
// LoserBracketFromGroupStageView.swift
// PadelClub
//
// Created by razmig on 07/09/2024.
//
import SwiftUI
import LeStorage
struct LoserBracketFromGroupStageView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State var loserBracket: Round
@State private var isEditingLoserBracketGroupStage: Bool
init(loserBracket: Round) {
self.loserBracket = loserBracket
_isEditingLoserBracketGroupStage = .init(wrappedValue: loserBracket._matches().isEmpty)
}
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
var displayableMatches: [Match] {
loserBracket.playedMatches().sorted(by: \.index)
}
var body: some View {
List {
if isEditingLoserBracketGroupStage == true && displayableMatches.isEmpty == false {
Section {
RowButtonView("Ajouter un match", role: .destructive) {
_addNewMatch()
}
}
}
ForEach(displayableMatches) { match in
Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
.environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage)
} header: {
let tournamentTeamCount = tournament.teamCount
let seedIntervalPointRange = tournament.tournamentLevel.pointsRange(first: match.index, last: match.index + displayableMatches.filter({ $0.index == match.index }).count, teamsCount: tournamentTeamCount)
HStack {
Text(match.matchTitle(.wide))
Spacer()
Text(seedIntervalPointRange)
.font(.caption)
}
} footer: {
if isEditingLoserBracketGroupStage == true {
GroupStageLoserBracketMatchFooterView(match: match, samePlaceThanAboveOption: displayableMatches.count > 1)
}
}
}
Section {
if displayableMatches.count > 1 && isEditingLoserBracketGroupStage == true {
Section {
RowButtonView("Effacer tous les matchs", role: .destructive) {
_deleteAllMatches()
}
} footer: {
Text("Efface tous les matchs de classement de poules ci-dessus.")
}
}
}
}
.overlay {
if displayableMatches.isEmpty {
ContentUnavailableView {
Label("Aucun match de classement", systemImage: "figure.tennis")
} description: {
Text("Vous n'avez créé aucun match de classement entre les perdants de poules.")
} actions: {
RowButtonView("Ajouter un match") {
isEditingLoserBracketGroupStage = true
_addNewMatch()
}
.padding(.horizontal)
}
}
}
.headerProminence(.increased)
.navigationTitle("Classement de poules")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if displayableMatches.isEmpty == false {
Button(isEditingLoserBracketGroupStage == true ? "Valider" : "Modifier") {
if isEditingLoserBracketGroupStage == true {
isEditingLoserBracketGroupStage = false
} else {
isEditingLoserBracketGroupStage = true
}
}
}
}
}
}
private func _addNewMatch() {
let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace()
let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 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)
}
}
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)
}
}
}
struct GroupStageLoserBracketMatchFooterView: View {
@Environment(Tournament.self) var tournament: Tournament
@Bindable var match: Match
let samePlaceThanAboveOption: Bool
@State private var selectPlacePlayed: Bool = false
@State private var index: Int = 0
var body: some View {
HStack {
Menu {
if samePlaceThanAboveOption {
Button("Même place qu'au-dessus") {
_updateIndex(match.index-2)
}
}
Button("Choisir la place") {
index = match.index
selectPlacePlayed = true
}
} label: {
Text("Éditer la place jouée")
.underline()
}
Spacer()
FooterButtonView("Effacer", role: .destructive) {
do {
try match.deleteDependencies()
try match.tournamentStore.matches.delete(instance: match)
} catch {
Logger.error(error)
}
}
}
.alert("Place jouée", isPresented: $selectPlacePlayed) {
TextField("Place jouée", value: $index, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.onSubmit {
_updateIndex(index)
}
Button("Confirmer") {
_updateIndex(index)
}
Button("Annuler", role: .cancel) {
}
}
}
private func _updateIndex(_ newIndex: Int) {
let newIndexValidated = max(1,abs(newIndex))
let teamScores = match.teamScores
teamScores.forEach { ts in
if let luckyLoser = ts.luckyLoser {
ts.luckyLoser = (luckyLoser - match.index * 2) % 2 + newIndexValidated * 2
}
}
match.index = newIndexValidated
match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place"
do {
try match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)
} catch {
Logger.error(error)
}
do {
try match.tournamentStore.matches.addOrUpdate(instance: match)
} 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)
} }

@ -24,18 +24,20 @@ struct EditSharingView: View {
func shareMessage(displayRank: Bool, displayTeamName: Bool) -> String { func shareMessage(displayRank: Bool, displayTeamName: Bool) -> String {
var messageData: [String] = [] var messageData: [String] = []
var locAndTime: String = "" if match.hasEnded() == false {
var locAndTime: String?
if let courtName = match.courtName() { if let courtName = match.courtName() {
locAndTime.append("\(courtName)") locAndTime = "\(courtName)"
} }
if let startDate = match.startDate { if let startDate = match.startDate {
locAndTime = locAndTime + " à " + startDate.formattedAsHourMinute() locAndTime = [locAndTime, startDate.formattedAsHourMinute()].compactMap({ $0 }).joined(separator: " à ")
} }
if locAndTime.isEmpty == false { if let locAndTime, locAndTime.isEmpty == false {
messageData.append(locAndTime) messageData.append(locAndTime)
} }
}
if let tournament = match.currentTournament() { if let tournament = match.currentTournament() {
messageData.append(tournament.tournamentTitle()) messageData.append(tournament.tournamentTitle())
@ -52,6 +54,8 @@ struct EditSharingView: View {
let players = "\(labelOne)\ncontre\n\(labelTwo)" let players = "\(labelOne)\ncontre\n\(labelTwo)"
messageData.append(players) messageData.append(players)
messageData.append(match.scoreLabel())
return messageData.joined(separator: "\n") return messageData.joined(separator: "\n")
} }

@ -9,7 +9,8 @@ import SwiftUI
struct MatchRowView: View { struct MatchRowView: View {
var match: Match @EnvironmentObject var dataStore: DataStore
@State var match: Match
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
var title: String? = nil var title: String? = nil

@ -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,6 +150,8 @@ struct MatchSetupView: View {
.underline() .underline()
} }
.disabled(availableSeedGroups.isEmpty && walkOutSpot == false && availableQualifiedTeams.isEmpty) .disabled(availableSeedGroups.isEmpty && walkOutSpot == false && availableQualifiedTeams.isEmpty)
if matchTypeContext == .bracket {
Spacer() Spacer()
if match.isSeedLocked(atTeamPosition: teamPosition) { if match.isSeedLocked(atTeamPosition: teamPosition) {
Button { Button {
@ -174,6 +180,7 @@ struct MatchSetupView: View {
} }
} }
} }
}
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
@ -207,6 +214,14 @@ struct MatchSetupView: View {
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} else if match.isLoserBracket {
if let score = match.teamScore(ofTeam: team) {
do {
try tournamentStore.teamScores.delete(instance: score)
} catch {
Logger.error(error)
}
}
} else { } else {
match.teamWillBeWalkOut(team) match.teamWillBeWalkOut(team)
do { do {

@ -8,7 +8,8 @@
import SwiftUI import SwiftUI
struct MatchSummaryView: View { struct MatchSummaryView: View {
var match: Match @EnvironmentObject var dataStore: DataStore
@State var match: Match
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
let matchTitle: String let matchTitle: String
let roundTitle: String? let roundTitle: String?

@ -16,12 +16,13 @@ struct ActivityView: View {
@State private var presentFilterView: Bool = false @State private var presentFilterView: Bool = false
@State private var presentToolbar: Bool = false @State private var presentToolbar: Bool = false
@State private var newTournament: Tournament? @State private var newTournament: Tournament?
@State private var viewStyle: AgendaDestination.ViewStyle = .list @State private var viewStyle: ViewStyle = .list
@State private var isGatheringFederalTournaments: Bool = false @State private var isGatheringFederalTournaments: Bool = false
@State private var error: Error? @State private var error: Error?
@State private var uuid: UUID = UUID() @State private var uuid: UUID = UUID()
@State private var presentClubSearchView: Bool = false @State private var presentClubSearchView: Bool = false
@State private var quickAccessScreen: QuickAccessScreen? = nil @State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false
enum QuickAccessScreen : Identifiable, Hashable { enum QuickAccessScreen : Identifiable, Hashable {
case inscription(pasteString: String) case inscription(pasteString: String)
@ -67,6 +68,8 @@ struct ActivityView: View {
return endedTournaments return endedTournaments
case .tenup: case .tenup:
return federalDataViewModel.filteredFederalTournaments return federalDataViewModel.filteredFederalTournaments
case .around:
return federalDataViewModel.filteredSearchedFederalTournaments
} }
} }
@ -87,17 +90,27 @@ struct ActivityView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
ScrollViewReader { proxy in
List { List {
switch navigation.agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:
EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true) EventListView(tournaments: runningTournaments, sortAscending: true)
case .history: case .history:
EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false) EventListView(tournaments: endedTournaments, sortAscending: false)
case .tenup: case .tenup:
EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle, sortAscending: true) EventListView(tournaments: federalDataViewModel.federalTournaments, sortAscending: true)
.id(uuid) .id(uuid)
case .around:
EventListView(tournaments: federalDataViewModel.searchedFederalTournaments, sortAscending: true)
} }
} }
.onChange(of: navigation.agendaDestination) {
withAnimation {
proxy.scrollTo(0, anchor: .center)
}
}
}
.environment(\.viewStyle, viewStyle)
.environment(federalDataViewModel) .environment(federalDataViewModel)
.overlay { .overlay {
if let error, navigation.agendaDestination == .tenup { if let error, navigation.agendaDestination == .tenup {
@ -119,14 +132,18 @@ struct ActivityView: View {
ContentUnavailableView.search(text: searchText) ContentUnavailableView.search(text: searchText)
} else if federalDataViewModel.areFiltersEnabled() { } else if federalDataViewModel.areFiltersEnabled() {
ContentUnavailableView { ContentUnavailableView {
Text("Aucun résultat") Text("Aucun tournoi")
} description: { } description: {
Text(federalDataViewModel.filterStatus()) Text("Aucun tournoi ne correspond aux fitres que vous avez choisis : \(federalDataViewModel.filterStatus())")
} actions: { } actions: {
RowButtonView("supprimer le filtre") { FooterButtonView("supprimer vos filtres") {
federalDataViewModel.removeFilters() federalDataViewModel.removeFilters()
} }
.padding(.horizontal) .padding(.horizontal)
FooterButtonView("modifier vos filtres") {
presentFilterView = true
}
.padding(.horizontal)
} }
} else { } else {
_dataEmptyView() _dataEmptyView()
@ -134,14 +151,10 @@ struct ActivityView: View {
} }
} }
} }
}
//.searchable(text: $searchText) //.searchable(text: $searchText)
.onAppear { presentToolbar = true } .onAppear { presentToolbar = true }
.onDisappear { presentToolbar = false } .onDisappear { presentToolbar = false }
.sheet(item: $newTournament) { tournament in
EventCreationView(tournaments: [tournament], selectedClub: federalDataViewModel.selectedClub())
.environment(navigation)
.tint(.master)
}
.refreshable { .refreshable {
if navigation.agendaDestination == .tenup { if navigation.agendaDestination == .tenup {
federalDataViewModel.federalTournaments.removeAll() federalDataViewModel.federalTournaments.removeAll()
@ -157,21 +170,29 @@ struct ActivityView: View {
} }
} }
.onChange(of: navigation.agendaDestination) { .onChange(of: navigation.agendaDestination) {
if tournaments.isEmpty, viewStyle == .calendar {
viewStyle = .list
}
if navigation.agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.user.hasTenupClubs() == true && dataStore.user.hasTenupClubs() == true
&& federalDataViewModel.federalTournaments.isEmpty { && federalDataViewModel.federalTournaments.isEmpty {
_gatherFederalTournaments() _gatherFederalTournaments()
} }
} }
.toolbar { .onChange(of: presentFilterView, { old, new in
if presentToolbar { if old == true, new == false { //closing filter view
//let _activityStatus = _activityStatus() if tournaments.isEmpty, viewStyle == .calendar {
if federalDataViewModel.areFiltersEnabled() { viewStyle = .list
ToolbarItem(placement: .status) {
Text(federalDataViewModel.filterStatus())
} }
} }
})
.toolbarTitleDisplayMode(.large)
.navigationTitle(TabDestination.activity.title)
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
}
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) { ToolbarItemGroup(placement: .topBarLeading) {
Button { Button {
switch viewStyle { switch viewStyle {
@ -184,7 +205,7 @@ struct ActivityView: View {
Image(systemName: "calendar.circle") Image(systemName: "calendar.circle")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(minHeight: 28) .frame(minHeight: 32)
} }
.symbolVariant(viewStyle == .calendar ? .fill : .none) .symbolVariant(viewStyle == .calendar ? .fill : .none)
@ -194,7 +215,7 @@ struct ActivityView: View {
Image(systemName: "line.3.horizontal.decrease.circle") Image(systemName: "line.3.horizontal.decrease.circle")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(minHeight: 28) .frame(minHeight: 32)
} }
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none) .symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
@ -209,14 +230,42 @@ struct ActivityView: View {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(minHeight: 28) .frame(minHeight: 32)
} }
} }
if presentToolbar, tournaments.isEmpty == false, federalDataViewModel.areFiltersEnabled() || navigation.agendaDestination == .around {
ToolbarItemGroup(placement: .bottomBar) {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack {
if navigation.agendaDestination == .around {
FooterButtonView("modifier votre recherche") {
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
}
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
.padding(.bottom, 8)
}
} }
} }
.navigationTitle(TabDestination.activity.title)
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
} }
.sheet(isPresented: $presentFilterView) { .sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel) TournamentFilterView(federalDataViewModel: federalDataViewModel)
@ -232,7 +281,16 @@ struct ActivityView: View {
ClubImportView() ClubImportView()
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $displaySearchView) {
NavigationStack {
TournamentLookUpView()
.environment(federalDataViewModel)
}
} }
.sheet(item: $newTournament) { tournament in
EventCreationView(tournaments: [tournament], selectedClub: federalDataViewModel.selectedClub())
.environment(navigation)
.tint(.master)
} }
.sheet(item: $quickAccessScreen) { screen in .sheet(item: $quickAccessScreen) { screen in
switch screen { switch screen {
@ -274,6 +332,31 @@ struct ActivityView: View {
} }
} }
} }
}
private func _searchStatus() -> String {
var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments
let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix
searchStatus.append(status)
}
if federalDataViewModel.areFiltersEnabled(), tournaments.isEmpty == false {
searchStatus.append(federalDataViewModel.filterStatus())
}
return searchStatus.joined(separator: " ")
}
private func _filterButtonTitle() -> String {
var prefix = "modifier "
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
prefix = ""
}
return prefix + "vos filtres"
}
private func _gatherFederalTournaments() { private func _gatherFederalTournaments() {
isGatheringFederalTournaments = true isGatheringFederalTournaments = true
@ -298,6 +381,8 @@ struct ActivityView: View {
_endedEmptyView() _endedEmptyView()
case .tenup: case .tenup:
_tenupEmptyView() _tenupEmptyView()
case .around:
_searchTenupEmptyView()
} }
} }
@ -363,6 +448,33 @@ struct ActivityView: View {
} }
} }
@ViewBuilder
private func _searchTenupEmptyView() -> some View {
if federalDataViewModel.searchAttemptCount == 0 {
ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass")
} description: {
Text("Chercher les tournois autour de vous pour mieux décider les tournois à proposer dans votre club. Padel Club vous facilite même l'inscription !")
} actions: {
RowButtonView("Lancer la recherche") {
displaySearchView = true
}
.padding()
}
} else {
ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash")
} description: {
Text("Aucun tournoi ne correspond aux critères sélectionnés.")
} actions: {
FooterButtonView("modifier vos critères de recherche") {
displaySearchView = true
}
.padding()
}
}
}
} }
//#Preview { //#Preview {

@ -91,11 +91,18 @@ struct CalendarView: View {
Menu { Menu {
ForEach(tournament.tournaments, id: \.id) { build in ForEach(tournament.tournaments, id: \.id) { build in
if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) { if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) {
if navigation.agendaDestination == .around {
NavigationLink(build.buildHolderTitle()) {
TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user)
}
} else {
Button(build.buildHolderTitle()) { Button(build.buildHolderTitle()) {
_createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build) _createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build)
} }
} }
} }
}
} label: { } label: {
Text(tournament.clubLabel()) Text(tournament.clubLabel())
Text("sur " + tournament.dayDuration.formatted() + " jour" + tournament.dayDuration.pluralSuffix) Text("sur " + tournament.dayDuration.formatted() + " jour" + tournament.dayDuration.pluralSuffix)

@ -12,16 +12,19 @@ struct EventListView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel @Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@Environment(\.viewStyle) var viewStyle
let tournaments: [FederalTournamentHolder] let tournaments: [FederalTournamentHolder]
let viewStyle: AgendaDestination.ViewStyle
let sortAscending: Bool let sortAscending: Bool
var body: some View { var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: navigation.agendaDestination == .tenup ? federalDataViewModel.filteredFederalTournaments : tournaments) { $0.startDate.startOfMonth } let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
switch viewStyle { switch viewStyle {
case .list: case .list:
ForEach(groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 }), id: \.self) { section in let nextMonths = groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 })
ForEach(nextMonths.indices, id: \.self) { sectionIndex in
let section = nextMonths[sectionIndex]
if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate } if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate }
) { ) {
Section { Section {
@ -34,11 +37,14 @@ struct EventListView: View {
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }
.id(sectionIndex)
.headerProminence(.increased) .headerProminence(.increased)
} }
} }
case .calendar: case .calendar:
ForEach(_nextMonths(), id: \.self) { section in let nextMonths = _nextMonths()
ForEach(nextMonths.indices, id: \.self) { sectionIndex in
let section = nextMonths[sectionIndex]
let _tournaments = groupedTournamentsByDate[section] ?? [] let _tournaments = groupedTournamentsByDate[section] ?? []
Section { Section {
CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id) CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id)
@ -50,6 +56,7 @@ struct EventListView: View {
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }
.id(sectionIndex)
.headerProminence(.increased) .headerProminence(.increased)
.task { .task {
if navigation.agendaDestination == .tenup if navigation.agendaDestination == .tenup
@ -77,16 +84,25 @@ struct EventListView: View {
} }
private func _nextMonths() -> [Date] { private func _nextMonths() -> [Date] {
var result: [Date] = [] let currentDate = Date().startOfMonth
var currentDate = Date().startOfMonth let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted()
let firstMonthOfDate = uniqueDates.first
let lastMonthOfDate = uniqueDates.last
let calendar = Calendar.current
// Generate 100 future months if let firstMonthOfDate, let lastMonthOfDate {
for _ in 0..<12 { if navigation.agendaDestination == .history {
result.append(currentDate) return calendar.generateMonthRange(startDate: firstMonthOfDate, endDate: lastMonthOfDate).reversed()
currentDate = Calendar.current.date(byAdding: .month, value: 1, to: currentDate)! } else if navigation.agendaDestination == .around || navigation.agendaDestination == .tenup {
return calendar.generateMonthRange(startDate: firstMonthOfDate, endDate: lastMonthOfDate)
} else {
let min = min(currentDate, firstMonthOfDate)
let max = max(currentDate, lastMonthOfDate)
return calendar.generateMonthRange(startDate: min, endDate: calendar.addMonths(3, to: max))
}
} else {
return calendar.generateMonthRange(startDate: currentDate, endDate: calendar.addMonths(3, to: currentDate))
} }
return result
} }
private func _listView(_ tournaments: [FederalTournamentHolder]) -> some View { private func _listView(_ tournaments: [FederalTournamentHolder]) -> some View {

@ -0,0 +1,483 @@
//
// TournamentLookUpView.swift
// PadelClub
//
// Created by razmig on 08/09/2024.
//
import SwiftUI
import CoreLocation
import CoreLocationUI
struct TournamentLookUpView: View {
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss
@State private var searchField: String = ""
@State var page: Int = 0
@State var total: Int = 0
@State private var tournamentCategories = Set<TournamentCategory.ID>()
@State private var tournamentLevels = Set<TournamentLevel.ID>()
@State private var tournamentAges = Set<FederalTournamentAge.ID>()
@State private var tournamentTypes = Set<FederalTournamentType.ID>()
@State private var searching: Bool = false
@State private var startDate: Date = Date()
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
@AppStorage("lastCity") private var city: String = ""
@State private var ligue: String = ""
@State private var distance: Double = 30
@State private var sortingOption: String = "dateDebut+asc"
@State private var requestedToGetAllPages: Bool = false
@State private var nationalCup: Bool = false
@State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
}
var body: some View {
List {
searchParametersView
}
.alert("Attention", isPresented: $presentAlert, actions: {
Button {
presentAlert = false
requestedToGetAllPages = true
page += 1
searching = true
Task {
await getNewPage()
searching = false
dismiss()
}
} label: {
Label("Tout voir", systemImage: "arrow.down.circle")
}
Button("Annuler") {
revealSearchParameters = true
presentAlert = false
}
}, message: {
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.")
})
.toolbarBackground(.visible, for: .bottomBar, .navigationBar)
.navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city) {
if let newValue = locationManager.city, city.isEmpty {
city = newValue
}
}
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .bottomBar) {
if revealSearchParameters {
FooterButtonView("Lancer la recherche") {
runSearch()
}
.disabled(searching)
} else if searching {
HStack(spacing: 20) {
Spacer()
ProgressView()
if total > 0 {
let percent = Double(tournaments.count) / Double(total)
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup")
.font(.caption)
}
Spacer()
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
#if DEBUG
if tournaments.isEmpty == false {
Section {
ShareLink(item: pastedTournaments) {
Label("Par texte", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
ShareLink(item: japList) {
Label("JAP liste", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
} header: {
Text("Partager les résultats")
}
}
Divider()
#endif
Button(role: .destructive) {
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
locationManager.location = nil
locationManager.city = nil
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0
} label: {
Text("Ré-initialiser la recherche")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
}
var pastedTournaments: String {
tournaments.map { $0.shareMessage }.joined()
}
var japList: String {
Set(tournaments.map { $0.japMessage }).joined(separator: "\n")
}
private var clubsFound: [String] {
Set(tournaments.compactMap { $0.nomClub }).sorted()
}
private var liguesFound: [String] {
Set(tournaments.compactMap { $0.nomLigue }).sorted()
}
private func runSearch() {
revealSearchParameters = false
total = 0
page = 0
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
federalDataViewModel.searchAttemptCount += 1
Task {
await getNewPage()
searching = false
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
presentAlert = true
} else {
dismiss()
}
}
}
private var distanceLimit: Measurement<UnitLength> {
distanceLimit(distance: distance)
}
private func distanceLimit(distance: Double) -> Measurement<UnitLength> {
Measurement(value: distance, unit: .kilometers)
}
private var categories: [TournamentCategory] {
tournamentCategories.compactMap { TournamentCategory(rawValue: $0) }
}
private var levels: [TournamentLevel] {
tournamentLevels.compactMap { TournamentLevel(rawValue: $0) }
}
private var ages: [FederalTournamentAge] {
tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) }
}
private var types: [FederalTournamentType] {
tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) }
}
func getNewPage() async {
do {
if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm()
} else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: nationalCup)
let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
if tournaments.contains(where: { $0.id == ft.id }) == false {
federalDataViewModel.searchedFederalTournaments.append(ft)
}
}
}
if let count = resultCommand?.results?.nb_results {
print("count", count, total, tournaments.count, page)
total = count
if tournaments.count < count && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await getNewPage()
}
} else {
print("finished")
}
} else {
print("total results not found")
}
}
} catch {
print("getNewPage", error)
await getNewBuildForm()
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
} catch {
print("getNewBuildForm", error)
}
}
@ViewBuilder
var searchContollerView: some View {
Section {
Button {
runSearch()
} label: {
HStack {
Label("Chercher un tournoi", systemImage: "magnifyingglass")
if searching {
Spacer()
ProgressView()
}
}
}
Button {
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
locationManager.location = nil
locationManager.city = nil
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
revealSearchParameters = true
} label: {
Label("Ré-initialiser la recherche", systemImage: "xmark.circle")
}
}
}
@ViewBuilder
var searchParametersView: some View {
@Bindable var federalDataViewModel = federalDataViewModel
Section {
DatePicker("Début", selection: $startDate, displayedComponents: .date)
DatePicker("Fin", selection: $endDate, displayedComponents: .date)
Picker(selection: $federalDataViewModel.dayDuration) {
Text("aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?)
Text(2.formatted()).tag(2 as Int?)
Text(3.formatted()).tag(3 as Int?)
} label: {
Text("Durée max (en jours)")
}
Picker(selection: $federalDataViewModel.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel()).tag($0)
}
} label: {
Text("En semaine ou week-end")
}
HStack {
TextField("Ville", text: $city)
if let city = locationManager.city {
Divider()
Text(city).italic()
}
if locationManager.requestStarted {
ProgressView()
} else {
LocationButton {
locationManager.requestLocation()
}
.symbolVariant(.fill)
.foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12))
}
}
Picker(selection: $distance) {
Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0)
Text(distanceLimit(distance:90).formatted()).tag(90.0)
Text(distanceLimit(distance:150).formatted()).tag(150.0)
Text(distanceLimit(distance:200).formatted()).tag(200.0)
Text(distanceLimit(distance:400).formatted()).tag(400.0)
Text("Aucune").tag(3000.0)
} label: {
Text("Distance max")
}
Picker(selection: $sortingOption) {
Text("Distance").tag("_DIST_")
Text("Date de début").tag("dateDebut+asc")
Text("Date de fin").tag("dateFin+asc")
} label: {
Text("Trier par")
}
NavigationLink {
List(TournamentCategory.allCases, selection: $tournamentCategories) { type in
Text(type.localizedLabel())
}
.navigationTitle("Catégories")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List(TournamentLevel.allCases, selection: $tournamentLevels) { type in
Text(type.localizedLabel())
}
.navigationTitle("Niveaux")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Niveau")
Spacer()
levelsLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List(FederalTournamentAge.allCases, selection: $tournamentAges) { type in
Text(type.localizedLabel())
}
.navigationTitle("Limites d'âge")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Limite d'âge")
Spacer()
if tournamentAges.isEmpty || tournamentAges.count == FederalTournamentAge.allCases.count {
Text("Tous les âges")
.foregroundStyle(.secondary)
} else {
Text(ages.map({ $0.localizedLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
NavigationLink {
List(FederalTournamentType.allCases, selection: $tournamentTypes) { type in
Text(type.localizedLabel())
}
.navigationTitle("Types de tournoi")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Type de tournoi")
Spacer()
if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count {
Text("Tous les types")
.foregroundStyle(.secondary)
} else {
Text(types.map({ $0.localizedLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
Picker(selection: $nationalCup) {
Text("N'importe").tag(false)
Text("Uniquement").tag(true)
} label: {
Text("Circuit National Padel Cup")
}
} header: {
Text("Critères de recherche")
}
.headerProminence(.increased)
.disabled(searching)
}
var categoriesLabel: some View {
if tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count {
Text("Toutes les catégories")
} else {
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", "))
}
}
var levelsLabel: some View {
if tournamentLevels.isEmpty || tournamentLevels.count == TournamentLevel.allCases.count {
Text("Tous les niveaux")
} else {
Text(levels.map({ $0.localizedLabel() }).joined(separator: ", "))
}
}
@ViewBuilder
var searchParametersSummaryView: some View {
VStack(alignment: .leading) {
HStack {
Text("Lieu")
Spacer()
Text(city)
if distance >= 3000 {
Text("sans limite de distance")
} else {
Text("à moins de " + distanceLimit.formatted())
}
}
HStack {
Text("Période")
Spacer()
Text("Du")
Text(startDate.twoDigitsYearFormatted)
Text("Au")
Text(endDate.twoDigitsYearFormatted)
}
HStack {
Text("Niveau")
Spacer()
levelsLabel
}
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
}
HStack {
Text("Tri")
Spacer()
Text(sortingOptionLabel)
}
}
}
var sortingOptionLabel: String {
switch sortingOption {
case "_DIST_": return "Distance"
case "dateDebut+asc": return "Date de début"
case "dateFin+asc": return "Date de fin"
default:
return "Distance"
}
}
}

@ -0,0 +1,232 @@
//
// TournamentSubscriptionView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 09/09/2024.
//
import SwiftUI
struct TournamentSubscriptionView: View {
@EnvironmentObject var networkMonitor: NetworkMonitor
let federalTournament: FederalTournament
let build: any TournamentBuildHolder
let user: User
@State private var selectedPlayers: [ImportedPlayer]
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) {
self.federalTournament = federalTournament
self.build = build
self.user = user
_selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 }))
}
var body: some View {
List {
Section {
LabeledContent("Tournoi") {
Text(federalTournament.libelle ?? "Tournoi")
}
LabeledContent("Club") {
Text(federalTournament.clubLabel())
}
LabeledContent("Épreuve") {
Text(build.buildHolderTitle())
}
LabeledContent("JAP") {
Text(federalTournament.umpireLabel())
}
LabeledContent("Mail") {
Text(federalTournament.mailLabel())
}
LabeledContent("Téléphone") {
Text(federalTournament.phoneLabel())
}
} header: {
Text("Informations")
}
Section {
ForEach(selectedPlayers) { teamPlayer in
NavigationLink {
SelectablePlayerListView(allowSelection: 1, playerSelectionAction: { players in
if let player = players.first {
selectedPlayers.remove(elements: [teamPlayer])
selectedPlayers.append(player)
}
})
} label: {
ImportedPlayerView(player: teamPlayer)
}
}
if selectedPlayers.count < 2 {
NavigationLink {
SelectablePlayerListView(allowSelection: 1, playerSelectionAction: { players in
if let player = players.first {
selectedPlayers.append(player)
}
})
} label: {
Text("Choisir un partenaire")
}
}
} header: {
if selectedPlayers.isEmpty == false {
HStack {
Text("Poids de l'équipe")
Spacer()
Text(selectedPlayers.map { $0.rank }.reduce(0, +).formatted())
}
}
}
if let courrielEngagement = federalTournament.courrielEngagement {
Section {
RowButtonView("S'inscrire par email") {
contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild)
}
}
}
if let installation = federalTournament.installation, let telephone = installation.telephone {
if telephone.isMobileNumber() {
Section {
RowButtonView("S'inscrire par message") {
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild)
}
}
}
let number = telephone.replacingOccurrences(of: " ", with: "")
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
}
}
Section {
Text(messageBody)
} header: {
Text("Message preparé par Padel Club")
} footer: {
CopyPasteButtonView(pasteValue: messageBody)
}
}
}
.toolbar(content: {
Menu {
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) {
Label("Voir sur Tenup", systemImage: "tennisball")
}
ShareLink(item: federalTournament.shareMessage) {
Label("Partager les infos", systemImage: "info")
}
} label: {
LabelOptions()
}
})
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}
} message: {
Text(_networkErrorMessage)
}
.sheet(item: $contactType) { contactType in
Group {
switch contactType {
case .message(_, let recipients, let body, _):
MessageComposeView(recipients: recipients, body: body) { result in
switch result {
case .cancelled:
break
case .failed:
self.sentError = .messageFailed
case .sent:
if networkMonitor.connected == false {
self.sentError = .messageNotSent
}
@unknown default:
break
}
}
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
switch result {
case .cancelled, .saved:
self.contactType = nil
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil
self.sentError = .mailNotSent
}
@unknown default:
break
}
}
}
}
.tint(.master)
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Détail du tournoi")
}
var teamsString: String {
selectedPlayers.map { $0.pasteData() }.joined(separator: "\n")
}
var messageBody: String {
let bonjourOuBonsoir = Date().timeOfDay.hello
let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye
let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\nVotre tournoi n'est pas encore dessus ? \(URLs.main.rawValue)", "Téléchargez l'app : \(URLs.appStore.rawValue)", "En savoir plus : \(URLs.appDescription.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n"
return body
}
var messageBodyShort: String {
let body = [[build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " "), federalTournament.computedStartDate, teamsString].compacted().joined(separator: "\n") + "\n"
return body
}
var messageSubject: String {
let subject = [build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " ")
return subject
}
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
private var _networkErrorMessage: String {
var errors: [String] = []
if networkMonitor.connected == false {
errors.append("L'appareil n'est pas connecté à internet.")
}
if sentError == .mailNotSent {
errors.append("Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer.")
}
if (sentError == .messageFailed || sentError == .messageNotSent) {
errors.append("Le SMS n'a pas été envoyé")
}
if sentError == .mailFailed {
errors.append("Le mail n'a pas été envoyé")
}
return errors.joined(separator: "\n")
}
}

@ -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)
@ -132,7 +132,8 @@ struct RoundSettingsView: View {
Logger.error(error) Logger.error(error)
} }
round.buildLoserBracket() round.buildLoserBracket()
matches.filter { $0.disabled }.forEach { $0._toggleLoserMatchDisableState(true) matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
} }
} }
} }
@ -153,6 +154,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) {

@ -74,6 +74,7 @@ struct RoundView: View {
let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle())
TipView(bracketTip).tipStyle(tint: .green, asSection: true) TipView(bracketTip).tipStyle(tint: .green, asSection: true)
if upperRound.round.hasStarted() == false {
Section { Section {
let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount)
LabeledContent { LabeledContent {
@ -85,6 +86,7 @@ struct RoundView: View {
Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement") Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement")
} }
} }
}
if isEditingTournamentSeed.wrappedValue == false { if isEditingTournamentSeed.wrappedValue == false {
//(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue })

@ -9,12 +9,13 @@ import SwiftUI
struct TournamentFilterView: View { struct TournamentFilterView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var levels: Set<TournamentLevel> @State private var levels: Set<TournamentLevel>
@State private var categories: Set<TournamentCategory> @State private var categories: Set<TournamentCategory>
@State private var ageCategories: Set<FederalTournamentAge> @State private var ageCategories: Set<FederalTournamentAge>
@State private var selectedClubs: Set<String> @State private var selectedClubs: Set<String>
var federalDataViewModel: FederalDataViewModel @State private var federalDataViewModel: FederalDataViewModel
init(federalDataViewModel: FederalDataViewModel) { init(federalDataViewModel: FederalDataViewModel) {
self.federalDataViewModel = federalDataViewModel self.federalDataViewModel = federalDataViewModel
@ -27,30 +28,26 @@ struct TournamentFilterView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
Form { Form {
let clubs : [Club] = dataStore.user.clubsObjects()
if clubs.filter({ $0.code != nil }).isEmpty == false {
Section { Section {
ForEach(clubs.filter({ $0.code != nil })) { club in Picker(selection: $federalDataViewModel.dayDuration) {
LabeledContent { Text("aucune").tag(nil as Int?)
Button { Text(1.formatted()).tag(1 as Int?)
if selectedClubs.contains(club.code!) { Text(2.formatted()).tag(2 as Int?)
selectedClubs.remove(club.code!) Text(3.formatted()).tag(3 as Int?)
} else {
selectedClubs.insert(club.code!)
}
} label: { } label: {
if selectedClubs.contains(club.code!) { Text("Durée max (en jours)")
Image(systemName: "checkmark.circle.fill")
} }
Picker(selection: $federalDataViewModel.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel()).tag($0)
} }
} label: { } label: {
Text(club.clubTitle()) Text("En semaine ou week-end")
}
}
} header: {
Text("Clubs")
} }
} }
Section { Section {
ForEach(TournamentLevel.allCases) { level in ForEach(TournamentLevel.allCases) { level in
LabeledContent { LabeledContent {
@ -74,7 +71,7 @@ struct TournamentFilterView: View {
} }
Section { Section {
ForEach(TournamentCategory.allCases) { category in ForEach([TournamentCategory.men, TournamentCategory.women, TournamentCategory.mix]) { category in
LabeledContent { LabeledContent {
Button { Button {
if categories.contains(category) { if categories.contains(category) {
@ -88,7 +85,7 @@ struct TournamentFilterView: View {
} }
} }
} label: { } label: {
Text(category.localizedLabel(.title)) Text(category.localizedLabel(.wide))
} }
} }
} header: { } header: {
@ -116,6 +113,58 @@ struct TournamentFilterView: View {
} header: { } header: {
Text("Catégories d'âge") Text("Catégories d'âge")
} }
if navigation.agendaDestination == .around {
let clubs : [FederalClub] = federalDataViewModel.searchedClubs
if clubs.isEmpty == false {
Section {
ForEach(clubs) { club in
LabeledContent {
Button {
if selectedClubs.contains(club.federalClubCode) {
selectedClubs.remove(club.federalClubCode)
} else {
selectedClubs.insert(club.federalClubCode)
}
} label: {
if selectedClubs.contains(club.federalClubCode) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(club.federalClubName)
}
}
} header: {
Text("Clubs")
}
}
} else {
let clubs : [Club] = dataStore.user.clubsObjects().filter({ $0.code != nil })
if clubs.isEmpty == false {
Section {
ForEach(clubs) { club in
LabeledContent {
Button {
if selectedClubs.contains(club.code!) {
selectedClubs.remove(club.code!)
} else {
selectedClubs.insert(club.code!)
}
} label: {
if selectedClubs.contains(club.code!) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(club.clubTitle())
}
}
} header: {
Text("Clubs")
}
}
}
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {

@ -81,6 +81,7 @@ struct EditingTeamView: View {
Text("Confirmation reçue") Text("Confirmation reçue")
Text("L'équipe vous a confirmé votre convocation") Text("L'équipe vous a confirmé votre convocation")
} }
} else { } else {
Text("Cette équipe n'a pas été convoquée") Text("Cette équipe n'a pas été convoquée")
} }

@ -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,51 @@ 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: .topBarLeading) {
Button("Annuler", role: .cancel) {
presentTeamPickerView = false
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
Section {
Picker(selection: $sortOrder) {
Label("Trier les équipes par poids croissant", systemImage: "chevron.up").tag(SortOrder.ascending)
.labelStyle(.titleAndIcon)
Label("Trier les équipes par poids décroissant", systemImage: "chevron.down").tag(SortOrder.descending)
.labelStyle(.titleAndIcon)
} label: {
Text("Trier les équipes par poids")
}
.pickerStyle(.inline)
} header: {
Text("Trier les équipes par poids")
}
} label: {
HStack {
Text("Poids")
Image(systemName: sortOrder == .ascending ? "chevron.up" : "chevron.down")
}
}
}
}
} }
.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())

@ -106,6 +106,7 @@ struct TournamentRankView: View {
} }
} }
} }
.id(calculating)
.alert("Position", isPresented: isEditingTeam) { .alert("Position", isPresented: isEditingTeam) {
if let selectedTeam { if let selectedTeam {
@Bindable var team = selectedTeam @Bindable var team = selectedTeam
@ -154,6 +155,7 @@ struct TournamentRankView: View {
} }
struct TeamRankCellView: View { struct TeamRankCellView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode @Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var isEditingTeam: Bool = false @State private var isEditingTeam: Bool = false
@ -192,7 +194,17 @@ 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) {
ZStack {
HStack(spacing: 0.0) {
Text(tournament.teamCount.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.down.fill")
.imageScale(.small)
}
.opacity(0)
let rankingDifference = index - (key - 1) let rankingDifference = index - (key - 1)
if rankingDifference > 0 { if rankingDifference > 0 {
HStack(spacing: 0.0) { HStack(spacing: 0.0) {
@ -215,6 +227,7 @@ struct TournamentRankView: View {
} }
} }
} }
}
Divider() Divider()
@ -295,6 +308,7 @@ struct TournamentRankView: View {
calculating = true calculating = true
} }
self.rankings.removeAll()
let finalRanks = await tournament.finalRanking() let finalRanks = await tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] { if let rankedTeamIds = finalRanks[rank] {

@ -23,8 +23,16 @@ struct TournamentCellView: View {
var body: some View { var body: some View {
ForEach(tournament.tournaments, id: \.id) { build in ForEach(tournament.tournaments, id: \.id) { build in
if navigation.agendaDestination == .around, let federalTournament = tournament as? FederalTournament {
NavigationLink {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
} label: {
_buildView(build, existingTournament: event?.existingBuild(build)) _buildView(build, existingTournament: event?.existingBuild(build))
} }
} else {
_buildView(build, existingTournament: event?.existingBuild(build))
}
}
} }
var teamCount: Int? { var teamCount: Int? {
@ -98,7 +106,7 @@ struct TournamentCellView: View {
} else if let teamCount { } else if let teamCount {
Text(teamCount.formatted()) Text(teamCount.formatted())
} }
} else if let federalTournament = tournament as? FederalTournament { } else if let federalTournament = tournament as? FederalTournament, navigation.agendaDestination != .around {
Button { Button {
_createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build) _createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build)
} label: { } label: {

@ -22,7 +22,12 @@ struct TournamentInitView: View {
LabeledContent { LabeledContent {
Text(tournaments.count.formatted() + " tournoi" + tournaments.count.pluralSuffix) Text(tournaments.count.formatted() + " tournoi" + tournaments.count.pluralSuffix)
} label: { } label: {
Text("Gestion de l'événement") Text("Réglages de l'événement")
if let eventName = event.name, eventName.isEmpty == false {
Text(eventName).foregroundStyle(.secondary)
} else {
Text("Aucune description").foregroundStyle(.secondary)
}
} }
} }
} }

@ -21,7 +21,8 @@ struct TournamentRunningView: View {
MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches), hideWhenEmpty: tournament.hasEnded()) MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches), hideWhenEmpty: tournament.hasEnded())
// MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false) // MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false)
// MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) // MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false)
MatchListView(section: "terminés", matches: tournament.finishedMatches(allMatches), isExpanded: tournament.hasEnded()) let finishedMatches = tournament.finishedMatches(allMatches, limit: tournament.courtCount)
MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded())
} }
} }

@ -131,7 +131,7 @@ struct TournamentView: View {
} }
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Gestion de l'événement") Text("Réglages de l'événement")
} }
} }
@ -176,7 +176,7 @@ struct TournamentView: View {
} }
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Gestion de l'événement") Text("Réglages de l'événement")
} }
NavigationLink(value: Screen.settings) { NavigationLink(value: Screen.settings) {
LabelSettings() LabelSettings()

Loading…
Cancel
Save