Laurent 8 months ago
commit 2127f089f7
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/Federal/FederalTournament.swift
  3. 24
      PadelClub/Data/GroupStage.swift
  4. 19
      PadelClub/Data/Match.swift
  5. 20
      PadelClub/Data/PlayerRegistration.swift
  6. 20
      PadelClub/Data/TeamRegistration.swift
  7. 148
      PadelClub/Data/Tournament.swift
  8. 2
      PadelClub/InscriptionLegendView.swift
  9. 60
      PadelClub/OnlineRegistrationWarningView.swift
  10. 34
      PadelClub/Utils/PadelRule.swift
  11. 69
      PadelClub/Utils/SwiftParser.swift
  12. 12
      PadelClub/Views/Calling/CallView.swift
  13. 7
      PadelClub/Views/Calling/SendToAllView.swift
  14. 29
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  15. 81
      PadelClub/Views/GroupStage/GroupStagesView.swift
  16. 2
      PadelClub/Views/Match/Components/MatchDateView.swift
  17. 2
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  18. 2
      PadelClub/Views/Match/MatchDetailView.swift
  19. 67
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  20. 2
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  21. 72
      PadelClub/Views/Planning/MatchFormatGuideView.swift
  22. 23
      PadelClub/Views/Planning/PlanningSettingsView.swift
  23. 2
      PadelClub/Views/Round/RoundSettingsView.swift
  24. 5
      PadelClub/Views/Score/FollowUpMatchView.swift
  25. 2
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  26. 7
      PadelClub/Views/Team/Components/TeamWeightView.swift
  27. 127
      PadelClub/Views/Team/EditingTeamView.swift
  28. 12
      PadelClub/Views/Team/TeamPickerView.swift
  29. 9
      PadelClub/Views/Team/TeamRowView.swift
  30. 21
      PadelClub/Views/Tournament/FileImportView.swift
  31. 2
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  32. 19
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  33. 184
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  34. 10
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  35. 21
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  36. 15
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -798,6 +798,9 @@
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.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 */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.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 */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
@ -1201,6 +1204,7 @@
FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
@ -2073,6 +2077,7 @@
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */,
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */,
FF1162882BD0523B000C4809 /* Components */, FF1162882BD0523B000C4809 /* Components */,
); );
path = Planning; path = Planning;
@ -2494,6 +2499,7 @@
FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
@ -2785,6 +2791,7 @@
FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FFB378362D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */,
@ -3055,6 +3062,7 @@
FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */,
@ -3310,12 +3318,11 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3337,14 +3344,13 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.12; MARKETING_VERSION = 1.1.18;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3358,11 +3364,10 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3384,14 +3389,13 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.12; MARKETING_VERSION = 1.1.18;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3482,7 +3486,6 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3511,7 +3514,6 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3529,7 +3531,6 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3559,7 +3560,6 @@
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = PRODTEST; SWIFT_ACTIVE_COMPILATION_CONDITIONS = PRODTEST;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };

@ -236,12 +236,12 @@ struct CategorieAge: Codable {
var tournamentAge: FederalTournamentAge? { var tournamentAge: FederalTournamentAge? {
if let id { if let id {
return FederalTournamentAge(rawValue: id) return FederalTournamentAge(rawValue: id) ?? .senior
} }
if let libelle { if let libelle {
return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) ?? .senior
} }
return nil return .senior
} }
} }
@ -295,7 +295,7 @@ struct Serie: Codable {
var sexe: String? var sexe: String?
var tournamentCategory: TournamentCategory? { var tournamentCategory: TournamentCategory? {
TournamentCategory.allCases.first(where: { $0.requestLabel == code }) TournamentCategory.allCases.first(where: { $0.requestLabel == code }) ?? .men
} }
} }
@ -348,9 +348,9 @@ struct TypeEpreuve: Codable {
var tournamentLevel: TournamentLevel? { var tournamentLevel: TournamentLevel? {
if let code, let value = Int(code.removingFirstCharacter) { if let code, let value = Int(code.removingFirstCharacter) {
return TournamentLevel(rawValue: value) return TournamentLevel(rawValue: value) ?? .p100
} }
return nil return .p100
} }
} }

@ -438,7 +438,7 @@ final class GroupStage: ModelObject, Storable {
unsortedTeams().flatMap({ $0.unsortedPlayers() }) unsortedTeams().flatMap({ $0.unsortedPlayers() })
} }
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
@ -492,20 +492,24 @@ final class GroupStage: ModelObject, Storable {
var scoreCache: [Int: TeamGroupStageScore] = [:] var scoreCache: [Int: TeamGroupStageScore] = [:]
func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? {
if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] {
return cachedScore
} else {
let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
if let score = score {
scoreCache[team.groupStagePositionAtStep(step)!] = score
}
return score
}
}
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
if sortedByScore { if sortedByScore {
return unsortedTeams().compactMap({ team in return unsortedTeams().compactMap({ team in
// Check cache or use provided scores, otherwise calculate and store in cache // Check cache or use provided scores, otherwise calculate and store in cache
scores?.first(where: { $0.team.id == team.id }) ?? { scores?.first(where: { $0.team.id == team.id }) ?? {
if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] { return computedScore(forTeam: team, step: step)
return cachedScore
} else {
let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
if let score = score {
scoreCache[team.groupStagePositionAtStep(step)!] = score
}
return score
}
}() }()
}).sorted { (lhs, rhs) in }).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [ let predicates: [TeamScoreAreInIncreasingOrder] = [

@ -410,12 +410,15 @@ defer {
} }
} }
//byeState = false //byeState = false
roundObject?._cachedSeedInterval = nil
name = nil if state != currentState {
do { roundObject?._cachedSeedInterval = nil
try self.tournamentStore.matches.addOrUpdate(instance: self) name = nil
} catch { do {
Logger.error(error) try self.tournamentStore.matches.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
} }
if single == false { if single == false {
@ -514,7 +517,7 @@ defer {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
} }
guard let roundObject else { return index } guard let roundObject else { return index }
return roundObject.isLoserBracket() ? (roundObject.index + 1) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound() return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + indexInRound()
} }
func previousMatches() -> [Match] { func previousMatches() -> [Match] {
@ -537,8 +540,10 @@ defer {
func setWalkOut(_ teamPosition: TeamPosition) { func setWalkOut(_ teamPosition: TeamPosition) {
let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition)) let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition))
teamScoreWalkout.walkOut = 0 teamScoreWalkout.walkOut = 0
teamScoreWalkout.score = matchFormat.defaultWalkOutScore(true).compactMap({ String($0) }).joined(separator: ",")
let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam)) let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam))
teamScoreWinning.walkOut = nil teamScoreWinning.walkOut = nil
teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",")
do { do {
try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
} catch { } catch {

@ -301,21 +301,23 @@ final class PlayerRegistration: ModelObject, Storable {
return await withTaskGroup(of: Line?.self) { group in return await withTaskGroup(of: Line?.self) { group in
for source in filteredSources { for source in filteredSources {
group.addTask { group.addTask {
guard !Task.isCancelled else { print("Cancelled"); return nil } guard !Task.isCancelled else { return nil }
return try? await source.first { $0.rawValue.contains(";\(license);") } return try? await source.first { $0.rawValue.contains(";\(license);") }
} }
} }
if let first = await group.first(where: { $0 != nil }) { for await result in group {
group.cancelAll() if let result {
return first group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
} }
return nil return nil
} }
} }
func historyFromName(from sources: [CSVParser]) async throws -> Line? { func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME #if DEBUG
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -338,9 +340,11 @@ final class PlayerRegistration: ModelObject, Storable {
} }
} }
if let first = await group.first(where: { $0 != nil }) { for await result in group {
group.cancelAll() if let result {
return first group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
} }
return nil return nil
} }

@ -338,9 +338,11 @@ final class TeamRegistration: ModelObject, Storable {
func initialRoundColor() -> Color? { func initialRoundColor() -> Color? {
if walkOut { return Color.logoRed } if walkOut { return Color.logoRed }
if groupStagePosition != nil { return Color.blue } if groupStagePosition != nil || wildCardGroupStage { 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)) return Color(uiColor: .init(fromHex: colorHex))
} else if wildCardBracket {
return Color.mint
} else { } else {
return nil return nil
} }
@ -632,14 +634,26 @@ final class TeamRegistration: ModelObject, Storable {
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex { if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition return drawMatchIndex != bracketPosition
} else if let bracketPosition { } else if bracketPosition != nil {
return true return true
} else if let drawMatchIndex { } else if drawMatchIndex != nil {
return true return true
} }
return false return false
} }
func shouldDisplayRankAndWeight() -> Bool {
unsortedPlayers().count > 0
}
func bracketMatchTitleAndQualifiedStatus() -> String? {
let values = [qualified ? "Qualifié" : nil, initialMatch()?.roundAndMatchTitle()].compactMap({ $0 })
if values.isEmpty {
return nil
}
return values.joined(separator: " -> ")
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _id = "id" case _id = "id"
case _tournament = "tournament" case _tournament = "tournament"

@ -70,6 +70,10 @@ final class Tournament : ModelObject, Storable {
var maximumPlayerPerTeam: Int = 2 var maximumPlayerPerTeam: Int = 2
var information: String? = nil var information: String? = nil
//local variable
var refreshInProgress: Bool = false
var refreshRanking: Bool = false
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -428,6 +432,11 @@ final class Tournament : ModelObject, Storable {
return Array(self.tournamentStore.teamRegistrations) return Array(self.tournamentStore.teamRegistrations)
} }
func unsortedTeamsCount() -> Int {
return self.tournamentStore.teamRegistrations.count
}
func groupStages(atStep step: Int = 0) -> [GroupStage] { func groupStages(atStep step: Int = 0) -> [GroupStage] {
let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step } let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
return groupStages.sorted(by: \.index) return groupStages.sorted(by: \.index)
@ -581,7 +590,8 @@ defer {
} }
func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let selectedSortedTeams = selectedSortedTeams() + waitingListSortedTeams() let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat { switch exportFormat {
case .rawText: 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)) return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
@ -878,13 +888,13 @@ defer {
return rounds.sorted(by: \.index).reversed() return rounds.sorted(by: \.index).reversed()
} }
func sortedTeams() -> [TeamRegistration] { func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams() let teams = selectedSortedTeams
return teams + waitingListTeams(in: teams, includingWalkOuts: true) return teams + waitingListTeams(in: teams, includingWalkOuts: true)
} }
func waitingListSortedTeams() -> [TeamRegistration] { func waitingListSortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let teams = selectedSortedTeams() let teams = selectedSortedTeams
return waitingListTeams(in: teams, includingWalkOuts: false) return waitingListTeams(in: teams, includingWalkOuts: false)
} }
@ -919,7 +929,7 @@ defer {
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
let groupStageSpots: Int = self.groupStageSpots() let groupStageSpots: Int = self.groupStageSpots()
var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count var bracketSeeds: Int = teamCount - groupStageSpots - wcBracket.count
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 } if bracketSeeds < 0 { bracketSeeds = 0 }
@ -963,8 +973,8 @@ defer {
} }
} }
func bracketCut(teamCount: Int) -> Int { func bracketCut(teamCount: Int, groupStageCut: Int) -> Int {
return max(0, teamCount - groupStageCut()) return self.teamCount - groupStageCut
} }
func groupStageCut() -> Int { func groupStageCut() -> Int {
@ -973,10 +983,12 @@ defer {
func cutLabel(index: Int, teamCount: Int?) -> String { func cutLabel(index: Int, teamCount: Int?) -> String {
let _teamCount = teamCount ?? selectedSortedTeams().count let _teamCount = teamCount ?? selectedSortedTeams().count
let bracketCut = bracketCut(teamCount: _teamCount) let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut { if index < bracketCut {
return "Tableau" return "Tableau"
} else if index - bracketCut < groupStageCut() && _teamCount > 0 { } else if index - bracketCut < groupStageCut && _teamCount > 0 {
return "Poule" return "Poule"
} else { } else {
return "Attente" return "Attente"
@ -986,11 +998,12 @@ defer {
func cutLabelColor(index: Int?, teamCount: Int?) -> Color { func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
guard let index else { return Color.gray } guard let index else { return Color.gray }
let _teamCount = teamCount ?? selectedSortedTeams().count let _teamCount = teamCount ?? selectedSortedTeams().count
let bracketCut = bracketCut(teamCount: _teamCount) let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut { if index < bracketCut {
return Color.mint return Color.mint
} else if index - bracketCut < groupStageCut() && _teamCount > 0 { } else if index - bracketCut < groupStageCut && _teamCount > 0 {
return Color.cyan return Color.indigo
} else { } else {
return Color.gray return Color.gray
} }
@ -1183,9 +1196,8 @@ defer {
} }
} }
func registrationIssues() -> Int { func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players) let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
@ -1505,8 +1517,8 @@ defer {
} }
} }
func updateRank(to newDate: Date?) async throws { func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws {
refreshRanking = true
#if DEBUG_TIME #if DEBUG_TIME
let start = Date() let start = Date()
defer { defer {
@ -1541,16 +1553,42 @@ defer {
let lastRankMan = monthData?.maleUnrankedValue ?? 0 let lastRankMan = monthData?.maleUnrankedValue ?? 0
let lastRankWoman = monthData?.femaleUnrankedValue ?? 0 let lastRankWoman = monthData?.femaleUnrankedValue ?? 0
// Fetch only the required files var chunkedParsers: [CSVParser] = []
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } if let providedSources {
guard !dataURLs.isEmpty else { return } // Early return if no files found chunkedParsers = providedSources
} else {
// Fetch only the required files
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) }
chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
}
let sources = dataURLs.map { CSVParser(url: $0) }
let players = unsortedPlayers() let players = unsortedPlayers()
try await players.concurrentForEach { player in try await players.concurrentForEach { player in
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: sources, lastRank: lastRank) try await player.updateRank(from: chunkedParsers, lastRank: lastRank)
player.setComputedRank(in: self)
}
if providedSources == nil {
try chunkedParsers.forEach { chunk in
try FileManager.default.removeItem(at: chunk.url)
}
}
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
} }
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
refreshRanking = false
} }
@ -1852,6 +1890,8 @@ defer {
team.wildCardGroupStage = true team.wildCardGroupStage = true
} }
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
team.weight += 200_000
return team return team
} }
@ -1865,6 +1905,7 @@ defer {
func addEmptyTeamRegistration(_ count: Int) { func addEmptyTeamRegistration(_ count: Int) {
let teams = (0..<count).map { _ in let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id) let team = TeamRegistration(tournament: id)
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team return team
} }
@ -1900,9 +1941,11 @@ defer {
return bracketTeamCount return bracketTeamCount
} }
func buildBracket() { func buildBracket(minimalBracketTeamCount: Int? = nil) {
guard rounds().isEmpty else { return } guard rounds().isEmpty else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) let roundCount = RoundRule.numberOfRounds(forTeams: minimalBracketTeamCount ?? bracketTeamCount())
let matchCount = RoundRule.numberOfMatches(forTeams: minimalBracketTeamCount ?? bracketTeamCount())
let rounds = (0..<roundCount).map { //index 0 is the final let rounds = (0..<roundCount).map { //index 0 is the final
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode) return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
@ -1917,7 +1960,6 @@ defer {
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
let matches = (0..<matchCount).map { //0 is final match let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0) let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
@ -2413,12 +2455,15 @@ defer {
func updateSeedsBracketPosition() async { func updateSeedsBracketPosition() async {
await removeAllSeeds() await removeAllSeeds(saveTeamsAtTheEnd: false)
let drawLogs = drawLogs().reversed() let drawLogs = drawLogs().reversed()
let seeds = seeds() let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { await MainActor.run {
drawLog.updateTeamBracketPosition(seed) for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
} }
} }
@ -2429,11 +2474,13 @@ defer {
} }
} }
func removeAllSeeds() async { func removeAllSeeds(saveTeamsAtTheEnd: Bool) async {
let teams = unsortedTeams() let teams = unsortedTeams()
teams.forEach({ team in teams.forEach({ team in
team.bracketPosition = nil team.bracketPosition = nil
team._cachedRestingTime = nil team._cachedRestingTime = nil
team.finalRanking = nil
team.pointsEarned = nil
}) })
let allMatches = allRoundMatches() let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in let ts = allMatches.flatMap { match in
@ -2460,12 +2507,13 @@ defer {
Logger.error(error) Logger.error(error)
} }
do { if saveTeamsAtTheEnd {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) do {
} catch { try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
Logger.error(error) } catch {
Logger.error(error)
}
} }
updateTournamentState()
} }
func addNewRound(_ roundIndex: Int) async { func addNewRound(_ roundIndex: Int) async {
@ -2597,6 +2645,31 @@ defer {
return false return false
} }
func rankSourceShouldBeRefreshed() -> Date? {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate = rankSourceDate, currentRankSourceDate < mostRecentDate, hasEnded() == false {
return mostRecentDate
} else {
return nil
}
}
func onlineTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
func shouldWarnOnlineRegistrationUpdates() -> Bool {
onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false
}
func refreshTeamList() async throws {
guard enableOnlineRegistration, refreshInProgress == false, hasEnded() == false else { return }
refreshInProgress = true
try await self.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
refreshInProgress = false
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {
@ -2820,10 +2893,7 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? { func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
var daysOffset = type.daysOffset var daysOffset = type.daysOffset(level: tournamentLevel)
if tournamentLevel == .p500 {
daysOffset += 7
}
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) { if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date) let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)

@ -36,7 +36,7 @@ struct InscriptionLegendView: View {
Text("Équipe estimée en tableau") Text("Équipe estimée en tableau")
.listRowView(isActive: true, color: .mint, hideColorVariation: true, alignment: .leading) .listRowView(isActive: true, color: .mint, hideColorVariation: true, alignment: .leading)
Text("Équipe estimée en poule") Text("Équipe estimée en poule")
.listRowView(isActive: true, color: .cyan, hideColorVariation: true, alignment: .leading) .listRowView(isActive: true, color: .indigo, hideColorVariation: true, alignment: .leading)
} }

@ -0,0 +1,60 @@
//
// WaitingListView.swift
// PadelClub
//
// Created by razmig on 26/02/2025.
//
import SwiftUI
struct WaitingListView: View {
@Environment(Tournament.self) var tournament: Tournament
let teamCount: Int
@ViewBuilder
var body: some View {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Pour l'instant Padel Club ne saura pas les prévenir automatiquement, vous devrez les contacter via l'écran de gestion des inscriptions.")
.foregroundStyle(.logoRed)
let selection = tournament.selectedSortedTeams()
if teamCount > tournament.teamCount {
Section {
let teams = tournament.waitingListSortedTeams(selectedSortedTeams: selection)
.prefix(teamCount - tournament.teamCount)
.filter { $0.hasRegisteredOnline() }
ForEach(teams) { team in
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
}
}
} header: {
Text("Équipes entrantes dans la sélection")
} footer: {
Text("Équipes inscrites en ligne à prévenir rentrant dans votre liste")
}
}
if teamCount < tournament.teamCount {
Section {
let teams = selection.suffix(tournament.teamCount - teamCount)
.filter { $0.hasRegisteredOnline() }
ForEach(teams) { team in
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
}
}
} header: {
Text("Équipes sortantes de la sélection")
} footer: {
Text("Équipes inscrites en ligne à prévenir retirées de votre liste")
}
}
}
}

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

@ -70,18 +70,18 @@ struct Line: Identifiable {
struct CSVParser: AsyncSequence, AsyncIteratorProtocol { struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Line typealias Element = Line
private let url: URL let url: URL
private var lineIterator: LineIterator private var lineIterator: LineIterator
private let seperator: Character private let separator: Character
private let quoteCharacter: Character = "\"" private let quoteCharacter: Character = "\""
private var lineNumber = 0 private var lineNumber = 0
private let date: Date private let date: Date
let maleData: Bool let maleData: Bool
init(url: URL, seperator: Character = ";") { init(url: URL, separator: Character = ";") {
self.date = url.dateFromPath self.date = url.dateFromPath
self.url = url self.url = url
self.seperator = seperator self.separator = separator
self.lineIterator = url.lines.makeAsyncIterator() self.lineIterator = url.lines.makeAsyncIterator()
self.maleData = url.path().contains(SourceFile.messieurs.rawValue) self.maleData = url.path().contains(SourceFile.messieurs.rawValue)
} }
@ -139,7 +139,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
inQuote = !inQuote inQuote = !inQuote
continue continue
case seperator: case separator:
if !inQuote { if !inQuote {
data.append(currentString.isEmpty ? nil : currentString) data.append(currentString.isEmpty ? nil : currentString)
currentString = "" currentString = ""
@ -157,4 +157,63 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
return data return data
} }
/// Splits the CSV file into multiple temporary CSV files, each containing `size` lines.
/// Returns an array of new `CSVParser` instances pointing to these chunked files.
func getChunkedParser(size: Int) async throws -> [CSVParser] {
var chunkedParsers: [CSVParser] = []
var currentChunk: [String] = []
var iterator = self.makeAsyncIterator()
var chunkIndex = 0
while let line = try await iterator.next()?.rawValue {
currentChunk.append(line)
// When the chunk reaches the desired size, write it to a file
if currentChunk.count == size {
let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex)
chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator))
chunkIndex += 1
currentChunk.removeAll()
}
}
// Handle remaining lines (if any)
if !currentChunk.isEmpty {
let chunkURL = try writeChunkToFile(chunk: currentChunk, index: chunkIndex)
chunkedParsers.append(CSVParser(url: chunkURL, separator: self.separator))
}
return chunkedParsers
}
/// Writes a chunk of CSV lines to a temporary file and returns its URL.
private func writeChunkToFile(chunk: [String], index: Int) throws -> URL {
let tempDirectory = FileManager.default.temporaryDirectory
let chunkURL = tempDirectory.appendingPathComponent("\(url.lastPathComponent)-\(index).csv")
let chunkData = chunk.joined(separator: "\n")
try chunkData.write(to: chunkURL, atomically: true, encoding: .utf8)
return chunkURL
}
}
/// Process all large CSV files concurrently and gather all mini CSVs.
func chunkAllSources(sources: [CSVParser], size: Int) async throws -> [CSVParser] {
var allChunks: [CSVParser] = []
await withTaskGroup(of: [CSVParser].self) { group in
for source in sources {
group.addTask {
return (try? await source.getChunkedParser(size: size)) ?? []
}
}
for await miniCSVs in group {
allChunks.append(contentsOf: miniCSVs)
}
}
return allChunks
} }

@ -199,14 +199,16 @@ struct CallView: View {
let uncalledTeams = teams.filter { $0.getPhoneNumbers().isEmpty } let uncalledTeams = teams.filter { $0.getPhoneNumbers().isEmpty }
if networkMonitor.connected == false { if networkMonitor.connected == false {
if uncalledTeams.isEmpty == false { if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams) self.sentError = .uncalledTeams(uncalledTeams)
} else { } else {
self.sentError = .messageNotSent self.sentError = .messageNotSent
} }
} else { } else {
if uncalledTeams.isEmpty == false { if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams) self.sentError = .uncalledTeams(uncalledTeams)
} else if uncalledTeams.isEmpty == false, calledTeams.isEmpty {
self._called(uncalledTeams, true)
} }
self._called(calledTeams, true) self._called(calledTeams, true)
} }
@ -228,14 +230,16 @@ struct CallView: View {
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
if uncalledTeams.isEmpty == false { if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams) self.sentError = .uncalledTeams(uncalledTeams)
} else { } else {
self.sentError = .mailNotSent self.sentError = .mailNotSent
} }
} else { } else {
if uncalledTeams.isEmpty == false { if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams) self.sentError = .uncalledTeams(uncalledTeams)
} else if uncalledTeams.isEmpty == false, calledTeams.isEmpty {
self._called(uncalledTeams, true)
} }
self._called(calledTeams, true) self._called(calledTeams, true)
} }

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

@ -129,6 +129,18 @@ struct GroupStagesSettingsView: View {
} }
#endif #endif
Section {
menuGenerateGroupStage(.random)
} footer: {
Text("Redistribue les équipes par tirage au sort par chapeau")
}
Section {
menuGenerateGroupStage(.snake)
} footer: {
Text("Redistribue les équipes par la méthode du serpentin")
}
Section { Section {
RowButtonView("Retirer tous les horaires", role: .destructive) { RowButtonView("Retirer tous les horaires", role: .destructive) {
let matches = tournament.groupStages().flatMap({ $0._matches() }) let matches = tournament.groupStages().flatMap({ $0._matches() })
@ -144,30 +156,17 @@ struct GroupStagesSettingsView: View {
} }
} }
} footer: { } footer: {
Text("Retire les horaires pré-définis des matchs. Utile si vous avez convoqué mais que l'ordre des matchs à lancer n'est pas important.") Text("Retire les horaires pré-définis des matchs. Utile si vous avez convoqué mais que l'ordre des matchs à lancer n'est pas important.").foregroundStyle(.logoRed).bold()
} }
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false { if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false {
Section { Section {
menuBuildAllGroupStages menuBuildAllGroupStages
} footer: { } footer: {
Text("Efface et recréé les poules, les horaires et les résultats existants seront perdus") Text("Efface et recréé les poules, les horaires et les résultats existants seront perdus").foregroundStyle(.logoRed).bold()
} }
} }
Section {
menuGenerateGroupStage(.random)
} footer: {
Text("Redistribue les équipes par tirage au sort par chapeau")
}
Section {
menuGenerateGroupStage(.snake)
} footer: {
Text("Redistribue les équipes par la méthode du serpentin")
}
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
Section { Section {

@ -106,6 +106,80 @@ struct GroupStagesView: View {
return allDestinations return allDestinations
} }
func sortedTeams(missingQualifiedFromGroupStages: [TeamRegistration]) -> [GroupStage.TeamGroupStageScore] {
let sortedTeams = missingQualifiedFromGroupStages.compactMap({ team in
team.groupStageObject()?.computedScore(forTeam: team)
}).sorted { (lhs, rhs) in
let predicates: [GroupStage.TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
{ $0.gameDifference < $1.gameDifference},
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}
return sortedTeams
}
func _sortedAdditionnalTeams(missingQualifiedFromGroupStages: [TeamRegistration]) -> some View {
Section {
let teamGroupStageScores = self.sortedTeams(missingQualifiedFromGroupStages: missingQualifiedFromGroupStages).reversed()
ForEach(teamGroupStageScores, id: \.team.id) { teamGroupStageScore in
let team = teamGroupStageScore.team
let groupStage = team.groupStageObject()!
let groupStagePosition = team.groupStagePosition!
NavigationLink {
GroupStageTeamView(groupStage: groupStage, team: team)
.environment(self.tournament)
} label: {
HStack {
VStack(alignment: .leading) {
if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1)
}
}
Spacer()
if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: teamGroupStageScore) {
VStack(alignment: .trailing) {
HStack(spacing: 0.0) {
Text(score.wins)
Text("/")
Text(score.losses)
}.font(.headline).monospacedDigit()
if let setsDifference = score.setsDifference {
HStack(spacing: 4.0) {
Text(setsDifference)
}.font(.footnote)
}
if let gamesDifference = score.gamesDifference {
HStack(spacing: 4.0) {
Text(gamesDifference)
}.font(.footnote)
}
}
}
}
}
}
} header: {
let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())"
Text("Meilleurs \(name) de poule")
}
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
@ -116,9 +190,8 @@ struct GroupStagesView: View {
List { List {
if tournament.groupStageAdditionalQualified > 0 { if tournament.groupStageAdditionalQualified > 0 {
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())"
Section { Section {
let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())"
NavigationLink { NavigationLink {
SpinDrawView(drawees: ["Qualification d'un \(name) de poule"], segments: missingQualifiedFromGroupStages) { results in SpinDrawView(drawees: ["Qualification d'un \(name) de poule"], segments: missingQualifiedFromGroupStages) { results in
results.forEach { drawResult in results.forEach { drawResult in
@ -134,6 +207,8 @@ struct GroupStagesView: View {
Text("Qualifier un \(name) de poule par tirage au sort") Text("Qualifier un \(name) de poule par tirage au sort")
} }
.disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty)
} header: {
Text("Tirage au sort d'un \(name) de poule")
} footer: { } footer: {
if tournament.moreQualifiedToDraw() == 0 { if tournament.moreQualifiedToDraw() == 0 {
Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.") Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.")
@ -141,6 +216,8 @@ struct GroupStagesView: View {
Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.")
} }
} }
_sortedAdditionnalTeams(missingQualifiedFromGroupStages: missingQualifiedFromGroupStages)
} }
let runningMatches = Tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)

@ -30,7 +30,7 @@ struct MatchDateView: View {
} }
var currentDate: Date { var currentDate: Date {
Date().withoutSeconds() Date()
} }
var body: some View { var body: some View {

@ -151,7 +151,7 @@ struct PlayerBlockView: View {
} }
} }
} }
} else if let team { } else if let team, hasWon == false, isWalkOut == false {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} }
} }

@ -495,7 +495,7 @@ struct MatchDetailView: View {
Text("Horaire") Text("Horaire")
} }
.onChange(of: startDateSetup) { .onChange(of: startDateSetup) {
let date = Date().withoutSeconds() let date = Date()
switch startDateSetup { switch startDateSetup {
case .customDate: case .customDate:
break break

@ -17,6 +17,11 @@ struct EventListView: View {
let tournaments: [FederalTournamentHolder] let tournaments: [FederalTournamentHolder]
let sortAscending: Bool let sortAscending: Bool
var lastDataSource: Date? {
guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil }
return URL.importDateFormatter.date(from: _lastDataSource)
}
var body: some View { var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth } let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
switch viewStyle { switch viewStyle {
@ -101,6 +106,41 @@ struct EventListView: View {
@ViewBuilder @ViewBuilder
private func _options(_ pcTournaments: [Tournament]) -> some View { private func _options(_ pcTournaments: [Tournament]) -> some View {
if let lastDataSource, pcTournaments.anySatisfy({ $0.rankSourceShouldBeRefreshed() != nil && $0.hasEnded() == false }) {
Section {
Button {
Task {
do {
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == lastDataSource }
guard !dataURLs.isEmpty else { return } // Early return if no files found
let sources = dataURLs.map { CSVParser(url: $0) }
let chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
try await pcTournaments.concurrentForEach { tournament in
if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
try await tournament.updateRank(to: mostRecentDate, forceRefreshLockWeight: false, providedSources: chunkedParsers)
}
}
try chunkedParsers.forEach { chunk in
try FileManager.default.removeItem(at: chunk.url)
}
try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
} catch {
Logger.error(error)
}
}
} label: {
Text("Rafraîchir les classements")
}
} header: {
Text("Source disponible : \(lastDataSource.monthYearFormatted)")
}
Divider()
}
Section { Section {
if pcTournaments.anySatisfy({ $0.isPrivate == true }) { if pcTournaments.anySatisfy({ $0.isPrivate == true }) {
Button { Button {
@ -135,7 +175,7 @@ struct EventListView: View {
Text("Visibilité sur Padel Club") Text("Visibilité sur Padel Club")
} }
Divider() Divider()
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) {
Section { Section {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) { if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) {
Button { Button {
@ -152,7 +192,23 @@ struct EventListView: View {
} }
} }
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) { if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) {
Button {
Task {
do {
try await pcTournaments.concurrentForEach { tournament in
try await tournament.refreshTeamList()
}
} catch {
Logger.error(error)
}
}
} label: {
Text("Rafraîchir la liste des équipes inscrites en ligne")
}
Button { Button {
pcTournaments.forEach { tournament in pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = false tournament.enableOnlineRegistration = false
@ -207,6 +263,13 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View { private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) { NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver()) TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver())
.task {
do {
try await tournament.refreshTeamList()
} catch {
Logger.error(error)
}
}
} }
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
.contextMenu { .contextMenu {

@ -22,7 +22,7 @@ struct MatchFormatStorageView: View {
var body: some View { var body: some View {
Section { Section {
LabeledContent { LabeledContent {
StepperView(title: "minutes", count: $estimatedDuration, step: 5) StepperView(title: "minute", count: $estimatedDuration, step: 5)
} label: { } label: {
MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) 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 parallelType: Bool = false
@State private var deletingDateMatchesDone: Bool = false @State private var deletingDateMatchesDone: Bool = false
@State private var deletingDone: Bool = false @State private var deletingDone: Bool = false
@State private var presentFormatHelperView: Bool = false
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -145,6 +146,28 @@ struct PlanningSettingsView: View {
_smartView() _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) .headerProminence(.increased)
.onAppear { .onAppear {
do { do {

@ -158,7 +158,7 @@ struct RoundSettingsView: View {
private func _removeAllSeeds() async { private func _removeAllSeeds() async {
await tournament.removeAllSeeds() await tournament.removeAllSeeds(saveTeamsAtTheEnd: true)
self.isEditingTournamentSeed.wrappedValue = true self.isEditingTournamentSeed.wrappedValue = true
} }

@ -217,6 +217,11 @@ struct FollowUpMatchView: View {
} }
#if DEBUG #if DEBUG
Spacer() Spacer()
if let roundObject = match.roundObject {
Text(roundObject.index.formatted())
Text(roundObject.theoryCumulativeMatchCount.formatted())
}
Text(match.computedOrder.formatted())
FooterButtonView("copier l'id") { FooterButtonView("copier l'id") {
let pasteboard = UIPasteboard.general let pasteboard = UIPasteboard.general
pasteboard.string = match.id pasteboard.string = match.id

@ -427,7 +427,9 @@ struct MySearchView: View {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true)
.contentShape(Rectangle())
} }
.frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} }
} header: { } header: {

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

@ -20,11 +20,11 @@ struct EditingTeamView: View {
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false @State private var showSubscriptionView: Bool = false
@State private var registrationDate : Date @State private var registrationDate : Date
@State private var walkOut : Bool
@State private var wildCardBracket : Bool
@State private var wildCardGroupStage : Bool
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys? @FocusState private var focusedField: TeamRegistration.CodingKeys?
@State private var presentOnlineRegistrationWarning: Bool = false
@State private var currentWaitingList: TeamRegistration?
@State private var presentTeamToWarn: Bool = false
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
@ -36,6 +36,22 @@ struct EditingTeamView: View {
} }
} }
var hasChanged: Binding<Bool> {
Binding {
if canSaveWithoutWarning() {
return false
}
return
registrationDate != team.registrationDate
|| walkOut != team.walkOut
|| wildCardBracket != team.wildCardBracket
|| wildCardGroupStage != team.wildCardGroupStage
} set: { _ in
}
}
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
@ -44,24 +60,19 @@ struct EditingTeamView: View {
self.team = team self.team = team
_name = .init(wrappedValue: team.name ?? "") _name = .init(wrappedValue: team.name ?? "")
_registrationDate = State(wrappedValue: team.registrationDate ?? Date()) _registrationDate = State(wrappedValue: team.registrationDate ?? Date())
_walkOut = State(wrappedValue: team.walkOut)
_wildCardBracket = State(wrappedValue: team.wildCardBracket)
_wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage)
} }
private func _resetTeam() { private func _resetTeam() {
self.currentWaitingList = tournament.waitingListSortedTeams().filter({ $0.hasRegisteredOnline() }).first
team.resetPositions() team.resetPositions()
team.qualified = false
team.wildCardGroupStage = false team.wildCardGroupStage = false
team.walkOut = false team.walkOut = false
team.wildCardBracket = false team.wildCardBracket = false
} }
private func _checkOnlineRegistrationWarning() {
guard let currentWaitingList else { return }
let selectedSortedTeams = tournament.selectedSortedTeams().map({ $0.id })
if selectedSortedTeams.contains(currentWaitingList.id) {
presentOnlineRegistrationWarning = true
}
}
var body: some View { var body: some View {
List { List {
Section { Section {
@ -91,10 +102,6 @@ struct EditingTeamView: View {
.headerProminence(.increased) .headerProminence(.increased)
Section { Section {
DatePicker(selection: $registrationDate) {
Text("Inscription")
Text(registrationDate.localizedWeekDay().capitalized)
}
if let callDate = team.callDate { if let callDate = team.callDate {
LabeledContent() { LabeledContent() {
Text(callDate.localizedDate()) Text(callDate.localizedDate())
@ -116,34 +123,23 @@ struct EditingTeamView: View {
Text("Équipe sur place") Text("Équipe sur place")
} }
} }
}
Toggle(isOn: .init(get: { Section {
return team.wildCardBracket DatePicker(selection: $registrationDate) {
}, set: { value in Text("Inscription")
_resetTeam() Text(registrationDate.localizedWeekDay().capitalized)
team.wildCardBracket = value }
_save()
})) { Toggle(isOn: $wildCardBracket) {
Text("Wildcard Tableau") Text("Wildcard Tableau")
} }
Toggle(isOn: .init(get: { Toggle(isOn: $wildCardGroupStage) {
return team.wildCardGroupStage
}, set: { value in
_resetTeam()
team.wildCardGroupStage = value
_save()
})) {
Text("Wildcard Poule") Text("Wildcard Poule")
} }.disabled(tournament.groupStageCount == 0)
Toggle(isOn: .init(get: { Toggle(isOn: $walkOut) {
return team.walkOut
}, set: { value in
_resetTeam()
team.walkOut = value
_save()
})) {
Text("Forfait") Text("Forfait")
} }
} }
@ -215,30 +211,24 @@ struct EditingTeamView: View {
} }
} }
} }
.sheet(isPresented: $presentTeamToWarn) { .alert("Attention", isPresented: hasChanged, actions: {
if let currentWaitingList { Button("Confirmer") {
NavigationStack { _resetTeam()
EditingTeamView(team: currentWaitingList) team.registrationDate = registrationDate
} team.wildCardBracket = wildCardBracket
.tint(.master) team.wildCardGroupStage = wildCardGroupStage
team.walkOut = walkOut
_save()
} }
}
.alert("Attention", isPresented: $presentOnlineRegistrationWarning, actions: {
if currentWaitingList != nil {
Button("Voir l'équipe") { Button("Annuler", role: .cancel) {
self.presentTeamToWarn = true registrationDate = team.registrationDate ?? Date()
} walkOut = team.walkOut
wildCardBracket = team.wildCardBracket
Button("OK") { wildCardGroupStage = team.wildCardGroupStage
self.currentWaitingList = nil
self.presentOnlineRegistrationWarning = false
}
} }
}, message: { }, message: {
if let currentWaitingList { Text("Ce changement peut entraîner l'entrée ou la sortie d'une équipe de votre sélection. Padel Club préviendra automatiquement une équipe inscrite en ligne de son nouveau statut.")
Text("L'équipe \(currentWaitingList.teamLabel(separator: "/")), inscrite en ligne, rentre dans votre sélection suite à la modification que vous venez de faire, voulez-vous les prévenir ?")
}
}) })
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
@ -326,14 +316,29 @@ struct EditingTeamView: View {
} }
} }
.onChange(of: registrationDate) { .onChange(of: registrationDate) {
team.registrationDate = registrationDate if canSaveWithoutWarning() {
_save() team.registrationDate = registrationDate
_save()
}
}
.onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) {
if canSaveWithoutWarning() {
_resetTeam()
team.walkOut = walkOut
team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage
_save()
}
} }
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Édition de l'équipe") .navigationTitle("Édition de l'équipe")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
func canSaveWithoutWarning() -> Bool {
(tournament.shouldWarnOnlineRegistrationUpdates() && tournament.teamCount <= tournament.unsortedTeamsCount()) == false
}
private var confirmationReceived: Binding<Bool> { private var confirmationReceived: Binding<Bool> {
Binding { Binding {
team.confirmed() team.confirmed()
@ -368,8 +373,6 @@ struct EditingTeamView: View {
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
_checkOnlineRegistrationWarning()
} }
private var _networkErrorMessage: String { private var _networkErrorMessage: String {

@ -134,11 +134,19 @@ struct TeamPickerView: View {
// presentTeamPickerView = false // presentTeamPickerView = false
// } // }
} label: { } label: {
TeamRowView(team: team) VStack(alignment: .leading) {
.contentShape(Rectangle()) if let roundAndMatchTitle = team.bracketMatchTitleAndQualifiedStatus() {
Text(roundAndMatchTitle)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
}
TeamRowView(team: team)
}
.contentShape(Rectangle())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
.id(team.id)
.listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true) .listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) { // Button("Retirer du tableau", role: .destructive) {

@ -9,17 +9,22 @@ import SwiftUI
struct TeamRowView: View { struct TeamRowView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
var team: TeamRegistration var team: TeamRegistration
var teamPosition: TeamPosition? = nil var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false var displayCallDate: Bool = false
var displayRestingTime: Bool = false var displayRestingTime: Bool = false
var teamIndex: Int?
var body: some View { var body: some View {
LabeledContent { LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition, teamIndex: teamIndex)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
TeamHeadlineView(team: team) if isEditingTournamentSeed.wrappedValue == false {
TeamHeadlineView(team: team)
}
TeamView(team: team) TeamView(team: team)
} }
if displayCallDate { if displayCallDate {

@ -120,10 +120,10 @@ struct FileImportView: View {
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight) return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight)
} }
private func _deleteTeams() async { private func _deleteTeams(teams: [TeamRegistration]) async {
await MainActor.run { await MainActor.run {
do { do {
try tournamentStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams()) try tournamentStore.teamRegistrations.delete(contentOfs: teams)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -140,9 +140,18 @@ struct FileImportView: View {
} }
} }
if tournament.unsortedTeams().count > 0, tournament.enableOnlineRegistration == false { let unsortedTeams = tournament.unsortedTeams()
RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) { let onlineTeams = unsortedTeams.filter({ $0.hasRegisteredOnline() })
await _deleteTeams() if unsortedTeams.count > 0 {
Section {
RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) {
await _deleteTeams(teams: unsortedTeams)
}
.disabled(onlineTeams.isEmpty == false)
} footer: {
if onlineTeams.isEmpty == false {
Text("Ce tournoi contient des inscriptions en ligne, vous ne pouvez pas effacer toute votre liste d'inscription d'un coup.")
}
} }
} }
@ -312,7 +321,7 @@ struct FileImportView: View {
} }
} else if didImport { } else if didImport {
let _filteredTeams = filteredTeams let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams() let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams())
if previousTeams.isEmpty == false { if previousTeams.isEmpty == false {
Section { Section {

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

@ -42,24 +42,7 @@ struct UpdateSourceRankDateView: View {
updatingRank = true updatingRank = true
Task { Task {
do { do {
try await tournament.updateRank(to: currentRankSourceDate) try await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil)
let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament)
}
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
let unsortedTeams = tournament.unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
if forceRefreshLockWeight {
team.lockedWeight = team.weight
}
}
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try dataStore.tournaments.addOrUpdate(instance: tournament) try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)

@ -51,7 +51,6 @@ struct InscriptionManagerView: View {
@State private var pasteString: String? @State private var pasteString: String?
@State private var registrationIssues: Int? = nil @State private var registrationIssues: Int? = nil
@State private var refreshResult: String? = nil @State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool? @State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false @State private var showLegendView: Bool = false
@ -180,7 +179,7 @@ struct InscriptionManagerView: View {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2) return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
} }
private func _setHash() { private func _setHash(currentSelectedSortedTeams: [TeamRegistration]? = nil) {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -188,18 +187,17 @@ struct InscriptionManagerView: View {
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let selectedSortedTeams = tournament.selectedSortedTeams() let selectedSortedTeams = currentSelectedSortedTeams == nil ? tournament.selectedSortedTeams() : currentSelectedSortedTeams!
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
self.registrationIssues = nil self.registrationIssues = nil
DispatchQueue.main.async { Task {
self.registrationIssues = tournament.registrationIssues() self.registrationIssues = await tournament.registrationIssues(selectedTeams: selectedSortedTeams)
} }
} }
private func _handleHashDiff() { private func _handleHashDiff(selectedSortedTeams: [TeamRegistration]) {
let selectedSortedTeams = tournament.selectedSortedTeams()
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) { if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash self.teamsHash = newHash
@ -225,9 +223,10 @@ struct InscriptionManagerView: View {
} }
var body: some View { var body: some View {
Group { let selectedSortedTeams = tournament.selectedSortedTeams()
if tournament.unsortedTeams().isEmpty == false { return Group {
_teamRegisteredView() if tournament.unsortedTeamsCount() > 0 {
_teamRegisteredView(selectedSortedTeams: selectedSortedTeams)
} else { } else {
List { List {
@ -248,7 +247,7 @@ struct InscriptionManagerView: View {
if tournament.enableOnlineRegistration { if tournament.enableOnlineRegistration {
RowButtonView("Rafraîchir la liste", cornerRadius: 20) { RowButtonView("Rafraîchir la liste", cornerRadius: 20) {
await _refreshList() await _refreshList(forced: true)
} }
} else if tournament.onlineRegistrationCanBeEnabled() { } else if tournament.onlineRegistrationCanBeEnabled() {
RowButtonView("Inscription en ligne") { RowButtonView("Inscription en ligne") {
@ -259,14 +258,19 @@ struct InscriptionManagerView: View {
} }
} }
} }
.task {
await _refreshList(forced: false)
}
.refreshable { .refreshable {
await _refreshList() await _refreshList(forced: true)
} }
.onAppear { .onAppear {
_setHash() if tournament.enableOnlineRegistration == false || refreshStatus == true {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
} }
.onDisappear { .onDisappear {
_handleHashDiff() _handleHashDiff(selectedSortedTeams: selectedSortedTeams)
} }
.sheet(isPresented: $isLearningMore) { .sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament) LearnMoreSheetView(tournament: tournament)
@ -490,47 +494,43 @@ struct InscriptionManagerView: View {
tournament.unsortedPlayers() tournament.unsortedPlayers()
} }
var sortedTeams: [TeamRegistration] { func sortedTeams(selectedSortedTeams: [TeamRegistration]) -> [TeamRegistration] {
if filterMode == .waiting { if filterMode == .waiting {
return tournament.waitingListSortedTeams() return tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams)
} else { } else {
return tournament.sortedTeams() return tournament.sortedTeams(selectedSortedTeams: selectedSortedTeams)
} }
} }
var filteredTeams: [TeamRegistration] { func filteredTeams(sortedTeams: [TeamRegistration]) -> [TeamRegistration] {
let filtered = sortedTeams.lazy.filter { team in
var teams = sortedTeams switch filterMode {
switch filterMode { case .wildcardBracket:
case .wildcardBracket: return team.wildCardBracket
teams = teams.filter({ $0.wildCardBracket }) case .wildcardGroupStage:
case .wildcardGroupStage: return team.wildCardGroupStage
teams = teams.filter({ $0.wildCardGroupStage }) case .walkOut:
case .walkOut: return team.walkOut
teams = teams.filter({ $0.walkOut }) case .bracket:
case .bracket: return team.inRound() && !team.inGroupStage()
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) case .groupStage:
case .groupStage: return team.inGroupStage()
teams = teams.filter({ $0.inGroupStage() }) case .notImported:
case .notImported: return !team.isImported()
teams = teams.filter({ $0.isImported() == false }) case .registeredLocally:
case .registeredLocally: return !team.hasRegisteredOnline()
teams = teams.filter({ $0.hasRegisteredOnline() == false }) case .registeredOnline:
case .registeredOnline: return team.hasRegisteredOnline()
teams = teams.filter({ $0.hasRegisteredOnline() == true }) default:
default: return true
break }
} }
if sortingMode == .registrationDate { let sorted = sortingMode == .registrationDate
teams = teams.sorted(by: \.computedRegistrationDate) ? filtered.sorted(by: { $0.computedRegistrationDate < $1.computedRegistrationDate })
} : Array(filtered)
if byDecreasingOrdering { return byDecreasingOrdering ? sorted.reversed() : sorted
return teams.reversed()
} else {
return teams
}
} }
// private func _fixModel() { // private func _fixModel() {
@ -546,38 +546,36 @@ struct InscriptionManagerView: View {
// try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) // try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// } // }
// //
private func _refreshList() async { private func _refreshList(forced: Bool) async {
if refreshInProgress { return } if refreshStatus == true, forced == false { return }
if tournament.enableOnlineRegistration == false { return }
if tournament.hasEnded() { return }
if tournament.refreshInProgress { return }
refreshResult = nil refreshResult = nil
refreshStatus = nil refreshStatus = nil
refreshInProgress = true
do { do {
try await self.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamScores.loadDataFromServerIfAllowed(clear: true) try await self.tournament.refreshTeamList()
try await self.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
_setHash() _setHash()
self.refreshResult = "la synchronization a réussi" self.refreshResult = "La synchronization a réussi"
self.refreshStatus = true self.refreshStatus = true
refreshInProgress = false
} catch { } catch {
Logger.error(error) Logger.error(error)
self.refreshResult = "la synchronization a échoué" self.refreshResult = "La synchronization a échoué"
self.refreshStatus = false self.refreshStatus = false
refreshInProgress = false tournament.refreshInProgress = false
} }
} }
private func _teamRegisteredView() -> some View { private func _teamRegisteredView(selectedSortedTeams: [TeamRegistration]) -> some View {
List { List {
let selectedSortedTeams = tournament.selectedSortedTeams()
if presentSearch == false { if presentSearch == false {
_informationView() _informationView(for: selectedSortedTeams)
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
_rankHandlerView() _rankHandlerView()
@ -585,7 +583,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 { if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView { ContentUnavailableView {
@ -622,7 +621,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team) EditingTeamView(team: team)
.environment(tournament) .environment(tournament)
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team, teamIndex: teamIndex)
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
if tournament.enableOnlineRegistration == false { if tournament.enableOnlineRegistration == false {
@ -722,7 +721,7 @@ struct InscriptionManagerView: View {
@ViewBuilder @ViewBuilder
private func _rankHandlerView() -> some View { private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false { if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
Section { Section {
TipView(rankUpdateTip) { action in TipView(rankUpdateTip) { action in
self.currentRankSourceDate = mostRecentDate self.currentRankSourceDate = mostRecentDate
@ -735,18 +734,18 @@ struct InscriptionManagerView: View {
} }
} }
private func _teamCountForFilterMode(filterMode: FilterMode) -> String { private func _teamCountForFilterMode(filterMode: FilterMode, in teams: [TeamRegistration]) -> String {
switch filterMode { switch filterMode {
case .wildcardBracket: case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted() return teams.filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage: case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted() return teams.filter({ $0.wildCardGroupStage }).count.formatted()
case .all: case .all:
return unsortedTeamsWithoutWO.count.formatted() return unsortedTeamsWithoutWO.count.formatted()
case .bracket: 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: case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted() return teams.filter({ $0.inGroupStage() }).count.formatted()
case .walkOut: case .walkOut:
let wo = walkoutTeams.count.formatted() let wo = walkoutTeams.count.formatted()
return wo return wo
@ -754,20 +753,20 @@ struct InscriptionManagerView: View {
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
return waiting.formatted() return waiting.formatted()
case .notImported: 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() return notImported.formatted()
case .registeredLocally: 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() return registeredLocally.formatted()
case .registeredOnline: 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() return registeredOnline.formatted()
} }
} }
@ViewBuilder @ViewBuilder
private func _informationView() -> some View { private func _informationView(for teams: [TeamRegistration]) -> some View {
Section { Section {
HStack { HStack {
// VStack(alignment: .leading, spacing: 0) { // VStack(alignment: .leading, spacing: 0) {
@ -781,7 +780,7 @@ struct InscriptionManagerView: View {
// } // }
// //
ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in
_filterModeView(filterMode: filterMode) _filterModeView(filterMode: filterMode, in: teams)
} }
Button { Button {
@ -809,7 +808,7 @@ struct InscriptionManagerView: View {
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
HStack { HStack {
ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in
_filterModeView(filterMode: filterMode) _filterModeView(filterMode: filterMode, in: teams)
} }
} }
.padding(.bottom, -4) .padding(.bottom, -4)
@ -844,28 +843,21 @@ struct InscriptionManagerView: View {
// } // }
if tournament.enableOnlineRegistration { if tournament.enableOnlineRegistration {
Button { LabeledContent {
Task { Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
await _refreshList() .font(.largeTitle)
}
} label: { } label: {
LabeledContent { Text("Inscriptions en ligne")
if refreshInProgress { if let refreshResult {
ProgressView() Text(refreshResult).foregroundStyle(.secondary)
} else if let refreshStatus { } else {
if refreshStatus { Text(" ")
Image(systemName: "checkmark").foregroundStyle(.green).font(.headline)
} else {
Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline)
}
}
} label: {
Text("Récupérer les inscriptions en ligne")
if let refreshResult {
Text(refreshResult)
}
} }
} }
RowButtonView("Rafraîchir les inscriptions en ligne") {
await _refreshList(forced: true)
}
} }
} header: { } header: {
HStack { HStack {
@ -883,7 +875,7 @@ struct InscriptionManagerView: View {
} }
} }
private func _filterModeView(filterMode: FilterMode) -> some View { private func _filterModeView(filterMode: FilterMode, in teams: [TeamRegistration]) -> some View {
Button { Button {
if self.filterMode == filterMode { if self.filterMode == filterMode {
@ -894,7 +886,7 @@ struct InscriptionManagerView: View {
} label: { } label: {
VStack(alignment: .center, spacing: -2) { VStack(alignment: .center, spacing: -2) {
Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8) 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) .frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())

@ -72,8 +72,18 @@ struct RegistrationSetupView: View {
} }
func displayWarning() -> Bool {
let unsortedTeamsCount = tournament.unsortedTeamsCount()
return tournament.shouldWarnOnlineRegistrationUpdates() && targetTeamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || targetTeamCount <= unsortedTeamsCount)
}
var body: some View { var body: some View {
List { List {
if displayWarning() {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.")
.foregroundStyle(.logoRed)
}
Section { Section {
Toggle(isOn: $enableOnlineRegistration) { Toggle(isOn: $enableOnlineRegistration) {
Text("Activer") Text("Activer")

@ -23,6 +23,11 @@ struct TableStructureView: View {
@State private var buildWildcards: Bool = true @State private var buildWildcards: Bool = true
@FocusState private var stepperFieldIsFocused: Bool @FocusState private var stepperFieldIsFocused: Bool
func displayWarning() -> Bool {
let unsortedTeamsCount = tournament.unsortedTeamsCount()
return tournament.shouldWarnOnlineRegistrationUpdates() && teamCount != tournament.teamCount && (tournament.teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount)
}
var qualifiedFromGroupStage: Int { var qualifiedFromGroupStage: Int {
groupStageCount * qualifiedPerGroupStage groupStageCount * qualifiedPerGroupStage
} }
@ -43,7 +48,7 @@ struct TableStructureView: View {
var moreQualifiedLabel: String { var moreQualifiedLabel: String {
if groupStageAdditionalQualified == 0 { return "Aucun" } if groupStageAdditionalQualified == 0 { return "Aucun" }
return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " meilleur\(groupStageAdditionalQualified.pluralSuffix) " + (qualifiedPerGroupStage + 1).ordinalFormatted() return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " " + (qualifiedPerGroupStage + 1).ordinalFormatted()
} }
var maxGroupStages: Int { var maxGroupStages: Int {
@ -58,6 +63,10 @@ struct TableStructureView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
List { List {
if displayWarning() {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.")
.foregroundStyle(.logoRed)
}
if tournament.state() != .build { if tournament.state() != .build {
Section { Section {
@ -252,6 +261,16 @@ struct TableStructureView: View {
} }
} }
if tournament.rounds().isEmpty {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
tournament.buildBracket(minimalBracketTeamCount: 4)
}
} footer: {
Text("Vous pourrez ensuite modifier le nombre de tour dans l'écran de réglages du tableau.")
}
}
if tournament.state() != .initial { if tournament.state() != .initial {
Section { Section {

@ -114,7 +114,9 @@ struct TournamentCellView: View {
} }
Spacer() Spacer()
if let tournament = tournament as? Tournament, displayStyle == .wide { if let tournament = tournament as? Tournament, displayStyle == .wide {
if tournament.isCanceled { if tournament.refreshInProgress || tournament.refreshRanking {
ProgressView()
} else if tournament.isCanceled {
Text("Annulé".uppercased()) Text("Annulé".uppercased())
.capsule(foreground: .white, background: .logoRed) .capsule(foreground: .white, background: .logoRed)
} else if shouldTournamentBeOver { } else if shouldTournamentBeOver {
@ -164,6 +166,17 @@ struct TournamentCellView: View {
Text(build.category.localizedLabel()) Text(build.category.localizedLabel())
Text(build.age.localizedFederalAgeLabel()) Text(build.age.localizedFederalAgeLabel())
} }
if displayStyle == .wide, let tournament = tournament as? Tournament {
if tournament.enableOnlineRegistration {
let value: Int = tournament.onlineTeams().count
HStack {
Spacer()
if value > 0 {
Text("(dont " + value.formatted() + " inscrite\(value.pluralSuffix) en ligne)")
}
}
}
}
} }
} }
.font(.caption) .font(.caption)

Loading…
Cancel
Save