sync2
Laurent 9 months ago
commit f46890c445
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  3. 395
      PadelClub/Data/TeamRegistration.swift
  4. 571
      PadelClub/Data/Tournament.swift
  5. 5
      PadelClub/HTML Templates/bracket-template.html
  6. 1
      PadelClub/HTML Templates/groupstage-template.html
  7. 1
      PadelClub/HTML Templates/player-template.html
  8. 7
      PadelClub/HTML Templates/tournament-template.html
  9. 132
      PadelClub/PadelClubApp.swift
  10. 7
      PadelClub/Utils/HtmlGenerator.swift
  11. 62
      PadelClub/Utils/HtmlService.swift
  12. 34
      PadelClub/Utils/PadelRule.swift
  13. 7
      PadelClub/Utils/URLs.swift
  14. 33
      PadelClub/Utils/VersionComparator.swift
  15. 190
      PadelClub/ViewModel/SearchViewModel.swift
  16. 2
      PadelClub/Views/Calling/CallSettingsView.swift
  17. 19
      PadelClub/Views/Calling/GroupStageCallingView.swift
  18. 18
      PadelClub/Views/Calling/SeedsCallingView.swift
  19. 7
      PadelClub/Views/Calling/SendToAllView.swift
  20. 18
      PadelClub/Views/Calling/TeamsCallingView.swift
  21. 99
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  22. 2
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  23. 72
      PadelClub/Views/Planning/MatchFormatGuideView.swift
  24. 133
      PadelClub/Views/Planning/PlanningSettingsView.swift
  25. 2
      PadelClub/Views/Round/RoundView.swift
  26. 48
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  27. 7
      PadelClub/Views/Team/Components/TeamWeightView.swift
  28. 3
      PadelClub/Views/Team/EditingTeamView.swift
  29. 3
      PadelClub/Views/Team/TeamRowView.swift
  30. 2
      PadelClub/Views/Tournament/FileImportView.swift
  31. 9
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  32. 3
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  33. 2
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  34. 121
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  35. 60
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  36. 21
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  37. 2
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  38. 2
      PadelClub/Views/Tournament/TournamentBuildView.swift
  39. 4
      PadelClub/Views/Tournament/TournamentInscriptionView.swift
  40. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -130,6 +130,9 @@
C49C73142D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73152D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C73162D5B98D8008DD299 /* PlayerPaymentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */; };
C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */; };
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0182BD694290077B5AA /* PurchaseListView.swift */; };
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF01A2BD6A1E80077B5AA /* URLs.swift */; };
C49EF0262BD80AE80077B5AA /* SubscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */; };
@ -906,6 +909,9 @@
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; };
FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
@ -1089,6 +1095,7 @@
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusView.swift; sourceTree = "<group>"; };
C493B37D2C10AD3600862481 /* LoadingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewModifier.swift; sourceTree = "<group>"; };
C49C73132D5B98D7008DD299 /* PlayerPaymentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPaymentType.swift; sourceTree = "<group>"; };
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionComparator.swift; sourceTree = "<group>"; };
C49EF0182BD694290077B5AA /* PurchaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseListView.swift; sourceTree = "<group>"; };
C49EF01A2BD6A1E80077B5AA /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
C49EF0252BD80AE80077B5AA /* SubscriptionInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionInfoView.swift; sourceTree = "<group>"; };
@ -1346,6 +1353,7 @@
FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; };
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = "<group>"; };
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; };
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; };
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
@ -2225,6 +2233,7 @@
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
FF1DC5582BAB767000FD8220 /* Tips.swift */,
C49EF01A2BD6A1E80077B5AA /* URLs.swift */,
C49C731D2D5E3BE4008DD299 /* VersionComparator.swift */,
FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */,
);
path = Utils;
@ -2263,6 +2272,7 @@
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */,
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */,
FF1162882BD0523B000C4809 /* Components */,
);
path = Planning;
@ -2651,6 +2661,7 @@
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */,
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */,
C49C731F2D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */,
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
@ -2748,6 +2759,7 @@
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
@ -2962,6 +2974,7 @@
FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */,
FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */,
FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */,
C49C731E2D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */,
FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */,
FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */,
@ -3061,6 +3074,7 @@
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */,
@ -3252,6 +3266,7 @@
FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */,
FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */,
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */,
C49C73202D5E3BE8008DD299 /* VersionComparator.swift in Sources */,
FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */,
FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */,
FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */,
@ -3351,6 +3366,7 @@
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */,
@ -3613,7 +3629,6 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3635,14 +3650,13 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.8;
MARKETING_VERSION = 1.1.13;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -3660,7 +3674,6 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3682,14 +3695,13 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.8;
MARKETING_VERSION = 1.1.13;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};

@ -22,7 +22,6 @@ protocol FederalTournamentHolder {
}
extension FederalTournamentHolder {
func durationLabel() -> String {
switch dayDuration {
case 1:

@ -11,43 +11,50 @@ import SwiftUI
@Observable
final class TeamRegistration: BaseTeamRegistration, SideStorable {
// static func resourceName() -> String { "team-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var tournament: String
// var groupStage: String?
// var registrationDate: Date?
// var callDate: Date?
// var bracketPosition: Int?
// var groupStagePosition: Int?
// var comment: String?
// var source: String?
// var sourceValue: String?
// var logo: String?
// var name: String?
//
// var walkOut: Bool = false
// var wildCardBracket: Bool = false
// var wildCardGroupStage: Bool = false
// var weight: Int = 0
// var lockedWeight: Int?
// var confirmationDate: Date?
// var qualified: Bool = false
// var finalRanking: Int?
// var pointsEarned: Int?
//
// var storeId: String? = nil
init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) {
// static func resourceName() -> String { "team-registrations" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var tournament: String
// var groupStage: String?
// var registrationDate: Date?
// var callDate: Date?
// var bracketPosition: Int?
// var groupStagePosition: Int?
// var comment: String?
// var source: String?
// var sourceValue: String?
// var logo: String?
// var name: String?
//
// var walkOut: Bool = false
// var wildCardBracket: Bool = false
// var wildCardGroupStage: Bool = false
// var weight: Int = 0
// var lockedWeight: Int?
// var confirmationDate: Date?
// var qualified: Bool = false
// var finalRanking: Int?
// var pointsEarned: Int?
//
// var storeId: String? = nil
init(
tournament: String, groupStage: String? = nil, registrationDate: Date? = nil,
callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil,
comment: String? = nil, source: String? = nil, sourceValue: String? = nil,
logo: String? = nil, name: String? = nil, walkOut: Bool = false,
wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0,
lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false
) {
super.init()
// self.storeId = tournament
// self.storeId = tournament
self.tournament = tournament
self.groupStage = groupStage
self.registrationDate = registrationDate ?? Date()
@ -67,83 +74,85 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.confirmationDate = confirmationDate
self.qualified = qualified
}
func hasRegisteredOnline() -> Bool {
players().anySatisfy({ $0.registeredOnline })
}
func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil })
}
func isOutOfTournament() -> Bool {
walkOut
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
// MARK: - Computed dependencies
func unsortedPlayers() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false }
return tournamentStore.playerRegistrations.filter {
$0.teamRegistration == self.id && $0.coach == false
}
}
// MARK: -
func deleteTeamScores() {
guard let tournamentStore = self.tournamentStore else { return }
let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id })
tournamentStore.teamScores.delete(contentOfs: ts)
}
override func deleteDependencies() {
let unsortedPlayers = unsortedPlayers()
for player in unsortedPlayers {
player.deleteDependencies()
}
self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers)
let teamScores = teamScores()
for teamScore in teamScores {
teamScore.deleteDependencies()
}
self.tournamentStore?.teamScores.deleteDependencies(teamScores)
}
func hasArrived(isHere: Bool = false) {
let unsortedPlayers = unsortedPlayers()
unsortedPlayers.forEach({ $0.hasArrived = !isHere })
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
}
func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.hasArrived })
}
func isSeedable() -> Bool {
bracketPosition == nil && groupStage == nil
}
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
var teamPosition : TeamPosition {
var teamPosition: TeamPosition {
if let slot {
return slot
} else {
let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
let isUpper =
RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
< (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
@ -160,7 +169,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
if let tournament = tournamentObject() {
if let index = index(in: tournament.selectedSortedTeams()) {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed)
let drawLog = DrawLog(
tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index,
drawTeamPosition: teamPosition, drawType: .seed)
do {
try tournamentStore?.drawLogs.addOrUpdate(instance: drawLog)
} catch {
@ -170,7 +181,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
tournament.updateTeamScores(in: bracketPosition)
}
}
func expectedSummonDate() -> Date? {
if let groupStageStartDate = groupStageObject()?.startDate {
return groupStageStartDate
@ -179,11 +190,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
return nil
}
var initialWeight: Int {
return lockedWeight ?? weight
}
func called() -> Bool {
return callDate != nil
}
@ -195,36 +206,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false })
}
func getMail() -> [String] {
let mails = players().compactMap({ $0.email })
return mails
}
func isImported() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.isImported() })
}
func isWildCard() -> Bool {
return wildCardBracket || wildCardGroupStage
}
func isPlaying() -> Bool {
return currentMatch() != nil
}
func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
func teamScores() -> [TeamScore] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamScores.filter({ $0.teamRegistration == id })
}
func wins() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.winningTeamId == id })
@ -234,7 +245,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id })
}
func matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id })
@ -243,62 +254,74 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
var tournamentCategory: TournamentCategory {
tournamentObject()?.tournamentCategory ?? .men
}
@objc
var canonicalName: String {
players().map { $0.canonicalName }.joined(separator: " ")
}
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
guard let codeClubOrClubName else { return true }
return unsortedPlayers().anySatisfy({
$0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true
$0.clubName?.contains(codeClubOrClubName) == true
|| $0.clubName?.contains(codeClubOrClubName) == true
})
}
func updateWeight(inTournamentCategory tournamentCategory: TournamentCategory) {
self.setWeight(from: self.players(), inTournamentCategory: tournamentCategory)
}
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&") -> String {
func teamLabel(
_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&"
) -> String {
if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " \(separator) ")
return players().map { $0.playerLabel(displayStyle) }.joined(
separator: twoLines ? "\n" : " \(separator) ")
}
func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String {
[displayTeamName ? name : nil, displayRank ? seedIndex() : nil, displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel()].compactMap({ $0 }).joined(separator: " ")
[
displayTeamName ? name : nil, displayRank ? seedIndex() : nil,
displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(),
].compactMap({ $0 }).joined(separator: " ")
}
func seedIndex() -> String? {
guard let tournament = tournamentObject() else { return nil }
guard let index = index(in: tournament.selectedSortedTeams()) else { return nil }
return "(\(index + 1))"
}
func index(in teams: [TeamRegistration]) -> Int? {
return teams.firstIndex(where: { $0.id == id })
}
func formattedSeed(in teams: [TeamRegistration]) -> String {
if let index = index(in: teams) {
func formattedSeed(in teams: [TeamRegistration]? = nil) -> String {
let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? []
if let index = index(in: selectedSortedTeams) {
return "#\(index + 1)"
} else {
return "###"
}
}
func contains(_ searchField: String) -> Bool {
return unsortedPlayers().anySatisfy({ $0.contains(searchField) }) || self.name?.localizedCaseInsensitiveContains(searchField) == true
return unsortedPlayers().anySatisfy({ $0.contains(searchField) })
|| self.name?.localizedCaseInsensitiveContains(searchField) == true
}
func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool {
let arrayOfIds : [String] = unsortedPlayers().compactMap({ $0.licenceId?.strippedLicense?.canonicalVersion })
let ids : Set<String> = Set<String>(arrayOfIds.sorted())
let searchedIds = Set<String>(playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
let arrayOfIds: [String] = unsortedPlayers().compactMap({
$0.licenceId?.strippedLicense?.canonicalVersion
})
let ids: Set<String> = Set<String>(arrayOfIds.sorted())
let searchedIds = Set<String>(
playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
if ids.isEmpty || searchedIds.isEmpty { return false }
return ids.hashValue == searchedIds.hashValue
}
func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false }
@ -308,32 +331,33 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
}
func includes(player: PlayerRegistration) -> Bool {
return unsortedPlayers().anySatisfy { _player in
_player.isSameAs(player)
}
}
func canPlay() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return matches().isEmpty == false || unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived })
return matches().isEmpty == false
|| unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived })
}
func availableForSeedPick() -> Bool {
return groupStage == nil && bracketPosition == nil
}
func inGroupStage() -> Bool {
return groupStagePosition != nil
}
func inRound() -> Bool {
return bracketPosition != nil
}
func positionLabel() -> String? {
if groupStagePosition != nil { return "Poule" }
if let initialRound = initialRound() {
@ -342,62 +366,75 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil
}
}
func initialRoundColor() -> Color? {
if walkOut { return Color.logoRed }
if groupStagePosition != nil { return Color.blue }
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] {
if let initialRound = initialRound(),
let colorHex = RoundRule.colors[safe: initialRound.index]
{
return Color(uiColor: .init(fromHex: colorHex))
} else {
return nil
}
}
func resetGroupeStagePosition() {
guard let tournamentStore = self.tournamentStore else { return }
if let groupStage {
let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { $0.id }
let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) })
let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map {
$0.id
}
let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores)
}
//groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
groupStage = nil
groupStagePosition = nil
}
func resetBracketPosition() {
guard let tournamentStore = self.tournamentStore else { return }
let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id }
let teamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) })
let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores)
self.bracketPosition = nil
}
func resetPositions() {
resetGroupeStagePosition()
resetBracketPosition()
resetBracketPosition()
}
func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
switch exportFormat {
case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .csv:
return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator())
return [
index.formatted(), playersPasteData(exportFormat),
isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator())
}
}
var computedRegistrationDate: Date {
return registrationDate ?? .distantFuture
}
func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? {
guard let registrationDate else { return nil }
let formattedDate = registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
let formattedDate = registrationDate.formatted(
.dateTime.weekday().day().month().hour().minute())
let onlineSuffix = hasRegisteredOnline() ? " en ligne" : ""
switch exportFormat {
case .rawText:
return "Inscrit\(onlineSuffix) le \(formattedDate)"
@ -405,13 +442,14 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return formattedDate
}
}
func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? {
switch exportFormat {
case .rawText:
if let callDate {
return "Convoqué le " + callDate.formatted(.dateTime.weekday().day().month().hour().minute())
return "Convoqué le "
+ callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
@ -423,22 +461,31 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
}
func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator())
return players().map { $0.pasteData(exportFormat) }.joined(
separator: exportFormat.newLineSeparator())
case .csv:
return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator())
return players().map {
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
}.joined(separator: exportFormat.separator())
}
}
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
func updatePlayers(
_ players: Set<PlayerRegistration>,
inTournamentCategory tournamentCategory: TournamentCategory
) {
let previousPlayers = Set(unsortedPlayers())
players.forEach { player in
previousPlayers.forEach { oldPlayer in
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense, player.licenceId?.strippedLicense != nil {
if player.licenceId?.strippedLicense == oldPlayer.licenceId?.strippedLicense,
player.licenceId?.strippedLicense != nil
{
player.registeredOnline = oldPlayer.registeredOnline
player.coach = oldPlayer.coach
player.tournamentPlayed = oldPlayer.tournamentPlayed
@ -449,8 +496,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
}
let playersToRemove = previousPlayers.subtracting(players)
self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove)
setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
@ -458,16 +504,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players.forEach { player in
player.teamRegistration = id
}
// do {
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } catch {
// Logger.error(error)
// }
}
// do {
// try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } catch {
// Logger.error(error)
// }
}
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
func replacementRange() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil }
guard let index = tournamentObject.indexOf(team: self) else { return nil }
@ -476,7 +522,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
let right = selectedSortedTeams[safe: index + 1]
return (left: left, right: right)
}
func replacementRangeExtended() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil }
guard let groupStagePosition else { return nil }
@ -485,19 +531,23 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
if groupStagePosition == 0 {
left = tournamentObject.seeds().last
} else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition - 1 }).sorted(by: \.weight)
let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition - 1
}).sorted(by: \.weight)
left = previousHat.last
}
var right: TeamRegistration? = nil
if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 {
right = nil
} else {
let previousHat = selectedSortedTeams.filter({ $0.groupStagePosition == groupStagePosition + 1 }).sorted(by: \.weight)
let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition + 1
}).sorted(by: \.weight)
right = previousHat.first
}
return (left: left, right: right)
}
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
func players() -> [PlayerRegistration] {
@ -506,36 +556,43 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName},
{ $0.firstName < $1.firstName }
{ $0.lastName < $1.lastName },
{ $0.firstName < $1.firstName },
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}
}
func coaches() -> [PlayerRegistration] {
guard let store = self.tournamentStore else { return [] }
return store.playerRegistrations.filter { $0.coach }
}
func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) {
func setWeight(
from players: [PlayerRegistration],
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount()
weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
weight =
(players.prefix(significantPlayerCount).map { $0.computedRank }
+ missingPlayerType(inTournamentCategory: tournamentCategory).map {
unrankValue(for: $0 == 1 ? true : false)
}).prefix(significantPlayerCount).reduce(0, +)
}
func significantPlayerCount() -> Int {
return tournamentObject()?.significantPlayerCount() ?? 2
}
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
let players = unsortedPlayers()
if players.count >= 2 { return [] }
@ -548,16 +605,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
return missing
}
func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
}
func groupStageObject() -> GroupStage? {
guard let groupStage else { return nil }
return self.tournamentStore?.groupStages.findById(groupStage)
}
func initialRound() -> Round? {
guard let bracketPosition else { return nil }
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
@ -567,22 +624,23 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func initialMatch() -> Match? {
guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil }
return self.tournamentStore?.matches.first(where: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 })
return self.tournamentStore?.matches.first(where: {
$0.round == initialRoundObject.id && $0.index == bracketPosition / 2
})
}
func toggleSummonConfirmation() {
if confirmationDate == nil { confirmationDate = Date() }
else { confirmationDate = nil }
if confirmationDate == nil { confirmationDate = Date() } else { confirmationDate = nil }
}
func didConfirmSummon() -> Bool {
confirmationDate != nil
}
func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
func groupStagePositionAtStep(_ step: Int) -> Int? {
guard let groupStagePosition else { return nil }
if step == 0 {
@ -592,33 +650,36 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
return nil
}
func wildcardLabel() -> String? {
if isWildCard() {
let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ")
let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")]
.joined(separator: " ")
return wildcardLabel
} else {
return nil
}
}
var _cachedRestingTime: (Bool, Date?)?
func restingTime() -> Date? {
if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
let restingTime = matches().filter({ $0.hasEnded() }).sorted(
by: \.computedEndDateForSorting
).last?.endDate
_cachedRestingTime = (true, restingTime)
return restingTime
}
func resetRestingTime() {
_cachedRestingTime = nil
}
var restingTimeForSorting: Date {
restingTime()!
}
func teamNameLabel() -> String {
if let name, name.isEmpty == false {
return name
@ -626,25 +687,29 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return "Toute l'équipe"
}
}
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if let bracketPosition {
} else if bracketPosition != nil {
return true
} else if let drawMatchIndex {
} else if drawMatchIndex != nil {
return true
}
return false
}
func shouldDisplayRankAndWeight() -> Bool {
unsortedPlayers().count > 0
}
func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {
playerRegistration.insertOnServer()
}
}
}
enum TeamDataSource: Int, Codable {

File diff suppressed because it is too large Load Diff

@ -1,4 +1,7 @@
<ul class="round">
<li class="spacer">&nbsp;{{roundLabel}}</li>
<li class="spacer">
&nbsp;{{roundLabel}}
<div>{{formatLabel}}</div>
</li>
{{match-template}}
</ul>

@ -82,6 +82,7 @@ body{
<caption>
<h2>{{bracketTitle}}</h2>
<h3>{{bracketStartDate}}</h3>
<h3>{{formatLabel}}</h3>
</caption>
<tr>
<th scope="col" style="visibility:hidden"></th>

@ -1,3 +1,4 @@
<div class="player">{{teamIndex}}</div>
<div class="player">{{playerOne}}<span>{{weightOne}}</span></div>
<div class="player">{{playerTwo}}<span>{{weightTwo}}</span></div>

@ -9,6 +9,7 @@
flex-direction:row;
padding: 1%;
}
.round{
display:flex;
flex-direction:column;
@ -27,7 +28,7 @@
.round .spacer{ flex-grow:1;
font-size:24px;
text-align: center;
color: #bbb;
color: #000000;
font-style:italic;
}
.round .spacer:first-child,
@ -65,7 +66,7 @@
li.game-spacer{
border-right:2px solid #4f7a38;
min-height:156px;
min-height:{{minHeight}}px;
text-align: right;
display : flex;
justify-content: center;
@ -95,7 +96,7 @@
</head>
<body>
<h1>{{tournamentTitle}}</h1>
<h3>{{tournamentTitle}} - {{tournamentStartDate}}</h3>
<main id="tournament">
{{brackets}}
</main>

@ -19,6 +19,8 @@ struct PadelClubApp: App {
@State private var importObserverViewModel = ImportObserver()
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var blockApp = false
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var presentError: Binding<Bool> {
@ -62,54 +64,86 @@ struct PadelClubApp: App {
var body: some Scene {
WindowGroup {
MainView()
.environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) {
Button("Contactez-nous") {
_openMail()
}
Button("Annuler", role: .cancel) {
registrationError = nil
if self.blockApp {
DownloadNewVersionView()
} else {
MainView()
.environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) {
Button("Contactez-nous") {
_openMail()
}
Button("Annuler", role: .cancel) {
registrationError = nil
}
}
}
.onOpenURL { url in
.onOpenURL { url in
#if targetEnvironment(simulator)
#else
_handleIncomingURL(url)
_handleIncomingURL(url)
#endif
}
.environmentObject(networkMonitor)
.environmentObject(dataStore)
.environment(importObserverViewModel)
.environment(navigationViewModel)
.accentColor(.master)
.onAppear {
}
.environmentObject(networkMonitor)
.environmentObject(dataStore)
.environment(importObserverViewModel)
.environment(navigationViewModel)
.accentColor(.master)
.onAppear {
self._checkVersion()
#if DEBUG
print("Running in Debug mode")
print("Running in Debug mode")
#elseif TESTFLIGHT
print("Running in TestFlight mode")
print("Running in TestFlight mode")
#elseif PRODTEST
print("Running in ProdTest mode")
print("Running in ProdTest mode")
#else
print("Running in Release mode")
print("Running in Release mode")
#endif
networkMonitor.checkConnection()
self._onAppear()
print(PersistenceController.getModelVersion())
}
.task {
try? Tips.resetDatastore()
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
networkMonitor.checkConnection()
self._onAppear()
print(PersistenceController.getModelVersion())
}
.task {
try? Tips.resetDatastore()
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext)
}
}
}
fileprivate func _checkVersion() {
Task.detached(priority: .high) {
if let requiredVersion = await self._retrieveRequiredVersion() {
let cleanedRequired = requiredVersion.replacingOccurrences(of: "\n", with: "")
Logger.log(">>> REQUIRED VERSION = \(requiredVersion)")
if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
await MainActor.run {
self.blockApp = VersionComparator.compare(cleanedRequired, currentVersion) == 1
}
}
.environment(\.managedObjectContext, persistenceController.localContainer.viewContext)
}
}
}
fileprivate func _retrieveRequiredVersion() async -> String? {
let requiredVersionURL = URLs.main.extend(path: "static/misc/required-version.txt")
do {
let (data, _) = try await URLSession.shared.data(from: requiredVersionURL)
return String(data: data, encoding: .utf8)
} catch {
Logger.log("Error fetching required version: \(error)")
return nil
}
}
private func _handleIncomingURL(_ url: URL) {
// Parse the URL
let pathComponents = url.pathComponents
@ -173,3 +207,31 @@ print("Running in Release mode")
}
}
}
struct DownloadNewVersionView: View {
var body: some View {
VStack {
// AngledStripesBackground()
Spacer()
Text("Veuillez télécharger la nouvelle version de Padel Club pour continuer à vous servir de l'app !")
.padding(32.0)
.background(.logoYellow)
.clipShape(.buttonBorder)
.foregroundStyle(.logoBackground)
.fontWeight(.medium)
.multilineTextAlignment(.center)
.padding(.horizontal, 64.0)
Image("logo").padding(.vertical, 50.0)
Spacer()
}.background(.logoBackground)
.onTapGesture {
UIApplication.shared.open(URLs.appStore.url)
}
}
}

@ -24,6 +24,9 @@ class HtmlGenerator: ObservableObject {
@Published var displayHeads: Bool = false
@Published var groupStageIsReady: Bool = false
@Published var displayRank: Bool = false
@Published var displayTeamIndex: Bool = false
@Published var displayScore: Bool = false
private var pdfDocument: PDFDocument = PDFDocument()
private var rects: [CGRect] = []
private var completionHandler: ((Result<Bool, Error>) -> ())?
@ -167,12 +170,12 @@ class HtmlGenerator: ObservableObject {
func generateHtml() -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false)
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
}
func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false)
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
}
var pdfURL: URL? {

@ -50,7 +50,7 @@ enum HtmlService {
}
}
func html(headName: Bool, withRank: Bool, withScore: Bool) -> String {
func html(headName: Bool, withRank: Bool, withTeamIndex: Bool, withScore: Bool) -> String {
guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else {
fatalError()
}
@ -69,12 +69,12 @@ enum HtmlService {
}
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short))
template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle())
template = template.replacingOccurrences(of: "{{formatLabel}}", with: bracket.matchFormat.formatTitle())
var col = ""
var row = ""
bracket.teams().forEach { entrant in
col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore))
row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore))
col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
template = template.replacingOccurrences(of: "{{teamsCol}}", with: col)
template = template.replacingOccurrences(of: "{{teamsRow}}", with: row)
@ -82,6 +82,12 @@ enum HtmlService {
return template
case .groupstageEntrant(let entrant):
var template = html
if withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.seedIndex() ?? "")
}
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank {
@ -108,7 +114,7 @@ enum HtmlService {
return template
case .groupstageRow(let entrant, let teamsPerBracket):
var template = html
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
var scores = ""
(0..<teamsPerBracket).forEach { index in
@ -117,28 +123,35 @@ enum HtmlService {
if shouldHide == false {
match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index)
}
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withScore: withScore))
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
template = template.replacingOccurrences(of: "{{scores}}", with: scores)
return template
case .groupstageColumn(let entrant, let position):
var template = html
template = template.replacingOccurrences(of: "{{tablePosition}}", with: position)
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
return template
case .groupstageScore(let match, let shouldHide):
var template = html
if match == nil || withScore == false {
template = template.replacingOccurrences(of: "{{winner}}", with: "")
template = template.replacingOccurrences(of: "{{score}}", with: "")
} else {
template = template.replacingOccurrences(of: "{{winner}}", with: match!.winner()!.teamLabel())
template = template.replacingOccurrences(of: "{{score}}", with: match!.scoreLabel())
} else if let match, let winner = match.winner() {
template = template.replacingOccurrences(of: "{{winner}}", with: winner.teamLabel())
template = template.replacingOccurrences(of: "{{score}}", with: match.scoreLabel())
}
template = template.replacingOccurrences(of: "{{hide}}", with: shouldHide ? "hide" : "")
return template
case .player(let entrant):
var template = html
if withTeamIndex == false {
template = template.replacingOccurrences(of: #"<div class="player">{{teamIndex}}</div>"#, with: "")
} else {
template = template.replacingOccurrences(of: "{{teamIndex}}", with: entrant.formattedSeed())
}
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank {
@ -164,18 +177,22 @@ enum HtmlService {
}
return template
case .hiddenPlayer:
return html + html
var template = html + html
if withTeamIndex {
template += html
}
return template
case .match(let match):
var template = html
if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
if match.disabled {
template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden")
@ -196,19 +213,20 @@ enum HtmlService {
var template = ""
var bracket = ""
for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle())
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))
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
var winnerName = ""
let winnerName = ""
let winner = """
<ul class="round" scope="last">
<li class="spacer">&nbsp;</li>
@ -224,15 +242,17 @@ enum HtmlService {
return template
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short))
template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.title))
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournament.formattedDate())
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore))
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
var winnerName = ""
if let tournamentWinner = tournament.tournamentWinner() {
winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore)
winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
}
let winner = """
<ul class="round" scope="last">

@ -1996,16 +1996,30 @@ enum TournamentDeadlineType: String, CaseIterable {
case wildcardLicensePurchase = "Prise de licence des WC"
case definitiveBroadcastList = "Publication définitive"
var daysOffset: Int {
switch self {
case .inscription:
return -13
case .broadcastList:
return -12
case .wildcardRequest:
return -9
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -8
func daysOffset(level: TournamentLevel) -> Int {
if level == .p500 {
switch self {
case .inscription:
return -6
case .broadcastList:
return -6
case .wildcardRequest:
return -4
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -4
}
} else {
switch self {
case .inscription:
return -13
case .broadcastList:
return -12
case .wildcardRequest:
return -9
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -8
}
}
}

@ -46,7 +46,12 @@ enum URLs: String, Identifiable {
var url: URL {
return URL(string: self.rawValue)!
}
}
func extend(path: String) -> URL {
return URL(string: self.rawValue + path)!
}
}
enum PageLink: String, Identifiable, CaseIterable {

@ -0,0 +1,33 @@
//
// VersionComparator.swift
// PadelClub
//
// Created by Laurent Morvillier on 13/02/2025.
//
class VersionComparator {
static func compare(_ version1: String, _ version2: String) -> Int {
// Split versions into components
let v1Components = version1.split(separator: ".").map { Int($0) ?? 0 }
let v2Components = version2.split(separator: ".").map { Int($0) ?? 0 }
// Get the maximum length to compare
let maxLength = max(v1Components.count, v2Components.count)
// Compare each component
for i in 0..<maxLength {
let v1Num = i < v1Components.count ? v1Components[i] : 0
let v2Num = i < v2Components.count ? v2Components[i] : 0
if v1Num < v2Num {
return -1 // version1 is smaller
} else if v1Num > v2Num {
return 1 // version1 is larger
}
}
return 0 // versions are equal
}
}

@ -89,8 +89,31 @@ class SearchViewModel: ObservableObject, Identifiable {
return nil
}
func shouldIncludeSearchTextPredicate() -> Bool {
if allowMultipleSelection {
return true
}
if allowSingleSelection {
return true
}
if tokens.isEmpty == false || hideAssimilation || selectedAgeCategory != .unlisted {
return true
}
return dataSet == .national && searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted)
}
func showIndex() -> Bool {
if (dataSet == .national || dataSet == .ligue) { return isFiltering() }
if dataSet == .national {
if searchText.isEmpty == false && (tokens.isEmpty == true && hideAssimilation == false && selectedAgeCategory == .unlisted) {
return false
} else {
return isFiltering()
}
}
if (dataSet == .ligue) { return isFiltering() }
if filterOption == .all { return isFiltering() }
return true
}
@ -149,66 +172,85 @@ class SearchViewModel: ObservableObject, Identifiable {
}
}
func searchTextPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates()
if let wordsPredicates {
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
}
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
let components = canonicalVersionWithoutPunctuation.split(separator: " ")
let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate)
}
if predicates.isEmpty {
return nil
}
return NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
}
func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
let canonicalVersionWithoutPunctuation = searchText.canonicalVersion.components(separatedBy: allowedCharacterSet.inverted).joined().trimmed
let canonicalVersionWithPunctuation = searchText.canonicalVersionWithPunctuation.trimmed
switch tokens.first {
case .none:
if canonicalVersionWithoutPunctuation.isEmpty == false {
let wordsPredicates = wordsPredicates()
if let wordsPredicates {
predicates.append(wordsPredicates)
} else {
predicates.append(NSPredicate(format: "license contains[cd] %@", canonicalVersionWithoutPunctuation))
if tokens.isEmpty {
if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false {
if let searchTextPredicate = searchTextPredicate() {
predicates.append(searchTextPredicate)
}
predicates.append(NSPredicate(format: "canonicalFullName contains[cd] %@", canonicalVersionWithoutPunctuation))
let components = canonicalVersionWithoutPunctuation.split(separator: " ")
let pattern = components.joined(separator: ".*")
let predicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
predicates.append(predicate)
}
case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .club:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
}
case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
}
case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
}
case .age:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "birthYear == 0"))
} else if let birthYear = Int(canonicalVersionWithoutPunctuation) {
predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString()))
}
for token in tokens {
switch token {
case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
} else {
predicates.append(NSPredicate(format: "ligueName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .club:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "clubName == nil"))
} else {
predicates.append(NSPredicate(format: "clubName contains[cd] %@", canonicalVersionWithoutPunctuation))
}
case .rankMoreThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank >= %@", canonicalVersionWithoutPunctuation))
}
case .rankLessThan:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank <= %@", canonicalVersionWithoutPunctuation))
}
case .rankBetween:
let values = canonicalVersionWithPunctuation.components(separatedBy: ",")
if canonicalVersionWithPunctuation.isEmpty || values.count != 2 {
predicates.append(NSPredicate(format: "rank == 0"))
} else {
predicates.append(NSPredicate(format: "rank BETWEEN {%@,%@}", values.first!, values.last!))
}
case .age:
if canonicalVersionWithoutPunctuation.isEmpty || Int(canonicalVersionWithoutPunctuation) == 0 {
predicates.append(NSPredicate(format: "birthYear == 0"))
} else if let birthYear = Int(canonicalVersionWithoutPunctuation) {
predicates.append(NSPredicate(format: "birthYear == %@", birthYear.formattedAsRawString()))
}
}
}
if predicates.isEmpty {
return nil
}
@ -314,6 +356,17 @@ class SearchViewModel: ObservableObject, Identifiable {
static func pastePredicate(pasteField: String, mostRecentDate: Date?, filterOption: PlayerFilterOption) -> NSPredicate? {
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
let matches = pasteField.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = licensesPredicates
if matches.count == 2 {
return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)
}
let allowedCharacterSet = CharacterSet.alphanumerics.union(.whitespaces)
// Remove all characters that are not in the allowedCharacterSet
@ -327,14 +380,8 @@ class SearchViewModel: ObservableObject, Identifiable {
let textStrings: [String] = text.components(separatedBy: .whitespacesAndNewlines)
let nonEmptyStrings: [String] = textStrings.compactMap { $0.isEmpty ? nil : $0 }
let nameComponents = nonEmptyStrings.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
orPredicates.append(slashPredicate)
}
//self.wordsCount = nameComponents.count
if filterOption == .female {
andPredicates.append(NSPredicate(format: "male == NO"))
}
@ -343,11 +390,20 @@ class SearchViewModel: ObservableObject, Identifiable {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {
orPredicates.append(contentsOf: nameComponents.pairs().map {
return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) })
} else {
orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) })
if let slashPredicate = getSpecialSlashPredicate(inputString: pasteField) {
orPredicates.append(slashPredicate)
}
print("nameComponents", nameComponents.count)
if nameComponents.count < 50 {
if nameComponents.count > 1 {
orPredicates.append(contentsOf: nameComponents.pairs().map {
return NSPredicate(format: "(firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@) OR (firstName BEGINSWITH[cd] %@ AND lastName BEGINSWITH[cd] %@)", $0, $1, $1, $0) })
} else {
orPredicates.append(contentsOf: nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) })
}
}
let components = text.split(separator: " ")
@ -355,11 +411,7 @@ class SearchViewModel: ObservableObject, Identifiable {
print(text, pattern)
let canonicalFullNamePredicate = NSPredicate(format: "canonicalFullName MATCHES[c] %@", pattern)
orPredicates.append(canonicalFullNamePredicate)
let matches = pasteField.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false {

@ -22,6 +22,8 @@ struct CallSettingsView: View {
var body: some View {
List {
Section {
NavigationLink {
CallMessageCustomizationView(tournament: tournament)

@ -6,15 +6,32 @@
//
import SwiftUI
import LeStorage
struct GroupStageCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@State private var displayByTeam: Bool = false
var body: some View {
let groupStages = tournament.groupStages()
List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })
if uncalled.isEmpty == false {
NavigationLink {

@ -6,13 +6,31 @@
//
import SwiftUI
import LeStorage
struct SeedsCallingView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var displayByMatch: Bool = true
var body: some View {
List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
let tournamentRounds = tournament.rounds()
let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil })

@ -223,13 +223,14 @@ struct SendToAllView: View {
}
func _teams() -> [TeamRegistration] {
let selectedSortedTeams = tournament.selectedSortedTeams()
if onlyWaitingList {
return tournament.waitingListSortedTeams()
return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
}
if _roundTeams().isEmpty && _groupStagesTeams().isEmpty {
return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : [])
return tournament.selectedSortedTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : [])
}
return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams() : [])
return _roundTeams() + _groupStagesTeams() + (includeWaitingList ? tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams) : [])
}
func _roundTeams() -> [TeamRegistration] {

@ -10,6 +10,7 @@ import LeStorage
struct TeamsCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
let teams : [TeamRegistration]
@State private var hideConfirmed: Bool = false
@ -31,6 +32,23 @@ struct TeamsCallingView: View {
var body: some View {
List {
if tournament.isPrivate {
Section {
RowButtonView("Rendre visible sur Padel Club") {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
} footer: {
Text("Si vous convoquez un tournoi privée, les joueurs n'auront pas le lien pour suivre le tournoi.")
.foregroundStyle(.logoRed)
}
}
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false }

@ -38,6 +38,10 @@ struct EventListView: View {
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix)
}
} footer: {
if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments)
}
}
.headerProminence(.increased)
}
@ -56,6 +60,10 @@ struct EventListView: View {
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix)
}
} footer: {
if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] {
_menuOptions(pcTournaments)
}
}
.id(sectionIndex)
.headerProminence(.increased)
@ -84,6 +92,88 @@ struct EventListView: View {
}
}
private func _menuOptions(_ pcTournaments: [Tournament]) -> some View {
Menu {
_options(pcTournaments)
} label: {
Text("Options rapides pour ce mois")
.underline()
}
}
@ViewBuilder
private func _options(_ pcTournaments: [Tournament]) -> some View {
Section {
if pcTournaments.anySatisfy({ $0.isPrivate == true }) {
Button {
pcTournaments.forEach { tournament in
tournament.isPrivate = false
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Afficher ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club")
}
}
if pcTournaments.anySatisfy({ $0.isPrivate == false }) {
Button {
pcTournaments.forEach { tournament in
tournament.isPrivate = true
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Masquer ce\(pcTournaments.count.pluralSuffix) tournoi\(pcTournaments.count.pluralSuffix) sur Padel Club")
}
}
} header: {
Text("Visibilité sur Padel Club")
}
Divider()
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
Section {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) {
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = true
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Activer l'inscription en ligne")
}
}
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = false
}
do {
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
} label: {
Text("Désactiver l'inscription en ligne")
}
}
} header: {
Text("Inscription en ligne")
}
}
}
private func _nextMonths() -> [Date] {
let currentDate = Date().startOfMonth
let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted()
@ -124,13 +214,20 @@ struct EventListView: View {
ShareModelView(instance: tournament)
}
}
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.contextMenu {
if tournament.hasEnded() == false {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
Divider()
_options([tournament])
}
}
#if DEBUG

@ -22,7 +22,7 @@ struct MatchFormatStorageView: View {
var body: some View {
Section {
LabeledContent {
StepperView(title: "minutes", count: $estimatedDuration, step: 5)
StepperView(title: "minute", count: $estimatedDuration, step: 5)
} label: {
MatchFormatRowView(matchFormat: matchFormat, hideDuration: true)
}

@ -0,0 +1,72 @@
//
// MatchFormatGuideView.swift
// PadelClub
//
// Created by razmig on 20/02/2025.
//
import SwiftUI
struct MatchFormatGuideView: View {
let matchCounts = Array(2...7)
let formats: [MatchFormat] = [
.twoSets, .twoSetsDecisivePoint,
.twoSetsSuperTie, .twoSetsDecisivePointSuperTie,
.twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint,
.nineGames, .nineGamesDecisivePoint,
.superTie
]
func getFormatDescription(for matchCount: Int) -> String {
var description = ""
// Group formats by their behavior
let formatGroups = Dictionary(grouping: formats) { format in
format.maximumMatchPerDay(for: matchCount)
}
// Sort by maximum matches allowed (descending)
let sortedMaxMatches = formatGroups.keys.sorted(by: >)
for maxMatches in sortedMaxMatches {
if let formatsForMax = formatGroups[maxMatches] {
let formatStrings = formatsForMax.map { $0.format }.joined(separator: "/")
if maxMatches > 0 && maxMatches <= matchCount {
description += "Maximum \(maxMatches) matchs en format \(formatStrings)\n"
} else if maxMatches == 0 {
description += "Aucun match au format \(formatStrings)\n"
}
}
}
if matchCount >= 7 {
description += "Format \(MatchFormat.superTie.format) principalement"
}
return description.isEmpty ? "Aucun match possible" : description
}
var body: some View {
List {
Section {
ForEach(matchCounts, id: \.self) { count in
VStack {
Text("\(count) matchs par jour")
.font(.headline)
Text(getFormatDescription(for: count))
}
}
// Special case for 7+ matches
VStack {
Text("7+ matchs par jour")
.font(.headline)
Text("Tournois P 25 uniquement (soirée/demi-journée/journée)")
}
}
}
.navigationTitle("Guide des Formats de Match")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -14,7 +14,7 @@ struct PlanningSettingsView: View {
@Bindable var tournament: Tournament
@Bindable var matchScheduler: MatchScheduler
@State private var groupStageChunkCount: Int
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
@ -23,11 +23,12 @@ struct PlanningSettingsView: View {
@State private var parallelType: Bool = false
@State private var deletingDateMatchesDone: Bool = false
@State private var deletingDone: Bool = false
@State private var presentFormatHelperView: Bool = false
var tournamentStore: TournamentStore? {
return self.tournament.tournamentStore
}
init(tournament: Tournament) {
self.tournament = tournament
if let matchScheduler = tournament.matchScheduler() {
@ -43,7 +44,7 @@ struct PlanningSettingsView: View {
self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue())
}
}
var body: some View {
List {
if tournament.payment == nil {
@ -62,7 +63,7 @@ struct PlanningSettingsView: View {
}
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
if let event = tournament.eventObject() {
NavigationLink {
CourtAvailabilitySettingsView(event: event)
@ -75,7 +76,7 @@ struct PlanningSettingsView: View {
}
}
}
NavigationLink {
MultiCourtPickerView(matchScheduler: matchScheduler)
.environment(tournament)
@ -118,7 +119,7 @@ struct PlanningSettingsView: View {
}
}
}
if issueFound {
Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.")
.foregroundStyle(.logoRed)
@ -131,7 +132,7 @@ struct PlanningSettingsView: View {
} label: {
Text("Voir plus d'options intelligentes")
}
if let event, event.tournaments.count > 1 {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois")
@ -142,9 +143,31 @@ struct PlanningSettingsView: View {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.")
}
}
_smartView()
}
.navigationTitle("Réglages")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
presentFormatHelperView = true
} label: {
Text("Aide-mémoire")
}
}
}
.sheet(isPresented: $presentFormatHelperView) {
NavigationStack {
MatchFormatGuideView()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) {
presentFormatHelperView = false
}
}
}
}
}
.headerProminence(.increased)
.onAppear {
do {
@ -165,13 +188,13 @@ struct PlanningSettingsView: View {
.deferredRendering(for: .seconds(2))
}
}
if deletingDone {
Label("Tous les horaires ont été supprimés", systemImage: "clock.badge.xmark")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
if deletingDateMatchesDone {
Label("Horaires des matchs supprimés", systemImage: "clock.badge.xmark")
.toastFormatted()
@ -189,7 +212,7 @@ struct PlanningSettingsView: View {
_save()
}
}
private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String {
let base = "Supprime les horaires des matchs restants non démarrés."
let extend = " Garde les horaires définis pour les "
@ -203,20 +226,20 @@ struct PlanningSettingsView: View {
return base + extend + "poules et les manches du tableau."
}
}
@ViewBuilder
private func _smartView() -> some View {
let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })
let allGroupStages = tournament.allGroupStages()
let allRounds = tournament.allRounds()
let matchesWithDate = allMatches.filter({ $0.startDate != nil })
let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate)
let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament)
_formatPerDayView(matchCountPerDay: countedSet)
let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil })
let roundsWithDate = allRounds.filter({ $0.startDate != nil })
if matchesWithDate.isEmpty == false {
@ -238,7 +261,7 @@ struct PlanningSettingsView: View {
Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty))
}
}
if groupStagesWithDate.isEmpty == false {
Section {
RowButtonView("Supprimer les horaires des poules", role: .destructive) {
@ -246,7 +269,7 @@ struct PlanningSettingsView: View {
deletingDone = false
allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
deletingDone = true
} catch {
Logger.error(error)
@ -254,7 +277,7 @@ struct PlanningSettingsView: View {
}
}
}
if roundsWithDate.isEmpty == false {
Section {
RowButtonView("Supprimer les horaires du tableau", role: .destructive) {
@ -271,7 +294,7 @@ struct PlanningSettingsView: View {
Text("Supprime les horaires définis pour les manches du tableau.")
}
}
if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false {
Section {
RowButtonView("Supprimer tous les horaires", role: .destructive) {
@ -282,10 +305,10 @@ struct PlanningSettingsView: View {
$0.confirmed = false
})
try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
allRounds.forEach({ $0.startDate = nil })
try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
deletingDone = true
@ -297,7 +320,7 @@ struct PlanningSettingsView: View {
Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.")
}
}
#if DEBUG
Section {
RowButtonView("Debug delete all dates", role: .destructive) {
@ -309,10 +332,10 @@ struct PlanningSettingsView: View {
$0.confirmed = false
})
try self.tournamentStore?.matches.addOrUpdate(contentOfs: tournament.allMatches())
allGroupStages.forEach({ $0.startDate = nil })
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: allGroupStages)
allRounds.forEach({ $0.startDate = nil })
try self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
deletingDone = true
@ -325,7 +348,7 @@ struct PlanningSettingsView: View {
}
#endif
Section {
if groupStagesWithDate.isEmpty == false {
Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.")
@ -348,7 +371,7 @@ struct PlanningSettingsView: View {
Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline()
}
}
@ViewBuilder
private func _optionsView() -> some View {
List {
@ -375,7 +398,7 @@ struct PlanningSettingsView: View {
matchScheduler.groupStageChunkCount = nil
}
}
if parallelType {
TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount)
.onChange(of: groupStageChunkCount) {
@ -385,7 +408,7 @@ struct PlanningSettingsView: View {
} footer: {
Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.")
}
Section {
Toggle(isOn: $matchScheduler.simultaneousStart) {
Text("Démarrage simultané")
@ -394,7 +417,7 @@ struct PlanningSettingsView: View {
Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.")
}
}
Section {
Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard")
@ -408,7 +431,7 @@ struct PlanningSettingsView: View {
} footer: {
Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
}
Section {
Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
Text("Équilibrer les matchs d'une manche")
@ -416,13 +439,13 @@ struct PlanningSettingsView: View {
} footer: {
Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.")
}
Section {
Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) {
Text("Finir une manche, classement inclus avant de continuer")
}
}
Section {
Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) {
Text("Tenir compte des temps de pause réglementaires")
@ -430,7 +453,7 @@ struct PlanningSettingsView: View {
} header: {
Text("Tableau")
}
Section {
Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) {
Text("Tenir compte des temps de pause réglementaires")
@ -438,19 +461,19 @@ struct PlanningSettingsView: View {
} header: {
Text("Classement")
}
Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
Text("Forcer une rotation d'attente supplémentaire entre 2 phases")
}
LabeledContent {
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Tableau")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
@ -460,7 +483,7 @@ struct PlanningSettingsView: View {
} footer: {
Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.")
}
Section {
LabeledContent {
StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5)
@ -478,11 +501,11 @@ struct PlanningSettingsView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _setupSchedule() async -> Bool {
return matchScheduler.updateSchedule(tournament: tournament)
}
private func _save() {
do {
try self.tournamentStore?.matchSchedulers.addOrUpdate(instance: matchScheduler)
@ -491,21 +514,21 @@ struct PlanningSettingsView: View {
Logger.error(error)
}
}
private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] {
var matchesByDay = [Date: [Match]]()
let calendar = Calendar.current
for match in matches {
// Extract day/month/year and create a date with only these components
let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting)
let strippedDate = calendar.date(from: components)!
// Group matches by the strippedDate (only day/month/year)
if matchesByDay[strippedDate] == nil {
matchesByDay[strippedDate] = []
}
let shouldIncludeMatch: Bool
switch match.matchType {
case .groupStage:
@ -515,24 +538,24 @@ struct PlanningSettingsView: View {
case .loserBracket:
shouldIncludeMatch = true
}
if shouldIncludeMatch {
matchesByDay[strippedDate]!.append(match)
}
}
return matchesByDay
}
private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] {
let days = matchesByDay.keys
var matchCountPerDay = [Date: NSCountedSet]()
for day in days {
if let matches = matchesByDay[day] {
var groupStageCount = 0
let countedSet = NSCountedSet()
for match in matches {
switch match.matchType {
case .groupStage:
@ -547,15 +570,15 @@ struct PlanningSettingsView: View {
break
}
}
if groupStageCount > 0 {
for _ in 0..<groupStageCount {
countedSet.add(tournament.groupStageMatchFormat)
}
}
if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() {
let ids = matches.map { $0.id }
for loserRound in loserRounds {
if let first = loserRound.playedMatches().first {
@ -568,7 +591,7 @@ struct PlanningSettingsView: View {
matchCountPerDay[day] = countedSet
}
}
return matchCountPerDay
}
@ -578,7 +601,7 @@ struct PlanningSettingsView: View {
Section {
let totalMatches = countedSet.totalCount()
ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in
let count = countedSet.count(for: matchFormat)
let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches)
let error = count > totalForThisFormat
@ -605,7 +628,7 @@ struct PlanningSettingsView: View {
}
}
}
// Helper function to format date to string (you can customize the format)
private func _formattedDate(_ date: Date) -> String {
let formatter = DateFormatter()

@ -262,7 +262,7 @@ struct RoundView: View {
.foregroundStyle(.green)
}
} label: {
Text("Classement final des équipes")
Text("Classement final")
if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
}

@ -337,6 +337,21 @@ struct MySearchView: View {
_searchViewModel = ObservedObject(wrappedValue: searchViewModel)
_players = FetchRequest<ImportedPlayer>(sortDescriptors: searchViewModel.sortDescriptors(), predicate: searchViewModel.predicate())
}
func searchedPlayers() -> [ImportedPlayer] {
if searchViewModel.searchText.isEmpty {
return Array(players)
}
if let searchPredicate = searchViewModel.searchTextPredicate() {
let filteredPlayers = players.filter { player in
searchPredicate.evaluate(with: player)
}
return filteredPlayers
}
return Array(players)
}
var body: some View {
playersView
@ -371,8 +386,6 @@ struct MySearchView: View {
@ViewBuilder
var playersView: some View {
let showProgression = true
let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation
if searchViewModel.allowMultipleSelection {
List(selection: $searchViewModel.selectedPlayers) {
if searchViewModel.filterSelectionEnabled {
@ -423,7 +436,7 @@ struct MySearchView: View {
}
}
.id(UUID())
} else {
} else if searchViewModel.shouldIncludeSearchTextPredicate() {
Section {
ForEach(players.indices, id: \.self) { index in
let player = players[index]
@ -435,26 +448,45 @@ struct MySearchView: View {
}
}
.id(UUID())
} else {
let filteredPlayers = searchedPlayers()
Section {
ForEach(filteredPlayers.indices, id: \.self) { index in
let player = filteredPlayers[index]
let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil
let computedIndex = realIndex != nil ? realIndex! + 1 : nil
ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
} header: {
if filteredPlayers.isEmpty == false {
headerView()
}
}
.id(UUID())
}
} else {
let filteredPlayers = searchedPlayers()
Section {
ForEach(players.indices, id: \.self) { index in
let player = players[index]
ForEach(filteredPlayers.indices, id: \.self) { index in
let player = filteredPlayers[index]
let realIndex = searchViewModel.showIndex() ? players.firstIndex(of: player) : nil
let computedIndex = realIndex != nil ? realIndex! + 1 : nil
if searchViewModel.allowSingleSelection {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
} else {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
ImportedPlayerView(player: player, index: computedIndex, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
}
}
} header: {
if players.isEmpty == false {
if filteredPlayers.isEmpty == false {
headerView()
}
}

@ -12,16 +12,15 @@ struct TeamWeightView: View {
let team: TeamRegistration
var teamPosition: TeamPosition? = nil
var teamIndex: Int? {
team.tournamentObject()?.indexOf(team: team)
}
var teamIndex: Int?
var displayWeight: Bool {
team.tournamentObject()?.hideWeight() == false
team.shouldDisplayRankAndWeight() && team.tournamentObject()?.hideWeight() == false
}
var body: some View {
VStack(alignment: .trailing, spacing: 0) {
let displayWeight = self.displayWeight
if (teamPosition == .one || teamPosition == nil) && displayWeight {
Text(team.weight.formatted())
.monospacedDigit()

@ -47,7 +47,8 @@ struct EditingTeamView: View {
}
private func _resetTeam() {
self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first
let selectedSortedTeams = tournament.selectedSortedTeams()
self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false

@ -13,10 +13,11 @@ struct TeamRowView: View {
var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false
var displayRestingTime: Bool = false
var teamIndex: Int?
var body: some View {
LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition)
TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex)
} label: {
VStack(alignment: .leading) {
TeamHeadlineView(team: team)

@ -312,7 +312,7 @@ struct FileImportView: View {
}
} else if didImport {
let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams()
let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams())
if previousTeams.isEmpty == false {
Section {

@ -595,14 +595,11 @@ struct AddTeamView: View {
return 1
}
@MainActor
private func handlePasteString(_ first: String) {
if first.isEmpty == false {
DispatchQueue.main.async {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
autoSelect = true
}
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
autoSelect = true
}
pasteString = first
editableTextField = first

@ -93,8 +93,9 @@ struct TournamentGeneralSettingsView: View {
}
.frame(maxHeight: 200)
.overlay {
if tournamentInformation.isEmpty {
if tournamentInformation.isEmpty, focusedField != ._information {
Text("Texte visible dans l'onglet informations sur Padel Club.").italic()
.foregroundStyle(.secondary)
}
}
} header: {

@ -34,7 +34,7 @@ struct TournamentMatchFormatsSettingsView: View {
Section {
LabeledContent {
StepperView(title: "minutes", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10)
StepperView(title: "minute", count: $tournament.additionalEstimationDuration, step: 5, minimum: -10)
} label: {
Text("Modifier les durées moyennes")
}

@ -180,7 +180,7 @@ struct InscriptionManagerView: View {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
}
private func _setHash() {
private func _setHash(currentSelectedSortedTeams: [TeamRegistration]? = nil) {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
@ -188,18 +188,17 @@ struct InscriptionManagerView: View {
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let selectedSortedTeams = tournament.selectedSortedTeams()
let selectedSortedTeams = currentSelectedSortedTeams == nil ? tournament.selectedSortedTeams() : currentSelectedSortedTeams!
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
self.registrationIssues = nil
DispatchQueue.main.async {
self.registrationIssues = tournament.registrationIssues()
self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams)
}
}
private func _handleHashDiff() {
let selectedSortedTeams = tournament.selectedSortedTeams()
private func _handleHashDiff(selectedSortedTeams: [TeamRegistration]) {
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash
@ -225,9 +224,10 @@ struct InscriptionManagerView: View {
}
var body: some View {
Group {
if tournament.unsortedTeams().isEmpty == false {
_teamRegisteredView()
let selectedSortedTeams = tournament.selectedSortedTeams()
return Group {
if tournament.unsortedTeamsCount() > 0 {
_teamRegisteredView(selectedSortedTeams: selectedSortedTeams)
} else {
List {
@ -263,10 +263,10 @@ struct InscriptionManagerView: View {
await _refreshList()
}
.onAppear {
_setHash()
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
.onDisappear {
_handleHashDiff()
_handleHashDiff(selectedSortedTeams: selectedSortedTeams)
}
.sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament)
@ -490,47 +490,43 @@ struct InscriptionManagerView: View {
tournament.unsortedPlayers()
}
var sortedTeams: [TeamRegistration] {
func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
if filterMode == .waiting {
return tournament.waitingListSortedTeams()
return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
} else {
return tournament.sortedTeams()
return tournament.sortedTeams(selectedSortedTeams: selectedSortedTeams)
}
}
var filteredTeams: [TeamRegistration] {
var teams = sortedTeams
switch filterMode {
case .wildcardBracket:
teams = teams.filter({ $0.wildCardBracket })
case .wildcardGroupStage:
teams = teams.filter({ $0.wildCardGroupStage })
case .walkOut:
teams = teams.filter({ $0.walkOut })
case .bracket:
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false })
case .groupStage:
teams = teams.filter({ $0.inGroupStage() })
case .notImported:
teams = teams.filter({ $0.isImported() == false })
case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false })
case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true })
default:
break
}
if sortingMode == .registrationDate {
teams = teams.sorted(by: \.computedRegistrationDate)
func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let filtered = sortedTeams.lazy.filter { team in
switch filterMode {
case .wildcardBracket:
return team.wildCardBracket
case .wildcardGroupStage:
return team.wildCardGroupStage
case .walkOut:
return team.walkOut
case .bracket:
return team.inRound() && !team.inGroupStage()
case .groupStage:
return team.inGroupStage()
case .notImported:
return !team.isImported()
case .registeredLocally:
return !team.hasRegisteredOnline()
case .registeredOnline:
return team.hasRegisteredOnline()
default:
return true
}
}
if byDecreasingOrdering {
return teams.reversed()
} else {
return teams
}
let sorted = sortingMode == .registrationDate
? filtered.sorted(by: { $0.computedRegistrationDate < $1.computedRegistrationDate })
: Array(filtered)
return byDecreasingOrdering ? sorted.reversed() : sorted
}
// private func _fixModel() {
@ -572,12 +568,10 @@ struct InscriptionManagerView: View {
}
}
private func _teamRegisteredView() -> some View {
private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View {
List {
let selectedSortedTeams = tournament.selectedSortedTeams()
if presentSearch == false {
_informationView()
_informationView(for: selectedSortedTeams)
if tournament.isAnimation() == false {
_rankHandlerView()
@ -585,7 +579,8 @@ struct InscriptionManagerView: View {
}
}
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
let sortedTeams = sortedTeams(selectedSortedTeams: selectedSortedTeams)
let teams = searchField.isEmpty ? filteredTeams(sortedTeams: sortedTeams) : filteredTeams(sortedTeams: sortedTeams).filter({ $0.contains(searchField.canonicalVersion) })
if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView {
@ -622,7 +617,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
TeamRowView(team: team, teamIndex: teamIndex)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if tournament.enableOnlineRegistration == false {
@ -735,18 +730,18 @@ struct InscriptionManagerView: View {
}
}
private func _teamCountForFilterMode(filterMode: FilterMode) -> String {
private func _teamCountForFilterMode(filterMode: FilterMode, in teams: [TeamRegistration]) -> String {
switch filterMode {
case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted()
return teams.filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted()
return teams.filter({ $0.wildCardGroupStage }).count.formatted()
case .all:
return unsortedTeamsWithoutWO.count.formatted()
case .bracket:
return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
return teams.filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted()
return teams.filter({ $0.inGroupStage() }).count.formatted()
case .walkOut:
let wo = walkoutTeams.count.formatted()
return wo
@ -754,20 +749,20 @@ struct InscriptionManagerView: View {
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
return waiting.formatted()
case .notImported:
let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count)
let notImported: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.isImported() == false }).count)
return notImported.formatted()
case .registeredLocally:
let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count)
let registeredLocally: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() == false }).count)
return registeredLocally.formatted()
case .registeredOnline:
let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count)
let registeredOnline: Int = max(0, sortedTeams(selectedSortedTeams: teams).filter({ $0.hasRegisteredOnline() }).count)
return registeredOnline.formatted()
}
}
@ViewBuilder
private func _informationView() -> some View {
private func _informationView(for teams: [TeamRegistration]) -> some View {
Section {
HStack {
// VStack(alignment: .leading, spacing: 0) {
@ -781,7 +776,7 @@ struct InscriptionManagerView: View {
// }
//
ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in
_filterModeView(filterMode: filterMode)
_filterModeView(filterMode: filterMode, in: teams)
}
Button {
@ -809,7 +804,7 @@ struct InscriptionManagerView: View {
.listRowSeparator(.hidden)
HStack {
ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in
_filterModeView(filterMode: filterMode)
_filterModeView(filterMode: filterMode, in: teams)
}
}
.padding(.bottom, -4)
@ -883,7 +878,7 @@ struct InscriptionManagerView: View {
}
}
private func _filterModeView(filterMode: FilterMode) -> some View {
private func _filterModeView(filterMode: FilterMode, in teams: [TeamRegistration]) -> some View {
Button {
if self.filterMode == filterMode {
@ -894,7 +889,7 @@ struct InscriptionManagerView: View {
} label: {
VStack(alignment: .center, spacing: -2) {
Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8)
Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle)
Text(_teamCountForFilterMode(filterMode: filterMode, in: teams)).font(.largeTitle)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())

@ -28,10 +28,20 @@ struct PrintSettingsView: View {
// Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries")
// })
Toggle(isOn: $generator.displayTeamIndex, label: {
Text("Afficher le poids et le rang de l'équipe")
})
Toggle(isOn: $generator.displayRank, label: {
Text("Afficher le classement du joueur")
})
Toggle(isOn: $generator.displayScore, label: {
Text("Afficher le score")
Text("Affiche le score des matchs terminés")
})
Toggle(isOn: $generator.includeBracket, label: {
Text("Tableau")
})
@ -152,32 +162,34 @@ struct PrintSettingsView: View {
.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])
}
}
#if DEBUG
.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, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)) {
Text("Poule")
}
}
} header: {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
#endif
}
@ViewBuilder
@ -199,7 +211,7 @@ struct PrintSettingsView: 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
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore), loadStatusChanged: { loaded, error, webView in
if let error {
print("preparePDF", error)
} else if loaded == false {
@ -301,7 +313,7 @@ struct WebViewPreview: View {
ProgressView()
.onAppear {
if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withTeamIndex: generator.displayTeamIndex, withScore: generator.displayScore)
} else if let round {
html = generator.generateLoserBracketHtml(upperRound: round)
} else {

@ -141,17 +141,18 @@ struct RegistrationSetupView: View {
}
Section {
Toggle(isOn: $targetTeamCountEnabled) {
Text("Activer une limite")
}
if targetTeamCountEnabled {
StepperView(count: $targetTeamCount, minimum: 4)
}
} header: {
// Toggle(isOn: $targetTeamCountEnabled) {
// Text("Activer une limite")
// }
//
// if targetTeamCountEnabled {
// StepperView(count: $targetTeamCount, minimum: 4)
// }
StepperView(count: $targetTeamCount, minimum: 4)
} header: {
Text("Paires admises")
} footer: {
Text("Si une limite de paire existe, les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.")
Text("Les inscriptions seront indiqués en attente pour les joueurs au-délà de cette limite dans le cas où aucune limite de liste d'attente n'est active ou non atteinte. Dans le cas contraire, plus aucune inscription ne seront possibles.")
}
Section {
@ -160,7 +161,7 @@ struct RegistrationSetupView: View {
}
if waitingListLimitEnabled {
StepperView(count: $waitingListLimit, minimum: 1)
StepperView(count: $waitingListLimit, minimum: 0)
}
} header: {
Text("Liste d'attente")

@ -71,7 +71,7 @@ struct TournamentRankView: View {
} footer: {
if let url = tournament.shareURL(.rankings) {
Link(destination: url) {
Text("Voir la page des classements sur Padel Club")
Text("Voir les classements sur Padel Club")
}
}
}

@ -124,7 +124,7 @@ struct TournamentBuildView: View {
.foregroundStyle(.green)
}
} label: {
Text("Classement final des équipes")
Text("Classement final")
if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
}

@ -21,6 +21,10 @@ struct TournamentInscriptionView: View {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened))
} else if tournament.enableOnlineRegistration {
Text("Inscription en ligne activée")
} else if tournament.onlineRegistrationCanBeEnabled() {
Text("Inscription en ligne désactivée")
}
}
}

@ -249,7 +249,7 @@ struct TournamentView: View {
.foregroundStyle(.green)
}
} label: {
Text("Classement final des équipes")
Text("Classement final")
if tournament.publishRankings == false {
Text("Vérifiez le classement avant de publier").foregroundStyle(.logoRed)
}

Loading…
Cancel
Save