sync2
Laurent 8 months ago
commit a16b104757
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/AppDelegate.swift
  3. 15
      PadelClub/Data/DataStore.swift
  4. 12
      PadelClub/Data/Federal/FederalTournament.swift
  5. 16
      PadelClub/Data/GroupStage.swift
  6. 13
      PadelClub/Data/Match.swift
  7. 25
      PadelClub/Data/PlayerRegistration.swift
  8. 23
      PadelClub/Data/TeamRegistration.swift
  9. 132
      PadelClub/Data/Tournament.swift
  10. 22
      PadelClub/Extensions/Sequence+Extensions.swift
  11. 2
      PadelClub/InscriptionLegendView.swift
  12. 60
      PadelClub/OnlineRegistrationWarningView.swift
  13. 4
      PadelClub/Utils/Network/NetworkManager.swift
  14. 69
      PadelClub/Utils/SwiftParser.swift
  15. 12
      PadelClub/Views/Calling/CallView.swift
  16. 29
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  17. 81
      PadelClub/Views/GroupStage/GroupStagesView.swift
  18. 2
      PadelClub/Views/Match/Components/MatchDateView.swift
  19. 2
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  20. 2
      PadelClub/Views/Match/MatchDetailView.swift
  21. 6
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  22. 64
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  23. 2
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  24. 2
      PadelClub/Views/Player/PlayerView.swift
  25. 2
      PadelClub/Views/Round/RoundSettingsView.swift
  26. 5
      PadelClub/Views/Score/FollowUpMatchView.swift
  27. 2
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  28. 124
      PadelClub/Views/Team/EditingTeamView.swift
  29. 8
      PadelClub/Views/Team/TeamPickerView.swift
  30. 4
      PadelClub/Views/Team/TeamRowView.swift
  31. 31
      PadelClub/Views/Tournament/FileImportView.swift
  32. 10
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  33. 19
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  34. 65
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  35. 10
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  36. 21
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  37. 20
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  38. 10
      PadelClub/Views/Tournament/TournamentBuildView.swift
  39. 6
      PadelClub/Views/Tournament/TournamentView.swift

@ -3624,7 +3624,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3650,7 +3650,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.13;
MARKETING_VERSION = 1.1.20;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3670,7 +3670,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3695,7 +3695,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.13;
MARKETING_VERSION = 1.1.20;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3792,7 +3792,6 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3821,7 +3820,6 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -3839,7 +3837,6 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
@ -3869,7 +3866,6 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = PRODTEST;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};

@ -23,6 +23,8 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel
return true
}
// MARK: - Remote Notifications
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if StoreCenter.main.hasToken() {

@ -103,6 +103,11 @@ class DataStore: ObservableObject {
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(_willEnterForegroundNotification),
name: UIScene.willEnterForegroundNotification,
object: nil)
}
@ -120,10 +125,6 @@ class DataStore: ObservableObject {
@objc func collectionDidLoad(notification: Notification) {
DispatchQueue.main.async {
self.objectWillChange.send()
}
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> {
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if let clubsCollection: StoredCollection<Club> = notification.object as? StoredCollection<Club> {
@ -166,6 +167,12 @@ class DataStore: ObservableObject {
self.objectWillChange.send()
}
@objc func _willEnterForegroundNotification() {
Task {
try await self.purchases.loadDataFromServerIfAllowed(clear: true)
}
}
func disconnect() {
Task {

@ -236,12 +236,12 @@ struct CategorieAge: Codable {
var tournamentAge: FederalTournamentAge? {
if let id {
return FederalTournamentAge(rawValue: id)
return FederalTournamentAge(rawValue: id) ?? .senior
}
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 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? {
if let code, let value = Int(code.removingFirstCharacter) {
return TournamentLevel(rawValue: value)
return TournamentLevel(rawValue: value) ?? .p100
}
return nil
return .p100
}
}

@ -402,7 +402,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
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)
@ -457,11 +457,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
var scoreCache: [Int: TeamGroupStageScore] = [:]
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
if sortedByScore {
return unsortedTeams().compactMap({ team in
// Check cache or use provided scores, otherwise calculate and store in cache
scores?.first(where: { $0.team.id == team.id }) ?? {
func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? {
if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] {
return cachedScore
} else {
@ -471,6 +467,14 @@ final class GroupStage: BaseGroupStage, SideStorable {
}
return score
}
}
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
if sortedByScore {
return unsortedTeams().compactMap({ team in
// Check cache or use provided scores, otherwise calculate and store in cache
scores?.first(where: { $0.team.id == team.id }) ?? {
return computedScore(forTeam: team, step: step)
}()
}).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [

@ -354,6 +354,8 @@ defer {
}
}
//byeState = false
if state != currentState {
roundObject?._cachedSeedInterval = nil
name = nil
do {
@ -361,6 +363,7 @@ defer {
} catch {
Logger.error(error)
}
}
if single == false {
_toggleLoserMatchDisableState(state)
@ -459,7 +462,7 @@ defer {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(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] {
@ -484,9 +487,15 @@ defer {
func setWalkOut(_ teamPosition: TeamPosition) {
let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition))
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))
teamScoreWinning.walkOut = nil
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",")
do {
try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
} catch {
Logger.error(error)
}
if endDate == nil {
endDate = Date()

@ -276,21 +276,23 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return await withTaskGroup(of: Line?.self) { group in
for source in filteredSources {
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);") }
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
for await result in group {
if let result {
group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
}
return nil
}
}
func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME
#if DEBUG
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -313,20 +315,17 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
}
if let first = await group.first(where: { $0 != nil }) {
group.cancelAll()
return first
for await result in group {
if let result {
group.cancelAll() // Stop other tasks as soon as we find a match
return result
}
}
return nil
}
}
func setComputedRank(in tournament: Tournament) {
if tournament.isAnimation() {
computedRank = rank ?? 0
return
}
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000
switch tournament.tournamentCategory {
case .men:

@ -369,11 +369,11 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
func initialRoundColor() -> Color? {
if walkOut { return Color.logoRed }
if groupStagePosition != nil { return Color.blue }
if let initialRound = initialRound(),
let colorHex = RoundRule.colors[safe: initialRound.index]
{
if groupStagePosition != nil || wildCardGroupStage { return Color.blue }
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] {
return Color(uiColor: .init(fromHex: colorHex))
} else if wildCardBracket {
return Color.mint
} else {
return nil
}
@ -582,11 +582,8 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount()
weight =
(players.prefix(significantPlayerCount).map { $0.computedRank }
+ missingPlayerType(inTournamentCategory: tournamentCategory).map {
unrankValue(for: $0 == 1 ? true : false)
}).prefix(significantPlayerCount).reduce(0, +)
let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending)
weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
}
func significantPlayerCount() -> Int {
@ -703,6 +700,14 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
unsortedPlayers().count > 0
}
func bracketMatchTitleAndQualifiedStatus() -> String? {
let values = [qualified ? "Qualifié" : nil, initialMatch()?.roundAndMatchTitle()].compactMap({ $0 })
if values.isEmpty {
return nil
}
return values.joined(separator: " -> ")
}
func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {

@ -12,6 +12,16 @@ import SwiftUI
@Observable
final class Tournament: BaseTournament {
//local variable
var refreshInProgress: Bool = false
var lastTeamRefresh: Date?
var refreshRanking: Bool = false
func shouldRefreshTeams() -> Bool {
guard let lastTeamRefresh else { return true }
return lastTeamRefresh.timeIntervalSinceNow < -60
}
@ObservationIgnored
var navigationPath: [Screen] = []
@ -620,7 +630,7 @@ defer {
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
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
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
@ -664,8 +674,8 @@ defer {
}
}
func bracketCut(teamCount: Int) -> Int {
return max(0, teamCount - groupStageCut())
func bracketCut(teamCount: Int, groupStageCut: Int) -> Int {
return self.teamCount - groupStageCut
}
func groupStageCut() -> Int {
@ -674,10 +684,12 @@ defer {
func cutLabel(index: Int, teamCount: Int?) -> String {
let _teamCount = teamCount ?? selectedSortedTeams().count
let bracketCut = bracketCut(teamCount: _teamCount)
let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut {
return "Tableau"
} else if index - bracketCut < groupStageCut() && _teamCount > 0 {
} else if index - bracketCut < groupStageCut && _teamCount > 0 {
return "Poule"
} else {
return "Attente"
@ -687,11 +699,12 @@ defer {
func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
guard let index else { return Color.gray }
let _teamCount = teamCount ?? selectedSortedTeams().count
let bracketCut = bracketCut(teamCount: _teamCount)
let groupStageCut = groupStageCut()
let bracketCut = bracketCut(teamCount: _teamCount, groupStageCut: groupStageCut)
if index < bracketCut {
return Color.mint
} else if index - bracketCut < groupStageCut() && _teamCount > 0 {
return Color.cyan
} else if index - bracketCut < groupStageCut && _teamCount > 0 {
return Color.indigo
} else {
return Color.gray
}
@ -882,7 +895,7 @@ defer {
}
}
func registrationIssues(selectedTeams: [TeamRegistration]) -> Int {
func registrationIssues(selectedTeams: [TeamRegistration]) async -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
@ -1188,8 +1201,8 @@ defer {
self.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
func updateRank(to newDate: Date?) async throws {
func updateRank(to newDate: Date?, forceRefreshLockWeight: Bool, providedSources: [CSVParser]?) async throws {
refreshRanking = true
#if DEBUG_TIME
let start = Date()
defer {
@ -1225,16 +1238,42 @@ defer {
let lastRankMan = monthData?.maleUnrankedValue ?? 0
let lastRankWoman = monthData?.femaleUnrankedValue ?? 0
var chunkedParsers: [CSVParser] = []
if let providedSources {
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 players = unsortedPlayers()
try await players.concurrentForEach { player in
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
}
@ -1278,7 +1317,7 @@ defer {
}
func hideWeight() -> Bool {
return hideTeamsWeight || tournamentLevel.hideWeight()
return hideTeamsWeight
}
func isAnimation() -> Bool {
@ -1537,6 +1576,7 @@ defer {
}
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
team.weight += 200_000
return team
}
@ -1584,9 +1624,10 @@ defer {
return bracketTeamCount
}
func buildBracket() {
func buildBracket(minimalBracketTeamCount: Int? = nil) {
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
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
@ -1601,7 +1642,6 @@ defer {
} catch {
Logger.error(error)
}
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
@ -2079,14 +2119,17 @@ defer {
func updateSeedsBracketPosition() async {
await removeAllSeeds()
await removeAllSeeds(saveTeamsAtTheEnd: false)
let drawLogs = drawLogs().reversed()
let seeds = seeds()
await MainActor.run {
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
}
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: seeds)
@ -2095,11 +2138,13 @@ defer {
}
}
func removeAllSeeds() async {
func removeAllSeeds(saveTeamsAtTheEnd: Bool) async {
let teams = unsortedTeams()
teams.forEach({ team in
team.bracketPosition = nil
team._cachedRestingTime = nil
team.finalRanking = nil
team.pointsEarned = nil
})
let allMatches = allRoundMatches()
let ts = allMatches.flatMap { match in
@ -2120,18 +2165,13 @@ defer {
Logger.error(error)
}
do {
try tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
} catch {
Logger.error(error)
}
if saveTeamsAtTheEnd {
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
updateTournamentState()
}
}
func addNewRound(_ roundIndex: Int) async {
@ -2238,8 +2278,7 @@ defer {
}
// MARK: - Status
func shouldTournamentBeOver() -> Bool {
func shouldTournamentBeOver() async -> Bool {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
@ -2263,6 +2302,45 @@ defer {
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 {
enableOnlineRegistration && onlineTeams().isEmpty == false && hasEnded() == false && hasStarted() == false
}
func refreshTeamList() async {
guard StoreCenter.main.hasToken() else { return }
guard shouldRefreshTeams(), refreshInProgress == false, enableOnlineRegistration, hasEnded() == false else { return }
await MainActor.run {
refreshInProgress = true
}
do {
try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
await MainActor.run {
refreshInProgress = false
lastTeamRefresh = Date()
}
} catch {
Logger.error(error)
await MainActor.run {
refreshInProgress = false
lastTeamRefresh = Date()
}
}
}
// MARK: -
func insertOnServer() throws {

@ -32,19 +32,35 @@ extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async throws -> Void
) async throws {
// A task group automatically waits for all of its
// sub-tasks to complete, while also performing those
// tasks in parallel:
try await withThrowingTaskGroup(of: Void.self) { group in
// First, create all tasks
for element in self {
group.addTask {
try await operation(element)
}
}
// Then wait for all tasks to complete
for try await _ in group {}
}
}
func concurrentForEach(
_ operation: @escaping (Element) async -> Void
) async {
await withTaskGroup(of: Void.self) { group in
// First, add all tasks
for element in self {
group.addTask {
await operation(element)
}
}
// Then wait for all tasks to complete
for await _ in group {}
}
}
}
enum SortOrder {

@ -36,7 +36,7 @@ struct InscriptionLegendView: View {
Text("Équipe estimée en tableau")
.listRowView(isActive: true, color: .mint, hideColorVariation: true, alignment: .leading)
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")
}
}
}
}

@ -51,9 +51,9 @@ class NetworkManager {
let documentsUrl: URL = SourceFileManager.shared.rankingSourceDirectory
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(dateString)")
let fileURL = URLs.main.extend(path: "static/rankings/\(dateString)")
var request = URLRequest(url:fileURL!)
var request = URLRequest(url:fileURL)
request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition")
if FileManager.default.fileExists(atPath: destinationFileUrl.path()), let modificationDate = destinationFileUrl.creationDate() {
request.addValue(formatDateForHTTPHeader(modificationDate), forHTTPHeaderField: "If-Modified-Since")

@ -70,18 +70,18 @@ struct Line: Identifiable {
struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Line
private let url: URL
let url: URL
private var lineIterator: LineIterator
private let seperator: Character
private let separator: Character
private let quoteCharacter: Character = "\""
private var lineNumber = 0
private let date: Date
let maleData: Bool
init(url: URL, seperator: Character = ";") {
init(url: URL, separator: Character = ";") {
self.date = url.dateFromPath
self.url = url
self.seperator = seperator
self.separator = separator
self.lineIterator = url.lines.makeAsyncIterator()
self.maleData = url.path().contains(SourceFile.messieurs.rawValue)
}
@ -139,7 +139,7 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
inQuote = !inQuote
continue
case seperator:
case separator:
if !inQuote {
data.append(currentString.isEmpty ? nil : currentString)
currentString = ""
@ -157,4 +157,63 @@ struct CSVParser: AsyncSequence, AsyncIteratorProtocol {
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 }
if networkMonitor.connected == false {
if uncalledTeams.isEmpty == false {
if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else {
self.sentError = .messageNotSent
}
} else {
if uncalledTeams.isEmpty == false {
if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else if uncalledTeams.isEmpty == false, calledTeams.isEmpty {
self._called(uncalledTeams, true)
}
self._called(calledTeams, true)
}
@ -228,14 +230,16 @@ struct CallView: View {
if networkMonitor.connected == false {
self.contactType = nil
if uncalledTeams.isEmpty == false {
if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else {
self.sentError = .mailNotSent
}
} else {
if uncalledTeams.isEmpty == false {
if uncalledTeams.isEmpty == false, calledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else if uncalledTeams.isEmpty == false, calledTeams.isEmpty {
self._called(uncalledTeams, true)
}
self._called(calledTeams, true)
}

@ -129,6 +129,18 @@ struct GroupStagesSettingsView: View {
}
#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 {
RowButtonView("Retirer tous les horaires", role: .destructive) {
let matches = tournament.groupStages().flatMap({ $0._matches() })
@ -144,30 +156,17 @@ struct GroupStagesSettingsView: View {
}
}
} 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 {
Section {
menuBuildAllGroupStages
} 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()
Section {

@ -106,6 +106,80 @@ struct GroupStagesView: View {
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 {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
@ -116,9 +190,8 @@ struct GroupStagesView: View {
List {
if tournament.groupStageAdditionalQualified > 0 {
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
Section {
let name = "\((tournament.qualifiedPerGroupStage + 1).ordinalFormatted())"
Section {
NavigationLink {
SpinDrawView(drawees: ["Qualification d'un \(name) de poule"], segments: missingQualifiedFromGroupStages) { results in
results.forEach { drawResult in
@ -134,6 +207,8 @@ struct GroupStagesView: View {
Text("Qualifier un \(name) de poule par tirage au sort")
}
.disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty)
} header: {
Text("Tirage au sort d'un \(name) de poule")
} footer: {
if tournament.moreQualifiedToDraw() == 0 {
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.")
}
}
_sortedAdditionnalTeams(missingQualifiedFromGroupStages: missingQualifiedFromGroupStages)
}
let runningMatches = Tournament.runningMatches(allMatches)

@ -30,7 +30,7 @@ struct MatchDateView: View {
}
var currentDate: Date {
Date().withoutSeconds()
Date()
}
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)
}
}

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

@ -178,6 +178,12 @@ struct ActivityView: View {
federalDataViewModel.federalTournaments.removeAll()
NetworkFederalService.shared.formId = ""
_gatherFederalTournaments()
} else if navigation.agendaDestination == .activity {
runningTournaments.forEach { t in
if let tournament = t as? Tournament {
tournament.lastTeamRefresh = nil
}
}
}
}
.task {

@ -19,6 +19,11 @@ struct EventListView: View {
@State var showUserSearch: Bool = false
var lastDataSource: Date? {
guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil }
return URL.importDateFormatter.date(from: _lastDataSource)
}
var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
switch viewStyle {
@ -103,6 +108,41 @@ struct EventListView: View {
@ViewBuilder
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 {
if pcTournaments.anySatisfy({ $0.isPrivate == true }) {
Button {
@ -137,7 +177,7 @@ struct EventListView: View {
Text("Visibilité sur Padel Club")
}
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 {
if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) {
Button {
@ -154,7 +194,19 @@ struct EventListView: View {
}
}
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true }) {
if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) {
Button {
Task {
await pcTournaments.concurrentForEach { tournament in
await tournament.refreshTeamList()
}
}
} label: {
Text("Rafraîchir la liste des équipes inscrites en ligne")
}
Button {
pcTournaments.forEach { tournament in
tournament.enableOnlineRegistration = false
@ -208,10 +260,10 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some View {
NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament, shouldTournamentBeOver: tournament.shouldTournamentBeOver())
.popover(isPresented: self.$showUserSearch) {
ShareModelView(instance: tournament)
TournamentCellView(tournament: tournament)
.id(tournament.lastTeamRefresh)
.task(priority: .background) {
await tournament.refreshTeamList()
}
}
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)

@ -97,7 +97,7 @@ struct EditablePlayerView: View {
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
VStack(alignment: .leading, spacing: 0.0) {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: player.tournament()?.tournamentCategory.showFemaleInMaleAssimilation ?? false)
// HStack {
// Text(player.isImported() ? "importé via beach padel" : "")
// Text(player.formattedLicense().isLicenseNumber ? "licence valide" : "non valide")

@ -20,7 +20,7 @@ struct PlayerView: View {
}
var body: some View {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
do {

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

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

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

@ -20,11 +20,11 @@ struct EditingTeamView: View {
@State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false
@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
@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> {
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? {
return self.tournament.tournamentStore
}
@ -44,25 +60,19 @@ struct EditingTeamView: View {
self.team = team
_name = .init(wrappedValue: team.name ?? "")
_registrationDate = State(wrappedValue: team.registrationDate ?? Date())
_walkOut = State(wrappedValue: team.walkOut)
_wildCardBracket = State(wrappedValue: team.wildCardBracket)
_wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage)
}
private func _resetTeam() {
let selectedSortedTeams = tournament.selectedSortedTeams()
self.currentWaitingList = tournament.waitingListSortedTeams(selectedSortedTeams: selectedSortedTeams).filter({ $0.hasRegisteredOnline() }).first
team.resetPositions()
team.qualified = false
team.wildCardGroupStage = false
team.walkOut = 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 {
List {
Section {
@ -92,10 +102,6 @@ struct EditingTeamView: View {
.headerProminence(.increased)
Section {
DatePicker(selection: $registrationDate) {
Text("Inscription")
Text(registrationDate.localizedWeekDay().capitalized)
}
if let callDate = team.callDate {
LabeledContent() {
Text(callDate.localizedDate())
@ -117,36 +123,23 @@ struct EditingTeamView: View {
Text("Équipe sur place")
}
}
}
Toggle(isOn: .init(get: {
return team.wildCardBracket
}, set: { value in
_resetTeam()
team.wildCardBracket = value
Section {
DatePicker(selection: $registrationDate) {
Text("Inscription")
Text(registrationDate.localizedWeekDay().capitalized)
}
_save()
})) {
Toggle(isOn: $wildCardBracket) {
Text("Wildcard Tableau")
}
Toggle(isOn: .init(get: {
return team.wildCardGroupStage
}, set: { value in
_resetTeam()
team.wildCardGroupStage = value
_save()
})) {
Toggle(isOn: $wildCardGroupStage) {
Text("Wildcard Poule")
}
}.disabled(tournament.groupStageCount == 0)
Toggle(isOn: .init(get: {
return team.walkOut
}, set: { value in
_resetTeam()
team.walkOut = value
_save()
})) {
Toggle(isOn: $walkOut) {
Text("Forfait")
}
}
@ -218,30 +211,24 @@ struct EditingTeamView: View {
}
}
}
.sheet(isPresented: $presentTeamToWarn) {
if let currentWaitingList {
NavigationStack {
EditingTeamView(team: currentWaitingList)
}
.tint(.master)
}
}
.alert("Attention", isPresented: $presentOnlineRegistrationWarning, actions: {
if currentWaitingList != nil {
Button("Voir l'équipe") {
self.presentTeamToWarn = true
.alert("Attention", isPresented: hasChanged, actions: {
Button("Confirmer") {
_resetTeam()
team.registrationDate = registrationDate
team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage
team.walkOut = walkOut
_save()
}
Button("OK") {
self.currentWaitingList = nil
self.presentOnlineRegistrationWarning = false
}
Button("Annuler", role: .cancel) {
registrationDate = team.registrationDate ?? Date()
walkOut = team.walkOut
wildCardBracket = team.wildCardBracket
wildCardGroupStage = team.wildCardGroupStage
}
}, message: {
if let currentWaitingList {
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 ?")
}
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.")
})
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
@ -329,14 +316,29 @@ struct EditingTeamView: View {
}
}
.onChange(of: registrationDate) {
if canSaveWithoutWarning() {
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)
.navigationTitle("Édition de l'équipe")
.navigationBarTitleDisplayMode(.inline)
}
func canSaveWithoutWarning() -> Bool {
(tournament.shouldWarnOnlineRegistrationUpdates() && tournament.teamCount <= tournament.unsortedTeamsCount()) == false
}
private var confirmationReceived: Binding<Bool> {
Binding {
team.confirmed()
@ -371,8 +373,6 @@ struct EditingTeamView: View {
} catch {
Logger.error(error)
}
_checkOnlineRegistrationWarning()
}
private var _networkErrorMessage: String {

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

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

@ -120,10 +120,10 @@ struct FileImportView: View {
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 {
do {
try tournamentStore?.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
try tournamentStore?.teamRegistrations.delete(contentOfs: teams)
} catch {
Logger.error(error)
}
@ -140,9 +140,18 @@ struct FileImportView: View {
}
}
if tournament.unsortedTeams().count > 0, tournament.enableOnlineRegistration == false {
let unsortedTeams = tournament.unsortedTeams()
let onlineTeams = unsortedTeams.filter({ $0.hasRegisteredOnline() })
if unsortedTeams.count > 0 {
Section {
RowButtonView("Effacer les équipes déjà inscrites", role: .destructive) {
await _deleteTeams()
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.")
}
}
}
@ -500,20 +509,6 @@ struct FileImportView: View {
private func _validate(tournament: Tournament) async {
let filteredTeams = filteredTeams(tournament: tournament)
let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams)
unfound.forEach { team in
if team.isWildCard() == false {
team.resetPositions()
team.walkOut = true
}
}
do {
try tournament.tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unfound)
} catch {
Logger.error(error)
}
tournament.importTeams(filteredTeams)
validatedTournamentIds.insert(tournament.id)

@ -81,6 +81,14 @@ struct AddTeamView: View {
fetchRequest = FetchRequest(fetchRequest: request, animation: .default)
}
var selectionLimit: Int {
if tournament.isAnimation() {
return -1
} else {
return tournament.significantPlayerCount() - _currentSelectionIds().count
}
}
var body: some View {
if let pasteString, pasteString.isEmpty == false, fetchPlayers.isEmpty == false {
computedBody
@ -163,7 +171,7 @@ struct AddTeamView: View {
}
.sheet(isPresented: $presentPlayerSearch) {
NavigationStack {
SelectablePlayerListView(allowSelection: 2 - _currentSelectionIds().count, isPresented: true, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in
SelectablePlayerListView(allowSelection: selectionLimit, isPresented: true, searchField: searchField, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in
players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player)
newPlayer.setComputedRank(in: tournament)

@ -42,25 +42,8 @@ struct UpdateSourceRankDateView: View {
updatingRank = true
Task {
do {
try await tournament.updateRank(to: currentRankSourceDate)
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 await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)

@ -51,7 +51,6 @@ struct InscriptionManagerView: View {
@State private var pasteString: String?
@State private var registrationIssues: Int? = nil
@State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false
@ -193,8 +192,8 @@ struct InscriptionManagerView: View {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
self.registrationIssues = nil
DispatchQueue.main.async {
self.registrationIssues = tournament.registrationIssues(selectedTeams: selectedSortedTeams)
Task {
self.registrationIssues = await tournament.registrationIssues(selectedTeams: selectedSortedTeams)
}
}
@ -248,7 +247,7 @@ struct InscriptionManagerView: View {
if tournament.enableOnlineRegistration {
RowButtonView("Rafraîchir la liste", cornerRadius: 20) {
await _refreshList()
await _refreshList(forced: true)
}
} else if tournament.onlineRegistrationCanBeEnabled() {
RowButtonView("Inscription en ligne") {
@ -259,12 +258,17 @@ struct InscriptionManagerView: View {
}
}
}
.task(priority: .background) {
await _refreshList(forced: false)
}
.refreshable {
await _refreshList()
await _refreshList(forced: true)
}
.onAppear {
if tournament.enableOnlineRegistration == false || refreshStatus == true {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
}
.onDisappear {
_handleHashDiff(selectedSortedTeams: selectedSortedTeams)
}
@ -542,29 +546,29 @@ struct InscriptionManagerView: View {
// try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// }
//
private func _refreshList() async {
if refreshInProgress { return }
private func _refreshList(forced: Bool) async {
if refreshStatus == true, forced == false { return }
if tournament.enableOnlineRegistration == false { return }
if tournament.hasEnded() { return }
if tournament.refreshInProgress { return }
refreshResult = nil
refreshStatus = nil
refreshInProgress = true
do {
try await self.tournamentStore?.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore?.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
await self.tournament.refreshTeamList()
_setHash()
self.refreshResult = "la synchronization a réussi"
if let lastTeamRefresh = self.tournament.lastTeamRefresh?.formatted(date: .abbreviated, time: .shortened) {
self.refreshResult = "Dernière m-à-j : \(lastTeamRefresh)"
} else {
self.refreshResult = "La synchronization a réussi"
}
self.refreshStatus = true
refreshInProgress = false
} catch {
Logger.error(error)
self.refreshResult = "la synchronization a échoué"
self.refreshResult = "La synchronization a échoué"
self.refreshStatus = false
refreshInProgress = false
}
}
@ -717,7 +721,7 @@ struct InscriptionManagerView: View {
@ViewBuilder
private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
Section {
TipView(rankUpdateTip) { action in
self.currentRankSourceDate = mostRecentDate
@ -839,27 +843,20 @@ struct InscriptionManagerView: View {
// }
if tournament.enableOnlineRegistration {
Button {
Task {
await _refreshList()
}
} label: {
LabeledContent {
if refreshInProgress {
ProgressView()
} else if let refreshStatus {
if refreshStatus {
Image(systemName: "checkmark").foregroundStyle(.green).font(.headline)
} else {
Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline)
}
}
Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
.font(.largeTitle)
} label: {
Text("Récupérer les inscriptions en ligne")
Text("Inscriptions en ligne")
if let refreshResult {
Text(refreshResult)
Text(refreshResult).foregroundStyle(.secondary)
} else {
Text(" ")
}
}
RowButtonView("Rafraîchir les inscriptions en ligne") {
await _refreshList(forced: true)
}
}
} header: {

@ -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 {
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 {
Toggle(isOn: $enableOnlineRegistration) {
Text("Activer")

@ -23,6 +23,11 @@ struct TableStructureView: View {
@State private var buildWildcards: Bool = true
@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 {
groupStageCount * qualifiedPerGroupStage
}
@ -43,7 +48,7 @@ struct TableStructureView: View {
var moreQualifiedLabel: String {
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 {
@ -58,6 +63,10 @@ struct TableStructureView: View {
@ViewBuilder
var body: some View {
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 {
Section {
@ -252,6 +261,16 @@ struct TableStructureView: View {
}
}
if tournament.rounds().isEmpty, tournament.state() == .build {
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 {
Section {

@ -15,7 +15,7 @@ struct TournamentCellView: View {
let tournament: FederalTournamentHolder
// let color: Color = .black
var displayStyle: DisplayStyle = .wide
var shouldTournamentBeOver: Bool = false
@State var shouldTournamentBeOver: Bool = false
var event: Event? {
guard let federalTournament = tournament as? FederalTournament else { return nil }
@ -114,7 +114,9 @@ struct TournamentCellView: View {
}
Spacer()
if let tournament = tournament as? Tournament, displayStyle == .wide {
if tournament.isCanceled {
if tournament.refreshInProgress || tournament.refreshRanking {
ProgressView()
} else if tournament.isCanceled {
Text("Annulé".uppercased())
.capsule(foreground: .white, background: .logoRed)
} else if shouldTournamentBeOver {
@ -157,6 +159,9 @@ struct TournamentCellView: View {
let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted()
let word = hasStarted ? "équipe" : "inscription"
Text(word + teamCount.pluralSuffix)
.task(priority: .background) {
self.shouldTournamentBeOver = await tournament.shouldTournamentBeOver()
}
}
}
}
@ -164,6 +169,17 @@ struct TournamentCellView: View {
Text(build.category.localizedLabel())
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)

@ -54,7 +54,7 @@ struct TournamentBuildView: View {
}
}
}
.task {
.task(priority: .background) {
groupStageStatus = await tournament.groupStageStatus()
}
}
@ -105,7 +105,7 @@ struct TournamentBuildView: View {
}
}
}
.task {
.task(priority: .background) {
bracketStatus = await tournament.bracketStatus()
}
}
@ -157,7 +157,7 @@ struct TournamentBuildView: View {
}
}
}
.task {
.task(priority: .background) {
scheduleStatus = await tournament.scheduleStatus()
}
@ -178,7 +178,7 @@ struct TournamentBuildView: View {
}
}
}
.task {
.task(priority: .background) {
callStatus = await tournament.callStatus()
}
@ -199,7 +199,7 @@ struct TournamentBuildView: View {
}
}
}
.task {
.task(priority: .background) {
cashierStatus = await tournament.cashierStatus()
}
}

@ -15,6 +15,7 @@ struct TournamentView: View {
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@State var tournament: Tournament
@State private var showMoreInfos: Bool = false
@State var shouldTournamentBeOver: Bool = false
var presentationContext: PresentationContext = .agenda
@ -106,7 +107,7 @@ struct TournamentView: View {
TournamentBuildView(tournament: tournament)
TournamentInitView(tournament: tournament)
case .running:
if tournament.shouldTournamentBeOver() {
if shouldTournamentBeOver {
Section {
TipView(shouldTournamentBeOverTip) { actions in
navigation.path.append(Screen.stateSettings)
@ -125,6 +126,9 @@ struct TournamentView: View {
RegistrationInfoSheetView()
}
}
.task(priority: .background) {
self.shouldTournamentBeOver = await tournament.shouldTournamentBeOver()
}
.environment(tournament)
.id(tournament.id)
.toolbarBackground(.visible, for: .navigationBar)

Loading…
Cancel
Save