Laurent 1 year ago
commit e936cd7555
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 53
      PadelClub/Data/GroupStage.swift
  3. 45
      PadelClub/Data/Match.swift
  4. 35
      PadelClub/Data/Round.swift
  5. 19
      PadelClub/Data/TeamRegistration.swift
  6. 113
      PadelClub/Data/Tournament.swift
  7. 4
      PadelClub/Utils/HtmlGenerator.swift
  8. 2
      PadelClub/Utils/HtmlService.swift
  9. 28
      PadelClub/Utils/Tips.swift
  10. 8
      PadelClub/ViewModel/AgendaDestination.swift
  11. 5
      PadelClub/ViewModel/SearchViewModel.swift
  12. 7
      PadelClub/ViewModel/Selectable.swift
  13. 4
      PadelClub/Views/Calling/CallSettingsView.swift
  14. 264
      PadelClub/Views/Cashier/CashierView.swift
  15. 101
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  16. 6
      PadelClub/Views/Cashier/Event/EventView.swift
  17. 7
      PadelClub/Views/Club/ClubSearchView.swift
  18. 150
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  19. 27
      PadelClub/Views/Components/MatchListView.swift
  20. 16
      PadelClub/Views/GroupStage/GroupStageView.swift
  21. 31
      PadelClub/Views/GroupStage/GroupStagesView.swift
  22. 2
      PadelClub/Views/Match/MatchDetailView.swift
  23. 8
      PadelClub/Views/Match/MatchSetupView.swift
  24. 26
      PadelClub/Views/Match/MatchSummaryView.swift
  25. 42
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  26. 12
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  27. 2
      PadelClub/Views/Navigation/MainView.swift
  28. 1
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  29. 31
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  30. 4
      PadelClub/Views/Round/LoserRoundView.swift
  31. 20
      PadelClub/Views/Round/LoserRoundsView.swift
  32. 38
      PadelClub/Views/Round/RoundSettingsView.swift
  33. 204
      PadelClub/Views/Round/RoundView.swift
  34. 2
      PadelClub/Views/Round/RoundsView.swift
  35. 2
      PadelClub/Views/Subscription/Guard.swift
  36. 4
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  37. 16
      PadelClub/Views/Team/EditingTeamView.swift
  38. 2
      PadelClub/Views/Team/TeamPickerView.swift
  39. 69
      PadelClub/Views/Tournament/FileImportView.swift
  40. 22
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  41. 44
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  42. 333
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  43. 207
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  44. 8
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift
  45. 29
      PadelClub/Views/Tournament/Screen/TournamentCashierView.swift
  46. 6
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  47. 6
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  48. 80
      PadelClub/Views/Tournament/TournamentBuildView.swift
  49. 15
      PadelClub/Views/Tournament/TournamentInitView.swift
  50. 1
      PadelClub/Views/Tournament/TournamentView.swift
  51. 14
      PadelClub/Views/ViewModifiers/ListRowViewModifier.swift

@ -157,6 +157,7 @@
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; };
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; }; FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; };
FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */; }; FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */; };
FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */; };
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; };
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; };
@ -478,6 +479,7 @@
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; }; FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; };
FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = "<group>"; }; FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = "<group>"; };
FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubCourtSetupView.swift; sourceTree = "<group>"; }; FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubCourtSetupView.swift; sourceTree = "<group>"; };
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundSettingsView.swift; sourceTree = "<group>"; };
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
@ -1235,6 +1237,7 @@
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */, FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1621,6 +1624,7 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */, FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */, FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */,
FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */, FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */,
@ -1935,7 +1939,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1973,7 +1977,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -79,7 +79,7 @@ class GroupStage: ModelObject, Storable {
guard teams().count == size else { return false } guard teams().count == size else { return false }
let _matches = _matches() let _matches = _matches()
if _matches.isEmpty { return false } if _matches.isEmpty { return false }
return _matches.allSatisfy { $0.hasEnded() } return _matches.anySatisfy { $0.hasEnded() == false } == false
} }
func buildMatches() { func buildMatches() {
@ -178,20 +178,60 @@ class GroupStage: ModelObject, Storable {
return _matches().first(where: { matchIndexes.contains($0.index) }) return _matches().first(where: { matchIndexes.contains($0.index) })
} }
func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] { func availableToStart(playedMatches: [Match], in runningMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })
} }
func runningMatches(playedMatches: [Match]) -> [Match] { func runningMatches(playedMatches: [Match]) -> [Match] {
playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) #if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
}
func asyncRunningMatches(playedMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
} }
func readyMatches(playedMatches: [Match]) -> [Match] {
playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) func readyMatches(playedMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
} }
func finishedMatches(playedMatches: [Match]) -> [Match] { func finishedMatches(playedMatches: [Match]) -> [Match] {
playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() #if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
} }
private func _matchOrder() -> [Int] { private func _matchOrder() -> [Int] {
@ -376,7 +416,6 @@ extension GroupStage: Selectable {
} }
func badgeValue() -> Int? { func badgeValue() -> Int? {
if teams().count < size { return nil }
return runningMatches(playedMatches: _matches()).count return runningMatches(playedMatches: _matches()).count
} }

@ -69,10 +69,10 @@ class Match: ModelObject, Storable {
try Store.main.deleteDependencies(items: self.teamScores) try Store.main.deleteDependencies(items: self.teamScores)
} }
func indexInRound() -> Int { func indexInRound(in matches: [Match]? = nil) -> Int {
if groupStage != nil { if groupStage != nil {
return index return index
} else if let index = roundObject?.playedMatches().sorted(by: \.index).firstIndex(where: { $0.id == id }) { } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) {
return index return index
} }
return RoundRule.matchIndexWithinRound(fromMatchIndex: index) return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
@ -86,16 +86,16 @@ class Match: ModelObject, Storable {
[roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n")
} }
func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String { func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String {
if let groupStageObject { if let groupStageObject {
return groupStageObject.localizedMatchUpLabel(for: index) return groupStageObject.localizedMatchUpLabel(for: index)
} }
switch displayStyle { switch displayStyle {
case .wide: case .wide:
return "Match \(indexInRound() + 1)" return "Match \(indexInRound(in: matches) + 1)"
case .short: case .short:
return "#\(indexInRound() + 1)" return "#\(indexInRound(in: matches) + 1)"
} }
} }
@ -184,15 +184,11 @@ class Match: ModelObject, Storable {
} }
func resetScores() { func resetScores() {
if hasEnded() == false { teamScores.forEach({ $0.score = nil })
teamScores.forEach({ $0.score = nil }) do {
do { try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores)
try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores) } catch {
} catch { Logger.error(error)
Logger.error(error)
}
} else {
updateTeamScores()
} }
} }
@ -468,6 +464,9 @@ class Match: ModelObject, Storable {
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
} }
if startDate == nil {
startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
}
winningTeamId = team(matchDescriptor.winner)?.id winningTeamId = team(matchDescriptor.winner)?.id
losingTeamId = team(matchDescriptor.winner.otherTeam)?.id losingTeamId = team(matchDescriptor.winner.otherTeam)?.id
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
@ -617,7 +616,7 @@ class Match: ModelObject, Storable {
} }
func isReady() -> Bool { func isReady() -> Bool {
teamScores.count == 2 teamScores.count >= 2
// teams().count == 2 // teams().count == 2
} }
@ -627,7 +626,7 @@ class Match: ModelObject, Storable {
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
endDate != nil || hasWalkoutTeam() || winningTeamId != nil endDate != nil
} }
func isGroupStage() -> Bool { func isGroupStage() -> Bool {
@ -638,8 +637,9 @@ class Match: ModelObject, Storable {
round != nil round != nil
} }
func walkoutTeam() -> [TeamRegistration] { func walkoutTeam() -> [TeamRegistration] {
scores().filter({ $0.walkOut != nil }).compactMap { $0.team } //walkout 0 means real walkout, walkout 1 means lucky loser situation
scores().filter({ $0.walkOut == 0 }).compactMap { $0.team }
} }
func hasWalkoutTeam() -> Bool { func hasWalkoutTeam() -> Bool {
@ -705,6 +705,13 @@ class Match: ModelObject, Storable {
} }
func team(_ team: TeamPosition) -> TeamRegistration? { func team(_ team: TeamPosition) -> TeamRegistration? {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func match get team", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if groupStage != nil { if groupStage != nil {
switch team { switch team {
case .one: case .one:
@ -721,7 +728,7 @@ class Match: ModelObject, Storable {
} }
} }
} }
func teamNames(_ team: TeamRegistration?) -> [String]? { func teamNames(_ team: TeamRegistration?) -> [String]? {
team?.players().map { $0.playerLabel() } team?.players().map { $0.playerLabel() }
} }

@ -56,7 +56,7 @@ class Round: ModelObject, Storable {
} }
func hasEnded() -> Bool { func hasEnded() -> Bool {
playedMatches().allSatisfy({ $0.hasEnded() }) playedMatches().anySatisfy({ $0.hasEnded() == false }) == false
} }
func upperMatches(ofMatch match: Match) -> [Match] { func upperMatches(ofMatch match: Match) -> [Match] {
@ -222,9 +222,9 @@ class Round: ModelObject, Storable {
func playedMatches() -> [Match] { func playedMatches() -> [Match] {
if parent == nil { if parent == nil {
enabledMatches() return enabledMatches()
} else { } else {
_matches() return _matches()
} }
} }
@ -243,6 +243,10 @@ class Round: ModelObject, Storable {
func isDisabled() -> Bool { func isDisabled() -> Bool {
_matches().allSatisfy({ $0.disabled }) _matches().allSatisfy({ $0.disabled })
} }
func isRankDisabled() -> Bool {
_matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty })
}
func resetFromRoundAllMatchesStartDate() { func resetFromRoundAllMatchesStartDate() {
_matches().forEach({ _matches().forEach({
@ -285,11 +289,12 @@ class Round: ModelObject, Storable {
_matches.forEach { match in _matches.forEach { match in
match.disabled = disable match.disabled = disable
match.resetMatch() match.resetMatch()
do { //we need to keep teamscores to handle disable ranking match round stuff
try DataStore.shared.teamScores.delete(contentOfs: match.teamScores) // do {
} catch { // try DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
Logger.error(error) // } catch {
} // Logger.error(error)
// }
} }
do { do {
try DataStore.shared.matches.addOrUpdate(contentOfs: _matches) try DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
@ -398,9 +403,10 @@ class Round: ModelObject, Storable {
} }
func roundStatus() -> String { func roundStatus() -> String {
if hasStarted() && hasEnded() == false { let hasEnded = hasEnded()
if hasStarted() && hasEnded == false {
return "en cours" return "en cours"
} else if hasEnded() { } else if hasEnded {
return "terminée" return "terminée"
} else { } else {
return "à démarrer" return "à démarrer"
@ -543,12 +549,17 @@ class Round: ModelObject, Storable {
} }
extension Round: Selectable { extension Round: Selectable, Equatable {
static func == (lhs: Round, rhs: Round) -> Bool {
lhs.id == rhs.id
}
func selectionLabel() -> String { func selectionLabel() -> String {
if let parentRound { if let parentRound {
return "Tour #\(parentRound.loserRounds().count - index)" return "Tour #\(parentRound.loserRounds().count - index)"
} else { } else {
return roundTitle() return roundTitle(.short)
} }
} }

@ -220,12 +220,25 @@ class TeamRegistration: ModelObject, Storable {
bracketPosition != nil bracketPosition != nil
} }
func resetPositions() { func resetGroupeStagePosition() {
groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
groupStage = nil groupStage = nil
groupStagePosition = nil groupStagePosition = nil
tournamentObject()?.resetTeamScores(in: bracketPosition) }
bracketPosition = nil
func resetBracketPosition() {
guard let bracketPosition else { return }
guard let tournamentObject = tournamentObject() else { return }
if let match = tournamentObject.match(for: bracketPosition) {
let teamScores = match.teamScores.filter({ $0.teamRegistration != self.id })
tournamentObject.resetTeamScores(in: bracketPosition, outsideOf: teamScores)
}
self.bracketPosition = nil
}
func resetPositions() {
resetGroupeStagePosition()
resetBracketPosition()
} }
func pasteData() -> String { func pasteData() -> String {

@ -538,11 +538,11 @@ class Tournament : ModelObject, Storable {
} }
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? [] getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.isEmpty() } ?? []
} }
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? [] getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
} }
func availableSeedGroups() -> [SeedInterval] { func availableSeedGroups() -> [SeedInterval] {
@ -615,7 +615,7 @@ class Tournament : ModelObject, Storable {
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup return availableSeedGroup
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count { } else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
return availableSeedGroup return availableSeedGroup
} else if let chunks = availableSeedGroup.chunks() { } else if let chunks = availableSeedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in if let chunk = chunks.first(where: { seedInterval in
@ -727,7 +727,13 @@ class Tournament : ModelObject, Storable {
} }
func selectedSortedTeams() -> [TeamRegistration] { func selectedSortedTeams() -> [TeamRegistration] {
//let start = Date() #if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
var _sortedTeams : [TeamRegistration] = [] var _sortedTeams : [TeamRegistration] = []
let _teams = unsortedTeams().filter({ $0.walkOut == false }) let _teams = unsortedTeams().filter({ $0.walkOut == false })
@ -757,9 +763,6 @@ class Tournament : ModelObject, Storable {
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending) _sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
} }
//let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
//print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams return _sortedTeams
} }
@ -817,10 +820,6 @@ class Tournament : ModelObject, Storable {
unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank) unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
} }
func femalePlayers() -> [PlayerRegistration] {
unsortedPlayers().filter({ $0.isMalePlayer() == false })
}
func unrankValue(for malePlayer: Bool) -> Int? { func unrankValue(for malePlayer: Bool) -> Int? {
switch tournamentCategory { switch tournamentCategory {
case .men: case .men:
@ -930,7 +929,7 @@ class Tournament : ModelObject, Storable {
} }
} }
func registrationIssues() -> Int { func registrationIssues() async -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams() let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
@ -957,46 +956,95 @@ class Tournament : ModelObject, Storable {
return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
} }
func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting)
} }
func asyncRunningMatches(_ allMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting)
}
func runningMatches(_ allMatches: [Match]) -> [Match] { func runningMatches(_ allMatches: [Match]) -> [Match] {
allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) #if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting)
} }
func readyMatches(_ allMatches: [Match]) -> [Match] { func readyMatches(_ allMatches: [Match]) async -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
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? = nil) -> [Match] {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let _limit = limit ?? courtCount let _limit = limit ?? courtCount
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit)) return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit))
} }
func finalRanking() -> [Int: [String]] { func finalRanking() -> [Int: [String]] {
var teams: [Int: [String]] = [:] var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
let rounds = rounds() let rounds = rounds()
let final = rounds.last?.playedMatches().last let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId { if let winner = final?.winningTeamId {
teams[1] = [winner] teams[1] = [winner]
ids.insert(winner)
} }
if let finalist = final?.losingTeamId { if let finalist = final?.losingTeamId {
teams[2] = [finalist] teams[2] = [finalist]
ids.insert(finalist)
} }
let others : [Round] = rounds.flatMap { round in let others : [Round] = rounds.flatMap { round in
round.loserRoundsAndChildren().filter { $0.isDisabled() == false && $0.hasNextRound() == false } round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
}.compactMap({ $0 }) }.compactMap({ $0 })
others.forEach { round in others.forEach { round in
if let interval = round.seedInterval() { if let interval = round.seedInterval() {
let playedMatches = round.playedMatches().filter { $0.disabled == false } let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
let winners = playedMatches.compactMap({ $0.winningTeamId }) let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
let losers = playedMatches.compactMap({ $0.losingTeamId }) let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
teams[interval.first + winners.count - 1] = winners if winners.isEmpty {
teams[interval.last] = losers let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
teams[interval.last] = disabledIds
disabledIds.forEach { ids.insert($0) }
} else {
teams[interval.first + winners.count - 1] = winners
winners.forEach { ids.insert($0) }
teams[interval.last] = losers
losers.forEach { ids.insert($0) }
}
} }
} }
@ -1181,7 +1229,7 @@ class Tournament : ModelObject, Storable {
} }
typealias TournamentStatus = (label:String, completion: String) typealias TournamentStatus = (label:String, completion: String)
func cashierStatus() -> TournamentStatus { func cashierStatus() async -> TournamentStatus {
let selectedPlayers = selectedPlayers() let selectedPlayers = selectedPlayers()
let paid = selectedPlayers.filter({ $0.hasPaid() }) let paid = selectedPlayers.filter({ $0.hasPaid() })
let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
@ -1190,7 +1238,7 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
func scheduleStatus() -> TournamentStatus { func scheduleStatus() async -> TournamentStatus {
let allMatches = allMatches() let allMatches = allMatches()
let ready = allMatches.filter({ $0.startDate != nil }) let ready = allMatches.filter({ $0.startDate != nil })
let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés" let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
@ -1199,7 +1247,7 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
func callStatus() -> TournamentStatus { func callStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams() let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " convoquées au bon horaire" let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " convoquées au bon horaire"
@ -1208,7 +1256,7 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
func confirmedSummonStatus() -> TournamentStatus { func confirmedSummonStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams() let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { $0.confirmationDate != nil } let called = selectedSortedTeams.filter { $0.confirmationDate != nil }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " confirmées" let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " confirmées"
@ -1217,7 +1265,7 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
func bracketStatus() -> String { func bracketStatus() async -> String {
let availableSeeds = availableSeeds() let availableSeeds = availableSeeds()
if availableSeeds.isEmpty == false { if availableSeeds.isEmpty == false {
return "placer \(availableSeeds.count) tête\(availableSeeds.count.pluralSuffix) de série" return "placer \(availableSeeds.count) tête\(availableSeeds.count.pluralSuffix) de série"
@ -1233,7 +1281,7 @@ class Tournament : ModelObject, Storable {
} }
} }
func groupStageStatus() -> String { func groupStageStatus() async -> String {
let groupStageTeamsCount = groupStageTeams().count let groupStageTeamsCount = groupStageTeams().count
if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount { if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount {
return "à faire" return "à faire"
@ -1343,9 +1391,9 @@ class Tournament : ModelObject, Storable {
return nil return nil
} }
func resetTeamScores(in matchOfBracketPosition: Int?) { func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) {
guard let match = match(for: matchOfBracketPosition) else { return } guard let match = match(for: matchOfBracketPosition) else { return }
match.resetTeamScores(outsideOf: []) match.resetTeamScores(outsideOf: outsideOf)
} }
func updateTeamScores(in matchOfBracketPosition: Int?) { func updateTeamScores(in matchOfBracketPosition: Int?) {
@ -1626,7 +1674,10 @@ class Tournament : ModelObject, Storable {
func getGroupStageChunkValue() -> Int { func getGroupStageChunkValue() -> Int {
if teamsPerGroupStage >= 2 { if teamsPerGroupStage >= 2 {
return min(groupStageCount, courtCount / (teamsPerGroupStage / 2)) let result = courtCount / (teamsPerGroupStage / 2)
let remainder = courtCount % (teamsPerGroupStage / 2)
let value = remainder == 0 ? result : result + 1
return min(groupStageCount, value)
} else { } else {
return 1 return 1
} }

@ -58,6 +58,10 @@ class HtmlGenerator: ObservableObject {
self.width = width as! CGFloat self.width = width as! CGFloat
}) })
} }
if self.completionHandler != nil {
self.buildPDF()
}
}) })
} }

@ -183,7 +183,7 @@ enum HtmlService {
var template = "" var template = ""
var bracket = "" var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) { if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
for (_, match) in round.playedMatches().enumerated() { for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
} }
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)

@ -412,6 +412,34 @@ struct TournamentRunningTip: Tip {
} }
} }
struct CreateAccountTip: Tip {
var title: Text {
Text("Créer votre compte Padel Club")
}
var message: Text? {
let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes du site, comme le mode TV pour transformer l'expérience de vos tournois !"
return Text(.init(message))
}
var image: Image? {
Image(systemName: "person.crop.circle")
}
var actions: [Action] {
Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte")
//todo
//Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus")
Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Jeter un oeil au site Padel Club")
}
enum ActionKey: String {
case createAccount = "createAccount"
case learnMore = "learnMore"
case accessPadelClubWebPage = "accessPadelClubWebPage"
}
}
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -8,8 +8,12 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
enum AgendaDestination: CaseIterable, Identifiable, Selectable { enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
var id: Self { self } var id: Int { self.rawValue }
static func == (lhs: AgendaDestination, rhs: AgendaDestination) -> Bool {
return lhs.id == rhs.id
}
case activity case activity
case history case history

@ -7,6 +7,11 @@
import SwiftUI import SwiftUI
class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
class SearchViewModel: ObservableObject, Identifiable { class SearchViewModel: ObservableObject, Identifiable {
let id: UUID = UUID() let id: UUID = UUID()
var allowSelection : Int = 0 var allowSelection : Int = 0

@ -13,6 +13,13 @@ protocol Selectable {
func badgeValue() -> Int? func badgeValue() -> Int?
func badgeImage() -> Badge? func badgeImage() -> Badge?
func badgeValueColor() -> Color? func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool
}
extension Selectable {
func displayImageIfValueZero() -> Bool {
return false
}
} }
enum Badge { enum Badge {

@ -51,7 +51,7 @@ struct CallSettingsView: View {
} }
} }
#if DEBUG // #if DEBUG
Section { Section {
RowButtonView("Annuler toutes les convocations", role: .destructive) { RowButtonView("Annuler toutes les convocations", role: .destructive) {
let teams = tournament.unsortedTeams() let teams = tournament.unsortedTeams()
@ -79,7 +79,7 @@ struct CallSettingsView: View {
} }
} }
} }
#endif //#endif
} }
.sheet(isPresented: $showSendToAllView) { .sheet(isPresented: $showSendToAllView) {
SendToAllView(addLink: false) SendToAllView(addLink: false)

@ -8,39 +8,62 @@
import SwiftUI import SwiftUI
import Combine import Combine
struct CashierView: View { struct ShareableObject {
@EnvironmentObject var dataStore: DataStore
var tournaments : [Tournament]
var teams: [TeamRegistration]
@State private var sortOption: SortOption = .callDate
@State private var filterOption: FilterOption = .all
@State private var sortOrder: SortOrder = .ascending
@State private var searchText = ""
@State private var isSearching: Bool = false
init(tournament: Tournament, teams: [TeamRegistration]) {
self.tournaments = [tournament]
self.teams = teams
if tournament.hasEnded(), tournament.players().anySatisfy({ $0.hasPaid() == false }) {
_filterOption = .init(wrappedValue: .didNotPay)
}
if teams.filter({ $0.callDate != nil }).isEmpty {
_sortOption = .init(wrappedValue: .teamRank)
} else {
_sortOption = .init(wrappedValue: .callDate)
}
}
private func _sharedData() -> String { let cashierViewModel: CashierViewModel
let players = teams.filter({ _shouldDisplayTeam($0) }) let teams: [TeamRegistration]
.flatMap({ $0.players().filter({ _shouldDisplayPlayer($0) }) }) let fileName: String
func sharedData() async -> Data? {
let players = teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
.flatMap({ $0.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) })
.map { .map {
[$0.pasteData()] [$0.pasteData()]
.compacted() .compacted()
.joined(separator: "\n") .joined(separator: "\n")
} }
.joined(separator: "\n\n") .joined(separator: "\n\n")
return players return players.data(using: .utf8)
}
}
extension ShareableObject: Transferable {
enum ShareError: Error {
case failed
}
static var transferRepresentation: some TransferRepresentation {
let rep = DataRepresentation<ShareableObject>(exportedContentType: .plainText) { object in
guard let data = await object.sharedData() else {
throw ShareError.failed
}
return data
}
return rep.suggestedFileName { object in object.fileName }
}
}
class CashierViewModel: ObservableObject {
let id: UUID = UUID()
@Published var sortOption: SortOption = .callDate
@Published var filterOption: FilterOption = .all
@Published var sortOrder: SortOrder = .ascending
@Published var searchText: String = ""
@Published var isSearching: Bool = false
func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.unsortedPlayers().anySatisfy({
_shouldDisplayPlayer($0)
})
}
func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false {
filterOption.shouldDisplayPlayer(player) && player.contains(searchText)
} else {
filterOption.shouldDisplayPlayer(player)
}
} }
enum SortOption: Int, Identifiable, CaseIterable { enum SortOption: Int, Identifiable, CaseIterable {
@ -100,28 +123,43 @@ struct CashierView: View {
} }
} }
} }
}
struct CashierView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var cashierViewModel: CashierViewModel
var tournaments : [Tournament]
var teams: [TeamRegistration]
@State private var shareableObject: ShareableObject?
init(tournament: Tournament, teams: [TeamRegistration]) {
self.tournaments = [tournament]
self.teams = teams
}
var body: some View { var body: some View {
List { List {
if isSearching == false { if cashierViewModel.isSearching == false {
Section { Section {
Picker(selection: $filterOption) { Picker(selection: $cashierViewModel.filterOption) {
ForEach(FilterOption.allCases) { filterOption in ForEach(CashierViewModel.FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption) Text(filterOption.localizedLabel()).tag(filterOption)
} }
} label: { } label: {
Text("Statut du règlement") Text("Statut du règlement")
} }
Picker(selection: $sortOption) { Picker(selection: $cashierViewModel.sortOption) {
ForEach(SortOption.allCases) { sortOption in ForEach(CashierViewModel.SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption) Text(sortOption.localizedLabel()).tag(sortOption)
} }
} label: { } label: {
Text("Affichage par") Text("Affichage par")
} }
Picker(selection: $sortOrder) { Picker(selection: $cashierViewModel.sortOrder) {
Text("Croissant").tag(SortOrder.ascending) Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending) Text("Décroissant").tag(SortOrder.descending)
} label: { } label: {
@ -131,12 +169,8 @@ struct CashierView: View {
Text("Options d'affichage") Text("Options d'affichage")
} }
} }
if _isContentUnavailable() {
_contentUnavailableView()
}
switch sortOption { switch cashierViewModel.sortOption {
case .teamRank: case .teamRank:
_byTeamRankView() _byTeamRankView()
case .alphabeticalLastName: case .alphabeticalLastName:
@ -151,51 +185,53 @@ struct CashierView: View {
_byCallDateView() _byCallDateView()
} }
} }
.searchable(text: $cashierViewModel.searchText, isPresented: $cashierViewModel.isSearching, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur"))
.onAppear {
cashierViewModel.searchText = ""
// if tournaments.count == 1 {
// if tournaments.first!.hasEnded() == true, tournaments.first!.players().anySatisfy({ $0.hasPaid() == false }) {
// filterOption = .didNotPay
// }
// }
if cashierViewModel.sortOption == .callDate && teams.first(where: { $0.callDate != nil }) == nil {
cashierViewModel.sortOption = .teamRank
}
self.shareableObject = ShareableObject(cashierViewModel: cashierViewModel, teams: teams, fileName: "Encaissement.txt")
}
.headerProminence(.increased) .headerProminence(.increased)
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur"))
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: _sharedData().createTxtFile("bilan")) if let shareableObject {
ShareLink(
item: shareableObject,
preview: SharePreview(shareableObject.fileName)
)
}
} }
} }
} }
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.players().anySatisfy({
_shouldDisplayPlayer($0)
})
}
private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false {
filterOption.shouldDisplayPlayer(player) && player.contains(searchText)
} else {
filterOption.shouldDisplayPlayer(player)
}
}
@ViewBuilder @ViewBuilder
private func _byPlayer(_ players: [PlayerRegistration]) -> some View { private func _byPlayer(_ players: [PlayerRegistration]) -> some View {
let _players = sortOrder == .ascending ? players : players.reversed() let _players = cashierViewModel.sortOrder == .ascending ? players : players.reversed()
ForEach(_players) { player in if _players.isEmpty {
Section { _contentUnavailableView()
computedPlayerView(player) } else {
} header: { ForEach(_players) { player in
HStack { Section {
if let teamCallDate = player.team()?.callDate { EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
Text(teamCallDate.localizedDate()) } header: {
HStack {
if let teamCallDate = player.team()?.callDate {
Text(teamCallDate.localizedDate())
}
Spacer()
Text(player.formattedRank())
}
} footer: {
if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() {
Text(tournamentTitle)
} }
Spacer()
Text(player.computedRank.formatted())
}
} footer: {
if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() {
Text(tournamentTitle)
} }
} }
} }
@ -203,42 +239,52 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
private func _byPlayerRank() -> some View { private func _byPlayerRank() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.computedRank)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.computedRank)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerAge() -> some View { private func _byPlayerAge() -> some View {
let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerLastName() -> some View { private func _byPlayerLastName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.lastName)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerFirstName() -> some View { private func _byPlayerFirstName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.firstName)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byTeamRankView() -> some View { private func _byTeamRankView() -> some View {
let _teams = sortOrder == .ascending ? teams : teams.reversed() let _teams = cashierViewModel.sortOrder == .ascending ? teams : teams.reversed()
ForEach(_teams) { team in let _filteredTeams = _teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
if _shouldDisplayTeam(team) { if _filteredTeams.isEmpty {
_contentUnavailableView()
} else {
ForEach(_filteredTeams) { team in
Section { Section {
_cashierPlayersView(team.players()) ForEach(team.players()) { player in
if cashierViewModel._shouldDisplayPlayer(player) {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
}
} header: { } header: {
HStack { HStack {
if let callDate = team.callDate { if let callDate = team.callDate {
Text(callDate.localizedDate()) Text(callDate.localizedDate())
} }
Spacer() Spacer()
Text(team.weight.formatted()) VStack(alignment: .trailing, spacing: 0) {
Text("Poids").font(.caption)
Text(team.weight.formatted())
}
} }
} footer: { } footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
@ -252,52 +298,40 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
private func _byCallDateView() -> some View { private func _byCallDateView() -> some View {
let groupedTeams = Dictionary(grouping: teams) { team in let _teams = teams.filter({ $0.callDate != nil && cashierViewModel._shouldDisplayTeam($0) })
if _teams.isEmpty {
_contentUnavailableView()
}
let groupedTeams = Dictionary(grouping: _teams) { team in
team.callDate team.callDate
} }
let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed()
ForEach(keys, id: \.self) { key in ForEach(keys, id: \.self) { key in
if let _teams = groupedTeams[key] { if let _teams = groupedTeams[key] {
ForEach(_teams) { team in ForEach(_teams) { team in
if _shouldDisplayTeam(team) { Section {
Section { ForEach(team.players()) { player in
_cashierPlayersView(team.players()) if cashierViewModel._shouldDisplayPlayer(player) {
} header: { EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
Text(key.localizedDate())
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle)
} }
} }
} header: {
Text(key.localizedDate())
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle)
}
} }
} }
} }
} }
} }
@ViewBuilder
private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View {
ForEach(players) { player in
if _shouldDisplayPlayer(player) {
computedPlayerView(player)
}
}
}
private func _isContentUnavailable() -> Bool {
switch sortOption {
case .callDate:
return teams.filter({ $0.callDate != nil && _shouldDisplayTeam($0) }).isEmpty
case .teamRank:
return teams.filter({ _shouldDisplayTeam($0) }).isEmpty
default:
return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty
}
}
private func _unavailableIcon() -> String { private func _unavailableIcon() -> String {
switch sortOption { switch cashierViewModel.sortOption {
case .teamRank, .callDate: case .teamRank, .callDate:
return "person.2.slash.fill" return "person.2.slash.fill"
default: default:
@ -307,8 +341,8 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
private func _contentUnavailableView() -> some View { private func _contentUnavailableView() -> some View {
if isSearching { if cashierViewModel.isSearching {
ContentUnavailableView.search(text: searchText) ContentUnavailableView.search(text: cashierViewModel.searchText)
} else { } else {
ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon())
} }

@ -90,39 +90,6 @@ struct EventCreationView: View {
case .animation: case .animation:
animationEditorView animationEditorView
} }
Section {
RowButtonView("Valider") {
let event = Event(creator: Store.main.userId, name: eventName)
event.club = selectedClub?.id
tournaments.forEach { tournament in
tournament.event = event.id
}
do {
try dataStore.events.addOrUpdate(instance: event)
} catch {
Logger.error(error)
}
tournaments.forEach { tournament in
tournament.courtCount = selectedClub?.courtCount ?? 2
tournament.startDate = startingDate
tournament.dayDuration = duration
tournament.setupFederalSettings()
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
} catch {
Logger.error(error)
}
dismiss()
navigation.path.append(tournaments.first!)
}
.disabled(tournaments.isEmpty)
}
} }
.toolbar { .toolbar {
if textFieldIsFocus { if textFieldIsFocus {
@ -144,11 +111,10 @@ struct EventCreationView: View {
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une épreuve", icon: "plus.circle.fill") { ButtonValidateView {
let tournament = Tournament.newEmptyInstance() _validate()
self.tournaments.append(tournament)
} }
.popoverTip(multiTournamentsEventTip) .disabled(tournaments.isEmpty)
} }
} }
.navigationTitle("Nouvel événement") .navigationTitle("Nouvel événement")
@ -162,24 +128,63 @@ struct EventCreationView: View {
} }
} }
private func _validate() {
let event = Event(creator: Store.main.userId, name: eventName)
event.club = selectedClub?.id
tournaments.forEach { tournament in
tournament.event = event.id
}
do {
try dataStore.events.addOrUpdate(instance: event)
} catch {
Logger.error(error)
}
tournaments.forEach { tournament in
tournament.courtCount = selectedClub?.courtCount ?? 2
tournament.startDate = startingDate
tournament.dayDuration = duration
tournament.setupFederalSettings()
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
} catch {
Logger.error(error)
}
dismiss()
navigation.path.append(tournaments.first!)
}
@ViewBuilder @ViewBuilder
private var approvedTournamentEditorView: some View { private var approvedTournamentEditorView: some View {
ForEach(tournaments) { tournament in ForEach(tournaments.indices, id: \.self) { index in
let tournament = tournaments[index]
Section { Section {
TournamentConfigurationView(tournament: tournament) TournamentConfigurationView(tournament: tournament)
} footer: { } header: {
if tournaments.count > 1 { if tournaments.count > 1 {
FooterButtonView("effacer") { HStack {
tournaments.removeAll(where: { $0 == tournament }) Spacer()
FooterButtonView("effacer") {
tournaments.removeAll(where: { $0 == tournament })
}
.textCase(nil)
}
}
} footer: {
if index == tournaments.count - 1 {
HStack {
Spacer()
FooterButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") {
let tournament = Tournament.newEmptyInstance()
self.tournaments.append(tournament)
}
.popoverTip(multiTournamentsEventTip)
} }
} }
}
}
Section {
RowButtonView("Ajouter une \((tournaments.count + 1).ordinalFormatted()) épreuve") {
let tournament = Tournament.newEmptyInstance()
self.tournaments.append(tournament)
} }
} }
} }

@ -8,7 +8,11 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
enum EventDestination: Identifiable, Selectable { enum EventDestination: Identifiable, Selectable, Equatable {
static func == (lhs: EventDestination, rhs: EventDestination) -> Bool {
return lhs.id == rhs.id
}
case links case links
case tournaments(Event) case tournaments(Event)
case cashier case cashier

@ -41,11 +41,6 @@ struct ClubSearchView: View {
var club: Club? var club: Club?
var selection: ((Club) -> ())? = nil var selection: ((Club) -> ())? = nil
fileprivate class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
private var distanceLimit: Measurement<UnitLength> { private var distanceLimit: Measurement<UnitLength> {
Measurement(value: radius, unit: .kilometers) Measurement(value: radius, unit: .kilometers)
} }
@ -96,9 +91,9 @@ struct ClubSearchView: View {
_importClub(clubToEdit: clubToEdit, clubMarker: clubMark) _importClub(clubToEdit: clubToEdit, clubMarker: clubMark)
} label: { } label: {
clubView(clubMark) clubView(clubMark)
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} header: { } header: {

@ -7,75 +7,109 @@
import SwiftUI import SwiftUI
struct GenericDestinationPickerView<T: Identifiable & Selectable>: View { struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Binding var selectedDestination: T? @Binding var selectedDestination: T?
let destinations: [T] let destinations: [T]
let nilDestinationIsValid: Bool let nilDestinationIsValid: Bool
var body: some View { var body: some View {
ScrollView(.horizontal) { ScrollViewReader { proxy in
HStack { ScrollView(.horizontal) {
if nilDestinationIsValid { HStack {
Button { if nilDestinationIsValid {
selectedDestination = nil Button {
} label: { selectedDestination = nil
Image(systemName: "wrench.and.screwdriver") } label: {
.foregroundColor(selectedDestination == nil ? .white : .black) Image(systemName: "wrench.and.screwdriver")
} .foregroundColor(selectedDestination == nil ? .white : .black)
.padding() }
.background { .padding()
Circle() .background {
.fill(selectedDestination == nil ? .master : .beige) Circle()
} .fill(selectedDestination == nil ? .master : .beige)
.buttonStyle(.plain) }
} .buttonStyle(.plain)
.id("settings")
ForEach(destinations) { destination in
Button {
selectedDestination = destination
} label: {
Text(destination.selectionLabel())
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
}
.padding()
.background {
Capsule()
.fill(selectedDestination?.id == destination.id ? .master : .beige)
} }
.buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) { ForEach(destinations) { destination in
if let badge = destination.badgeImage() { Button {
Image(systemName: badge.systemName()) selectedDestination = destination
.foregroundColor(badge.color()) } label: {
.imageScale(.medium) Text(destination.selectionLabel())
.background ( .foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
Color(.systemBackground) }
.clipShape(.circle) .padding()
) .background {
.offset(x: 3, y: 3) Capsule()
} else if let count = destination.badgeValue(), count > 0 { .fill(selectedDestination?.id == destination.id ? .master : .beige)
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") }
.foregroundColor(destination.badgeValueColor() ?? .red) .id(destination.id)
.imageScale(.medium) .buttonStyle(.plain)
.background ( .overlay(alignment: .bottomTrailing) {
Color(.systemBackground) if destination.displayImageIfValueZero() {
.clipShape(.circle) let count = destination.badgeValue()
) if let count, count == 0, let badge = destination.badgeImage() {
.offset(x: 3, y: 3) Image(systemName: badge.systemName())
.foregroundColor(badge.color())
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
} else if let count, count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .red)
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
}
} else {
if let badge = destination.badgeImage() {
Image(systemName: badge.systemName())
.foregroundColor(badge.color())
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
} else if let count = destination.badgeValue(), count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .red)
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
}
}
} }
} }
} }
.fixedSize()
.padding(8)
} }
.fixedSize() .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(8) .background(Material.ultraThinMaterial)
} .overlay {
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) VStack(spacing: 0) {
.background(Material.ultraThinMaterial) Spacer()
.overlay { Divider()
VStack(spacing: 0) { }
Spacer() }
Divider() .onAppear {
if let selectedDestination {
proxy.scrollTo(selectedDestination.id)
} else {
proxy.scrollTo("settings")
}
} }
} }
} }

@ -10,23 +10,38 @@ import SwiftUI
struct MatchListView: View { struct MatchListView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
let section: String let section: String
let matches: [Match] let matches: [Match]?
var matchViewStyle: MatchViewStyle = .standardStyle var matchViewStyle: MatchViewStyle = .standardStyle
var hideWhenEmpty: Bool = false
@State var isExpanded: Bool = true @State var isExpanded: Bool = true
private func _shouldHide() -> Bool {
if matches != nil && matches!.isEmpty && hideWhenEmpty == true {
return true
} else {
return false
}
}
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if matches.isEmpty == false { if _shouldHide() == false {
Section { Section {
DisclosureGroup(isExpanded: $isExpanded) { DisclosureGroup(isExpanded: $isExpanded) {
ForEach(matches) { match in if let matches {
MatchRowView(match: match, matchViewStyle: matchViewStyle) ForEach(matches) { match in
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
}
} }
} label: { } label: {
LabeledContent { LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) if matches == nil {
ProgressView()
} else {
Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix)
}
} label: { } label: {
Text(section.firstCapitalized) Text(section.firstCapitalized)
} }

@ -17,6 +17,10 @@ struct GroupStageView: View {
@State private var confirmResetMatch: Bool = false @State private var confirmResetMatch: Bool = false
let playedMatches: [Match] let playedMatches: [Match]
@State private var runningMatches: [Match]?
@State private var readyMatches: [Match]?
@State private var availableToStart: [Match]?
init(groupStage: GroupStage) { init(groupStage: GroupStage) {
self.groupStage = groupStage self.groupStage = groupStage
self.playedMatches = groupStage.playedMatches() self.playedMatches = groupStage.playedMatches()
@ -44,12 +48,16 @@ struct GroupStageView: View {
} }
.headerProminence(.increased) .headerProminence(.increased)
let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: true)
MatchListView(section: "disponible", matches: groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches)) MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true)
MatchListView(section: "en cours", matches: runningMatches) MatchListView(section: "à lancer", matches: self.readyMatches, hideWhenEmpty: true)
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches))
MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), isExpanded: false) MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), isExpanded: false)
} }
.task {
self.runningMatches = await groupStage.asyncRunningMatches(playedMatches: playedMatches)
self.readyMatches = await groupStage.readyMatches(playedMatches: playedMatches)
self.availableToStart = await groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches ?? [])
}
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView() _groupStageMenuView()

@ -11,7 +11,11 @@ struct GroupStagesView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedDestination: GroupStageDestination? @State private var selectedDestination: GroupStageDestination?
enum GroupStageDestination: Selectable, Identifiable { enum GroupStageDestination: Selectable, Identifiable, Equatable {
static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool {
lhs.id == rhs.id
}
case all case all
case groupStage(GroupStage) case groupStage(GroupStage)
@ -47,7 +51,12 @@ struct GroupStagesView: View {
} }
func badgeImage() -> Badge? { func badgeImage() -> Badge? {
nil switch self {
case .all:
return nil
case .groupStage(let groupStage):
return groupStage.badgeImage()
}
} }
} }
@ -76,30 +85,36 @@ struct GroupStagesView: View {
return allDestinations return allDestinations
} }
@State private var runningMatches: [Match]?
@State private var readyMatches: [Match]?
@State private var availableToStart: [Match]?
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination { switch selectedDestination {
case .all: case .all:
let runningMatches = tournament.runningMatches(allMatches)
let availableToStart = tournament.availableToStart(allMatches, in: runningMatches)
let readyMatches = tournament.readyMatches(allMatches)
let finishedMatches = tournament.finishedMatches(allMatches) let finishedMatches = tournament.finishedMatches(allMatches)
List { List {
MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "disponible", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "prêt à démarrer", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false)
} }
.task {
runningMatches = await tournament.asyncRunningMatches(allMatches)
availableToStart = await tournament.availableToStart(allMatches, in: runningMatches ?? [])
readyMatches = await tournament.readyMatches(allMatches)
}
.overlay { .overlay {
if availableToStart.isEmpty && runningMatches.isEmpty && readyMatches.isEmpty && finishedMatches.isEmpty { if availableToStart?.isEmpty == true && runningMatches?.isEmpty == true && readyMatches?.isEmpty == true && finishedMatches.isEmpty == true {
ContentUnavailableView("Aucun match à afficher", systemImage: "tennisball") ContentUnavailableView("Aucun match à afficher", systemImage: "tennisball")
} }
} }
.navigationTitle("Toutes les poules") .navigationTitle("Toutes les poules")
case .groupStage(let groupStage): case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage) GroupStageView(groupStage: groupStage).id(groupStage.id)
case nil: case nil:
GroupStageSettingsView() GroupStageSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")

@ -308,6 +308,8 @@ struct MatchDetailView: View {
Button(role: .destructive) { Button(role: .destructive) {
match.resetScores() match.resetScores()
match.resetMatch()
match.confirmed = false
save() save()
} label: { } label: {
Text("Supprimer les scores") Text("Supprimer les scores")

@ -22,8 +22,11 @@ struct MatchSetupView: View {
@ViewBuilder @ViewBuilder
func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View { func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View {
let team = match.team(teamPosition) let scores = match.teamScores
let teamScore = match.teamScore(ofTeam: team) let team = scores.isEmpty ? nil : match.team(teamPosition)
let teamScore = (team != nil) ? scores.first(where: { $0.teamRegistration == team!.id }) : nil
let walkOutSpot = teamScore?.walkOut == 1
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 {
@ -67,7 +70,6 @@ struct MatchSetupView: View {
.strikethrough() .strikethrough()
} }
HStack { HStack {
let walkOutSpot = match.isWalkOutSpot(teamPosition)
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())

@ -44,32 +44,6 @@ struct MatchSummaryView: View {
} }
var body: some View { var body: some View {
matchSummaryView
// .contextMenu {
// ForEach(match.teamScores) { entrant in
// if let team = entrant.team, team.orderedPlayers.count > 2 {
// NavigationLink {
// PlayerPickerView(match: match, team: team)
// } label: {
// if let teamTitle = team.entrant?.brand?.title {
// Text(teamTitle).foregroundStyle(.secondary)
// } else {
// let index = match.orderedEntrants.firstIndex(where: { $0 == entrant }) ?? 0
// Text("Équipe \(index + 1)")
// }
// if match.players(from: team).isEmpty {
// Text("Choisir la paire")
// } else {
// Text("Modifier la paire")
// }
// }
// }
// }
// }
}
@ViewBuilder
var matchSummaryView: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if matchViewStyle != .plainStyle { if matchViewStyle != .plainStyle {
if matchViewStyle == .feedStyle, let tournament = match.currentTournament() { if matchViewStyle == .feedStyle, let tournament = match.currentTournament() {

@ -30,18 +30,17 @@ struct ActivityView: View {
var endedTournaments: [Tournament] { var endedTournaments: [Tournament] {
dataStore.tournaments.filter({ $0.endDate != nil }) dataStore.tournaments.filter({ $0.endDate != nil })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) }) .filter({ federalDataViewModel.isTournamentValidForFilters($0) })
.sorted(using: SortDescriptor(\.startDate, order: .reverse))
}
func _activityStatus() -> String? {
let tournaments = tournaments
if tournaments.isEmpty && federalDataViewModel.areFiltersEnabled() == false {
return nil
} else {
let count = tournaments.map { $0.tournaments.count }.reduce(0,+)
return "\(count) tournoi" + count.pluralSuffix
}
} }
//
// func _activityStatus() -> String? {
// let tournaments = tournaments
// if tournaments.isEmpty && federalDataViewModel.areFiltersEnabled() == false {
// return nil
// } else {
// let count = tournaments.map { $0.tournaments.count }.reduce(0,+)
// return "\(count) tournoi" + count.pluralSuffix
// }
// }
var tournaments: [FederalTournamentHolder] { var tournaments: [FederalTournamentHolder] {
switch navigation.agendaDestination! { switch navigation.agendaDestination! {
@ -63,11 +62,11 @@ struct ActivityView: View {
List { List {
switch navigation.agendaDestination! { switch navigation.agendaDestination! {
case .activity: case .activity:
EventListView(tournaments: runningTournaments, viewStyle: viewStyle) EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true)
case .history: case .history:
EventListView(tournaments: endedTournaments, viewStyle: viewStyle) EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false)
case .tenup: case .tenup:
EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle) EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle, sortAscending: true)
.id(uuid) .id(uuid)
} }
} }
@ -136,19 +135,10 @@ struct ActivityView: View {
} }
.toolbar { .toolbar {
if presentToolbar { if presentToolbar {
let _activityStatus = _activityStatus() //let _activityStatus = _activityStatus()
if federalDataViewModel.areFiltersEnabled() || _activityStatus != nil { if federalDataViewModel.areFiltersEnabled() {
ToolbarItem(placement: .status) { ToolbarItem(placement: .status) {
VStack(spacing: -2) { Text(federalDataViewModel.filterStatus())
if federalDataViewModel.areFiltersEnabled() {
Text(federalDataViewModel.filterStatus())
}
if let _activityStatus {
Text(_activityStatus)
.foregroundStyle(.secondary)
}
}
.font(.footnote)
} }
} }

@ -15,13 +15,15 @@ struct EventListView: View {
let tournaments: [FederalTournamentHolder] let tournaments: [FederalTournamentHolder]
let viewStyle: AgendaDestination.ViewStyle let viewStyle: AgendaDestination.ViewStyle
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: navigation.agendaDestination == .tenup ? federalDataViewModel.filteredFederalTournaments : tournaments) { $0.startDate.startOfMonth }
switch viewStyle { switch viewStyle {
case .list: case .list:
ForEach(groupedTournamentsByDate.keys.sorted(by: <), id: \.self) { section in ForEach(groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 }), id: \.self) { section in
if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: \.startDate) { if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate }
) {
Section { Section {
_listView(_tournaments) _listView(_tournaments)
} header: { } header: {
@ -37,7 +39,7 @@ struct EventListView: View {
} }
case .calendar: case .calendar:
ForEach(_nextMonths(), id: \.self) { section in ForEach(_nextMonths(), id: \.self) { section in
let _tournaments = groupedTournamentsByDate[section]?.sorted(by: \.startDate) ?? [] let _tournaments = groupedTournamentsByDate[section] ?? []
Section { Section {
CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id) CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id)
} header: { } header: {
@ -117,5 +119,5 @@ struct EventListView: View {
} }
#Preview { #Preview {
EventListView(tournaments: [], viewStyle: .calendar) EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true)
} }

@ -48,7 +48,7 @@ struct MainView: View {
dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }) dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil })
} }
var badgeText: Text? = Store.main.userName() == nil ? Text("!").font(.headline) : nil var badgeText: Text? = Store.main.userId == nil ? Text("!").font(.headline) : nil
var body: some View { var body: some View {
TabView(selection: selectedTabHandler) { TabView(selection: selectedTabHandler) {

@ -17,7 +17,6 @@ struct OngoingView: View {
var matches: [Match] { var matches: [Match] {
let sorting = sortByField ? fieldSorting : defaultSorting let sorting = sortByField ? fieldSorting : defaultSorting
let now = Date()
return dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending) return dataStore.matches.filter({ $0.confirmed && $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending)
} }

@ -0,0 +1,31 @@
//
// LoserRoundSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/06/2024.
//
import SwiftUI
import LeStorage
struct LoserRoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
List {
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?
}
}
}
#Preview {
LoserRoundSettingsView()
}

@ -25,9 +25,9 @@ struct LoserRoundView: View {
} }
ForEach(loserRounds) { loserRound in ForEach(loserRounds) { loserRound in
if isEditingTournamentSeed || loserRound.isDisabled() == false { if true {
Section { Section {
let matches = (isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false })).sorted(by: \.index) let matches = loserRound.playedMatches().sorted(by: \.index)
ForEach(matches) { match in ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
.overlay { .overlay {

@ -34,7 +34,12 @@ struct LoserRound: Identifiable, Selectable {
} }
extension LoserRound { extension LoserRound: Equatable {
static func == (lhs: LoserRound, rhs: LoserRound) -> Bool {
lhs.id == rhs.id
}
func selectionLabel() -> String { func selectionLabel() -> String {
return "Tour #\(turnIndex + 1)" return "Tour #\(turnIndex + 1)"
} }
@ -54,7 +59,6 @@ extension LoserRound {
struct LoserRoundsView: View { struct LoserRoundsView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
var upperBracketRound: Round var upperBracketRound: Round
@State private var selectedRound: LoserRound? @State private var selectedRound: LoserRound?
let loserRounds: [Round] let loserRounds: [Round]
@ -64,8 +68,7 @@ struct LoserRoundsView: View {
self.upperBracketRound = upperBracketRound self.upperBracketRound = upperBracketRound
let _loserRounds = upperBracketRound.loserRounds() let _loserRounds = upperBracketRound.loserRounds()
self.loserRounds = _loserRounds self.loserRounds = _loserRounds
let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound) let rounds = LoserRound.updateDestinations(fromLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound)
let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound)
_allDestinations = State(wrappedValue: rounds) _allDestinations = State(wrappedValue: rounds)
_selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first) _selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first)
@ -78,14 +81,5 @@ struct LoserRoundsView: View {
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.onChange(of: isEditingTournamentSeed.wrappedValue) {
_updateDestinations()
}
}
private func _updateDestinations() {
let enabledLoserRounds = isEditingTournamentSeed.wrappedValue ? loserRounds : LoserRound.enabledLoserRounds(inLoserRounds: loserRounds, inUpperBracketRound: upperBracketRound)
self.allDestinations = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound)
} }
} }

@ -42,19 +42,7 @@ struct RoundSettingsView: View {
// } // }
Section { Section {
RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ team in await _removeAllSeeds()
tournament.resetTeamScores(in: team.bracketPosition)
team.bracketPosition = nil
})
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
tournament.allRounds().forEach({ round in
round.enableRound()
})
self.isEditingTournamentSeed.wrappedValue = true
} }
} }
@ -94,6 +82,30 @@ struct RoundSettingsView: View {
} }
} }
} }
private func _removeAllSeeds() async {
tournament.unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = tournament.allRoundMatches().flatMap { match in
match.teamScores
}
do {
try DataStore.shared.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
tournament.allRounds().forEach({ round in
round.enableRound()
})
self.isEditingTournamentSeed.wrappedValue = true
}
} }
#Preview { #Preview {

@ -13,6 +13,27 @@ struct RoundView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State private var selectedSeedGroup: SeedInterval? @State private var selectedSeedGroup: SeedInterval?
@State private var spaceLeft: [Match] = []
@State private var seedSpaceLeft: [Match] = []
@State private var availableSeedGroup: SeedInterval?
private func _getAvailableSeedGroup() async {
availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index)
}
private func _getSpaceLeft() async {
spaceLeft.removeAll()
seedSpaceLeft.removeAll()
round.displayableMatches().forEach({
let count = $0.teamScores.count
if count == 0 {
seedSpaceLeft.append($0)
} else if count == 1 {
spaceLeft.append($0)
}
})
}
var showVisualDrawView: Binding<Bool> { Binding( var showVisualDrawView: Binding<Bool> { Binding(
get: { selectedSeedGroup != nil }, get: { selectedSeedGroup != nil },
set: { set: {
@ -26,14 +47,13 @@ struct RoundView: View {
var body: some View { var body: some View {
List { List {
let loserRounds = round.loserRounds()
let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
let displayableMatches = round.displayableMatches().sorted(by: \.index) let displayableMatches = round.displayableMatches().sorted(by: \.index)
let spaceLeft = displayableMatches.filter({ $0.hasSpaceLeft() }) let loserRounds = round.loserRounds()
let seedSpaceLeft = displayableMatches.filter({ $0.isEmpty() }) if displayableMatches.isEmpty {
if isEditingTournamentSeed.wrappedValue == false { Section {
ContentUnavailableView("Aucun match dans cette manche", systemImage: "tennisball")
}
} else if isEditingTournamentSeed.wrappedValue == false {
//(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue })
if loserRounds.isEmpty == false { if loserRounds.isEmpty == false {
let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle() let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle()
@ -48,14 +68,19 @@ struct RoundView: View {
} }
} }
} else { } else {
if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) { let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
if availableSeeds.isEmpty == false, let availableSeedGroup {
Section { Section {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup)
_save() //_save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false self.isEditingTournamentSeed.wrappedValue = false
} }
await _getSpaceLeft()
await _getAvailableSeedGroup()
} }
} footer: { } footer: {
if availableSeedGroup.isFixed() == false { if availableSeedGroup.isFixed() == false {
@ -74,86 +99,95 @@ struct RoundView: View {
} }
} }
if availableQualifiedTeams.isEmpty == false {
if availableQualifiedTeams.isEmpty == false && spaceLeft.isEmpty == false { if spaceLeft.isEmpty == false {
Section { Section {
DisclosureGroup { DisclosureGroup {
ForEach(availableQualifiedTeams) { team in ForEach(availableQualifiedTeams) { team in
NavigationLink { NavigationLink {
SpinDrawView(drawees: [team], segments: spaceLeft) { results in SpinDrawView(drawees: [team], segments: spaceLeft) { results in
Task { Task {
results.forEach { drawResult in results.forEach { drawResult in
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
} }
_save() await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
} }
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count)
} }
} label: { } header: {
Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
} }
} header: {
Text("Tirage au sort visuel d'un qualifié").font(.subheadline)
} }
} }
if availableSeeds.isEmpty == false && seedSpaceLeft.isEmpty == false { if availableSeeds.isEmpty == false {
Section { if seedSpaceLeft.isEmpty == false {
DisclosureGroup { Section {
ForEach(availableSeeds) { team in DisclosureGroup {
NavigationLink { ForEach(availableSeeds) { team in
SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in NavigationLink {
Task { SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in
results.forEach { drawResult in Task {
team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) results.forEach { drawResult in
} team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false)
_save() }
if availableSeeds.isEmpty && tournament.availableQualifiedTeams().isEmpty { await _save()
self.isEditingTournamentSeed.wrappedValue = false if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
await _getSpaceLeft()
await _getAvailableSeedGroup()
} }
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
} }
} label: { } header: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
} }
} header: { } else if spaceLeft.isEmpty == false {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline) Section {
} DisclosureGroup {
} else if availableSeeds.isEmpty == false && spaceLeft.isEmpty == false { ForEach(availableSeeds) { team in
Section { NavigationLink {
DisclosureGroup { SpinDrawView(drawees: [team], segments: spaceLeft) { results in
ForEach(availableSeeds) { team in Task {
NavigationLink { results.forEach { drawResult in
SpinDrawView(drawees: [team], segments: spaceLeft) { results in team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
Task { }
results.forEach { drawResult in await _save()
team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
} self.isEditingTournamentSeed.wrappedValue = false
_save() }
if availableSeeds.isEmpty && tournament.availableQualifiedTeams().isEmpty { await _getSpaceLeft()
self.isEditingTournamentSeed.wrappedValue = false await _getAvailableSeedGroup()
} }
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
TeamRowView(team: team, displayCallDate: false)
} }
} label: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count)
} }
} label: { } header: {
Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
} }
} header: {
Text("Tirage au sort visuel d'une tête de série").font(.subheadline)
} }
} }
} }
@ -164,19 +198,30 @@ struct RoundView: View {
HStack { HStack {
Text(round.roundTitle(.wide)) Text(round.roundTitle(.wide))
if round.index > 0 { if round.index > 0 {
Text(match.matchTitle(.short)) Text(match.matchTitle(.short, inMatches: displayableMatches))
} else { } else {
let tournamentTeamCount = tournament.teamCount let tournamentTeamCount = tournament.teamCount
if let seedIntervalPointRange = loserRounds.first?.seedInterval()?.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournamentTeamCount) { if let seedIntervalPointRange = round.seedInterval()?.pointsRange(tournamentLevel: tournament.tournamentLevel, teamsCount: tournamentTeamCount) {
Spacer() Spacer()
Text(seedIntervalPointRange) Text(seedIntervalPointRange)
.font(.caption) .font(.caption)
} }
} }
#if DEBUG
Spacer()
Text(match.teamScores.count.formatted())
#endif
} }
} }
} }
} }
.onAppear {
Task {
await _prepareRound()
}
}
.fullScreenCover(isPresented: showVisualDrawView) { .fullScreenCover(isPresented: showVisualDrawView) {
if let availableSeedGroup = selectedSeedGroup { if let availableSeedGroup = selectedSeedGroup {
let seeds = tournament.seeds(inSeedGroup: availableSeedGroup) let seeds = tournament.seeds(inSeedGroup: availableSeedGroup)
@ -187,7 +232,7 @@ struct RoundView: View {
draws.forEach { drawResult in draws.forEach { drawResult in
seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: false) seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: false)
} }
_save() await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false self.isEditingTournamentSeed.wrappedValue = false
} }
@ -201,7 +246,9 @@ struct RoundView: View {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
if isEditingTournamentSeed.wrappedValue { if isEditingTournamentSeed.wrappedValue {
_save() Task {
await _save()
}
} }
isEditingTournamentSeed.wrappedValue.toggle() isEditingTournamentSeed.wrappedValue.toggle()
} }
@ -209,7 +256,7 @@ struct RoundView: View {
} }
} }
private func _save() { private func _save() async {
do { do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch { } catch {
@ -221,7 +268,7 @@ struct RoundView: View {
rounds.forEach { round in rounds.forEach { round in
let matches = round.playedMatches() let matches = round.playedMatches()
matches.forEach { match in matches.forEach { match in
match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound()) match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches))
} }
} }
let allRoundMatches = tournament.allRoundMatches() let allRoundMatches = tournament.allRoundMatches()
@ -231,6 +278,15 @@ struct RoundView: View {
Logger.error(error) Logger.error(error)
} }
} }
private func _prepareRound() async {
Task {
await _getSpaceLeft()
}
Task {
await _getAvailableSeedGroup()
}
}
} }
#Preview { #Preview {

@ -33,7 +33,7 @@ struct RoundsView: View {
RoundSettingsView() RoundSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")
case .some(let selectedRound): case .some(let selectedRound):
RoundView(round: selectedRound) RoundView(round: selectedRound).id(selectedRound.id)
.navigationTitle(selectedRound.roundTitle()) .navigationTitle(selectedRound.roundTitle())
} }
} }

@ -141,7 +141,7 @@ import LeStorage
var currentPlan: StoreItem? { var currentPlan: StoreItem? {
// #if DEBUG // #if DEBUG
// return .monthlyUnlimited return .monthlyUnlimited
// #else // #else
if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) { if let currentBestPlan = self.currentBestPlan, let plan = StoreItem(rawValue: currentBestPlan.productID) {
return plan return plan

@ -13,10 +13,6 @@ struct TeamHeaderView: View {
var tournament: Tournament? var tournament: Tournament?
var body: some View { var body: some View {
_teamHeaderView(team, teamIndex: teamIndex)
}
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View {
HStack(spacing: 16.0) { HStack(spacing: 16.0) {
if let teamIndex { if let teamIndex {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {

@ -25,6 +25,22 @@ struct EditingTeamView: View {
} header: { } header: {
Text("Date d'inscription") Text("Date d'inscription")
} }
Section {
RowButtonView("Retirer des poules", role: .destructive) {
team.resetGroupeStagePosition()
_save()
}
.disabled(team.inGroupStage() == false)
}
Section {
RowButtonView("Retirer du tableau", role: .destructive) {
team.resetBracketPosition()
_save()
}
.disabled(team.inRound() == false)
}
} }
.onChange(of: registrationDate) { .onChange(of: registrationDate) {
team.registrationDate = registrationDate team.registrationDate = registrationDate

@ -80,7 +80,9 @@ struct TeamPickerView: View {
presentTeamPickerView = false presentTeamPickerView = false
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team)
.contentShape(Rectangle())
} }
.frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} }

@ -27,7 +27,8 @@ struct FileImportView: View {
@State private var selectedOptions: Set<TeamImportStrategy> = Set() @State private var selectedOptions: Set<TeamImportStrategy> = Set()
@State private var fileProvider: FileImportManager.FileProvider = .frenchFederation @State private var fileProvider: FileImportManager.FileProvider = .frenchFederation
@State private var validationInProgress: Bool = false
private var filteredTeams: [FileImportManager.TeamHolder] { private var filteredTeams: [FileImportManager.TeamHolder] {
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight) return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight)
} }
@ -104,6 +105,16 @@ struct FileImportView: View {
} }
} }
if validationInProgress {
Section {
LabeledContent {
ProgressView()
} label: {
Text("Mise à jour des équipes")
}
}
}
if let errorMessage { if let errorMessage {
Section { Section {
Text(errorMessage) Text(errorMessage)
@ -146,7 +157,7 @@ struct FileImportView: View {
Section { Section {
ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash") ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash")
} }
} else if didImport { } else if didImport && validationInProgress == false {
let _filteredTeams = filteredTeams let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams() let previousTeams = tournament.sortedTeams()
@ -222,38 +233,40 @@ struct FileImportView: View {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView { ButtonValidateView {
// if false { //selectedOptions.contains(.deleteBeforeImport) _validate()
// try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
// }
if true { //selectedOptions.contains(.notFoundAreWalkOut)
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
unfound.forEach { team in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = true
}
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound)
} catch {
Logger.error(error)
}
}
tournament.importTeams(filteredTeams)
dismiss()
} }
.disabled(teams.isEmpty) .disabled(teams.isEmpty)
} }
} }
.interactiveDismissDisabled(validationInProgress)
.disabled(validationInProgress)
}
private func _validate() {
validationInProgress = true
Task {
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
unfound.forEach { team in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = true
}
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound)
} catch {
Logger.error(error)
}
tournament.importTeams(filteredTeams)
dismiss()
}
} }
func _startImport(fileContent: String) async throws { private func _startImport(fileContent: String) async throws {
await MainActor.run { await MainActor.run {
errorMessage = nil errorMessage = nil
teams.removeAll() teams.removeAll()

@ -17,18 +17,40 @@ extension String : Identifiable {
struct BroadcastView: View { struct BroadcastView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
let context = CIContext() let context = CIContext()
let filter = CIFilter.qrCodeGenerator() let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String? @State private var urlToShow: String?
@State private var tvMode: Bool = false @State private var tvMode: Bool = false
@State private var pageLink: PageLink = .teams @State private var pageLink: PageLink = .teams
let createAccountTip = CreateAccountTip()
let tournamentPublishingTip = TournamentPublishingTip() let tournamentPublishingTip = TournamentPublishingTip()
let tournamentTVBroadcastTip = TournamentTVBroadcastTip() let tournamentTVBroadcastTip = TournamentTVBroadcastTip()
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
List { List {
if Store.main.userId == nil {
Section {
TipView(createAccountTip) { action in
switch action.id {
case CreateAccountTip.ActionKey.accessPadelClubWebPage.rawValue:
UIApplication.shared.open(URLs.main.url)
case CreateAccountTip.ActionKey.createAccount.rawValue:
navigation.selectedTab = .umpire
default:
break
//todo
// case CreateAccountTip.ActionKey.learnMore.rawValue:
// UIApplication.shared.open(URLs.padelClubLandingPage.url)
}
}
.tipStyle(tint: .master)
}
}
Section { Section {
TipView(tournamentPublishingTip) { action in TipView(tournamentPublishingTip) { action in
UIApplication.shared.open(URLs.main.url) UIApplication.shared.open(URLs.main.url)

@ -11,20 +11,16 @@ struct InscriptionInfoView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament @Environment(Tournament.self) var tournament
var players : [PlayerRegistration] { tournament.unsortedPlayers() } @State private var players : [PlayerRegistration] = []
var selectedTeams : [TeamRegistration] { tournament.selectedSortedTeams() } @State private var selectedTeams : [TeamRegistration] = []
@State private var callDateIssue : [TeamRegistration] = []
var callDateIssue : [TeamRegistration] { @State private var waitingList : [TeamRegistration] = []
selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) } @State private var duplicates : [PlayerRegistration] = []
} @State private var problematicPlayers : [PlayerRegistration] = []
@State private var inadequatePlayers : [PlayerRegistration] = []
var waitingList : [TeamRegistration] { tournament.waitingListTeams(in: selectedTeams) } @State private var playersWithoutValidLicense : [PlayerRegistration] = []
var duplicates : [PlayerRegistration] { tournament.duplicates(in: players) } @State private var entriesFromBeachPadel : [TeamRegistration] = []
var problematicPlayers : [PlayerRegistration] { players.filter({ $0.sex == nil }) } @State private var playersMissing : [TeamRegistration] = []
var inadequatePlayers : [PlayerRegistration] { tournament.inadequatePlayers(in: players) }
var playersWithoutValidLicense : [PlayerRegistration] { tournament.playersWithoutValidLicense(in: players) }
var entriesFromBeachPadel : [TeamRegistration] { tournament.unsortedTeams().filter({ $0.isImported() }) }
var playersMissing : [TeamRegistration] { selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) }
var body: some View { var body: some View {
List { List {
@ -196,10 +192,28 @@ struct InscriptionInfoView: View {
.listRowView(color: .pink) .listRowView(color: .pink)
} }
} }
.task {
await _getIssues()
}
.navigationTitle("Synthèse") .navigationTitle("Synthèse")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
private func _getIssues() async {
Task {
players = tournament.unsortedPlayers()
selectedTeams = tournament.selectedSortedTeams()
callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) }
waitingList = tournament.waitingListTeams(in: selectedTeams)
duplicates = tournament.duplicates(in: players)
problematicPlayers = players.filter({ $0.sex == nil })
inadequatePlayers = tournament.inadequatePlayers(in: players)
playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players)
entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() })
playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
}
}
} }
#Preview { #Preview {

@ -9,12 +9,22 @@ import SwiftUI
import TipKit import TipKit
import LeStorage import LeStorage
let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
let fileTip = InscriptionManagerFileInputTip()
let pasteTip = InscriptionManagerPasteInputTip()
let searchTip = InscriptionManagerSearchInputTip()
let createTip = InscriptionManagerCreateInputTip()
let rankUpdateTip = InscriptionManagerRankUpdateTip()
let padelBeachExportTip = PadelBeachExportTip()
let padelBeachImportTip = PadelBeachImportTip()
struct InscriptionManagerView: View { struct InscriptionManagerView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor @EnvironmentObject var networkMonitor: NetworkMonitor
@FetchRequest( @FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], sortDescriptors: [],
animation: .default) animation: .default)
private var fetchPlayers: FetchedResults<ImportedPlayer> private var fetchPlayers: FetchedResults<ImportedPlayer>
@ -41,7 +51,14 @@ struct InscriptionManagerView: View {
@State private var contactType: ContactType? = nil @State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false @State private var showSubscriptionView: Bool = false
@State private var registrationIssues: Int? = nil
@State private var sortedTeams: [TeamRegistration] = []
@State private var unfilteredTeams: [TeamRegistration] = []
@State private var walkoutTeams: [TeamRegistration] = []
@State private var unsortedTeamsWithoutWO: [TeamRegistration] = []
@State private var unsortedPlayers: [PlayerRegistration] = []
@State private var teamPaste: URL?
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
sentError != nil sentError != nil
@ -83,20 +100,9 @@ struct InscriptionManagerView: View {
} }
} }
let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
let fileTip = InscriptionManagerFileInputTip()
let pasteTip = InscriptionManagerPasteInputTip()
let searchTip = InscriptionManagerSearchInputTip()
let createTip = InscriptionManagerCreateInputTip()
let rankUpdateTip = InscriptionManagerRankUpdateTip()
let padelBeachExportTip = PadelBeachExportTip()
let padelBeachImportTip = PadelBeachImportTip()
let categoryOption: PlayerFilterOption let categoryOption: PlayerFilterOption
let filterable: Bool let filterable: Bool
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
@ -110,6 +116,16 @@ struct InscriptionManagerView: View {
} }
} }
private func _clearScreen() {
teamPaste = nil
unsortedPlayers.removeAll()
unfilteredTeams.removeAll()
walkoutTeams.removeAll()
unsortedTeamsWithoutWO.removeAll()
sortedTeams.removeAll()
registrationIssues = nil
}
// Function to create a simple hash from a list of IDs // Function to create a simple hash from a list of IDs
private func _simpleHash(ids: [String]) -> Int { private func _simpleHash(ids: [String]) -> Int {
// Combine the hash values of each string // Combine the hash values of each string
@ -121,7 +137,44 @@ struct InscriptionManagerView: View {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
} }
private func _setHash() async {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let selectedSortedTeams = tournament.selectedSortedTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
}
private func _handleHashDiff() async {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
if let teamsHash, newHash != teamsHash {
self.teamsHash = newHash
if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false {
self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = true
let waitingList = self.tournament.waitingListTeams(in: self.tournament.selectedSortedTeams())
waitingList.forEach { team in
if team.bracketPosition != nil || team.groupStagePosition != nil {
team.resetPositions()
}
}
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: waitingList)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -130,38 +183,18 @@ struct InscriptionManagerView: View {
_buildingTeamView() _buildingTeamView()
} else if tournament.unsortedTeams().isEmpty { } else if tournament.unsortedTeams().isEmpty {
_inscriptionTipsView() _inscriptionTipsView()
} else { }
if _isEditingTeam() == false {
_teamRegisteredView() _teamRegisteredView()
} }
} }
.onAppear { .onAppear {
let selectedSortedTeams = tournament.selectedSortedTeams() _getTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
} }
.onDisappear { .onDisappear {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) Task {
if let teamsHash, newHash != teamsHash { await _handleHashDiff()
self.teamsHash = newHash
if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false {
self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = true
let waitingList = self.tournament.waitingListTeams(in: self.tournament.selectedSortedTeams())
waitingList.forEach { team in
if team.bracketPosition != nil || team.groupStagePosition != nil {
team.resetPositions()
}
}
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: waitingList)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} }
} }
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
@ -258,10 +291,14 @@ struct InscriptionManagerView: View {
.tint(.master) .tint(.master)
} }
.onChange(of: tournament.prioritizeClubMembers) { .onChange(of: tournament.prioritizeClubMembers) {
_clearScreen()
_save() _save()
_getTeams()
} }
.onChange(of: tournament.teamSorting) { .onChange(of: tournament.teamSorting) {
_clearScreen()
_save() _save()
_getTeams()
} }
.onChange(of: currentRankSourceDate) { .onChange(of: currentRankSourceDate) {
if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate { if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
@ -334,8 +371,10 @@ struct InscriptionManagerView: View {
Label("Clôturer", systemImage: "lock") Label("Clôturer", systemImage: "lock")
} }
Divider() Divider()
ShareLink(item: tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))) { if let teamPaste {
Label("Exporter les paires", systemImage: "square.and.arrow.up") ShareLink(item: teamPaste) {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
} }
Button { Button {
presentImportView = true presentImportView = true
@ -373,7 +412,30 @@ struct InscriptionManagerView: View {
createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil
} }
private func _getTeams(from sortedTeams: [TeamRegistration]) -> [TeamRegistration] { private func _prepareStats() async {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _prepareStats", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
unsortedPlayers = tournament.unsortedPlayers()
walkoutTeams = tournament.walkoutTeams()
unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
teamPaste = tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))
}
private func _prepareTeams() {
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _prepareTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
sortedTeams = tournament.sortedTeams()
var teams = sortedTeams var teams = sortedTeams
if filterMode == .walkOut { if filterMode == .walkOut {
teams = teams.filter({ $0.walkOut }) teams = teams.filter({ $0.walkOut })
@ -384,17 +446,34 @@ struct InscriptionManagerView: View {
} }
if byDecreasingOrdering { if byDecreasingOrdering {
return teams.reversed() self.unfilteredTeams = teams.reversed()
} else { } else {
return teams self.unfilteredTeams = teams
}
}
private func _getIssues() async {
#if DEBUG_TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _getIssues", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
await registrationIssues = tournament.registrationIssues()
}
private func _getTeams() {
_prepareTeams()
Task {
await _prepareStats()
await _getIssues()
await _setHash()
} }
} }
private func _teamRegisteredView() -> some View { private func _teamRegisteredView() -> some View {
List { List {
let sortedTeams = tournament.sortedTeams()
let unfilteredTeams = _getTeams(from: sortedTeams)
if presentSearch == false { if presentSearch == false {
_rankHandlerView() _rankHandlerView()
_relatedTips() _relatedTips()
@ -418,6 +497,7 @@ struct InscriptionManagerView: View {
Task { Task {
await MainActor.run() { await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField pasteString = searchField
} }
} }
@ -474,6 +554,7 @@ struct InscriptionManagerView: View {
Task { Task {
await MainActor.run { await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first pasteString = first
autoSelect = true autoSelect = true
} }
@ -504,6 +585,8 @@ struct InscriptionManagerView: View {
@ViewBuilder @ViewBuilder
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View { func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
Section { Section {
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
Picker(selection: $currentRankSourceDate) { Picker(selection: $currentRankSourceDate) {
if currentRankSourceDate == nil { if currentRankSourceDate == nil {
Text("inconnu").tag(nil as Date?) Text("inconnu").tag(nil as Date?)
@ -571,6 +654,7 @@ struct InscriptionManagerView: View {
Task { Task {
await MainActor.run { await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) fetchPlayers.nsPredicate = _pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = paste pasteString = paste
autoSelect = true autoSelect = true
} }
@ -621,9 +705,6 @@ struct InscriptionManagerView: View {
private func _informationView(count: Int) -> some View { private func _informationView(count: Int) -> some View {
Section { Section {
let walkoutTeams = tournament.walkoutTeams()
let unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
LabeledContent { LabeledContent {
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle) Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
} label: { } label: {
@ -647,7 +728,11 @@ struct InscriptionManagerView: View {
.environment(tournament) .environment(tournament)
} label: { } label: {
LabeledContent { LabeledContent {
Text(tournament.registrationIssues().formatted()).font(.largeTitle) if let registrationIssues {
Text(registrationIssues.formatted()).font(.largeTitle)
} else {
ProgressView()
}
} label: { } label: {
Text("Problèmes détéctés") Text("Problèmes détéctés")
if let closedRegistrationDate = tournament.closedRegistrationDate { if let closedRegistrationDate = tournament.closedRegistrationDate {
@ -660,43 +745,43 @@ struct InscriptionManagerView: View {
@ViewBuilder @ViewBuilder
private func _relatedTips() -> some View { private func _relatedTips() -> some View {
if pasteString == nil // if pasteString == nil
&& createdPlayerIds.isEmpty // && createdPlayerIds.isEmpty
&& tournament.unsortedTeams().count >= tournament.teamCount // && tournament.unsortedTeams().count >= tournament.teamCount
&& tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty { // && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
Section { // Section {
TipView(padelBeachExportTip) { action in // TipView(padelBeachExportTip) { action in
if action.id == "more-info-export" { // if action.id == "more-info-export" {
isLearningMore = true // isLearningMore = true
} // }
if action.id == "padel-beach" { // if action.id == "padel-beach" {
UIApplication.shared.open(URLs.beachPadel.url) // UIApplication.shared.open(URLs.beachPadel.url)
} // }
} // }
.tipStyle(tint: nil) // .tipStyle(tint: nil)
} // }
Section { // Section {
TipView(padelBeachImportTip) { action in // TipView(padelBeachImportTip) { action in
if action.id == "more-info-import" { // if action.id == "more-info-import" {
presentImportView = true // presentImportView = true
} // }
} // }
.tipStyle(tint: nil) // .tipStyle(tint: nil)
} // }
} // }
//
if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false { if tournament.tournamentCategory == .men && unsortedPlayers.filter({ $0.isMalePlayer() == false }).isEmpty == false {
Section { Section {
TipView(inscriptionManagerWomanRankTip) TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil) .tipStyle(tint: nil)
} }
} }
//
Section { // Section {
TipView(slideToDeleteTip) // TipView(slideToDeleteTip)
.tipStyle(tint: nil) // .tipStyle(tint: nil)
} // }
} }
private func _searchSource() -> String? { private func _searchSource() -> String? {
@ -777,6 +862,9 @@ struct InscriptionManagerView: View {
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
_clearScreen()
_getTeams()
} }
private func _updateTeam() { private func _updateTeam() {
@ -797,6 +885,8 @@ struct InscriptionManagerView: View {
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
self.editedTeam = nil self.editedTeam = nil
_clearScreen()
_getTeams()
} }
private func _buildingTeamView() -> some View { private func _buildingTeamView() -> some View {
@ -873,6 +963,7 @@ struct InscriptionManagerView: View {
} }
} }
} }
.headerProminence(.increased)
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true { if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
@ -999,14 +1090,19 @@ struct InscriptionManagerView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.wildCardBracket return team.wildCardBracket
}, set: { value in }, set: { value in
team.resetPositions() _clearScreen()
team.wildCardGroupStage = false
team.walkOut = false Task {
team.wildCardBracket = value team.resetPositions()
do { team.wildCardGroupStage = false
try dataStore.teamRegistrations.addOrUpdate(instance: team) team.walkOut = false
} catch { team.wildCardBracket = value
Logger.error(error) do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
} }
})) { })) {
Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle") Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle")
@ -1015,14 +1111,19 @@ struct InscriptionManagerView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.wildCardGroupStage return team.wildCardGroupStage
}, set: { value in }, set: { value in
team.resetPositions() _clearScreen()
team.wildCardBracket = false
team.walkOut = false Task {
team.wildCardGroupStage = value team.resetPositions()
do { team.wildCardBracket = false
try dataStore.teamRegistrations.addOrUpdate(instance: team) team.walkOut = false
} catch { team.wildCardGroupStage = value
Logger.error(error) do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
} }
})) { })) {
Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle") Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle")
@ -1032,24 +1133,32 @@ struct InscriptionManagerView: View {
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
return team.walkOut return team.walkOut
}, set: { value in }, set: { value in
team.resetPositions() _clearScreen()
team.wildCardBracket = false Task {
team.wildCardGroupStage = false team.resetPositions()
team.walkOut = value team.wildCardBracket = false
do { team.wildCardGroupStage = false
try dataStore.teamRegistrations.addOrUpdate(instance: team) team.walkOut = value
} catch { do {
Logger.error(error) try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
} }
})) { })) {
Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle") Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle")
} }
Divider() Divider()
Button(role: .destructive) { Button(role: .destructive) {
do { _clearScreen()
try dataStore.teamRegistrations.delete(instance: team) Task {
} catch { do {
Logger.error(error) try dataStore.teamRegistrations.delete(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
} }
} label: { } label: {
LabelDelete() LabelDelete()

@ -13,6 +13,9 @@ struct PrintSettingsView: View {
@StateObject var generator: HtmlGenerator @StateObject var generator: HtmlGenerator
@State private var presentShareView: Bool = false @State private var presentShareView: Bool = false
@State private var prepareGroupStage: Bool = false @State private var prepareGroupStage: Bool = false
@State private var generationId: UUID = UUID()
@State private var generationGroupStageId: UUID = UUID()
@State private var generating: Bool = false
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
@ -63,26 +66,55 @@ struct PrintSettingsView: View {
} header: { } header: {
Text("Tableau principal") Text("Tableau principal")
} }
if generating == false {
RowButtonView("Générer le PDF", systemImage: "printer") {
await MainActor.run() {
self.generating = true
}
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false && tournament.groupStages().isEmpty == false {
self.prepareGroupStage = true
self.generationGroupStageId = UUID()
} else {
self.presentShareView = true
self.generating = false
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
}
}
self.prepareGroupStage = false
self.generationId = UUID()
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false)
} else {
LabeledContent {
ProgressView()
} label: {
Text("Préparation du PDF")
}
.id(generationId)
}
} }
Section { Section {
NavigationLink { NavigationLink {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in WebViewPreview(bracket: true)
}) .environmentObject(generator)
} label: { } label: {
Text("Aperçu du tableau") Text("Aperçu du tableau")
} }
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
NavigationLink { NavigationLink {
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in WebViewPreview(groupStage: groupStage)
if let error { .environmentObject(generator)
print("preparePDF", error)
} else if loaded == false {
generator.generateGroupStage(webView: webView)
} else {
print("preparePDF", "is loading")
}
})
} label: { } label: {
Text("Aperçu de la \(groupStage.groupStageTitle())") Text("Aperçu de la \(groupStage.groupStageTitle())")
} }
@ -90,16 +122,59 @@ struct PrintSettingsView: View {
} }
} }
.background { .background {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in if generating {
if let error { _backgroundGenerationWebView()
print("preparePDF", error) _backgroundGroupStageWebView()
} else if loaded == false { }
generator.generateWebView(webView: webView) }
} else { .navigationTitle("Imprimer")
print("preparePDF", "is loading") .toolbarBackground(.visible, for: .navigationBar)
} .navigationBarTitleDisplayMode(.inline)
}).opacity(0) // .toolbar {
// ToolbarItem(placement: .topBarTrailing) {
// Menu {
// Section {
// ShareLink(item: generator.generateHtml()) {
// Text("Tableau")
// }
//
// if let groupStage = tournament.groupStages().first {
// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) {
// Text("Poule")
// }
// }
// } header: {
// Text("Partager le code source HTML")
// }
// } label: {
// Label("Options", systemImage: "ellipsis.circle")
// }
// }
// }
.sheet(isPresented: $presentShareView) {
if let pdfURL = generator.pdfURL {
ShareSheet(urls: [pdfURL])
}
}
}
@ViewBuilder
private func _backgroundGenerationWebView() -> some View {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in
if let error {
print("preparePDF", error)
} else if loaded == false {
generator.generateWebView(webView: webView)
} else {
print("preparePDF", "is loading")
}
})
.opacity(0)
.id(generationId)
}
private func _backgroundGroupStageWebView() -> some View {
Group {
if prepareGroupStage { if prepareGroupStage {
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in
@ -114,64 +189,7 @@ struct PrintSettingsView: View {
} }
} }
} }
.navigationTitle("Imprimer") .id(generationGroupStageId)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false {
self.prepareGroupStage = true
} else {
self.presentShareView = true
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
}
}
self.prepareGroupStage = false
self.generator.buildPDF()
} label: {
Text("Obtenir le PDF")
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false)
.buttonStyle(.borderedProminent)
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
Section {
ShareLink(item: generator.generateHtml()) {
Text("Tableau")
}
if let groupStage = tournament.groupStages().first {
ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) {
Text("Poule")
}
}
} header: {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
.sheet(isPresented: $presentShareView) {
if let pdfURL = generator.pdfURL {
ShareSheet(urls: [pdfURL])
}
}
} }
} }
@ -239,3 +257,34 @@ struct WebView: UIViewRepresentable {
} }
} }
struct WebViewPreview: View {
@EnvironmentObject var generator: HtmlGenerator
let bracket: Bool
let groupStage: GroupStage?
@State private var html: String?
init(bracket: Bool = false, groupStage: GroupStage? = nil) {
self.bracket = bracket
self.groupStage = groupStage
}
var body: some View {
Group {
if let html {
WebView(htmlRawData: html, loadStatusChanged: { loaded, error, webView in
})
} else {
ProgressView()
.onAppear {
if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)
} else {
html = generator.generateHtml()
}
}
}
}
}
}

@ -7,7 +7,13 @@
import SwiftUI import SwiftUI
enum CallDestination: Identifiable, Selectable { enum CallDestination: Identifiable, Selectable, Equatable {
static func == (lhs: CallDestination, rhs: CallDestination) -> Bool {
return lhs.id == rhs.id
}
case seeds(Tournament) case seeds(Tournament)
case groupStages(Tournament) case groupStages(Tournament)

@ -7,7 +7,12 @@
import SwiftUI import SwiftUI
enum CashierDestination: Identifiable, Selectable { enum CashierDestination: Identifiable, Selectable, Equatable {
static func == (lhs: CashierDestination, rhs: CashierDestination) -> Bool {
return lhs.id == rhs.id
}
case summary case summary
case groupStage(GroupStage) case groupStage(GroupStage)
case bracket(Round) case bracket(Round)
@ -37,6 +42,10 @@ enum CashierDestination: Identifiable, Selectable {
} }
} }
func displayImageIfValueZero() -> Bool {
return true
}
func badgeValue() -> Int? { func badgeValue() -> Int? {
switch self { switch self {
case .summary: case .summary:
@ -46,7 +55,7 @@ enum CashierDestination: Identifiable, Selectable {
case .bracket(let round): case .bracket(let round):
return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count
case .all(let tournament): case .all(let tournament):
return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count return nil
} }
} }
@ -58,12 +67,10 @@ enum CashierDestination: Identifiable, Selectable {
switch self { switch self {
case .summary: case .summary:
return nil return nil
case .groupStage(let groupStage): case .all:
return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil return nil
case .bracket(let round): default:
return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? .checkmark : nil return .checkmark
case .all(let tournament):
return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil
} }
} }
@ -72,7 +79,8 @@ enum CashierDestination: Identifiable, Selectable {
struct TournamentCashierView: View { struct TournamentCashierView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedDestination: CashierDestination? @State private var selectedDestination: CashierDestination?
@StateObject private var cashierViewModel: CashierViewModel = CashierViewModel()
func allDestinations() -> [CashierDestination] { func allDestinations() -> [CashierDestination] {
var allDestinations : [CashierDestination] = [] var allDestinations : [CashierDestination] = []
let tournamentHasEnded = tournament.hasEnded() let tournamentHasEnded = tournament.hasEnded()
@ -127,10 +135,13 @@ struct TournamentCashierView: View {
CashierDetailView(tournament: tournament) CashierDetailView(tournament: tournament)
case .groupStage(let groupStage): case .groupStage(let groupStage):
CashierView(tournament: tournament, teams: groupStage.teams()) CashierView(tournament: tournament, teams: groupStage.teams())
.environmentObject(cashierViewModel)
case .bracket(let round): case .bracket(let round):
CashierView(tournament: tournament, teams: round.seeds()) CashierView(tournament: tournament, teams: round.seeds())
.environmentObject(cashierViewModel)
case .all(let tournament): case .all(let tournament):
CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) CashierView(tournament: tournament, teams: tournament.selectedSortedTeams())
.environmentObject(cashierViewModel)
} }
} }
} }

@ -20,7 +20,11 @@ extension Schedulable {
} }
} }
enum ScheduleDestination: String, Identifiable, Selectable { enum ScheduleDestination: String, Identifiable, Selectable, Equatable {
static func == (lhs: ScheduleDestination, rhs: ScheduleDestination) -> Bool {
return lhs.id == rhs.id
}
var id: String { self.rawValue } var id: String { self.rawValue }
case planning case planning

@ -7,7 +7,11 @@
import SwiftUI import SwiftUI
enum TournamentSettings: Identifiable, Selectable { enum TournamentSettings: Identifiable, Selectable, Equatable {
static func == (lhs: TournamentSettings, rhs: TournamentSettings) -> Bool {
return lhs.id == rhs.id
}
case status case status
case general case general
case club(Tournament) case club(Tournament)

@ -9,13 +9,18 @@ import SwiftUI
struct TournamentBuildView: View { struct TournamentBuildView: View {
var tournament: Tournament var tournament: Tournament
@State private var bracketStatus: String?
@State private var groupStageStatus: String?
@State private var callStatus: Tournament.TournamentStatus?
@State private var scheduleStatus: Tournament.TournamentStatus?
@State private var cashierStatus: Tournament.TournamentStatus?
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if tournament.hasEnded() { if tournament.hasEnded() {
Section { Section {
NavigationLink(value: Screen.rankings) { NavigationLink(value: Screen.rankings) {
Text("Classement") Text("Classement final des équipes")
} }
} }
} }
@ -24,8 +29,12 @@ struct TournamentBuildView: View {
if tournament.groupStageCount > 0 { if tournament.groupStageCount > 0 {
NavigationLink(value: Screen.groupStage) { NavigationLink(value: Screen.groupStage) {
LabeledContent { LabeledContent {
Text(tournament.groupStageStatus()) if let groupStageStatus {
.multilineTextAlignment(.trailing) Text(groupStageStatus).lineLimit(1)
.multilineTextAlignment(.trailing)
} else {
ProgressView()
}
} label: { } label: {
Text("Poules") Text("Poules")
if tournament.shouldVerifyGroupStage { if tournament.shouldVerifyGroupStage {
@ -33,13 +42,20 @@ struct TournamentBuildView: View {
} }
} }
} }
.task {
groupStageStatus = await tournament.groupStageStatus()
}
} }
if tournament.rounds().isEmpty == false { if tournament.rounds().isEmpty == false {
NavigationLink(value: Screen.round) { NavigationLink(value: Screen.round) {
LabeledContent { LabeledContent {
Text(tournament.bracketStatus()) if let bracketStatus {
.multilineTextAlignment(.trailing) Text(bracketStatus).lineLimit(1)
.multilineTextAlignment(.trailing)
} else {
ProgressView()
}
} label: { } label: {
Text("Tableau") Text("Tableau")
if tournament.shouldVerifyBracket { if tournament.shouldVerifyBracket {
@ -47,42 +63,78 @@ struct TournamentBuildView: View {
} }
} }
} }
.task {
bracketStatus = await tournament.bracketStatus()
}
} }
} }
Section { Section {
if tournament.state() != .finished { if tournament.state() != .finished {
NavigationLink(value: Screen.schedule) { NavigationLink(value: Screen.schedule) {
let tournamentStatus = tournament.scheduleStatus() let tournamentStatus = scheduleStatus
LabeledContent { LabeledContent {
Text(tournamentStatus.completion) if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: { } label: {
Text("Horaires") Text("Horaires")
Text(tournamentStatus.label) if let tournamentStatus {
Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
}
} }
} }
.task {
scheduleStatus = await tournament.scheduleStatus()
}
NavigationLink(value: Screen.call) { NavigationLink(value: Screen.call) {
let tournamentStatus = tournament.callStatus() let tournamentStatus = callStatus
LabeledContent { LabeledContent {
Text(tournamentStatus.completion) if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: { } label: {
Text("Convocations") Text("Convocations")
Text(tournamentStatus.label) if let tournamentStatus {
Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
}
} }
} }
.task {
callStatus = await tournament.callStatus()
}
} }
if tournament.state() == .running || tournament.state() == .finished { if tournament.state() == .running || tournament.state() == .finished {
NavigationLink(value: Screen.cashier) { NavigationLink(value: Screen.cashier) {
let tournamentStatus = tournament.cashierStatus() let tournamentStatus = cashierStatus
LabeledContent { LabeledContent {
Text(tournamentStatus.completion) if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: { } label: {
Text("Encaissement") Text("Encaissement")
Text(tournamentStatus.label) if let tournamentStatus {
Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
}
} }
} }
.task {
cashierStatus = await tournament.cashierStatus()
}
} }
} }
} }

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct TournamentInitView: View { struct TournamentInitView: View {
var tournament: Tournament var tournament: Tournament
@ -43,13 +44,21 @@ struct TournamentInitView: View {
NavigationLink(value: Screen.broadcast) { NavigationLink(value: Screen.broadcast) {
LabeledContent { LabeledContent {
if tournament.isPrivate { if Store.main.userId == nil {
Text("tournoi privé").foregroundStyle(.logoRed) Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
} else { } else {
Text("Automatique") if tournament.isPrivate {
Text("tournoi privé").foregroundStyle(.logoRed)
} else {
Text("Automatique")
}
} }
} label: { } label: {
Text("Publication") Text("Publication")
if Store.main.userId == nil {
Text("Un compte Padel Club est nécessaire")
}
} }
} }

@ -93,6 +93,7 @@ struct TournamentView: View {
TournamentRankView() TournamentRankView()
case .broadcast: case .broadcast:
BroadcastView() BroadcastView()
.environment(navigation)
case .event: case .event:
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
EventView(event: event) EventView(event: event)

@ -14,12 +14,12 @@ struct ListRowViewModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
if isActive { if isActive {
content content
// .listRowBackground( .listRowBackground(
// color.variation() color.variation()
// .overlay(alignment: .leading, content: { .overlay(alignment: .leading, content: {
// color.frame(width: 8) color.frame(width: 8)
// }) })
// ) )
} else { } else {
content content
} }
@ -27,7 +27,7 @@ struct ListRowViewModifier: ViewModifier {
} }
extension View { extension View {
func listRowView(isActive: Bool = true, color: Color) -> some View { func listRowView(isActive: Bool = false, color: Color) -> some View {
modifier(ListRowViewModifier(isActive: isActive, color: color)) modifier(ListRowViewModifier(isActive: isActive, color: color))
} }
} }

Loading…
Cancel
Save