From 0a3110967aa8b42378a94ef38a9639edd3a787b8 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Mon, 13 May 2024 14:03:09 +0200 Subject: [PATCH 1/4] handle may 2024 anonymous stuff --- .../Coredata/ImportedPlayer+Extensions.swift | 4 +- PadelClub/Data/Federal/FederalPlayer.swift | 14 +-- PadelClub/Data/Federal/PlayerHolder.swift | 4 + PadelClub/Utils/FileImportManager.swift | 53 ++++++++-- PadelClub/Views/Cashier/CashierView.swift | 2 +- .../Components/GroupStageTeamView.swift | 2 +- .../Components/MatchTeamDetailView.swift | 2 +- .../Navigation/Toolbox/PadelClubView.swift | 97 ++++++++++++++----- .../Components/EditablePlayerView.swift | 29 +++++- .../Views/Shared/ImportedPlayerView.swift | 8 +- .../Views/Tournament/FileImportView.swift | 50 ++++------ .../Screen/InscriptionManagerView.swift | 2 +- 12 files changed, 184 insertions(+), 83 deletions(-) diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index 9b8ce71..7288d76 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -24,11 +24,11 @@ extension ImportedPlayer: PlayerHolder { } func getFirstName() -> String { - self.firstName ?? "prénom inconnu" + self.firstName ?? "" } func getLastName() -> String { - self.lastName ?? "nom inconnu" + self.lastName ?? "" } func formattedLicense() -> String { diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 60d2c9d..1963c02 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -7,7 +7,7 @@ import Foundation -struct FederalPlayer: Decodable { +class FederalPlayer: Decodable { var rank: Int var lastName: String var firstName: String @@ -27,7 +27,7 @@ struct FederalPlayer: Decodable { let code, codeFov: String } - init(from decoder: Decoder) throws { + required init(from decoder: Decoder) throws { enum CodingKeys: String, CodingKey { case nom case prenom @@ -77,10 +77,9 @@ struct FederalPlayer: Decodable { tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois) let assimile = try container.decode(Bool.self, forKey: .assimile) assimilation = assimile ? "Oui" : "Non" - fullNameCanonical = _lastName.canonicalVersion + " " + _firstName.canonicalVersion } - + func exportToCSV() -> String { let pointsString = points != nil ? String(Int(points!)) : "" let tournamentCountString = tournamentCount != nil ? String(tournamentCount!) : "" @@ -108,9 +107,7 @@ struct FederalPlayer: Decodable { } return modifiedString } - - var fullNameCanonical: String - + /* ;RANG;NOM;PRENOM;Nationalité;N° Licence;POINTS;Assimilation;NB. DE TOURNOIS JOUES;LIGUE;CODE CLUB;CLUB; */ @@ -139,7 +136,7 @@ struct FederalPlayer: Decodable { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })) -// print(result) + //print(result) if result.count < 11 { return nil } @@ -151,7 +148,6 @@ struct FederalPlayer: Decodable { lastName = result[1] firstName = result[2] - fullNameCanonical = result[1].canonicalVersion + " " + result[2].canonicalVersion country = result[3] license = result[4] diff --git a/PadelClub/Data/Federal/PlayerHolder.swift b/PadelClub/Data/Federal/PlayerHolder.swift index ba00af2..e2ab0eb 100644 --- a/PadelClub/Data/Federal/PlayerHolder.swift +++ b/PadelClub/Data/Federal/PlayerHolder.swift @@ -29,4 +29,8 @@ extension PlayerHolder { var isAssimilated: Bool { assimilation == "Oui" } + + func isAnonymous() -> Bool { + getFirstName().isEmpty && getLastName().isEmpty + } } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index d7ce2fc..c3a153d 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -11,6 +11,31 @@ import LeStorage class FileImportManager { static let shared = FileImportManager() + func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) { + guard let mostRecentDateAvailable = URL.importDateFormatter.date(from: "05-2024") else { return } + let replacements: [(Character, Character)] = [("Á", "ç"), ("‡", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("…", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")] + + var playersLeft = players + SourceFileManager.shared.allFilesSortedByDate(isMale).filter({ $0.dateFromPath.isEarlierThan(mostRecentDateAvailable) }).forEach({ url in + if playersLeft.isEmpty == false { + let federalPlayers = readCSV(inputFile: url) + let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements + + playersLeft.forEach { importedPlayer in + if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) { + var lastName = federalPlayer.lastName + lastName.replace(characters: replacementsCharacters) + var firstName = federalPlayer.firstName + firstName.replace(characters: replacementsCharacters) + importedPlayer.lastName = lastName + importedPlayer.firstName = firstName + } + } + playersLeft.removeAll(where: { $0.lastName.isEmpty == false }) + } + }) + } + func foundInWomenData(license: String?) -> Bool { guard let license = license?.strippedLicense else { return false @@ -68,11 +93,23 @@ class FileImportManager { let previousTeam: TeamRegistration? var registrationDate: Date? = nil - init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) { + init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil, tournament: Tournament) { self.players = Set(players) self.tournamentCategory = tournamentCategory self.previousTeam = previousTeam - self.weight = players.map { $0.computedRank }.reduce(0,+) + if players.count < 2 { + let s = players.compactMap { $0.sex?.rawValue } + var missing = tournamentCategory.mandatoryPlayerType() + s.forEach { i in + if let index = missing.firstIndex(of: i) { + missing.remove(at: index) + } + } + let significantPlayerCount = 2 + self.weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? 0 }).prefix(significantPlayerCount).reduce(0,+) + } else { + self.weight = players.map { $0.computedRank }.reduce(0,+) + } self.registrationDate = registrationDate } @@ -110,7 +147,7 @@ class FileImportManager { return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament) } } - + func importDataFromFFT() async -> String? { if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { for source in SourceFile.allCases { @@ -214,7 +251,7 @@ class FileImportManager { playerOne.setComputedRank(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) results.append(team) } } @@ -261,7 +298,7 @@ class FileImportManager { let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setComputedRank(in: tournament) - let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) results.append(team) } } @@ -270,11 +307,11 @@ class FileImportManager { } private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { - let lines = fileContent.components(separatedBy: "\n\n") + var lines = fileContent.components(separatedBy: "\n\n") var results: [TeamHolder] = [] let fetchRequest = ImportedPlayer.fetchRequest() let federalContext = PersistenceController.shared.localContainer.viewContext - + lines.removeAll(where: { $0.contains("Liste d'attente")}) lines.forEach { team in let data = team.components(separatedBy: "\n") let players = team.licencesFound() @@ -292,7 +329,7 @@ class FileImportManager { } return nil } - let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate) + let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate, tournament: tournament) results.append(team) } } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index 5b5700d..8fab647 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -159,7 +159,7 @@ struct CashierView: View { @ViewBuilder func computedPlayerView(_ player: PlayerRegistration) -> some View { - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 01e3d1d..f5ce7b8 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -17,7 +17,7 @@ struct GroupStageTeamView: View { List { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } } diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index b09a46e..b5fb3dd 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -31,7 +31,7 @@ struct MatchTeamDetailView: View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) + EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) } } header: { TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) diff --git a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift b/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift index f4401fd..2a5f834 100644 --- a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift @@ -26,7 +26,12 @@ struct PadelClubView: View { animation: .default) private var players: FetchedResults - + @FetchRequest( + sortDescriptors: [], + predicate: NSPredicate(format: "lastName == %@ && firstName == %@", "", ""), + animation: .default) + private var anonymousPlayers: FetchedResults + var _mostRecentDateAvailable: Date? { SourceFileManager.shared.mostRecentDateAvailable } @@ -38,6 +43,37 @@ struct PadelClubView: View { var body: some View { List { + #if targetEnvironment(simulator) + /* + ["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"] + ["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"] + + */ + Section { + RowButtonView("Exporter en csv") { + for fileURL in SourceFileManager.shared.jsonFiles() { + let decoder = JSONDecoder() + decoder.userInfo[.maleData] = fileURL.manData + + do { + let data = try Data(contentsOf: fileURL) + let players = try decoder.decode([FederalPlayer].self, from: data) + var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false } + + print("before anonymousPlayers.count", anonymousPlayers.count) + FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) + print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + .count) + SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) + } catch { + Logger.error(error) + } + } + } + } + #endif + if let _lastDataSourceDate { Section { LabeledContent { @@ -46,6 +82,11 @@ struct PadelClubView: View { Text(_lastDataSourceDate.monthYearFormatted) Text("Classement mensuel utilisé") } + .contextMenu { + Button("Ré-importer") { + _startImporting() + } + } } if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) { @@ -56,30 +97,26 @@ struct PadelClubView: View { } } - #if targetEnvironment(simulator) - /* - ["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"] - ["36435", "BRUL…", "Romain", "France", "2993139", "15,00", "Non", "2", "NOUVELLE AQUITAINE", "59 33 0447", "SAINT LOUBES TC"] - - */ Section { - RowButtonView("Exporter en csv") { - for fileURL in SourceFileManager.shared.jsonFiles() { - let decoder = JSONDecoder() - decoder.userInfo[.maleData] = fileURL.manData - - do { - let data = try Data(contentsOf: fileURL) - let players = try decoder.decode([FederalPlayer].self, from: data) - SourceFileManager.shared.exportToCSV(players: players, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) - } catch { - Logger.error(error) - } - } + LabeledContent { + Text(players.filter{ $0.male }.count.formatted()) + } label: { + Text("Messieurs") + } + LabeledContent { + Text(players.filter{ $0.male == false }.count.formatted()) + } label: { + Text("Dames") + } + + LabeledContent { + Text(anonymousPlayers.count.formatted()) + } label: { + Text("Joueurs anonymes") } + } header: { + Text(players.count.formatted() + " joueurs") } - - #endif } if importingFiles { @@ -104,16 +141,16 @@ struct PadelClubView: View { Text(maleUnrankedValue.formatted()) } } label: { - Text("Messieurs") Text("Rang d'un non classé") + Text("Messieurs") } LabeledContent { if let femaleUnrankedValue = monthData.femaleUnrankedValue { Text(femaleUnrankedValue.formatted()) } } label: { - Text("Dames") Text("Rang d'une non classée") + Text("Dames") } } header: { Text(monthData.monthKey) @@ -129,13 +166,20 @@ struct PadelClubView: View { } } .headerProminence(.increased) - .navigationTitle("Source des données fédérales") + .navigationTitle("Données fédérales") } @ViewBuilder func _activityStatus() -> some View { if checkingFiles || importingFiles { - ProgressView() + HStack(spacing: 20) { + ProgressView() + if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable { + if mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast { + Text("import " + mostRecentDateAvailable.monthYearFormatted) + } + } + } } else if let _mostRecentDateAvailable { if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation") @@ -168,6 +212,7 @@ struct PadelClubView: View { await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) } importingFiles = false + viewContext.refreshAllObjects() } } } diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index 21ae589..c957426 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -12,14 +12,17 @@ struct EditablePlayerView: View { enum PlayerEditingOption { case payment case licenceId + case name } @EnvironmentObject var dataStore: DataStore - var player: PlayerRegistration + @Bindable var player: PlayerRegistration var editingOptions: [PlayerEditingOption] @State private var editedLicenceId = "" @State private var shouldPresentLicenceIdEdition: Bool = false - + @State private var presentLastNameUpdate: Bool = false + @State private var presentFirstNameUpdate: Bool = false + var body: some View { computedPlayerView(player) .alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) { @@ -30,6 +33,19 @@ struct EditablePlayerView: View { try? dataStore.playerRegistrations.addOrUpdate(instance: player) } } + .alert("Prénom", isPresented: $presentFirstNameUpdate) { + TextField("Prénom", text: $player.firstName) + .onSubmit { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + .alert("Nom", isPresented: $presentLastNameUpdate) { + TextField("Nom", text: $player.lastName) + .onSubmit { + try? dataStore.playerRegistrations.addOrUpdate(instance: player) + } + } + } // TODO: Guard @@ -61,6 +77,15 @@ struct EditablePlayerView: View { } } + if editingOptions.contains(.name) { + Divider() + Button("Modifier le prénom") { + presentFirstNameUpdate = true + } + Button("Modifier le nom") { + presentLastNameUpdate = true + } + } if editingOptions.contains(.licenceId) { Divider() if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index 3bfa6e2..a254fc0 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -15,8 +15,12 @@ struct ImportedPlayerView: View { var body: some View { VStack(alignment: .leading) { HStack { - Text(player.getLastName().capitalized) - Text(player.getFirstName().capitalized) + if player.isAnonymous() { + Text("Joueur Anonyme") + } else { + Text(player.getLastName().capitalized) + Text(player.getFirstName().capitalized) + } if index == nil { Text(player.male ? "♂︎" : "♀︎") } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index d269e23..8b47214 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -14,9 +14,9 @@ struct FileImportView: View { @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) private var dismiss - let fileContent: String? let notFoundAreWalkOutTip = NotFoundAreWalkOutTip() + @State private var fileContent: String? @State private var teams: [FileImportManager.TeamHolder] = [] @State private var isShowing = false @State private var didImport = false @@ -40,24 +40,29 @@ struct FileImportView: View { convertingFile = false isShowing.toggle() } + } + + Section { + Picker(selection: $fileProvider) { + ForEach(FileImportManager.FileProvider.allCases) { + Text($0.localizedLabel).tag($0) + } + } label: { + Text("Source du fichier") + } + + RowButtonView("Démarrer l'importation") { + if let fileContent { + await _startImport(fileContent: fileContent) + } + } + .disabled(fileContent == nil) } footer: { if fileProvider == .frenchFederation { let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" Text(.init(footerString)) } } - - if fileContent != nil { - Section { - Picker(selection: $fileProvider) { - ForEach(FileImportManager.FileProvider.allCases) { - Text($0.localizedLabel).tag($0) - } - } label: { - Text("Source du fichier") - } - } - } } // if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false { @@ -160,13 +165,6 @@ struct FileImportView: View { } } } - .onAppear { - if let fileContent { - Task { - await _startImport(fileContent: fileContent) - } - } - } .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in switch results { @@ -178,16 +176,11 @@ struct FileImportView: View { teams.removeAll() Task { do { - var fileContent: String? if selectedFile.lastPathComponent.hasSuffix("xls") { fileContent = try await CloudConvert.manager.uploadFile(selectedFile) } else { fileContent = try String(contentsOf: selectedFile) } - - if let fileContent { - await _startImport(fileContent: fileContent) - } selectedFile.stopAccessingSecurityScopedResource() } catch { errorMessage = error.localizedDescription @@ -204,10 +197,7 @@ struct FileImportView: View { .onOpenURL { url in do { - let fileContent = try String(contentsOf: url) - Task { - await _startImport(fileContent: fileContent) - } + fileContent = try String(contentsOf: url) } catch { errorMessage = error.localizedDescription } @@ -302,7 +292,7 @@ struct FileImportView: View { } #Preview { - FileImportView(fileContent: nil) + FileImportView() .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index db50340..375b769 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -104,7 +104,7 @@ struct InscriptionManagerView: View { } .sheet(isPresented: $presentImportView) { NavigationStack { - FileImportView(fileContent: nil) + FileImportView() } .tint(.master) } From 588c7e88687c59eeb6971f0100b3ed05d6a87f85 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 14 May 2024 10:04:18 +0200 Subject: [PATCH 2/4] fix stuff --- PadelClub.xcodeproj/project.pbxproj | 2 +- PadelClub/Data/Federal/FederalPlayer.swift | 18 ++++- PadelClub/Data/MonthData.swift | 20 +++++- PadelClub/Data/Tournament.swift | 2 +- PadelClub/Utils/FileImportManager.swift | 15 ++++- PadelClub/Views/Navigation/MainView.swift | 5 ++ .../Organizer/TournamentOrganizerView.swift | 4 ++ .../{Toolbox => Umpire}/PadelClubView.swift | 67 +++++++++++-------- .../Team/Components/TeamHeaderView.swift | 29 +++++--- .../Views/Tournament/FileImportView.swift | 4 ++ .../Shared/TournamentCellView.swift | 8 ++- 11 files changed, 124 insertions(+), 50 deletions(-) rename PadelClub/Views/Navigation/{Toolbox => Umpire}/PadelClubView.swift (87%) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ad789ed..cc109c7 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -958,7 +958,6 @@ FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */, FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */, FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */, - FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, ); path = Toolbox; sourceTree = ""; @@ -967,6 +966,7 @@ isa = PBXGroup; children = ( FF3F74F52B919E45004CFE0E /* UmpireView.swift */, + FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, ); path = Umpire; sourceTree = ""; diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 1963c02..4742b83 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -169,7 +169,19 @@ class FederalPlayer: Decodable { club = result[10] } - static func lastRank(mostRecentDateAvailable: Date?, man: Bool) async -> Int? { + static func anonymousCount(mostRecentDateAvailable: Date?) async -> Int? { + let context = PersistenceController.shared.localContainer.newBackgroundContext() + let importedPlayerFetchRequest = ImportedPlayer.fetchRequest() + var predicate = NSPredicate(format: "lastName == %@ && firstName == %@", "", "") + if let mostRecentDateAvailable { + predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) + } + importedPlayerFetchRequest.predicate = predicate + let count = try? context.count(for: importedPlayerFetchRequest) + return count + } + + static func lastRank(mostRecentDateAvailable: Date?, man: Bool) async -> (Int, Int?)? { let context = PersistenceController.shared.localContainer.newBackgroundContext() let lastPlayerFetch = ImportedPlayer.fetchRequest() lastPlayerFetch.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: false)] @@ -178,7 +190,7 @@ class FederalPlayer: Decodable { predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) } lastPlayerFetch.predicate = predicate - + let count = try? context.count(for: lastPlayerFetch) do { if let lr = try context.fetch(lastPlayerFetch).first?.rank { let fetch = ImportedPlayer.fetchRequest() @@ -189,7 +201,7 @@ class FederalPlayer: Decodable { fetch.predicate = rankPredicate let lastPlayersCount = try context.count(for: fetch) - return Int(lr) + Int(lastPlayersCount) - 1 + return (Int(lr) + Int(lastPlayersCount) - 1, count) } } catch { print("ImportedPlayer.fetchRequest", error) diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift index 4820548..c586b9c 100644 --- a/PadelClub/Data/MonthData.swift +++ b/PadelClub/Data/MonthData.swift @@ -21,21 +21,32 @@ class MonthData : ModelObject, Storable { var maleUnrankedValue: Int? = nil var femaleUnrankedValue: Int? = nil + var maleCount: Int? = nil + var femaleCount: Int? = nil + var anonymousCount: Int? = nil + init(monthKey: String) { self.monthKey = monthKey self.creationDate = Date() } + func total() -> Int { + (maleCount ?? 0) + (femaleCount ?? 0) + } + static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) - + let anonymousCount = await FederalPlayer.anonymousCount(mostRecentDateAvailable: mostRecentDateAvailable) await MainActor.run { if let lastDataSource = DataStore.shared.appSettings.lastDataSource { let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource) - currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked - currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked + currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked?.0 + currentMonthData.maleCount = lastDataSourceMaleUnranked?.1 + currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0 + currentMonthData.femaleCount = lastDataSourceFemaleUnranked?.1 + currentMonthData.anonymousCount = anonymousCount try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) } } @@ -50,5 +61,8 @@ class MonthData : ModelObject, Storable { case _creationDate = "creationDate" case _maleUnrankedValue = "maleUnrankedValue" case _femaleUnrankedValue = "femaleUnrankedValue" + case _maleCount = "maleCount" + case _femaleCount = "femaleCount" + case _anonymousCount = "anonymousCount" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b9a5a2a..0447a27 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -562,7 +562,7 @@ class Tournament : ModelObject, Storable { let defaultSorting : [MySortDescriptor] = _defaultSorting() - let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false} + let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(by: \.initialWeight) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending) diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index c3a153d..bde0d70 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -106,7 +106,9 @@ class FileImportManager { } } let significantPlayerCount = 2 - self.weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? 0 }).prefix(significantPlayerCount).reduce(0,+) + let pl = players.prefix(significantPlayerCount).map { $0.computedRank } + let missingPl = (missing.map { tournament.unrankValue(for: $0 == 1 ? true : false ) ?? ($0 == 1 ? 100_000 : 10_000) }).prefix(significantPlayerCount) + self.weight = pl.reduce(0,+) + missingPl.reduce(0,+) } else { self.weight = players.map { $0.computedRank }.reduce(0,+) } @@ -307,6 +309,13 @@ class FileImportManager { } private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "fr_FR") + + // Set the date format to match the input string + dateFormatter.dateFormat = "EEE dd MMM yyyy 'à' HH:mm" + + var lines = fileContent.components(separatedBy: "\n\n") var results: [TeamHolder] = [] let fetchRequest = ImportedPlayer.fetchRequest() @@ -324,8 +333,8 @@ class FileImportManager { }) if let registeredPlayers, registeredPlayers.isEmpty == false { var registrationDate: Date? { - if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") { - return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute()) + if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "Inscrit le ", with: "") { + return dateFormatter.date(from: registrationDateData) } return nil } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 52479a1..08653e2 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -59,6 +59,11 @@ struct MainView: View { // PadelClubView() // .tabItem(for: .padelClub) } + .id(Store.main.currentUserUUID) + .onChange(of: Store.main.currentUserUUID) { + navigation.tournament = nil + navigation.path.removeLast(navigation.path.count) + } .environmentObject(dataStore) .task { await self._checkSourceFileAvailability() diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index ceeaa53..6c8f037 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import LeStorage struct TournamentOrganizerView: View { @EnvironmentObject var dataStore: DataStore @@ -46,6 +47,9 @@ struct TournamentOrganizerView: View { } } } + .onChange(of: Store.main.currentUserUUID) { + selectedTournamentId = nil + } } } diff --git a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift similarity index 87% rename from PadelClub/Views/Navigation/Toolbox/PadelClubView.swift rename to PadelClub/Views/Navigation/Umpire/PadelClubView.swift index 2a5f834..cb3f9e9 100644 --- a/PadelClub/Views/Navigation/Toolbox/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -26,12 +26,6 @@ struct PadelClubView: View { animation: .default) private var players: FetchedResults - @FetchRequest( - sortDescriptors: [], - predicate: NSPredicate(format: "lastName == %@ && firstName == %@", "", ""), - animation: .default) - private var anonymousPlayers: FetchedResults - var _mostRecentDateAvailable: Date? { SourceFileManager.shared.mostRecentDateAvailable } @@ -96,27 +90,6 @@ struct PadelClubView: View { } } } - - Section { - LabeledContent { - Text(players.filter{ $0.male }.count.formatted()) - } label: { - Text("Messieurs") - } - LabeledContent { - Text(players.filter{ $0.male == false }.count.formatted()) - } label: { - Text("Dames") - } - - LabeledContent { - Text(anonymousPlayers.count.formatted()) - } label: { - Text("Joueurs anonymes") - } - } header: { - Text(players.count.formatted() + " joueurs") - } } if importingFiles { @@ -136,6 +109,29 @@ struct PadelClubView: View { let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed() ForEach(monthData) { monthData in Section { + LabeledContent { + if let maleCount = monthData.maleCount { + Text(maleCount.formatted()) + } + } label: { + Text("Messieurs") + } + LabeledContent { + if let femaleCount = monthData.femaleCount { + Text(femaleCount.formatted()) + } + } label: { + Text("Dames") + } + + LabeledContent { + if let anonymousCount = monthData.anonymousCount { + Text(anonymousCount.formatted()) + } + } label: { + Text("Joueurs anonymes") + } + LabeledContent { if let maleUnrankedValue = monthData.maleUnrankedValue { Text(maleUnrankedValue.formatted()) @@ -153,7 +149,22 @@ struct PadelClubView: View { Text("Dames") } } header: { - Text(monthData.monthKey) + HStack { + Text(monthData.monthKey) + Spacer() + Text(monthData.total().formatted() + " joueurs") + } + } footer: { + HStack { + Spacer() + FooterButtonView("recalculer") { + Task { + if let monthKeyDate = URL.importDateFormatter.date(from: monthData.monthKey) { + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: monthKeyDate) + } + } + } + } } } } diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift index 76818a6..700ec47 100644 --- a/PadelClub/Views/Team/Components/TeamHeaderView.swift +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -19,20 +19,31 @@ struct TeamHeaderView: View { private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { HStack { if let teamIndex { - Text("#" + (teamIndex + 1).formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("rang").font(.caption) + Text("#" + (teamIndex + 1).formatted()) + } } if team.unsortedPlayers().isEmpty == false { - Text(team.weight.formatted()) - } - if team.isWildCard() { - Text("wildcard").italic().font(.caption) + VStack(alignment: .leading, spacing: 0) { + Text("poids").font(.caption) + Text(team.weight.formatted()) + } } Spacer() - if team.walkOut { - Text("WO") - } else if let teamIndex, let tournament { - Text(tournament.cutLabel(index: teamIndex)) + VStack(alignment: .trailing, spacing: 0) { + if team.walkOut { + Text("").font(.caption) + Text("WO") + } else if let teamIndex, let tournament { + if team.isWildCard() { + Text("wildcard").font(.caption).italic() + } else { + Text("").font(.caption) + } + Text(tournament.cutLabel(index: teamIndex)) + } } } } diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 8b47214..6b781b8 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -246,6 +246,10 @@ struct FileImportView: View { errorMessage = nil teams.removeAll() } + if let rankSourceDate = tournament.rankSourceDate, tournament.unrankValue(for: false) == nil || tournament.unrankValue(for: true) == nil { + await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: rankSourceDate) + } + self.teams = await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider) await MainActor.run { convertingFile = false diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 991d737..73c09f6 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -69,7 +69,9 @@ struct TournamentCellView: View { } Spacer() if let tournament = tournament as? Tournament, displayStyle == .wide { - Text(tournament.sortedTeams().count.formatted()) + let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() + let count = hasStarted ? tournament.selectedSortedTeams().count : tournament.unsortedTeams().count + Text(count.formatted()) } else if let federalTournament = tournament as? FederalTournament { Button { _createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build) @@ -89,7 +91,9 @@ struct TournamentCellView: View { Text(tournament.durationLabel()) Spacer() if let tournament = tournament as? Tournament { - Text("équipes") + let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() + let word = hasStarted ? "équipes" : "inscriptions" + Text(word) } } Text(tournament.subtitleLabel()) From c2933805e9b8beac1f859bcc621761c530d49523 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 14 May 2024 10:54:07 +0200 Subject: [PATCH 3/4] add a todo --- PadelClub/Utils/Network/NetworkManager.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PadelClub/Utils/Network/NetworkManager.swift b/PadelClub/Utils/Network/NetworkManager.swift index 6da1982..ed705bf 100644 --- a/PadelClub/Utils/Network/NetworkManager.swift +++ b/PadelClub/Utils/Network/NetworkManager.swift @@ -36,6 +36,18 @@ class NetworkManager { let task = try await URLSession.shared.download(for: request) if let urlResponse = task.1 as? HTTPURLResponse { if urlResponse.statusCode == 200 { + + //todo à voir si on en a besoin, permet de re-télécharger un csv si on détecte qu'il a été mis à jour +// if FileManager.default.fileExists(atPath: destinationFileUrl.path()) { +// if let creationDate = try checkFileCreationDate(filePath: task.0.path()), let previousCreationDate = try checkFileCreationDate(filePath: destinationFileUrl.path()) { +// print("File creation date:", creationDate) +// print("File previous creation date:", previousCreationDate) +// if previousCreationDate.isEarlierThan(creationDate) { +// try FileManager.default.removeItem(at: destinationFileUrl) +// } +// } +// } + try FileManager.default.copyItem(at: task.0, to: destinationFileUrl) print("dl rank data ok", lastDateString, fileName) } else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { @@ -44,4 +56,11 @@ class NetworkManager { } } } + + func checkFileCreationDate(filePath: String) throws -> Date? { + let fileManager = FileManager.default + let attributes = try fileManager.attributesOfItem(atPath: filePath) + return attributes[.creationDate] as? Date + } + } From ed0148694ff4ce61c2451f320d43e945418d46df Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Tue, 14 May 2024 12:04:09 +0200 Subject: [PATCH 4/4] add matchscheduler storage object --- PadelClub.xcodeproj/project.pbxproj | 2 +- PadelClub/Data/DataStore.swift | 6 +- .../{ViewModel => Data}/MatchScheduler.swift | 200 ++++++++++-------- PadelClub/Data/Tournament.swift | 9 + .../LoserRoundScheduleEditorView.swift | 2 +- .../LoserRoundStepScheduleEditorView.swift | 2 +- .../Planning/MatchScheduleEditorView.swift | 2 +- .../Views/Planning/PlanningSettingsView.swift | 181 +++++++--------- .../Planning/RoundScheduleEditorView.swift | 2 +- PadelClub/Views/Planning/SchedulerView.swift | 2 +- 10 files changed, 212 insertions(+), 196 deletions(-) rename PadelClub/{ViewModel => Data}/MatchScheduler.swift (88%) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index cc109c7..58b0252 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -690,6 +690,7 @@ FF8F263E2BAD7D5C00650388 /* Event.swift */, FF025AE82BD1307E00A86CF8 /* MonthData.swift */, FF1DC5522BAB354A00FD8220 /* MockData.swift */, + FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, @@ -987,7 +988,6 @@ FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, - FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */, ); path = ViewModel; diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 8dfc8ba..3f47775 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -42,7 +42,8 @@ class DataStore: ObservableObject { fileprivate(set) var teamScores: StoredCollection fileprivate(set) var monthData: StoredCollection fileprivate(set) var dateIntervals: StoredCollection - + fileprivate(set) var matchSchedulers: StoredCollection + fileprivate(set) var userStorage: StoredSingleton // fileprivate var _userStorage: OptionalStorage = OptionalStorage(fileName: "user.json") @@ -81,7 +82,8 @@ class DataStore: ObservableObject { self.matches = store.registerCollection(synchronized: synchronized, indexed: indexed) self.monthData = store.registerCollection(synchronized: false, indexed: indexed) self.dateIntervals = store.registerCollection(synchronized: synchronized, indexed: indexed) - + self.matchSchedulers = store.registerCollection(synchronized: false, indexed: indexed) + self.userStorage = store.registerObject(synchronized: synchronized) NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift similarity index 88% rename from PadelClub/ViewModel/MatchScheduler.swift rename to PadelClub/Data/MatchScheduler.swift index 85760fc..077ac2b 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -7,91 +7,71 @@ import Foundation import LeStorage - -struct GroupStageTimeMatch { - let matchID: String - let rotationIndex: Int - var courtIndex: Int - let groupIndex: Int -} - -struct TimeMatch { - let matchID: String - let rotationIndex: Int - var courtIndex: Int - var startDate: Date - var durationLeft: Int //in minutes - var minimumBreakTime: Int //in minutes - - func estimatedEndDate(includeBreakTime: Bool) -> Date { - let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) - return startDate.addingTimeInterval(minutesToAdd * 60.0) - } -} - -struct GroupStageMatchDispatcher { - let timedMatches: [GroupStageTimeMatch] - let freeCourtPerRotation: [Int: [Int]] - let rotationCount: Int - let groupLastRotation: [Int: Int] -} - -struct MatchDispatcher { - let timedMatches: [TimeMatch] - let freeCourtPerRotation: [Int: [Int]] - let rotationCount: Int -} - -extension Match { - func teamIds() -> [String] { - return teams().map { $0.id } +import SwiftUI + +@Observable +class MatchScheduler : ModelObject, Storable { + static func resourceName() -> String { return "match-scheduler" } + static func requestsRequiresToken() -> Bool { true } + + private(set) var id: String = Store.randomId() + var tournament: String + var timeDifferenceLimit: Int + var loserBracketRotationDifference: Int + var upperBracketRotationDifference: Int + var accountUpperBracketBreakTime: Bool + var accountLoserBracketBreakTime: Bool + var randomizeCourts: Bool + var rotationDifferenceIsImportant: Bool + var shouldHandleUpperRoundSlice: Bool + var shouldEndRoundBeforeStartingNext: Bool + + init(tournament: String, + timeDifferenceLimit: Int = 5, + loserBracketRotationDifference: Int = 0, + upperBracketRotationDifference: Int = 1, + accountUpperBracketBreakTime: Bool = true, + accountLoserBracketBreakTime: Bool = false, + randomizeCourts: Bool = true, + rotationDifferenceIsImportant: Bool = false, + shouldHandleUpperRoundSlice: Bool = true, + shouldEndRoundBeforeStartingNext: Bool = true) { + self.tournament = tournament + self.timeDifferenceLimit = timeDifferenceLimit + self.loserBracketRotationDifference = loserBracketRotationDifference + self.upperBracketRotationDifference = upperBracketRotationDifference + self.accountUpperBracketBreakTime = accountUpperBracketBreakTime + self.accountLoserBracketBreakTime = accountLoserBracketBreakTime + self.randomizeCourts = randomizeCourts + self.rotationDifferenceIsImportant = rotationDifferenceIsImportant + self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice + self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext } - func containsTeamId(_ id: String) -> Bool { - teamIds().contains(id) + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _timeDifferenceLimit = "timeDifferenceLimit" + case _loserBracketRotationDifference = "loserBracketRotationDifference" + case _upperBracketRotationDifference = "upperBracketRotationDifference" + case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime" + case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime" + case _randomizeCourts = "randomizeCourts" + case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant" + case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice" + case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext" } -} -enum MatchSchedulerOption: Hashable { - case accountUpperBracketBreakTime - case accountLoserBracketBreakTime - case randomizeCourts - case rotationDifferenceIsImportant - case shouldHandleUpperRoundSlice - case shouldEndRoundBeforeStartingNext -} - -class MatchScheduler { - static let shared = MatchScheduler() - var additionalEstimationDuration : Int = 0 - var options: Set = Set(arrayLiteral: .accountUpperBracketBreakTime) - var timeDifferenceLimit: Double = 300.0 - var loserBracketRotationDifference: Int = 0 - var upperBracketRotationDifference: Int = 1 - var courtsUnavailability: [DateInterval]? = nil - - func shouldEndRoundBeforeStartingNext() -> Bool { - options.contains(.shouldEndRoundBeforeStartingNext) + var courtsUnavailability: [DateInterval]? { + tournamentObject()?.eventObject()?.courtsUnavailability } - func shouldHandleUpperRoundSlice() -> Bool { - options.contains(.shouldHandleUpperRoundSlice) + var additionalEstimationDuration : Int { + tournamentObject()?.additionalEstimationDuration ?? 0 } - func accountLoserBracketBreakTime() -> Bool { - options.contains(.accountLoserBracketBreakTime) - } - - func accountUpperBracketBreakTime() -> Bool { - options.contains(.accountUpperBracketBreakTime) - } - - func randomizeCourts() -> Bool { - options.contains(.randomizeCourts) - } - - func rotationDifferenceIsImportant() -> Bool { - options.contains(.rotationDifferenceIsImportant) + func tournamentObject() -> Tournament? { + Store.main.findById(tournament) } @discardableResult @@ -99,7 +79,6 @@ class MatchScheduler { let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 let groupStages = tournament.groupStages() let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount - courtsUnavailability = tournament.eventObject()?.courtsUnavailability let matches = groupStages.flatMap({ $0._matches() }) matches.forEach({ @@ -197,7 +176,7 @@ class MatchScheduler { var organizedSlots = [GroupStageTimeMatch]() for i in 0..= minimumPossibleEndDate { - if rotationDifferenceIsImportant() { + if rotationDifferenceIsImportant { return previousMatchIsInPreviousRotation } else { return true @@ -375,14 +354,15 @@ class MatchScheduler { let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) print("difference w break", differenceWithBreak) print("difference w/o break", differenceWithoutBreak) + let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) var difference = differenceWithBreak if differenceWithBreak <= 0 { difference = differenceWithoutBreak - } else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit { + } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) } - if difference > timeDifferenceLimit { + if difference > timeDifferenceLimitInSeconds { courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) }) freeCourtPerRotation[rotationIndex] = courts @@ -399,7 +379,7 @@ class MatchScheduler { var organizedSlots = [TimeMatch]() for i in 0.. courts.count { @@ -502,14 +482,13 @@ class MatchScheduler { } func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) { - courtsUnavailability = tournament.eventObject()?.courtsUnavailability let upperRounds = tournament.rounds() let allMatches = tournament.allMatches() var rounds = [Round]() - if shouldEndRoundBeforeStartingNext() { + if shouldEndRoundBeforeStartingNext { rounds = upperRounds.flatMap { [$0] + $0.loserRoundsAndChildren() } @@ -607,8 +586,51 @@ class MatchScheduler { } func updateSchedule(tournament: Tournament) { - courtsUnavailability = tournament.eventObject()?.courtsUnavailability let lastDate = updateGroupStageSchedule(tournament: tournament) updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) } } + +struct GroupStageTimeMatch { + let matchID: String + let rotationIndex: Int + var courtIndex: Int + let groupIndex: Int +} + +struct TimeMatch { + let matchID: String + let rotationIndex: Int + var courtIndex: Int + var startDate: Date + var durationLeft: Int //in minutes + var minimumBreakTime: Int //in minutes + + func estimatedEndDate(includeBreakTime: Bool) -> Date { + let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) + return startDate.addingTimeInterval(minutesToAdd * 60.0) + } +} + +struct GroupStageMatchDispatcher { + let timedMatches: [GroupStageTimeMatch] + let freeCourtPerRotation: [Int: [Int]] + let rotationCount: Int + let groupLastRotation: [Int: Int] +} + +struct MatchDispatcher { + let timedMatches: [TimeMatch] + let freeCourtPerRotation: [Int: [Int]] + let rotationCount: Int +} + +extension Match { + func teamIds() -> [String] { + return teams().map { $0.id } + } + + func containsTeamId(_ id: String) -> Bool { + teamIds().contains(id) + } +} diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 0447a27..8233ad6 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1302,8 +1302,17 @@ class Tournament : ModelObject, Storable { try Store.main.deleteDependencies(items: self.unsortedTeams()) try Store.main.deleteDependencies(items: self.groupStages()) try Store.main.deleteDependencies(items: self.rounds()) + try Store.main.deleteDependencies(items: self._matchSchedulers()) } + private func _matchSchedulers() -> [MatchScheduler] { + Store.main.filter(isIncluded: { $0.id == self.id }) + } + + func matchScheduler() -> MatchScheduler? { + _matchSchedulers().first + } + func currentMonthData() -> MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index 2b35159..63c5eb0 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -66,7 +66,7 @@ struct LoserRoundScheduleEditorView: View { // _save() let loserRounds = upperRound.loserRounds().filter { $0.isDisabled() == false } - MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: loserRounds.first?.id, fromMatchId: nil, startDate: startDate) + tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: loserRounds.first?.id, fromMatchId: nil, startDate: startDate) loserRounds.first?.startDate = startDate _save() } diff --git a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift index 1904942..af61798 100644 --- a/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift @@ -59,7 +59,7 @@ struct LoserRoundStepScheduleEditorView: View { } private func _updateSchedule() async { - MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in round.startDate = startDate }) diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 7f45a07..6edf190 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -33,7 +33,7 @@ struct MatchScheduleEditorView: View { } private func _updateSchedule() async { - MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) + tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 42d5218..7b67c65 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -10,36 +10,25 @@ import LeStorage struct PlanningSettingsView: View { @EnvironmentObject var dataStore: DataStore - var tournament: Tournament - @State private var randomCourtDistribution: Bool + @Bindable var tournament: Tournament + @Bindable var matchScheduler: MatchScheduler + @State private var groupStageCourtCount: Int - @State private var upperBracketBreakTime: Bool - @State private var loserBracketBreakTime: Bool - @State private var rotationDifferenceIsImportant: Bool - @State private var loserBracketRotationDifference: Int - @State private var upperBracketRotationDifference: Int - @State private var timeDifferenceLimit: Double - @State private var shouldHandleUpperRoundSlice: Bool @State private var isScheduling: Bool = false @State private var schedulingDone: Bool = false @State private var showOptions: Bool = false - @State private var shouldEndBeforeStartNext: Bool = true init(tournament: Tournament) { self.tournament = tournament + if let matchScheduler = tournament.matchScheduler() { + self.matchScheduler = matchScheduler + } else { + self.matchScheduler = MatchScheduler(tournament: tournament.id) + } self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1) - self._loserBracketRotationDifference = State(wrappedValue: MatchScheduler.shared.loserBracketRotationDifference) - self._upperBracketRotationDifference = State(wrappedValue: MatchScheduler.shared.upperBracketRotationDifference) - self._timeDifferenceLimit = State(wrappedValue: MatchScheduler.shared.timeDifferenceLimit) - self._rotationDifferenceIsImportant = State(wrappedValue: MatchScheduler.shared.rotationDifferenceIsImportant()) - self._randomCourtDistribution = State(wrappedValue: MatchScheduler.shared.randomizeCourts()) - self._upperBracketBreakTime = State(wrappedValue: MatchScheduler.shared.accountUpperBracketBreakTime()) - self._loserBracketBreakTime = State(wrappedValue: MatchScheduler.shared.accountLoserBracketBreakTime()) - self._shouldHandleUpperRoundSlice = State(wrappedValue: MatchScheduler.shared.shouldHandleUpperRoundSlice()) } var body: some View { - @Bindable var tournament = tournament List { SubscriptionInfoView() @@ -80,10 +69,6 @@ struct PlanningSettingsView: View { await _setupSchedule() schedulingDone = true } - - if showOptions { - _optionsView() - } } footer: { Button { showOptions.toggle() @@ -93,23 +78,38 @@ struct PlanningSettingsView: View { } .buttonStyle(.borderless) } + + if showOptions { + _optionsView() + } Section { RowButtonView("Supprimer tous les horaires", role: .destructive) { - let allMatches = tournament.allMatches() - allMatches.forEach({ $0.startDate = nil }) - try? dataStore.matches.addOrUpdate(contentOfs: allMatches) - - let allGroupStages = tournament.groupStages() - allGroupStages.forEach({ $0.startDate = nil }) - try? dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages) - - let allRounds = tournament.allRounds() - allRounds.forEach({ $0.startDate = nil }) - try? dataStore.rounds.addOrUpdate(contentOfs: allRounds) + do { + let allMatches = tournament.allMatches() + allMatches.forEach({ $0.startDate = nil }) + try dataStore.matches.addOrUpdate(contentOfs: allMatches) + + let allGroupStages = tournament.groupStages() + allGroupStages.forEach({ $0.startDate = nil }) + try dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + let allRounds = tournament.allRounds() + allRounds.forEach({ $0.startDate = nil }) + try dataStore.rounds.addOrUpdate(contentOfs: allRounds) + } catch { + Logger.error(error) + } } } } + .onAppear { + do { + try dataStore.matchSchedulers.addOrUpdate(instance: matchScheduler) + } catch { + Logger.error(error) + } + } .overlay(alignment: .bottom) { if schedulingDone { Label("Horaires mis à jour", systemImage: "checkmark.circle.fill") @@ -137,87 +137,70 @@ struct PlanningSettingsView: View { @ViewBuilder private func _optionsView() -> some View { - Toggle(isOn: $randomCourtDistribution) { - Text("Distribuer les terrains au hasard") - } - Toggle(isOn: $shouldHandleUpperRoundSlice) { - Text("Équilibrer les matchs d'une manche sur plusieurs tours") - } - - Toggle(isOn: $shouldEndBeforeStartNext) { - Text("Finir une manche et les matchs de classements avant de continuer") - } - - Toggle(isOn: $upperBracketBreakTime) { - Text("Tableau : tenir compte des pauses") + Section { + Toggle(isOn: $matchScheduler.randomizeCourts) { + Text("Distribuer les terrains au hasard") + } + + Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { + Text("Équilibrer les matchs d'une manche sur plusieurs tours") + } + + Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { + Text("Finir une manche, classement inclus avant de continuer") + } } - Toggle(isOn: $loserBracketBreakTime) { - Text("Classement : tenir compte des pauses") + Section { + Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) { + Text("Tenir compte des pauses") + Text("Tableau") + } + + Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) { + Text("Tenir compte des pauses") + Text("Classement") + } } - Toggle(isOn: $rotationDifferenceIsImportant) { - Text("Forcer un créneau supplémentaire entre 2 phases") + Section { + Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) { + Text("Forcer un créneau supplémentaire entre 2 phases") + } + + LabeledContent { + StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2) + } label: { + Text("Tableau") + } + .disabled(matchScheduler.rotationDifferenceIsImportant == false) + + LabeledContent { + StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2) + } label: { + Text("Classement") + } + .disabled(matchScheduler.rotationDifferenceIsImportant == false) } - LabeledContent { - StepperView(count: $upperBracketRotationDifference, minimum: 0, maximum: 2) - } label: { - Text("Tableau") - } - .disabled(rotationDifferenceIsImportant == false) - - LabeledContent { - StepperView(count: $loserBracketRotationDifference, minimum: 0, maximum: 2) - } label: { - Text("Classement") + Section { + LabeledContent { + StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5) + } label: { + Text("Optimisation des créneaux") + Text("Si libre plus de \(matchScheduler.timeDifferenceLimit) minutes") + } } - .disabled(rotationDifferenceIsImportant == false) - - //timeDifferenceLimit - } private func _setupSchedule() async { - - let matchScheduler = MatchScheduler.shared - - matchScheduler.options.removeAll() - - if shouldEndBeforeStartNext { - matchScheduler.options.insert(.shouldEndRoundBeforeStartingNext) - } - - if randomCourtDistribution { - matchScheduler.options.insert(.randomizeCourts) - } - - if shouldHandleUpperRoundSlice { - matchScheduler.options.insert(.shouldHandleUpperRoundSlice) - } - - if upperBracketBreakTime { - matchScheduler.options.insert(.accountUpperBracketBreakTime) - } - - if loserBracketBreakTime { - matchScheduler.options.insert(.accountLoserBracketBreakTime) - } - - if rotationDifferenceIsImportant { - matchScheduler.options.insert(.rotationDifferenceIsImportant) - } - - matchScheduler.loserBracketRotationDifference = loserBracketRotationDifference - matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference - matchScheduler.timeDifferenceLimit = timeDifferenceLimit - matchScheduler.updateSchedule(tournament: tournament) } private func _save() { do { + try dataStore.matchSchedulers.addOrUpdate(instance: matchScheduler) try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index d9149da..cecf708 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -54,7 +54,7 @@ struct RoundScheduleEditorView: View { } private func _updateSchedule() async { - MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) + tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) round.startDate = startDate _save() } diff --git a/PadelClub/Views/Planning/SchedulerView.swift b/PadelClub/Views/Planning/SchedulerView.swift index 8785ecf..9b22844 100644 --- a/PadelClub/Views/Planning/SchedulerView.swift +++ b/PadelClub/Views/Planning/SchedulerView.swift @@ -32,7 +32,7 @@ struct SchedulerView: View { case .scheduleGroupStage: MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) { Task { - MatchScheduler.shared.updateSchedule(tournament: tournament) + tournament.matchScheduler()?.updateSchedule(tournament: tournament) } } .onChange(of: tournament.groupStageMatchFormat) {