sync2
Laurent 9 months ago
commit f46890c445
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  3. 227
      PadelClub/Data/TeamRegistration.swift
  4. 69
      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. 70
      PadelClub/PadelClubApp.swift
  10. 7
      PadelClub/Utils/HtmlGenerator.swift
  11. 62
      PadelClub/Utils/HtmlService.swift
  12. 16
      PadelClub/Utils/PadelRule.swift
  13. 5
      PadelClub/Utils/URLs.swift
  14. 33
      PadelClub/Utils/VersionComparator.swift
  15. 86
      PadelClub/ViewModel/SearchViewModel.swift
  16. 2
      PadelClub/Views/Calling/CallSettingsView.swift
  17. 17
      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. 23
      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. 3
      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. 99
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  35. 58
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  36. 17
      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:

@ -12,42 +12,49 @@ 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()
@ -76,7 +83,6 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
players().anySatisfy({ $0.source == nil })
}
func isOutOfTournament() -> Bool {
walkOut
}
@ -93,7 +99,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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: -
@ -130,20 +138,21 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 {
@ -252,7 +263,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
})
}
@ -260,13 +272,19 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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? {
@ -279,8 +297,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 "###"
@ -288,13 +307,17 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
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
}
@ -319,7 +342,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 {
@ -346,7 +370,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
@ -356,8 +382,12 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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() })
@ -368,7 +398,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
@ -382,9 +414,13 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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())
}
}
@ -395,7 +431,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 {
@ -411,7 +448,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
}
@ -427,18 +465,27 @@ 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
@ -450,7 +497,6 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
let playersToRemove = previousPlayers.subtracting(players)
self.tournamentStore?.playerRegistrations.delete(contentOfs: playersToRemove)
setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
@ -459,11 +505,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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?)
@ -485,14 +531,18 @@ 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)
@ -506,8 +556,8 @@ 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 {
@ -527,9 +577,16 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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 {
@ -567,12 +624,13 @@ 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 {
@ -595,7 +653,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
@ -606,7 +665,9 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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
}
@ -630,14 +691,18 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
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() {

@ -19,7 +19,8 @@ final class Tournament: BaseTournament {
// super.init()
// }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil) {
super.init()
self.event = event
self.name = name
@ -29,11 +30,7 @@ final class Tournament: BaseTournament {
#if DEBUG
self.isPrivate = false
#else
if Guard.main.currentPlan == .monthlyUnlimited {
self.isPrivate = true
} else {
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
}
self.isPrivate = isPrivate
#endif
self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat
@ -136,6 +133,10 @@ final class Tournament: BaseTournament {
return Array(tournamentStore.teamRegistrations)
}
func unsortedTeamsCount() -> Int {
return self.tournamentStore?.teamRegistrations.count ?? 0
}
func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
@ -277,7 +278,8 @@ defer {
}
func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams()
let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat {
case .rawText:
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
@ -577,13 +579,13 @@ defer {
return rounds.sorted(by: \.index).reversed()
}
func sortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams
return teams + waitingListTeams(in: teams, includingWalkOuts: true)
}
func waitingListSortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams
return waitingListTeams(in: teams, includingWalkOuts: false)
}
@ -880,9 +882,8 @@ defer {
}
}
func registrationIssues() -> Int {
func registrationIssues(selectedTeams: [TeamRegistration]) -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
@ -911,7 +912,7 @@ defer {
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
}
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)]
static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.computedOrder)]
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
@ -1535,6 +1536,7 @@ defer {
team.wildCardGroupStage = true
}
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team
}
@ -1551,6 +1553,7 @@ defer {
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team
}
@ -1836,6 +1839,25 @@ defer {
//enableOnlineRegistration = true
registrationDateLimit = deadline(for: .inscription)
}
//self.customizeUsingPreferences()
}
func customizeUsingPreferences() {
guard let lastTournamentWithSameBuild = DataStore.shared.tournaments.filter({ tournament in
tournament.tournamentLevel == self.tournamentLevel
&& tournament.tournamentCategory == self.tournamentCategory
&& tournament.federalTournamentAge == self.federalTournamentAge
&& tournament.hasEnded() == true
&& tournament.isCanceled == false
&& tournament.isDeleted == false
}).sorted(by: \.endDate!, order: .descending).first else {
return
}
self.dayDuration = lastTournamentWithSameBuild.dayDuration
self.teamCount = (lastTournamentWithSameBuild.teamCount / 2) * 2
self.enableOnlineRegistration = lastTournamentWithSameBuild.enableOnlineRegistration
}
func onlineRegistrationCanBeEnabled() -> Bool {
@ -2333,7 +2355,6 @@ extension Bool {
//}
extension Tournament: FederalTournamentHolder {
func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
if let name {
@ -2428,12 +2449,21 @@ extension Tournament {
}
let rankSourceDate = _mostRecentDateAvailable
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }.sorted(by: \.endDate!, order: .descending)
var shouldBePrivate = tournaments.first?.isPrivate ?? true
if Guard.main.currentPlan == .monthlyUnlimited {
shouldBePrivate = false
} else if Guard.main.purchasedTransactions.isEmpty == false {
shouldBePrivate = false
}
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//creator: DataStore.shared.user?.id
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode)
return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode)
}
static func fake() -> Tournament {
@ -2446,10 +2476,7 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
var daysOffset = type.daysOffset
if tournamentLevel == .p500 {
daysOffset += 7
}
var daysOffset = type.daysOffset(level: tournamentLevel)
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)

@ -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,6 +64,10 @@ struct PadelClubApp: App {
var body: some Scene {
WindowGroup {
if self.blockApp {
DownloadNewVersionView()
} else {
MainView()
.environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) {
@ -84,14 +90,15 @@ struct PadelClubApp: App {
.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()
@ -109,6 +116,33 @@ print("Running in Release mode")
.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
}
}
}
}
}
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
@ -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,7 +1996,19 @@ enum TournamentDeadlineType: String, CaseIterable {
case wildcardLicensePurchase = "Prise de licence des WC"
case definitiveBroadcastList = "Publication définitive"
var daysOffset: Int {
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
@ -2007,6 +2019,8 @@ enum TournamentDeadlineType: String, CaseIterable {
case .wildcardLicensePurchase, .definitiveBroadcastList:
return -8
}
}
}
var timeOffset: DateComponents {

@ -47,6 +47,11 @@ 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,13 +172,11 @@ class SearchViewModel: ObservableObject, Identifiable {
}
}
func orPredicate() -> NSPredicate? {
func searchTextPredicate() -> 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 {
@ -169,6 +190,27 @@ class SearchViewModel: ObservableObject, Identifiable {
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
if tokens.isEmpty {
if shouldIncludeSearchTextPredicate(), canonicalVersionWithoutPunctuation.isEmpty == false {
if let searchTextPredicate = searchTextPredicate() {
predicates.append(searchTextPredicate)
}
}
}
for token in tokens {
switch token {
case .ligue:
if canonicalVersionWithoutPunctuation.isEmpty {
predicates.append(NSPredicate(format: "ligueName == nil"))
@ -208,7 +250,7 @@ class SearchViewModel: ObservableObject, Identifiable {
}
}
}
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,12 +390,21 @@ class SearchViewModel: ObservableObject, Identifiable {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
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: " ")
let pattern = components.joined(separator: ".*")
@ -356,10 +412,6 @@ class SearchViewModel: ObservableObject, Identifiable {
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,14 +6,31 @@
//
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 {

@ -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)
}
}

@ -23,6 +23,7 @@ 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
@ -145,6 +146,28 @@ struct PlanningSettingsView: View {
_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 {

@ -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)
}

@ -338,6 +338,21 @@ struct MySearchView: View {
_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
.overlay {
@ -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,15 +595,12 @@ 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
}
}
pasteString = first
editableTextField = first
textHeight = Self._calculateHeight(text: 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
func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let filtered = sortedTeams.lazy.filter { team in
switch filterMode {
case .wildcardBracket:
teams = teams.filter({ $0.wildCardBracket })
return team.wildCardBracket
case .wildcardGroupStage:
teams = teams.filter({ $0.wildCardGroupStage })
return team.wildCardGroupStage
case .walkOut:
teams = teams.filter({ $0.walkOut })
return team.walkOut
case .bracket:
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false })
return team.inRound() && !team.inGroupStage()
case .groupStage:
teams = teams.filter({ $0.inGroupStage() })
return team.inGroupStage()
case .notImported:
teams = teams.filter({ $0.isImported() == false })
return !team.isImported()
case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false })
return !team.hasRegisteredOnline()
case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true })
return team.hasRegisteredOnline()
default:
break
return true
}
if sortingMode == .registrationDate {
teams = teams.sorted(by: \.computedRegistrationDate)
}
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 {
// 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