Laurent 1 year ago
commit e936cd7555
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 53
      PadelClub/Data/GroupStage.swift
  3. 41
      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. 260
      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. 148
      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. 10
      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. 67
      PadelClub/Views/Tournament/FileImportView.swift
  40. 22
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  41. 42
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  42. 325
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  43. 205
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  44. 8
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift
  45. 27
      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 */; };
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.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 */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.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>"; };
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>"; };
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>"; };
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>"; };
@ -1235,6 +1237,7 @@
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
);
path = Round;
sourceTree = "<group>";
@ -1621,6 +1624,7 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
FF5647132C0B6F390081F995 /* LoserRoundSettingsView.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */,
FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */,
@ -1935,7 +1939,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1973,7 +1977,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -79,7 +79,7 @@ class GroupStage: ModelObject, Storable {
guard teams().count == size else { return false }
let _matches = _matches()
if _matches.isEmpty { return false }
return _matches.allSatisfy { $0.hasEnded() }
return _matches.anySatisfy { $0.hasEnded() == false } == false
}
func buildMatches() {
@ -178,20 +178,60 @@ class GroupStage: ModelObject, Storable {
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 })
}
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] {
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] {
@ -376,7 +416,6 @@ extension GroupStage: Selectable {
}
func badgeValue() -> Int? {
if teams().count < size { return nil }
return runningMatches(playedMatches: _matches()).count
}

@ -69,10 +69,10 @@ class Match: ModelObject, Storable {
try Store.main.deleteDependencies(items: self.teamScores)
}
func indexInRound() -> Int {
func indexInRound(in matches: [Match]? = nil) -> Int {
if groupStage != nil {
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 RoundRule.matchIndexWithinRound(fromMatchIndex: index)
@ -86,16 +86,16 @@ class Match: ModelObject, Storable {
[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 {
return groupStageObject.localizedMatchUpLabel(for: index)
}
switch displayStyle {
case .wide:
return "Match \(indexInRound() + 1)"
return "Match \(indexInRound(in: matches) + 1)"
case .short:
return "#\(indexInRound() + 1)"
return "#\(indexInRound(in: matches) + 1)"
}
}
@ -184,15 +184,11 @@ class Match: ModelObject, Storable {
}
func resetScores() {
if hasEnded() == false {
teamScores.forEach({ $0.score = nil })
do {
try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores)
} catch {
Logger.error(error)
}
} else {
updateTeamScores()
teamScores.forEach({ $0.score = nil })
do {
try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores)
} catch {
Logger.error(error)
}
}
@ -468,6 +464,9 @@ class Match: ModelObject, Storable {
if endDate == nil {
endDate = Date()
}
if startDate == nil {
startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
}
winningTeamId = team(matchDescriptor.winner)?.id
losingTeamId = team(matchDescriptor.winner.otherTeam)?.id
groupStageObject?.updateGroupStageState()
@ -617,7 +616,7 @@ class Match: ModelObject, Storable {
}
func isReady() -> Bool {
teamScores.count == 2
teamScores.count >= 2
// teams().count == 2
}
@ -627,7 +626,7 @@ class Match: ModelObject, Storable {
}
func hasEnded() -> Bool {
endDate != nil || hasWalkoutTeam() || winningTeamId != nil
endDate != nil
}
func isGroupStage() -> Bool {
@ -639,7 +638,8 @@ class Match: ModelObject, Storable {
}
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 {
@ -705,6 +705,13 @@ class Match: ModelObject, Storable {
}
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 {
switch team {
case .one:

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

@ -220,12 +220,25 @@ class TeamRegistration: ModelObject, Storable {
bracketPosition != nil
}
func resetPositions() {
func resetGroupeStagePosition() {
groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
groupStage = 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 {

@ -538,11 +538,11 @@ class Tournament : ModelObject, Storable {
}
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] {
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? []
getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.hasSpaceLeft() } ?? []
}
func availableSeedGroups() -> [SeedInterval] {
@ -615,7 +615,7 @@ class Tournament : ModelObject, Storable {
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
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
} else if let chunks = availableSeedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
@ -727,7 +727,13 @@ class Tournament : ModelObject, Storable {
}
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] = []
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
_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
}
@ -817,10 +820,6 @@ class Tournament : ModelObject, Storable {
unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.computedRank)
}
func femalePlayers() -> [PlayerRegistration] {
unsortedPlayers().filter({ $0.isMalePlayer() == false })
}
func unrankValue(for malePlayer: Bool) -> Int? {
switch tournamentCategory {
case .men:
@ -930,7 +929,7 @@ class Tournament : ModelObject, Storable {
}
}
func registrationIssues() -> Int {
func registrationIssues() async -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
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!) })
}
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)
}
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] {
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)
}
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
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit))
}
func finalRanking() -> [Int: [String]] {
var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
let rounds = rounds()
let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId {
teams[1] = [winner]
ids.insert(winner)
}
if let finalist = final?.losingTeamId {
teams[2] = [finalist]
ids.insert(finalist)
}
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 })
others.forEach { round in
if let interval = round.seedInterval() {
let playedMatches = round.playedMatches().filter { $0.disabled == false }
let winners = playedMatches.compactMap({ $0.winningTeamId })
let losers = playedMatches.compactMap({ $0.losingTeamId })
teams[interval.first + winners.count - 1] = winners
teams[interval.last] = losers
let playedMatches = round.playedMatches().filter { $0.disabled == false || $0.isReady() }
let winners = playedMatches.compactMap({ $0.winningTeamId }).filter({ ids.contains($0) == false })
let losers = playedMatches.compactMap({ $0.losingTeamId }).filter({ ids.contains($0) == false })
if winners.isEmpty {
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)
func cashierStatus() -> TournamentStatus {
func cashierStatus() async -> TournamentStatus {
let selectedPlayers = selectedPlayers()
let paid = selectedPlayers.filter({ $0.hasPaid() })
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)
}
func scheduleStatus() -> TournamentStatus {
func scheduleStatus() async -> TournamentStatus {
let allMatches = allMatches()
let ready = allMatches.filter({ $0.startDate != nil })
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)
}
func callStatus() -> TournamentStatus {
func callStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
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)
}
func confirmedSummonStatus() -> TournamentStatus {
func confirmedSummonStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { $0.confirmationDate != nil }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " confirmées"
@ -1217,7 +1265,7 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel)
}
func bracketStatus() -> String {
func bracketStatus() async -> String {
let availableSeeds = availableSeeds()
if availableSeeds.isEmpty == false {
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
if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount {
return "à faire"
@ -1343,9 +1391,9 @@ class Tournament : ModelObject, Storable {
return nil
}
func resetTeamScores(in matchOfBracketPosition: Int?) {
func resetTeamScores(in matchOfBracketPosition: Int?, outsideOf: [TeamScore] = []) {
guard let match = match(for: matchOfBracketPosition) else { return }
match.resetTeamScores(outsideOf: [])
match.resetTeamScores(outsideOf: outsideOf)
}
func updateTeamScores(in matchOfBracketPosition: Int?) {
@ -1626,7 +1674,10 @@ class Tournament : ModelObject, Storable {
func getGroupStageChunkValue() -> Int {
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 {
return 1
}

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

@ -183,7 +183,7 @@ enum HtmlService {
var template = ""
var bracket = ""
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))
}
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 {
@Environment(\.colorScheme) var colorScheme
var tint: Color?

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

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

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

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

@ -8,39 +8,62 @@
import SwiftUI
import Combine
struct CashierView: View {
@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
struct ShareableObject {
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)
}
}
let cashierViewModel: CashierViewModel
let teams: [TeamRegistration]
let fileName: String
private func _sharedData() -> String {
let players = teams.filter({ _shouldDisplayTeam($0) })
.flatMap({ $0.players().filter({ _shouldDisplayPlayer($0) }) })
func sharedData() async -> Data? {
let players = teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
.flatMap({ $0.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) })
.map {
[$0.pasteData()]
.compacted()
.joined(separator: "\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 {
@ -101,27 +124,42 @@ 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 {
List {
if isSearching == false {
if cashierViewModel.isSearching == false {
Section {
Picker(selection: $filterOption) {
ForEach(FilterOption.allCases) { filterOption in
Picker(selection: $cashierViewModel.filterOption) {
ForEach(CashierViewModel.FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption)
}
} label: {
Text("Statut du règlement")
}
Picker(selection: $sortOption) {
ForEach(SortOption.allCases) { sortOption in
Picker(selection: $cashierViewModel.sortOption) {
ForEach(CashierViewModel.SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption)
}
} label: {
Text("Affichage par")
}
Picker(selection: $sortOrder) {
Picker(selection: $cashierViewModel.sortOrder) {
Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending)
} label: {
@ -132,11 +170,7 @@ struct CashierView: View {
}
}
if _isContentUnavailable() {
_contentUnavailableView()
}
switch sortOption {
switch cashierViewModel.sortOption {
case .teamRank:
_byTeamRankView()
case .alphabeticalLastName:
@ -151,51 +185,53 @@ struct CashierView: View {
_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)
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur"))
.toolbar {
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
private func _byPlayer(_ players: [PlayerRegistration]) -> some View {
let _players = sortOrder == .ascending ? players : players.reversed()
ForEach(_players) { player in
Section {
computedPlayerView(player)
} header: {
HStack {
if let teamCallDate = player.team()?.callDate {
Text(teamCallDate.localizedDate())
let _players = cashierViewModel.sortOrder == .ascending ? players : players.reversed()
if _players.isEmpty {
_contentUnavailableView()
} else {
ForEach(_players) { player in
Section {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
} 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
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)
}
@ViewBuilder
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)
}
@ViewBuilder
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)
}
@ViewBuilder
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)
}
@ViewBuilder
private func _byTeamRankView() -> some View {
let _teams = sortOrder == .ascending ? teams : teams.reversed()
ForEach(_teams) { team in
if _shouldDisplayTeam(team) {
let _teams = cashierViewModel.sortOrder == .ascending ? teams : teams.reversed()
let _filteredTeams = _teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
if _filteredTeams.isEmpty {
_contentUnavailableView()
} else {
ForEach(_filteredTeams) { team in
Section {
_cashierPlayersView(team.players())
ForEach(team.players()) { player in
if cashierViewModel._shouldDisplayPlayer(player) {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
}
} header: {
HStack {
if let callDate = team.callDate {
Text(callDate.localizedDate())
}
Spacer()
Text(team.weight.formatted())
VStack(alignment: .trailing, spacing: 0) {
Text("Poids").font(.caption)
Text(team.weight.formatted())
}
}
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
@ -252,52 +298,40 @@ struct CashierView: View {
@ViewBuilder
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
}
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
if let _teams = groupedTeams[key] {
ForEach(_teams) { team in
if _shouldDisplayTeam(team) {
Section {
_cashierPlayersView(team.players())
} header: {
Text(key.localizedDate())
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle)
Section {
ForEach(team.players()) { player in
if cashierViewModel._shouldDisplayPlayer(player) {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
}
} 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 {
switch sortOption {
switch cashierViewModel.sortOption {
case .teamRank, .callDate:
return "person.2.slash.fill"
default:
@ -307,8 +341,8 @@ struct CashierView: View {
@ViewBuilder
private func _contentUnavailableView() -> some View {
if isSearching {
ContentUnavailableView.search(text: searchText)
if cashierViewModel.isSearching {
ContentUnavailableView.search(text: cashierViewModel.searchText)
} else {
ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon())
}

@ -90,39 +90,6 @@ struct EventCreationView: View {
case .animation:
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 {
if textFieldIsFocus {
@ -144,11 +111,10 @@ struct EventCreationView: View {
}
ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une épreuve", icon: "plus.circle.fill") {
let tournament = Tournament.newEmptyInstance()
self.tournaments.append(tournament)
ButtonValidateView {
_validate()
}
.popoverTip(multiTournamentsEventTip)
.disabled(tournaments.isEmpty)
}
}
.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
private var approvedTournamentEditorView: some View {
ForEach(tournaments) { tournament in
ForEach(tournaments.indices, id: \.self) { index in
let tournament = tournaments[index]
Section {
TournamentConfigurationView(tournament: tournament)
} footer: {
} header: {
if tournaments.count > 1 {
FooterButtonView("effacer") {
tournaments.removeAll(where: { $0 == tournament })
HStack {
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 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 tournaments(Event)
case cashier

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

@ -7,75 +7,109 @@
import SwiftUI
struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >: View {
@EnvironmentObject var dataStore: DataStore
@Binding var selectedDestination: T?
let destinations: [T]
let nilDestinationIsValid: Bool
var body: some View {
ScrollView(.horizontal) {
HStack {
if nilDestinationIsValid {
Button {
selectedDestination = nil
} label: {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(selectedDestination == nil ? .white : .black)
}
.padding()
.background {
Circle()
.fill(selectedDestination == nil ? .master : .beige)
ScrollViewReader { proxy in
ScrollView(.horizontal) {
HStack {
if nilDestinationIsValid {
Button {
selectedDestination = nil
} label: {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(selectedDestination == nil ? .white : .black)
}
.padding()
.background {
Circle()
.fill(selectedDestination == nil ? .master : .beige)
}
.buttonStyle(.plain)
.id("settings")
}
.buttonStyle(.plain)
}
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) {
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)
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)
}
.id(destination.id)
.buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) {
if destination.displayImageIfValueZero() {
let count = destination.badgeValue()
if let count, count == 0, 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, 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()
.padding(8)
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.background(Material.ultraThinMaterial)
.overlay {
VStack(spacing: 0) {
Spacer()
Divider()
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.background(Material.ultraThinMaterial)
.overlay {
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 {
@EnvironmentObject var dataStore: DataStore
let section: String
let matches: [Match]
let matches: [Match]?
var matchViewStyle: MatchViewStyle = .standardStyle
var hideWhenEmpty: Bool = false
@State var isExpanded: Bool = true
private func _shouldHide() -> Bool {
if matches != nil && matches!.isEmpty && hideWhenEmpty == true {
return true
} else {
return false
}
}
@ViewBuilder
var body: some View {
if matches.isEmpty == false {
if _shouldHide() == false {
Section {
DisclosureGroup(isExpanded: $isExpanded) {
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
if let matches {
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
}
}
} label: {
LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
if matches == nil {
ProgressView()
} else {
Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix)
}
} label: {
Text(section.firstCapitalized)
}

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

@ -11,7 +11,11 @@ struct GroupStagesView: View {
var tournament: Tournament
@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 groupStage(GroupStage)
@ -47,7 +51,12 @@ struct GroupStagesView: View {
}
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
}
@State private var runningMatches: [Match]?
@State private var readyMatches: [Match]?
@State private var availableToStart: [Match]?
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination {
case .all:
let runningMatches = tournament.runningMatches(allMatches)
let availableToStart = tournament.availableToStart(allMatches, in: runningMatches)
let readyMatches = tournament.readyMatches(allMatches)
let finishedMatches = tournament.finishedMatches(allMatches)
List {
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: "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 {
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")
}
}
.navigationTitle("Toutes les poules")
case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage)
GroupStageView(groupStage: groupStage).id(groupStage.id)
case nil:
GroupStageSettingsView()
.navigationTitle("Réglages")

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

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

@ -44,32 +44,6 @@ struct MatchSummaryView: 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) {
if matchViewStyle != .plainStyle {
if matchViewStyle == .feedStyle, let tournament = match.currentTournament() {

@ -30,18 +30,17 @@ struct ActivityView: View {
var endedTournaments: [Tournament] {
dataStore.tournaments.filter({ $0.endDate != nil })
.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] {
switch navigation.agendaDestination! {
@ -63,11 +62,11 @@ struct ActivityView: View {
List {
switch navigation.agendaDestination! {
case .activity:
EventListView(tournaments: runningTournaments, viewStyle: viewStyle)
EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true)
case .history:
EventListView(tournaments: endedTournaments, viewStyle: viewStyle)
EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false)
case .tenup:
EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle)
EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle, sortAscending: true)
.id(uuid)
}
}
@ -136,19 +135,10 @@ struct ActivityView: View {
}
.toolbar {
if presentToolbar {
let _activityStatus = _activityStatus()
if federalDataViewModel.areFiltersEnabled() || _activityStatus != nil {
//let _activityStatus = _activityStatus()
if federalDataViewModel.areFiltersEnabled() {
ToolbarItem(placement: .status) {
VStack(spacing: -2) {
if federalDataViewModel.areFiltersEnabled() {
Text(federalDataViewModel.filterStatus())
}
if let _activityStatus {
Text(_activityStatus)
.foregroundStyle(.secondary)
}
}
.font(.footnote)
Text(federalDataViewModel.filterStatus())
}
}

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

@ -17,7 +17,6 @@ struct OngoingView: View {
var matches: [Match] {
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)
}

@ -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
if isEditingTournamentSeed || loserRound.isDisabled() == false {
if true {
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
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
.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 {
return "Tour #\(turnIndex + 1)"
}
@ -54,7 +59,6 @@ extension LoserRound {
struct LoserRoundsView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
var upperBracketRound: Round
@State private var selectedRound: LoserRound?
let loserRounds: [Round]
@ -64,8 +68,7 @@ struct LoserRoundsView: View {
self.upperBracketRound = upperBracketRound
let _loserRounds = upperBracketRound.loserRounds()
self.loserRounds = _loserRounds
let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound)
let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound)
let rounds = LoserRound.updateDestinations(fromLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound)
_allDestinations = State(wrappedValue: rounds)
_selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first)
@ -78,14 +81,5 @@ struct LoserRoundsView: View {
}
.navigationBarTitleDisplayMode(.inline)
.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 {
RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ team in
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
await _removeAllSeeds()
}
}
@ -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 {

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

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

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

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

@ -25,6 +25,22 @@ struct EditingTeamView: View {
} header: {
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) {
team.registrationDate = registrationDate

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

@ -27,6 +27,7 @@ struct FileImportView: View {
@State private var selectedOptions: Set<TeamImportStrategy> = Set()
@State private var fileProvider: FileImportManager.FileProvider = .frenchFederation
@State private var validationInProgress: Bool = false
private var filteredTeams: [FileImportManager.TeamHolder] {
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 {
Section {
Text(errorMessage)
@ -146,7 +157,7 @@ struct FileImportView: View {
Section {
ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash")
}
} else if didImport {
} else if didImport && validationInProgress == false {
let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams()
@ -222,38 +233,40 @@ struct FileImportView: View {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
// if false { //selectedOptions.contains(.deleteBeforeImport)
// 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()
_validate()
}
.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 {
errorMessage = nil
teams.removeAll()

@ -17,18 +17,40 @@ extension String : Identifiable {
struct BroadcastView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String?
@State private var tvMode: Bool = false
@State private var pageLink: PageLink = .teams
let createAccountTip = CreateAccountTip()
let tournamentPublishingTip = TournamentPublishingTip()
let tournamentTVBroadcastTip = TournamentTVBroadcastTip()
var body: some View {
@Bindable var tournament = tournament
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 {
TipView(tournamentPublishingTip) { action in
UIApplication.shared.open(URLs.main.url)

@ -11,20 +11,16 @@ struct InscriptionInfoView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament
var players : [PlayerRegistration] { tournament.unsortedPlayers() }
var selectedTeams : [TeamRegistration] { tournament.selectedSortedTeams() }
var callDateIssue : [TeamRegistration] {
selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) }
}
var waitingList : [TeamRegistration] { tournament.waitingListTeams(in: selectedTeams) }
var duplicates : [PlayerRegistration] { tournament.duplicates(in: players) }
var problematicPlayers : [PlayerRegistration] { players.filter({ $0.sex == nil }) }
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 }) }
@State private var players : [PlayerRegistration] = []
@State private var selectedTeams : [TeamRegistration] = []
@State private var callDateIssue : [TeamRegistration] = []
@State private var waitingList : [TeamRegistration] = []
@State private var duplicates : [PlayerRegistration] = []
@State private var problematicPlayers : [PlayerRegistration] = []
@State private var inadequatePlayers : [PlayerRegistration] = []
@State private var playersWithoutValidLicense : [PlayerRegistration] = []
@State private var entriesFromBeachPadel : [TeamRegistration] = []
@State private var playersMissing : [TeamRegistration] = []
var body: some View {
List {
@ -196,10 +192,28 @@ struct InscriptionInfoView: View {
.listRowView(color: .pink)
}
}
.task {
await _getIssues()
}
.navigationTitle("Synthèse")
.navigationBarTitleDisplayMode(.inline)
.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 {

@ -9,12 +9,22 @@ import SwiftUI
import TipKit
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 {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)],
sortDescriptors: [],
animation: .default)
private var fetchPlayers: FetchedResults<ImportedPlayer>
@ -41,6 +51,13 @@ struct InscriptionManagerView: View {
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
@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> {
Binding {
@ -83,19 +100,8 @@ 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 filterable: Bool
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
init(tournament: Tournament) {
self.tournament = tournament
@ -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
private func _simpleHash(ids: [String]) -> Int {
// Combine the hash values of each string
@ -121,7 +137,44 @@ struct InscriptionManagerView: View {
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 {
VStack(spacing: 0) {
@ -130,38 +183,18 @@ struct InscriptionManagerView: View {
_buildingTeamView()
} else if tournament.unsortedTeams().isEmpty {
_inscriptionTipsView()
} else {
}
if _isEditingTeam() == false {
_teamRegisteredView()
}
}
.onAppear {
let selectedSortedTeams = tournament.selectedSortedTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
_getTeams()
}
.onDisappear {
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()
}
}
Task {
await _handleHashDiff()
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) {
@ -258,10 +291,14 @@ struct InscriptionManagerView: View {
.tint(.master)
}
.onChange(of: tournament.prioritizeClubMembers) {
_clearScreen()
_save()
_getTeams()
}
.onChange(of: tournament.teamSorting) {
_clearScreen()
_save()
_getTeams()
}
.onChange(of: currentRankSourceDate) {
if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
@ -334,8 +371,10 @@ struct InscriptionManagerView: View {
Label("Clôturer", systemImage: "lock")
}
Divider()
ShareLink(item: tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))) {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
if let teamPaste {
ShareLink(item: teamPaste) {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
}
Button {
presentImportView = true
@ -373,7 +412,30 @@ struct InscriptionManagerView: View {
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
if filterMode == .walkOut {
teams = teams.filter({ $0.walkOut })
@ -384,17 +446,34 @@ struct InscriptionManagerView: View {
}
if byDecreasingOrdering {
return teams.reversed()
self.unfilteredTeams = teams.reversed()
} 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 {
List {
let sortedTeams = tournament.sortedTeams()
let unfilteredTeams = _getTeams(from: sortedTeams)
if presentSearch == false {
_rankHandlerView()
_relatedTips()
@ -418,6 +497,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField
}
}
@ -474,6 +554,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
autoSelect = true
}
@ -504,6 +585,8 @@ struct InscriptionManagerView: View {
@ViewBuilder
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
Section {
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
Picker(selection: $currentRankSourceDate) {
if currentRankSourceDate == nil {
Text("inconnu").tag(nil as Date?)
@ -571,6 +654,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = paste
autoSelect = true
}
@ -621,9 +705,6 @@ struct InscriptionManagerView: View {
private func _informationView(count: Int) -> some View {
Section {
let walkoutTeams = tournament.walkoutTeams()
let unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
LabeledContent {
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
} label: {
@ -647,7 +728,11 @@ struct InscriptionManagerView: View {
.environment(tournament)
} label: {
LabeledContent {
Text(tournament.registrationIssues().formatted()).font(.largeTitle)
if let registrationIssues {
Text(registrationIssues.formatted()).font(.largeTitle)
} else {
ProgressView()
}
} label: {
Text("Problèmes détéctés")
if let closedRegistrationDate = tournament.closedRegistrationDate {
@ -660,43 +745,43 @@ struct InscriptionManagerView: View {
@ViewBuilder
private func _relatedTips() -> some View {
if pasteString == nil
&& createdPlayerIds.isEmpty
&& tournament.unsortedTeams().count >= tournament.teamCount
&& tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
Section {
TipView(padelBeachExportTip) { action in
if action.id == "more-info-export" {
isLearningMore = true
}
if action.id == "padel-beach" {
UIApplication.shared.open(URLs.beachPadel.url)
}
}
.tipStyle(tint: nil)
}
Section {
TipView(padelBeachImportTip) { action in
if action.id == "more-info-import" {
presentImportView = true
}
}
.tipStyle(tint: nil)
}
}
// if pasteString == nil
// && createdPlayerIds.isEmpty
// && tournament.unsortedTeams().count >= tournament.teamCount
// && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
// Section {
// TipView(padelBeachExportTip) { action in
// if action.id == "more-info-export" {
// isLearningMore = true
// }
// if action.id == "padel-beach" {
// UIApplication.shared.open(URLs.beachPadel.url)
// }
// }
// .tipStyle(tint: nil)
// }
// Section {
// TipView(padelBeachImportTip) { action in
// if action.id == "more-info-import" {
// presentImportView = true
// }
// }
// .tipStyle(tint: nil)
// }
// }
//
if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false {
if tournament.tournamentCategory == .men && unsortedPlayers.filter({ $0.isMalePlayer() == false }).isEmpty == false {
Section {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
}
Section {
TipView(slideToDeleteTip)
.tipStyle(tint: nil)
}
//
// Section {
// TipView(slideToDeleteTip)
// .tipStyle(tint: nil)
// }
}
private func _searchSource() -> String? {
@ -777,6 +862,9 @@ struct InscriptionManagerView: View {
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
_clearScreen()
_getTeams()
}
private func _updateTeam() {
@ -797,6 +885,8 @@ struct InscriptionManagerView: View {
createdPlayerIds.removeAll()
pasteString = nil
self.editedTeam = nil
_clearScreen()
_getTeams()
}
private func _buildingTeamView() -> some View {
@ -873,6 +963,7 @@ struct InscriptionManagerView: View {
}
}
}
.headerProminence(.increased)
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
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: {
return team.wildCardBracket
}, set: { value in
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
_clearScreen()
Task {
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
}
})) {
Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle")
@ -1015,14 +1111,19 @@ struct InscriptionManagerView: View {
Toggle(isOn: .init(get: {
return team.wildCardGroupStage
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.walkOut = false
team.wildCardGroupStage = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
_clearScreen()
Task {
team.resetPositions()
team.wildCardBracket = false
team.walkOut = false
team.wildCardGroupStage = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
}
})) {
Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle")
@ -1032,24 +1133,32 @@ struct InscriptionManagerView: View {
Toggle(isOn: .init(get: {
return team.walkOut
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
_clearScreen()
Task {
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = value
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
}
})) {
Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle")
}
Divider()
Button(role: .destructive) {
do {
try dataStore.teamRegistrations.delete(instance: team)
} catch {
Logger.error(error)
_clearScreen()
Task {
do {
try dataStore.teamRegistrations.delete(instance: team)
} catch {
Logger.error(error)
}
_getTeams()
}
} label: {
LabelDelete()

@ -13,6 +13,9 @@ struct PrintSettingsView: View {
@StateObject var generator: HtmlGenerator
@State private var presentShareView: 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) {
self.tournament = tournament
@ -63,26 +66,55 @@ struct PrintSettingsView: View {
} header: {
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 {
NavigationLink {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in
})
WebViewPreview(bracket: true)
.environmentObject(generator)
} label: {
Text("Aperçu du tableau")
}
ForEach(tournament.groupStages()) { groupStage in
NavigationLink {
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in
if let error {
print("preparePDF", error)
} else if loaded == false {
generator.generateGroupStage(webView: webView)
} else {
print("preparePDF", "is loading")
}
})
WebViewPreview(groupStage: groupStage)
.environmentObject(generator)
} label: {
Text("Aperçu de la \(groupStage.groupStageTitle())")
}
@ -90,16 +122,59 @@ struct PrintSettingsView: View {
}
}
.background {
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)
if generating {
_backgroundGenerationWebView()
_backgroundGroupStageWebView()
}
}
.navigationTitle("Imprimer")
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
// .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 {
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
@ -114,64 +189,7 @@ struct PrintSettingsView: View {
}
}
}
.navigationTitle("Imprimer")
.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])
}
}
.id(generationGroupStageId)
}
}
@ -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
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 groupStages(Tournament)

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

@ -7,7 +7,11 @@
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 general
case club(Tournament)

@ -9,13 +9,18 @@ import SwiftUI
struct TournamentBuildView: View {
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
var body: some View {
if tournament.hasEnded() {
Section {
NavigationLink(value: Screen.rankings) {
Text("Classement")
Text("Classement final des équipes")
}
}
}
@ -24,8 +29,12 @@ struct TournamentBuildView: View {
if tournament.groupStageCount > 0 {
NavigationLink(value: Screen.groupStage) {
LabeledContent {
Text(tournament.groupStageStatus())
.multilineTextAlignment(.trailing)
if let groupStageStatus {
Text(groupStageStatus).lineLimit(1)
.multilineTextAlignment(.trailing)
} else {
ProgressView()
}
} label: {
Text("Poules")
if tournament.shouldVerifyGroupStage {
@ -33,13 +42,20 @@ struct TournamentBuildView: View {
}
}
}
.task {
groupStageStatus = await tournament.groupStageStatus()
}
}
if tournament.rounds().isEmpty == false {
NavigationLink(value: Screen.round) {
LabeledContent {
Text(tournament.bracketStatus())
.multilineTextAlignment(.trailing)
if let bracketStatus {
Text(bracketStatus).lineLimit(1)
.multilineTextAlignment(.trailing)
} else {
ProgressView()
}
} label: {
Text("Tableau")
if tournament.shouldVerifyBracket {
@ -47,42 +63,78 @@ struct TournamentBuildView: View {
}
}
}
.task {
bracketStatus = await tournament.bracketStatus()
}
}
}
Section {
if tournament.state() != .finished {
NavigationLink(value: Screen.schedule) {
let tournamentStatus = tournament.scheduleStatus()
let tournamentStatus = scheduleStatus
LabeledContent {
Text(tournamentStatus.completion)
if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: {
Text("Horaires")
Text(tournamentStatus.label)
if let tournamentStatus {
Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
}
}
}
.task {
scheduleStatus = await tournament.scheduleStatus()
}
NavigationLink(value: Screen.call) {
let tournamentStatus = tournament.callStatus()
let tournamentStatus = callStatus
LabeledContent {
Text(tournamentStatus.completion)
if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: {
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 {
NavigationLink(value: Screen.cashier) {
let tournamentStatus = tournament.cashierStatus()
let tournamentStatus = cashierStatus
LabeledContent {
Text(tournamentStatus.completion)
if let tournamentStatus {
Text(tournamentStatus.completion)
} else {
ProgressView()
}
} label: {
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 LeStorage
struct TournamentInitView: View {
var tournament: Tournament
@ -43,13 +44,21 @@ struct TournamentInitView: View {
NavigationLink(value: Screen.broadcast) {
LabeledContent {
if tournament.isPrivate {
Text("tournoi privé").foregroundStyle(.logoRed)
if Store.main.userId == nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
} else {
Text("Automatique")
if tournament.isPrivate {
Text("tournoi privé").foregroundStyle(.logoRed)
} else {
Text("Automatique")
}
}
} label: {
Text("Publication")
if Store.main.userId == nil {
Text("Un compte Padel Club est nécessaire")
}
}
}

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

@ -14,12 +14,12 @@ struct ListRowViewModifier: ViewModifier {
func body(content: Content) -> some View {
if isActive {
content
// .listRowBackground(
// color.variation()
// .overlay(alignment: .leading, content: {
// color.frame(width: 8)
// })
// )
.listRowBackground(
color.variation()
.overlay(alignment: .leading, content: {
color.frame(width: 8)
})
)
} else {
content
}
@ -27,7 +27,7 @@ struct ListRowViewModifier: ViewModifier {
}
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))
}
}

Loading…
Cancel
Save