multistore
Laurent 1 year ago
commit 0669760089
  1. 14
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/AppSettings.swift
  3. 2
      PadelClub/Data/Club.swift
  4. 2
      PadelClub/Data/Court.swift
  5. 2
      PadelClub/Data/DateInterval.swift
  6. 2
      PadelClub/Data/Event.swift
  7. 2
      PadelClub/Data/GroupStage.swift
  8. 43
      PadelClub/Data/Match.swift
  9. 2
      PadelClub/Data/MatchScheduler.swift
  10. 2
      PadelClub/Data/MonthData.swift
  11. 18
      PadelClub/Data/PlayerRegistration.swift
  12. 25
      PadelClub/Data/Round.swift
  13. 8
      PadelClub/Data/TeamRegistration.swift
  14. 2
      PadelClub/Data/TeamScore.swift
  15. 89
      PadelClub/Data/Tournament.swift
  16. 25
      PadelClub/Utils/FileImportManager.swift
  17. 36
      PadelClub/Utils/HtmlGenerator.swift
  18. 40
      PadelClub/Utils/HtmlService.swift
  19. 1
      PadelClub/Utils/URLs.swift
  20. 12
      PadelClub/ViewModel/SeedInterval.swift
  21. 2
      PadelClub/Views/Calling/GroupStageCallingView.swift
  22. 27
      PadelClub/Views/Club/ClubSearchView.swift
  23. 6
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  24. 85
      PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift
  25. 4
      PadelClub/Views/Match/MatchSetupView.swift
  26. 2
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  27. 2
      PadelClub/Views/Navigation/MainView.swift
  28. 2
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  29. 14
      PadelClub/Views/Round/LoserRoundView.swift
  30. 11
      PadelClub/Views/Round/LoserRoundsView.swift
  31. 28
      PadelClub/Views/Team/TeamPickerView.swift
  32. 4
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  33. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  34. 36
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  35. 354
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  36. 7
      PadelClub/Views/Tournament/TournamentBuildView.swift
  37. 1
      PadelClub/Views/Tournament/TournamentView.swift
  38. 3
      PadelClubTests/ServerDataTests.swift

@ -77,6 +77,7 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; };
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -407,6 +408,7 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserGroupStageSettingsView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -1146,6 +1148,7 @@
FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */,
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */,
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */,
FF9AC3932BE3625D00C2E883 /* Components */,
FF9AC3922BE3625200C2E883 /* Shared */,
);
@ -1532,6 +1535,7 @@
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */,
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */,
FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */,
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */,
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */,
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */,
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */,
@ -1886,13 +1890,14 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 76;
CURRENT_PROJECT_VERSION = 78;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_MODULE_VERIFIER = YES;
ENABLE_PREVIEWS = YES;
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -1910,10 +1915,12 @@
);
MARKETING_VERSION = 0.1;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_SWIFT_FLAGS = "";
OTHER_SWIFT_FLAGS = "-Onone";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -1926,7 +1933,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 76;
CURRENT_PROJECT_VERSION = 78;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1952,6 +1959,7 @@
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
class AppSettings: MicroStorable {
final class AppSettings: MicroStorable {
var lastDataSource: String? = nil
var didCreateAccount: Bool = false

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
class Club : ModelObject, Storable, Hashable {
final class Club : ModelObject, Storable, Hashable {
static func resourceName() -> String { return "clubs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
class Court : ModelObject, Storable, Hashable {
final class Court : ModelObject, Storable, Hashable {
static func resourceName() -> String { return "courts" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
class DateInterval: ModelObject, Storable {
final class DateInterval: ModelObject, Storable {
static func resourceName() -> String { return "date-intervals" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
class Event: ModelObject, Storable {
final class Event: ModelObject, Storable {
static func resourceName() -> String { return "events" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -11,7 +11,7 @@ import Algorithms
import SwiftUI
@Observable
class GroupStage: ModelObject, Storable {
final class GroupStage: ModelObject, Storable {
static func resourceName() -> String { "group-stages" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
class Match: ModelObject, Storable {
final class Match: ModelObject, Storable {
static func resourceName() -> String { "matches" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
@ -220,25 +220,10 @@ defer {
func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
existingTeamScore.walkOut = 1
do {
try self.tournamentStore.teamScores.delete(contentOfs: previousScores)
} catch {
Logger.error(error)
}
if let existingTeamScore = teamScore(ofTeam: team) {
do {
try self.tournamentStore.teamScores.delete(instance: existingTeamScore)
} catch {
Logger.error(error)
}
}
let teamScoreWalkout = TeamScore(match: id, team: team)
teamScoreWalkout.walkOut = 1
do {
try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreWalkout)
try self.tournamentStore.teamScores.addOrUpdate(instance: existingTeamScore)
} catch {
Logger.error(error)
}
@ -254,24 +239,18 @@ defer {
func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
let matchIndex = index
let position = matchIndex * 2 + teamPosition.rawValue
let previousScores = teamScores.filter({ $0.luckyLoser == position })
do {
try self.tournamentStore.teamScores.delete(contentOfs: previousScores)
} catch {
Logger.error(error)
}
if let existingTeamScore = teamScore(ofTeam: team) {
do {
try self.tournamentStore.teamScores.delete(instance: existingTeamScore)
} catch {
Logger.error(error)
}
}
let matchIndex = index
let position = matchIndex * 2 + teamPosition.rawValue
let teamScoreLuckyLoser = TeamScore(match: id, team: team)
let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
teamScoreLuckyLoser.luckyLoser = position
do {
try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
class MatchScheduler : ModelObject, Storable {
final class MatchScheduler : ModelObject, Storable {
static func resourceName() -> String { return "match-scheduler" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage
@Observable
class MonthData : ModelObject, Storable {
final class MonthData : ModelObject, Storable {
static func resourceName() -> String { return "month-data" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
class PlayerRegistration: ModelObject, Storable {
final class PlayerRegistration: ModelObject, Storable {
static func resourceName() -> String { "player-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
@ -75,13 +75,21 @@ class PlayerRegistration: ModelObject, Storable {
self.source = .frenchFederation
}
internal init(federalData: [String], sex: Int, sexUnknown: Bool) {
lastName = federalData[0].trimmed.uppercased()
firstName = federalData[1].trimmed.capitalized
internal init?(federalData: [String], sex: Int, sexUnknown: Bool) {
let _lastName = federalData[0].trimmed.uppercased()
let _firstName = federalData[1].trimmed.capitalized
if _lastName.isEmpty && _firstName.isEmpty { return nil }
lastName = _lastName
firstName = _firstName
birthdate = federalData[2]
licenceId = federalData[3]
clubName = federalData[4]
rank = Int(federalData[5])
let stringRank = federalData[5]
if stringRank.isEmpty {
rank = nil
} else {
rank = Int(stringRank)
}
let _email = federalData[6]
if _email.isEmpty == false {
self.email = _email

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI
@Observable
class Round: ModelObject, Storable {
final class Round: ModelObject, Storable {
static func resourceName() -> String { "rounds" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
@ -582,6 +582,29 @@ defer {
loserRounds().forEach { round in
round.buildLoserBracket()
}
/*
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
matches.forEach {
$0.name = $0.roundObject?.roundTitle()
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
*/
}
var parentRound: Round? {

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
class TeamRegistration: ModelObject, Storable {
final class TeamRegistration: ModelObject, Storable {
static func resourceName() -> String { "team-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
@ -207,8 +207,12 @@ class TeamRegistration: ModelObject, Storable {
}
func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false }
return players.allSatisfy { player in
includes(player: player)
unsortedPlayers.anySatisfy { _player in
_player.isSameAs(player)
}
}
}

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
class TeamScore: ModelObject, Storable {
final class TeamScore: ModelObject, Storable {
static func resourceName() -> String { "team-scores" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -9,7 +9,7 @@ import Foundation
import LeStorage
@Observable
class Tournament : ModelObject, Storable {
final class Tournament : ModelObject, Storable {
static func resourceName() -> String { "tournaments" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
@ -55,7 +55,8 @@ class Tournament : ModelObject, Storable {
var hideTeamsWeight: Bool = false
var publishTournament: Bool = false
var hidePointsEarned: Bool = false
var publishRankings: Bool = false
@ObservationIgnored
var navigationPath: [Screen] = []
@ -102,9 +103,10 @@ class Tournament : ModelObject, Storable {
case _hideTeamsWeight = "hideTeamsWeight"
case _publishTournament = "publishTournament"
case _hidePointsEarned = "hidePointsEarned"
case _publishRankings = "publishRankings"
}
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false) {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) {
self.event = event
self.name = name
self.startDate = startDate
@ -141,6 +143,7 @@ class Tournament : ModelObject, Storable {
self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
}
required init(from decoder: Decoder) throws {
@ -184,6 +187,7 @@ class Tournament : ModelObject, Storable {
hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
}
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -298,6 +302,7 @@ class Tournament : ModelObject, Storable {
try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
try container.encode(publishTournament, forKey: ._publishTournament)
try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
try container.encode(publishRankings, forKey: ._publishRankings)
}
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -487,8 +492,7 @@ class Tournament : ModelObject, Storable {
}
func courtUsed() -> [Int] {
#if DEBUG_TIME //DEBUGING TIME
#if DEBUG //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -1104,6 +1108,17 @@ defer {
return selected.sorted(by: \.finalRanking!, order: .ascending)
}
private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) {
for key in dictionary.keys {
if var stringArray = dictionary[key] {
// Remove all instances of each string in stringsToRemove
stringArray.removeAll { stringsToRemove.contains($0) }
dictionary[key] = stringArray
}
}
}
func finalRanking() async -> [Int: [String]] {
var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
@ -1119,6 +1134,14 @@ defer {
}
let others: [Round] = rounds.flatMap { round in
let losers = round.losers()
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
if teams[minimumFinalPosition] == nil {
teams[minimumFinalPosition] = losers.map { $0.id }
} else {
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
}
print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() })
@ -1137,28 +1160,40 @@ defer {
print("losers", losers.count)
if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
teams[interval.computedLast] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach { ids.insert($0) }
if disabledIds.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.computedLast] = disabledIds
let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach {
ids.insert($0)
}
}
} else {
teams[interval.computedFirst + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) }
teams[interval.computedLast] = losers
let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("losers", "\(interval.computedLast) : ", loserTeamNames)
losers.forEach { ids.insert($0) }
if winners.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: winners)
teams[interval.computedFirst + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) }
}
if losers.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.computedLast] = losers
let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0)
return t
}.map { $0.canonicalName }
print("losers", "\(interval.computedLast) : ", loserTeamNames)
losers.forEach { ids.insert($0) }
}
}
}
}
@ -1882,7 +1917,7 @@ defer {
let selected = selectedSortedTeams()
let allTeams = unsortedTeams()
let seedCount = max(selected.count - groupStageSpots(), 0)
let newGroup = selected.prefix(seedCount)
let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
let selectedIds = newGroup.map { $0.id }
let groupIds = currentGroup.map { $0.id }

@ -268,13 +268,17 @@ class FileImportManager {
case .mix: return 1
}
}
if tournamentCategory == tournament.tournamentCategory {
if tournamentCategory == tournament.tournamentCategory {
let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne.setComputedRank(in: tournament)
playerOne?.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament)
results.append(team)
playerTwo?.setComputedRank(in: tournament)
let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
results.append(team)
}
}
}
}
@ -320,12 +324,15 @@ class FileImportManager {
}
let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne.setComputedRank(in: tournament)
playerOne?.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament)
playerTwo?.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament)
results.append(team)
let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
results.append(team)
}
}
}
}

@ -31,10 +31,19 @@ class HtmlGenerator: ObservableObject {
@Published var height: CGFloat = 0
private var webView: WKWebView = WKWebView()
private var groupStageDone: Int = 0
@Published var landscape: Bool = false
var baseWidth: CGFloat {
landscape ? 842 : 595
}
var baseHeight: CGFloat {
landscape ? 595 : 842
}
var estimatedPageCount: Int {
if let zoomLevel {
let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel))
let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel))
let numberOfPageInWidth = Int(width / pageSize.width) + 1
let numberOfPageInHeight = Int(height / pageSize.height) + 1
return numberOfPageInWidth * numberOfPageInHeight
@ -50,17 +59,19 @@ class HtmlGenerator: ObservableObject {
func generateWebView(webView: WKWebView) {
self.webView = webView
self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
print("evaluateJavaScript", "readystage", complete, error)
if complete != nil {
self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
print("evaluateJavaScript", "height", height, error)
self.height = height as! CGFloat
self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in
print("evaluateJavaScript", "width", width, error)
self.width = width as! CGFloat
if self.completionHandler != nil {
self.buildPDF()
}
})
})
self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in
self.width = width as! CGFloat
})
}
if self.completionHandler != nil {
self.buildPDF()
}
})
}
@ -75,7 +86,7 @@ class HtmlGenerator: ObservableObject {
print("bracket", width, height)
let config = WKPDFConfiguration()
config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(width)))
config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(height)))
webView.createPDF(configuration: config){ result in
switch result{
case .success(let data):
@ -111,7 +122,7 @@ class HtmlGenerator: ObservableObject {
try? FileManager.default.removeItem(at: pdfURL!)
print("buildPDF", width, height, zoomLevel ?? 0)
if let zoomLevel {
let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel))
let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel))
let numberOfPageInWidth = Int(width / pageSize.width) + 1
let numberOfPageInHeight = Int(height / pageSize.height) + 1
for w in 0..<numberOfPageInWidth {
@ -159,6 +170,11 @@ class HtmlGenerator: ObservableObject {
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false)
}
func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false)
}
var pdfURL: URL? {
guard let pdfFolderURL = getFilePath() else {
return nil

@ -10,7 +10,8 @@ import Foundation
enum HtmlService {
case template(tournament: Tournament)
case bracket(tournament: Tournament, roundIndex: Int)
case bracket(round: Round)
case loserBracket(upperRound: Round)
case match(match: Match)
case player(entrant: TeamRegistration)
case hiddenPlayer
@ -26,7 +27,7 @@ enum HtmlService {
var fileName: String {
switch self {
case .template:
case .template, .loserBracket:
return "tournament-template"
case .bracket:
return "bracket-template"
@ -191,23 +192,42 @@ enum HtmlService {
}
template = template.replacingOccurrences(of: "{{matchDescription}}", with: "")
return template
case .bracket(let tournament, let roundIndex):
case .bracket(let round):
var template = ""
var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
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)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
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)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
return bracket
case .loserBracket(let upperRound):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle())
var brackets = ""
for round in upperRound.loserRounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore))
}
var winnerName = ""
let winner = """
<ul class="round" scope="last">
<li class="spacer">&nbsp;</li>
<li class="game game-top winner">\(winnerName)</li>
<li class="spacer">&nbsp;</li>
</ul>
<ul class="main" style="visibility:hidden">
</ul>
"""
brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
return template
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short))
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore))
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore))
}
var winnerName = ""

@ -14,6 +14,7 @@ enum URLs: String, Identifiable {
case api = "https://xlr.alwaysdata.net/roads/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr"
case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf"
var id: String { return self.rawValue }

@ -37,9 +37,15 @@ struct SeedInterval: Hashable, Comparable {
func chunks() -> [SeedInterval]? {
if dimension > 3 {
let split = dimension / 2
let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce)
let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce)
return [firstHalf, secondHalf]
if split%2 == 0 {
let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce)
let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce)
return [firstHalf, secondHalf]
} else {
let firstHalf = SeedInterval(first: first, last: first + split, reduce: reduce)
let secondHalf = SeedInterval(first: first + split + 1, last: last, reduce: reduce)
return [firstHalf, secondHalf]
}
} else {
return nil
}

@ -85,7 +85,7 @@ struct GroupStageCallingView: View {
}
}
.overlay {
if groupStage.startDate == nil {
if groupStage.startDate == nil && groupStage._matches().filter({ $0.startDate != nil }).isEmpty {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {

@ -27,7 +27,8 @@ struct ClubSearchView: View {
@State private var searchPresented: Bool = false
@State private var showingSettingsAlert = false
@State private var newClub: Club?
@State private var error: Error?
var presentClubCreationView: Binding<Bool> { Binding(
get: { newClub != nil },
set: { isPresented in
@ -59,7 +60,7 @@ struct ClubSearchView: View {
searching = false
searchAttempted = true
}
error = nil
clubMarkers = []
guard let city = locationManager.city else { return }
let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location)
@ -70,6 +71,8 @@ struct ClubSearchView: View {
}
} catch {
print("getclubs", error)
self.error = error
Logger.error(error)
}
}
@ -143,12 +146,20 @@ struct ClubSearchView: View {
} else if clubMarkers.isEmpty && searching == false && searchPresented == false {
ContentUnavailableView {
if searchAttempted {
Label("Aucun club trouvé", systemImage: "mappin.slash")
if error != nil {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} else {
Label("Aucun club trouvé", systemImage: "mappin.slash")
}
} else {
Label("Recherche de club", systemImage: "location.circle")
}
} description: {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
if searchAttempted && error != nil {
Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.")
} else {
Text("Padel Club recherche via Tenup un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
}
} actions: {
if locationManager.manager.authorizationStatus != .restricted {
RowButtonView("Chercher autour de moi") {
@ -161,6 +172,13 @@ struct ClubSearchView: View {
}
}
}
if error != nil {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
}
RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true
}
@ -343,6 +361,7 @@ struct ClubSearchView: View {
private func _resetSearch() {
searchAttempted = false
error = nil
debouncableViewModel.debouncableText = ""
searchedCity = ""
locationManager.city = nil

@ -82,6 +82,12 @@ struct GroupStageSettingsView: View {
}
#endif
// NavigationLink {
// LoserGroupStageSettingsView(tournament: tournament)
// } label: {
// Text("Match de perdant de poules")
// }
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false {
Section {
menuBuildAllGroupStages

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

@ -80,14 +80,14 @@ struct MatchSetupView: View {
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData())
if walkOutSpot {
if walkOutSpot || team.bracketPosition != nil {
match.setLuckyLoser(team: team, teamPosition: teamPosition)
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
} else {
} else if team.bracketPosition == nil {
team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
do {
try tournamentStore.matches.addOrUpdate(instance: match)

@ -102,7 +102,7 @@ struct ActivityView: View {
.overlay {
if let error, navigation.agendaDestination == .tenup {
ContentUnavailableView {
Label("Erreur", systemImage: "exclamationmark")
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: {
Text(error.localizedDescription)
} actions: {

@ -87,7 +87,7 @@ struct MainView: View {
}
.environmentObject(dataStore)
.task {
await self._checkSourceFileAvailability()
//await self._checkSourceFileAvailability()
if StoreCenter.main.hasToken() {
do {
try await dataStore.clubs.loadDataFromServerIfAllowed()

@ -128,7 +128,7 @@ struct CourtAvailabilitySettingsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible")
.popover(isPresented: $showingPopover) {
.sheet(isPresented: $showingPopover) {
NavigationStack {
Form {
Section {

@ -6,9 +6,9 @@
//
import SwiftUI
import LeStorage
struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ -22,7 +22,7 @@ struct LoserRoundView: View {
print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return loserBracket.allMatches.allSatisfy({ $0.disabled == false })
return loserBracket.allMatches.allSatisfy({ $0.disabled == true })
}
private func _matches(loserRoundId: String?) -> [Match] {
@ -87,6 +87,16 @@ struct LoserRoundView: View {
ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.wrappedValue.toggle()
if isEditingTournamentSeed.wrappedValue == false {
let allRoundMatches = loserBracket.allMatches
allRoundMatches.forEach({ $0.name = $0.roundTitle() })
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch {
Logger.error(error)
}
}
}
}
}

@ -177,13 +177,12 @@ struct LoserRoundsView: View {
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: false)
if let selectedRound {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: true)
switch selectedRound {
case .some(let selectedRound):
LoserRoundView(loserBracket: selectedRound)
} else {
Section {
ContentUnavailableView("Aucun tour à jouer", systemImage: "tennisball", description: Text("Il il n'y a aucun tour de match de classement prévu."))
}
default:
LoserRoundSettingsView()
}
}
.environment(\.isEditingTournamentSeed, $isEditingTournamentSeed)

@ -11,12 +11,20 @@ struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
@State private var confirmTeam: TeamRegistration?
@State private var presentTeamPickerView: Bool = false
@State private var searchField: String = ""
var groupStagePosition: Int? = nil
var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void))
var confirmationRequest: Binding<Bool> {
Binding {
confirmTeam != nil
} set: { _ in
}
}
var body: some View {
Button {
presentTeamPickerView = true
@ -86,12 +94,30 @@ struct TeamPickerView: View {
Button {
teamPicked(team)
presentTeamPickerView = false
// if team.inRound() {
// confirmTeam = team
// } else {
// teamPicked(team)
// presentTeamPickerView = false
// }
} label: {
TeamRowView(team: team)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!)
// presentTeamPickerView = false
// }
//
// Button("Annuler", role: .cancel) {
// confirmTeam = nil
// }
// } message: {
// Text("Vous êtes sur le point de retirer cette équipe du tableau pour le replacer, cela effacera les résultats des matchs déjà joués par cette équipe dans le tableau.")
// }
}
}
}

@ -106,9 +106,9 @@ struct InscriptionInfoView: View {
}
.listRowView(color: .red)
} header: {
Text("Équipes non sélectionnées")
Text("Équipes ne devant plus être sélectionnées")
} footer: {
Text("Il s'agit des équipes déjà placé en poule ou tableau qui sont actuellement en attente à cause de l'arrivée d'une nouvelle équipe ou une modification de classement.")
Text("Il s'agit des équipes précédement placées en poule ou tableau mais qui sont finalement maintenant en attente suite à l'arrivée d'une nouvelle équipe plus forte ou une modification de classement.")
}
Section {

@ -530,7 +530,7 @@ struct InscriptionManagerView: View {
RowButtonView("Créer une équipe") {
Task {
await MainActor.run() {
await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField

@ -28,7 +28,6 @@ struct PrintSettingsView: View {
// Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries")
// })
Toggle(isOn: $generator.displayRank, label: {
Text("Afficher le classement du joueur")
})
@ -57,6 +56,17 @@ struct PrintSettingsView: View {
} label: {
Text("Zoom")
}
.onChange(of: generator.zoomLevel) {
if generator.zoomLevel == nil {
generator.landscape = false
}
}
if generator.zoomLevel != nil {
Toggle(isOn: $generator.landscape, label: {
Text("Format paysage")
})
}
HStack {
Text("Nombre de page A4 à imprimer")
@ -106,11 +116,23 @@ struct PrintSettingsView: View {
Section {
NavigationLink {
WebViewPreview(bracket: true)
WebViewPreview()
.environmentObject(generator)
} label: {
Text("Aperçu du tableau")
}
//
// ForEach(tournament.rounds()) { round in
// if round.index > 0 {
// NavigationLink {
// WebViewPreview(round: round)
// .environmentObject(generator)
// } label: {
// Text("Aperçu \(round.correspondingLoserRoundTitle())")
// }
// }
// }
//
ForEach(tournament.groupStages()) { groupStage in
NavigationLink {
WebViewPreview(groupStage: groupStage)
@ -260,13 +282,13 @@ struct WebView: UIViewRepresentable {
struct WebViewPreview: View {
@EnvironmentObject var generator: HtmlGenerator
let bracket: Bool
let groupStage: GroupStage?
let round: Round?
@State private var html: String?
init(bracket: Bool = false, groupStage: GroupStage? = nil) {
self.bracket = bracket
init(groupStage: GroupStage? = nil, round: Round? = nil) {
self.round = round
self.groupStage = groupStage
}
@ -280,6 +302,8 @@ struct WebViewPreview: View {
.onAppear {
if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)
} else if let round {
html = generator.generateLoserBracketHtml(upperRound: round)
} else {
html = generator.generateHtml()
}

@ -11,7 +11,8 @@ import LeStorage
struct TournamentRankView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@State private var rankings: [Int: [TeamRegistration]] = [:]
@State private var calculating = false
@State private var selectedTeam: TeamRegistration?
@ -33,97 +34,71 @@ struct TournamentRankView: View {
var body: some View {
List {
@Bindable var tournament = tournament
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil })
Section {
LabeledContent {
if let matchesLeft {
Text(matchesLeft.count.formatted())
} else {
ProgressView()
let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if editMode?.wrappedValue.isEditing == false {
Section {
MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)
Toggle(isOn: $tournament.hidePointsEarned) {
Text("Masquer les points gagnés")
}
} label: {
Text("Matchs restant")
}
LabeledContent {
if let runningMatches {
Text(runningMatches.count.formatted())
} else {
ProgressView()
.onChange(of: tournament.hidePointsEarned) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} label: {
Text("Matchs en cours")
}
LabeledContent {
if rankingPublished {
Image(systemName: "checkmark")
.foregroundStyle(.green)
} else {
Image(systemName: "xmark")
.foregroundStyle(.logoRed)
Toggle(isOn: $tournament.publishRankings) {
Text("Publier sur Padel Club")
if let url = tournament.shareURL(.rankings) {
Link(destination: url) {
Text("Accéder à la page")
}
}
}
} label: {
Text("Classement publié")
}
Toggle(isOn: $tournament.hidePointsEarned) {
Text("Masquer les points gagnés")
}
.onChange(of: tournament.hidePointsEarned) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
.onChange(of: tournament.publishRankings) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
}
if rankingPublished == false {
RowButtonView("Publier le classement", role: .destructive) {
_publishRankings()
if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false {
Section {
RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) {
await _calculateRankings()
}
} else {
RowButtonView("Re-publier le classement", role: .destructive) {
_publishRankings()
} footer: {
if rankingsCalculated {
Text("Vos éditions seront perdus.")
}
}
}
if rankingPublished {
Section {
RowButtonView("Supprimer le classement", role: .destructive) {
tournament.unsortedTeams().forEach { team in
team.finalRanking = nil
team.pointsEarned = nil
if rankingsCalculated {
Section {
RowButtonView("Supprimer le classement", role: .destructive) {
tournament.unsortedTeams().forEach { team in
team.finalRanking = nil
team.pointsEarned = nil
}
_save()
}
_save()
}
} footer: {
Text(.init("Masque également le classement sur le site [Padel Club](\(URLs.main.rawValue))"))
}
}
if rankingPublished {
let teamsRanked = tournament.teamsRanked()
if calculating == false && rankingsCalculated && teamsRanked.isEmpty == false {
Section {
ForEach(tournament.teamsRanked()) { team in
let key = team.finalRanking ?? 0
Button {
selectedTeam = team
} label: {
TeamRankCellView(team: team, key: key)
.frame(maxWidth: .infinity)
}
.contentShape(Rectangle())
.buttonStyle(.plain)
}
} footer: {
Text("Vous pouvez appuyer sur une ligne pour éditer manuellement le classement calculé par Padel Club.")
}
} else {
let keys = rankings.keys.sorted()
ForEach(keys, id: \.self) { key in
if let rankedTeams = rankings[key] {
ForEach(rankedTeams) { team in
ForEach(teamsRanked) { team in
if let key = team.finalRanking {
TeamRankCellView(team: team, key: key)
}
}
@ -165,12 +140,10 @@ struct TournamentRankView: View {
}
})
.onAppear {
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil })
let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if rankingPublished == false {
calculating = true
Task {
await _calculateRankings()
calculating = false
}
}
}
@ -179,103 +152,156 @@ struct TournamentRankView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if let url = tournament.shareURL(.rankings) {
_actionForURL(url)
}
EditButton()
}
}
}
struct TeamRankCellView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
let key: Int
@State private var isEditingTeam: Bool = false
@Bindable var team: TeamRegistration
@State var key: Int
var body: some View {
HStack {
VStack(alignment: .trailing) {
VStack(alignment: .trailing, spacing: -8.0) {
ZStack(alignment: .trailing) {
Text(tournament.teamCount.formatted()).hidden()
Text(key.formatted())
}
.monospacedDigit()
.font(.largeTitle)
.fontWeight(.bold)
Text(key.ordinalFormattedSuffix()).font(.caption)
}
if let index = tournament.indexOf(team: team) {
let rankingDifference = index - (key - 1)
if rankingDifference > 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.up.fill")
.imageScale(.small)
}
.foregroundColor(.green)
} else if rankingDifference < 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.down.fill")
.imageScale(.small)
VStack(spacing: 0) {
if editMode?.wrappedValue.isEditing == true {
if key > 1 {
Button {
key -= 1
team.finalRanking = key
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
.foregroundColor(.red)
} else {
Text("--")
} label: {
Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
}
}
Divider()
VStack(alignment: .leading) {
if let name = team.name {
Text(name).foregroundStyle(.secondary)
}
ForEach(team.players()) { player in
VStack(alignment: .leading, spacing: -4.0) {
Text(player.playerLabel()).bold()
HStack(alignment: .firstTextBaseline, spacing: 0.0) {
Text(player.rankLabel())
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix())
.font(.caption)
Button {
isEditingTeam = true
} label: {
HStack {
VStack(alignment: .trailing) {
VStack(alignment: .trailing, spacing: -8.0) {
ZStack(alignment: .trailing) {
Text(tournament.teamCount.formatted()).hidden()
Text(key.formatted())
}
.monospacedDigit()
.font(.largeTitle)
.fontWeight(.bold)
Text(key.ordinalFormattedSuffix()).font(.caption)
}
if let index = tournament.indexOf(team: team) {
let rankingDifference = index - (key - 1)
if rankingDifference > 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.up.fill")
.imageScale(.small)
}
.foregroundColor(.green)
} else if rankingDifference < 0 {
HStack(spacing: 0.0) {
Text(rankingDifference.formatted(.number.sign(strategy: .always())))
.monospacedDigit()
Image(systemName: "arrowtriangle.down.fill")
.imageScale(.small)
}
.foregroundColor(.red)
} else {
Text("--")
}
}
}
Divider()
VStack(alignment: .leading) {
if let name = team.name {
Text(name).foregroundStyle(.secondary)
}
ForEach(team.players()) { player in
VStack(alignment: .leading, spacing: -4.0) {
Text(player.playerLabel()).bold()
HStack(alignment: .firstTextBaseline, spacing: 0.0) {
Text(player.rankLabel())
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix())
.font(.caption)
}
}
}
}
}
if tournament.isAnimation() == false && key > 0 {
Spacer()
VStack(alignment: .trailing) {
HStack(alignment: .lastTextBaseline, spacing: 0.0) {
Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always())))
Text("pts").font(.caption)
}
}
}
}
.frame(maxWidth: .infinity)
}
if tournament.isAnimation() == false && key > 0 {
Spacer()
VStack(alignment: .trailing) {
HStack(alignment: .lastTextBaseline, spacing: 0.0) {
Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always())))
Text("pts").font(.caption)
.contentShape(Rectangle())
.buttonStyle(.plain)
if editMode?.wrappedValue.isEditing == true {
Button {
key += 1
team.finalRanking = key
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
} label: {
Label("descendre", systemImage: "chevron.compact.down").labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
}
}
}
}
private func _publishRankings() {
rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount)
.alert("Position", isPresented: $isEditingTeam) {
TextField("Position", value: $team.finalRanking, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
Button("Valider") {
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount)
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
isEditingTeam = false
}
Button("Annuler", role: .cancel) {
isEditingTeam = false
}
}
}
_save()
}
private func _calculateRankings() async {
await MainActor.run {
calculating = true
}
let finalRanks = await tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] {
@ -283,27 +309,21 @@ struct TournamentRankView: View {
self.rankings[rank] = teams
}
}
}
@ViewBuilder
private func _actionForURL(_ url: URL, removeSource: Bool = false) -> some View {
Menu {
Button {
UIApplication.shared.open(url)
} label: {
Label("Voir", systemImage: "safari")
await MainActor.run {
rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount)
}
}
}
_save()
ShareLink(item: url) {
Label("Partager le lien", systemImage: "link")
}
} label: {
Image(systemName: "square.and.arrow.up")
calculating = false
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderless)
}
private func _save() {
do {

@ -89,12 +89,17 @@ struct TournamentBuildView: View {
Section {
#if DEBUG
NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes")
}
#else
if tournament.hasEnded() {
NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes")
}
}
#endif
if state == .running || state == .finished {
TournamentInscriptionView(tournament: tournament)
TournamentBroadcastRowView(tournament: tournament)

@ -78,6 +78,7 @@ struct TournamentView: View {
}
}
}
.environment(tournament)
.id(tournament.id)
.toolbarBackground(.visible, for: .navigationBar)
.navigationDestination(for: Screen.self, destination: { screen in

@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase {
return
}
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true)
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true)
let t = try await Store.main.service().post(tournament)
assert(t.event == tournament.event)
@ -138,6 +138,7 @@ final class ServerDataTests: XCTestCase {
assert(t.hideTeamsWeight == tournament.hideTeamsWeight)
assert(t.publishTournament == tournament.publishTournament)
assert(t.hidePointsEarned == tournament.hidePointsEarned)
assert(t.publishRankings == tournament.publishRankings)
}
func testGroupStage() async throws {

Loading…
Cancel
Save