diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a5b55a7..3bd322d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -231,6 +231,8 @@ FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; }; FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; }; FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; }; + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; }; FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; @@ -518,6 +520,8 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = ""; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = ""; }; + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = ""; }; + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = ""; }; FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; @@ -955,6 +959,7 @@ FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */, + FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, ); path = ViewModel; sourceTree = ""; @@ -1168,6 +1173,7 @@ FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */, FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */, FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */, + FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */, FF1162882BD0523B000C4809 /* Components */, ); path = Planning; @@ -1433,6 +1439,7 @@ FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */, FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, + FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, @@ -1464,6 +1471,7 @@ FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */, FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */, FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */, + FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, diff --git a/PadelClub/Data/Federal/FederalPlayer.swift b/PadelClub/Data/Federal/FederalPlayer.swift index 32a2010..091421b 100644 --- a/PadelClub/Data/Federal/FederalPlayer.swift +++ b/PadelClub/Data/Federal/FederalPlayer.swift @@ -22,6 +22,7 @@ protocol PlayerHolder { var ligueName: String? { get } var assimilation: String? { get } var computedAge: Int? { get } + func getAssimilatedAsMaleRank() -> Int? } extension PlayerHolder { @@ -30,8 +31,31 @@ extension PlayerHolder { } } +fileprivate extension Int { + var femaleInMaleAssimilation: Int { + self + femaleInMaleAssimilationAddition + } + + var femaleInMaleAssimilationAddition: Int { + switch self { + case 1...10: return 400 + case 11...30: return 1000 + case 31...60: return 2000 + case 61...100: return 3000 + case 101...200: return 8000 + case 201...500: return 12000 + default: + return 15000 + } + } +} extension ImportedPlayer: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + guard male == false else { return nil } + return getRank()?.femaleInMaleAssimilation + } + var computedAge: Int? { nil } var tournamentPlayed: Int? { diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index f70800a..43c74aa 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -184,7 +184,15 @@ class Match: ModelObject, Storable { } func next() -> Match? { - Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first + Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first + } + + func getDuration() -> Int { + if let tournament = currentTournament() { + matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + } else { + matchFormat.getEstimatedDuration() + } } func roundTitle() -> String? { diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 064e781..7cad2cb 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -42,15 +42,12 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable + let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed() + let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) + let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) + let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) - //todo - /* - tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments) - tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments) - tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments) - */ - - return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior) + return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge) } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index e912c37..5f06bf3 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -343,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable { return 15000 } } - } extension PlayerRegistration: Hashable { @@ -357,6 +356,9 @@ extension PlayerRegistration: Hashable { } extension PlayerRegistration: PlayerHolder { + func getAssimilatedAsMaleRank() -> Int? { + nil + } func getFirstName() -> String { firstName diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index bd44b9b..00bf56f 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -44,6 +44,8 @@ class Tournament : ModelObject, Storable { var payment: TournamentPayment = .free var additionalEstimationDuration: Int = 0 + var courtsUnavailability: [Int: [DateInterval]]? = nil + @ObservationIgnored var navigationPath: [Screen] = [] @@ -257,12 +259,16 @@ class Tournament : ModelObject, Storable { let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) - if availableSeeds.count == availableSeedSpot.count { + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.dimension == availableSeeds.count { return availableSeedGroup - } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) { + } else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.dimension == availableSeedOpponentSpot.count { return availableSeedGroup - } else if let chunk = availableSeedGroup.chunk() { - return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } else if let chunks = availableSeedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk) + } } } @@ -500,7 +506,9 @@ class Tournament : ModelObject, Storable { func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] { let licenseYearValidity = licenseYearValidity() - return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) }) + return players.filter({ + ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true)) + }) } func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? { @@ -515,7 +523,7 @@ class Tournament : ModelObject, Storable { previousTeam.updatePlayers(team.players) teamsToImport.append(previousTeam) } else { - let newTeam = addTeam(team.players) + let newTeam = addTeam(team.players, registrationDate: team.registrationDate) teamsToImport.append(newTeam) } } @@ -905,8 +913,8 @@ class Tournament : ModelObject, Storable { selectedSortedTeams().firstIndex(where: { $0.id == team.id }) } - func addTeam(_ players: Set) -> TeamRegistration { - let team = TeamRegistration(tournament: id, registrationDate: Date()) + func addTeam(_ players: Set, registrationDate: Date? = nil) -> TeamRegistration { + let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date()) team.tournamentCategory = tournamentCategory team.setWeight(from: Array(players)) players.forEach { player in @@ -1005,7 +1013,14 @@ class Tournament : ModelObject, Storable { return groupStageMatchFormat } } - + + func setupFederalSettings() { + teamSorting = tournamentLevel.defaultTeamSortingType + groupStageMatchFormat = groupStageSmartMatchFormat() + loserBracketMatchFormat = loserBracketSmartMatchFormat(1) + matchFormat = roundSmartMatchFormat(1) + } + func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { let format = tournamentLevel.federalFormatForBracketRound(roundIndex) if matchFormat.rank > format.rank { @@ -1129,6 +1144,4 @@ extension Tournament: TournamentBuildHolder { var age: FederalTournamentAge { federalTournamentAge } - - } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 835302e..bac5698 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -89,6 +89,10 @@ extension Date { } } + func atBeginningOfDay(hourInt: Int = 9) -> Date { + Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)! + } + static var firstDayOfWeek = Calendar.current.firstWeekday static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current @@ -180,9 +184,15 @@ extension Date { var dayInt: Int { Calendar.current.component(.day, from: self) } + var startOfDay: Date { Calendar.current.startOfDay(for: self) } + + func endOfDay() -> Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)! + } } extension Date { @@ -191,3 +201,12 @@ extension Date { } } +extension Date { + func localizedTime() -> String { + self.formatted(.dateTime.hour().minute()) + } + + func localizedDay() -> String { + self.formatted(.dateTime.weekday(.wide).day()) + } +} diff --git a/PadelClub/Manager/FileImportManager.swift b/PadelClub/Manager/FileImportManager.swift index 3810e7e..b2d6ca5 100644 --- a/PadelClub/Manager/FileImportManager.swift +++ b/PadelClub/Manager/FileImportManager.swift @@ -44,10 +44,13 @@ class FileImportManager { var id: Self { self } case frenchFederation + case padelClub case unknown var localizedLabel: String { switch self { + case .padelClub: + return "Padel Club" case .frenchFederation: return "FFT" case .unknown: @@ -58,24 +61,20 @@ class FileImportManager { struct TeamHolder: Identifiable { let id: UUID = UUID() - let playerOne: PlayerRegistration - let playerTwo: PlayerRegistration + let players: Set let weight: Int let tournamentCategory: TournamentCategory let previousTeam: TeamRegistration? - - init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) { - self.playerOne = playerOne - self.playerTwo = playerTwo + var registrationDate: Date? = nil + + init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) { + self.players = Set(players) self.tournamentCategory = tournamentCategory self.previousTeam = previousTeam - self.weight = playerOne.weight + playerTwo.weight - } - - var players: Set { - Set([playerOne, playerTwo]) + self.weight = players.map { $0.weight }.reduce(0,+) + self.registrationDate = registrationDate } - + func index(in teams: [TeamHolder]) -> Int? { teams.firstIndex(where: { $0.id == id }) } @@ -100,6 +99,60 @@ class FileImportManager { static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur" func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] { + + switch fileProvider { + case .frenchFederation: + return await _getFederalTeams(from: fileContent, tournament: tournament) + case .padelClub: + return await _getPadelClubTeams(from: fileContent, tournament: tournament) + case .unknown: + return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament) + } + } + + func importDataFromFFT() async -> String? { + if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { + for source in SourceFile.allCases { + for fileURL in source.currentURLs { + let p = readCSV(inputFile: fileURL) + await importingChunkOfPlayers(p, importingDate: importingDate) + } + } + return URL.importDateFormatter.string(from: importingDate) + } + return nil + } + + + func readCSV(inputFile: URL) -> [FederalPlayer] { + do { + let fileContent = try String(contentsOf: inputFile) + return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) + } catch { + print("error: \(error)") // to do deal with errors + } + return [] + } + + func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + let lines = fileContent.components(separatedBy: "\n") + return lines.compactMap { line in + if line.components(separatedBy: ";").count < 10 { + } else { + let data = line.components(separatedBy: ";").joined(separator: "\n") + return FederalPlayer(data, isMale: isMale) + } + return nil + } + } + + func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { + for chunk in players.chunked(into: 1000) { + await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + } + } + + private func _getFederalTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") guard let firstLine = lines.first else { return [] } var separator = "," @@ -108,58 +161,7 @@ class FileImportManager { } let headerCount = firstLine.components(separatedBy: separator).count var results: [TeamHolder] = [] - if headerCount == 23 && fileProvider == .unknown { //PBL - let fetchRequest = ImportedPlayer.fetchRequest() - let federalContext = PersistenceController.shared.localContainer.viewContext - - lines.dropFirst().forEach { line in - let data = line.components(separatedBy: separator) - if data.count == 23 { - -// let team = Team(context: context) -// let brand = Brand(context: context) -// brand.title = data[2].trimmed -// brand.qualifier = data[0].trimmed -// brand.country = data[1].trimmed -// brand.lineOfBusiness = data[3].trimmed -// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix -// brand.lineOfBusiness = "Bâtiment / Immo / Transport" -// } -// brand.name = data[4].trimmed -// team.brand = brand -// -// for i in 0...5 { -// let sex = data[i*3+5] -// let lastName = data[i*3+6].trimmed -// let firstName = data[i*3+7].trimmed -// if lastName.isEmpty == false { -// let playerOne = Player(context: context) -// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) -// fetchRequest.predicate = predicate -// if let playerFound = try? federalContext.fetch(fetchRequest).first { -// playerOne.updateWithImportedPlayer(playerFound) -// } else { -// playerOne.lastName = lastName -// playerOne.firstName = firstName -// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 -// playerOne.currentRank = tournament?.lastRankMan ?? 0 -// } -// team.addToPlayers(playerOne) -// } -// } -// team.category = TournamentCategory.men.importingRawValue -// -// if let players = team.players, players.count > 0 { -// results.append(team) -// } else { -// context.delete(team) -// } - } - } - - - return results - } else if headerCount <= 18 && fileProvider == .frenchFederation { + if headerCount <= 18 { Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in if teamLines.count == 2 { let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) @@ -211,13 +213,13 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } } return results - } else if headerCount > 18 && fileProvider == .frenchFederation { + } else { lines.dropFirst().forEach { line in let data = line.components(separatedBy: separator) if data.count > 18 { @@ -244,7 +246,7 @@ class FileImportManager { case .mix: return 1 } } - + var sexPlayerTwo : Int { switch tournamentCategory { case .men: return 1 @@ -257,56 +259,108 @@ class FileImportManager { playerOne.setWeight(in: tournament) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) playerTwo.setWeight(in: tournament) - - let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) + + let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo])) results.append(team) } } return results - } else { - return [] } } - func importDataFromFFT() async -> String? { - if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { - for source in SourceFile.allCases { - for fileURL in source.currentURLs { - let p = readCSV(inputFile: fileURL) - await importingChunkOfPlayers(p, importingDate: importingDate) + private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { + let lines = fileContent.components(separatedBy: "\n\n") + var results: [TeamHolder] = [] + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.forEach { team in + let data = team.components(separatedBy: "\n") + let players = team.licencesFound() + fetchRequest.predicate = NSPredicate(format: "license IN %@", players) + let found = try? federalContext.fetch(fetchRequest) + let registeredPlayers = found?.map({ importedPlayer in + let player = PlayerRegistration(importedPlayer: importedPlayer) + player.setWeight(in: tournament) + return player + }) + 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()) + } + return nil } + let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate) + results.append(team) } - return URL.importDateFormatter.string(from: importingDate) - } - return nil - } - - - func readCSV(inputFile: URL) -> [FederalPlayer] { - do { - let fileContent = try String(contentsOf: inputFile) - return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData) - } catch { - print("error: \(error)") // to do deal with errors } - return [] + + return results } - func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] { + private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { let lines = fileContent.components(separatedBy: "\n") - return lines.compactMap { line in - if line.components(separatedBy: ";").count < 10 { - } else { - let data = line.components(separatedBy: ";").joined(separator: "\n") - return FederalPlayer(data, isMale: isMale) - } - return nil + guard let firstLine = lines.first else { return [] } + var separator = "," + if firstLine.contains(";") { + separator = ";" } - } - - func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async { - for chunk in players.chunked(into: 1000) { - await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate) + let headerCount = firstLine.components(separatedBy: separator).count + var results: [TeamHolder] = [] + if headerCount == 23 { + //todo + let fetchRequest = ImportedPlayer.fetchRequest() + let federalContext = PersistenceController.shared.localContainer.viewContext + + lines.dropFirst().forEach { line in + let data = line.components(separatedBy: separator) + if data.count == 23 { + +// let team = Team(context: context) +// let brand = Brand(context: context) +// brand.title = data[2].trimmed +// brand.qualifier = data[0].trimmed +// brand.country = data[1].trimmed +// brand.lineOfBusiness = data[3].trimmed +// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix +// brand.lineOfBusiness = "Bâtiment / Immo / Transport" +// } +// brand.name = data[4].trimmed +// team.brand = brand +// +// for i in 0...5 { +// let sex = data[i*3+5] +// let lastName = data[i*3+6].trimmed +// let firstName = data[i*3+7].trimmed +// if lastName.isEmpty == false { +// let playerOne = Player(context: context) +// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens) +// fetchRequest.predicate = predicate +// if let playerFound = try? federalContext.fetch(fetchRequest).first { +// playerOne.updateWithImportedPlayer(playerFound) +// } else { +// playerOne.lastName = lastName +// playerOne.firstName = firstName +// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1 +// playerOne.currentRank = tournament?.lastRankMan ?? 0 +// } +// team.addToPlayers(playerOne) +// } +// } +// team.category = TournamentCategory.men.importingRawValue +// +// if let players = team.players, players.count > 0 { +// results.append(team) +// } else { +// context.delete(team) +// } + } + } + + + return results } + return [] } } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 47e5cc1..ed94063 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -159,20 +159,18 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case a45 = 450 case a55 = 550 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - .senior -// return tournaments.first?.federalTournamentAge ?? .a11_12 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.federalTournamentAge ?? .senior } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! FederalTournamentAge -// } else { -// return mostRecent(tournaments: tournaments) -// } - .senior + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! FederalTournamentAge + } else { + return mostRecent(inTournaments: tournaments) + } } var id: Int { self.rawValue } @@ -236,22 +234,20 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case p1500 = 1500 case p2000 = 2000 - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentLevel ?? .p25 - .p100 + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentLevel ?? .p100 } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentLevel -// } else { -// return mostRecent(tournaments: tournaments) -// } - .p100 + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentLevel + } else { + return mostRecent(inTournaments: tournaments) + } } - + var id: Int { self.rawValue } func maximumDuration() -> Double { @@ -631,20 +627,27 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { } } - static func mostRecent(tournaments: [Tournament] = []) -> Self { - //return tournaments.first?.tournamentCategory ?? .mix - .men + var showFemaleInMaleAssimilation: Bool { + switch self { + case .men: + return true + default: + return false + } + } + + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { + return tournaments.first?.tournamentCategory ?? .men } - static func mostUsed(tournaments: [Tournament] = []) -> Self { -// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) -// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } -// if mostFrequent != nil { -// return mostFrequent as! TournamentCategory -// } else { -// return mostRecent(tournaments: tournaments) -// } - .men + static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { + let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) + let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } + if mostFrequent != nil { + return mostFrequent as! TournamentCategory + } else { + return mostRecent(inTournaments: tournaments) + } } var next: TournamentCategory { @@ -1019,10 +1022,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat { - if UserDefaults.standard.object(forKey: matchType.rawValue + "MatchFormatPreference") == nil { - return .nineGamesDecisivePoint + switch matchType { + case .bracket: + MatchFormat(rawValue: DataStore.shared.appSettings.bracketMatchFormatPreference) ?? .nineGamesDecisivePoint + case .groupStage: + MatchFormat(rawValue: DataStore.shared.appSettings.groupStageMatchFormatPreference) ?? .nineGamesDecisivePoint + case .loserBracket: + MatchFormat(rawValue: DataStore.shared.appSettings.loserBracketMatchFormatPreference) ?? .nineGamesDecisivePoint } - return MatchFormat(rawValue: UserDefaults.standard.integer(forKey: matchType.rawValue + "MatchFormatPreference")) ?? .nineGamesDecisivePoint } static var allCases: [MatchFormat] { diff --git a/PadelClub/ViewModel/DateInterval.swift b/PadelClub/ViewModel/DateInterval.swift new file mode 100644 index 0000000..58876bf --- /dev/null +++ b/PadelClub/ViewModel/DateInterval.swift @@ -0,0 +1,32 @@ +// +// DateInterval.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import Foundation +import LeStorage + +struct DateInterval: Identifiable, Codable { + var id: String = Store.randomId() + + let startDate: Date + let endDate: Date + + var range: Range { + startDate.. Bool { + Calendar.current.isDate(startDate, inSameDayAs: endDate) + } + + func isDateInside(_ date: Date) -> Bool { + date >= startDate && date <= endDate + } + + func isDateOutside(_ date: Date) -> Bool { + date <= startDate && date <= endDate && date >= startDate && date >= endDate + } +} diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 6fa4495..90ecb2d 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -67,6 +67,7 @@ class MatchScheduler { var timeDifferenceLimit: Double = 300.0 var loserBracketRotationDifference: Int = 0 var upperBracketRotationDifference: Int = 1 + var courtsUnavailability: [Int: [DateInterval]]? = nil func shouldHandleUpperRoundSlice() -> Bool { options.contains(.shouldHandleUpperRoundSlice) @@ -176,7 +177,18 @@ class MatchScheduler { } func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { - //print(roundObject.roundTitle(), match.matchTitle()) + print(roundObject.roundTitle(), match.matchTitle()) + + if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { + print("can't start \(targetedStartDate) earlier than \(roundStartDate)") + if targetedStartDate == minimumTargetedEndDate { + minimumTargetedEndDate = roundStartDate + } else { + minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) + } + return false + } + let previousMatches = roundObject.precedentMatches(ofMatch: match) if previousMatches.isEmpty { return true } @@ -361,33 +373,55 @@ class MatchScheduler { func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) { var matchPerRound = [Int: Int]() var minimumTargetedEndDate: Date = rotationStartDate - courts.forEach { courtIndex in - //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) - + print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex) + courts.sorted().forEach { courtIndex in + print("trying to find a match for \(courtIndex) in \(rotationIndex)") if let first = availableMatchs.first(where: { match in let roundObject = match.roundObject! + let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration)) + print("courtsUnavailable \(courtsUnavailable)") + if courtIndex >= availableCourts - courtsUnavailable { + return false + } + let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 if shouldHandleUpperRoundSlice() { let roundMatchesCount = roundObject.playedMatches().count + print("shouldHandleUpperRoundSlice \(roundMatchesCount)") if roundObject.loser == nil && roundMatchesCount > courts.count { - if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false } + print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") + if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { + print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))") + return false + } } } - if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() { + let indexInRound = match.indexInRound() + + print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)") + if roundObject.loser == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() { if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + + print("next match and this match can be played, returning true") + return true } else { + print("next match and this match can not be played at the same time, returning false") return false } } + + print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)") if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 { + print("we return false") return false } + return canBePlayed }) { @@ -409,11 +443,17 @@ class MatchScheduler { } if freeCourtPerRotation[rotationIndex]!.count == availableCourts { + print("no match found to be put in this rotation, check if we can put anything to another date") freeCourtPerRotation[rotationIndex] = [] let courtsUsed = getNextEarliestAvailableDate(from: slots) - let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in - availableDate <= minimumTargetedEndDate - }.sorted(by: \.1).map { $0.0 } + var freeCourts: [Int] = [] + if courtsUsed.isEmpty { + freeCourts = (0.. Int { + let endDate = startDate.addingTimeInterval(Double(duration) * 60) + guard let courtsUnavailability else { return 0 } + let courts = courtsUnavailability.keys + return courts.filter { + courtUnavailable(courtIndex: $0, from: startDate, to: endDate) + }.count + } + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { + guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true } + return courtLockedSchedule.anySatisfy({ dateInterval in + dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate) + }) + } +} diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 6e4d80c..e906c6d 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -14,6 +14,8 @@ class SearchViewModel: ObservableObject, Identifiable { var codeClub: String? = nil var clubName: String? = nil var ligueName: String? = nil + var showFemaleInMaleAssimilation: Bool = false + @Published var debouncableText: String = "" @Published var searchText: String = "" @Published var task: DispatchWorkItem? diff --git a/PadelClub/ViewModel/SeedInterval.swift b/PadelClub/ViewModel/SeedInterval.swift index d0a82c0..e9d397c 100644 --- a/PadelClub/ViewModel/SeedInterval.swift +++ b/PadelClub/ViewModel/SeedInterval.swift @@ -15,10 +15,26 @@ struct SeedInterval: Hashable, Comparable { return lhs.first < rhs.first } + var dimension: Int { + (last - first) + } + + func chunks() -> [SeedInterval]? { + if dimension > 3 { + let split = dimension / 2 + let firstHalf = SeedInterval(first: first, last: first + split - 1) + let secondHalf = SeedInterval(first: first + split, last: last) + return [firstHalf, secondHalf] + } else { + return nil + } + } + func chunk() -> SeedInterval? { - if (last - first) / 2 > 0 { - if last - (last - first) / 2 > first { - return SeedInterval(first: first, last: last - (last - first) / 2) + if dimension / 2 > 0 { + let halfDimension = last - dimension / 2 + if halfDimension > first { + return SeedInterval(first: first, last: halfDimension - 1) } } return nil diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index c908431..9ee8d7e 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -100,6 +100,7 @@ struct EventCreationView: View { tournaments.forEach { tournament in tournament.startDate = startingDate tournament.dayDuration = duration + tournament.setupFederalSettings() } try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index fceeee2..399f2b3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -9,7 +9,6 @@ import SwiftUI struct MatchDetailView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament: Tournament @Environment(\.dismiss) var dismiss let matchViewStyle: MatchViewStyle @@ -321,7 +320,7 @@ struct MatchDetailView: View { Section { if match.hasEnded() == false { - let rotationDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + let rotationDuration = match.getDuration() Picker(selection: $startDateSetup) { if match.isReady() { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift new file mode 100644 index 0000000..3f34b5b --- /dev/null +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -0,0 +1,157 @@ +// +// CourtAvailabilitySettingsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/04/2024. +// + +import SwiftUI + +struct CourtAvailabilitySettingsView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]() + @State private var showingPopover: Bool = false + @State private var courtIndex: Int = 0 + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + + var body: some View { + List { + let keys = courtsUnavailability.keys.sorted(by: \.self) + ForEach(keys, id: \.self) { key in + if let dates = courtsUnavailability[key] { + Section { + ForEach(dates) { dateInterval in + HStack { + VStack(alignment: .leading, spacing: 0) { + Text(dateInterval.startDate.localizedTime()).font(.largeTitle) + Text(dateInterval.startDate.localizedDay()).font(.caption) + } + Spacer() + Image(systemName: "arrowshape.forward.fill") + .tint(.master) + Spacer() + VStack(alignment: .trailing, spacing: 0) { + Text(dateInterval.endDate.localizedTime()).font(.largeTitle) + Text(dateInterval.endDate.localizedDay()).font(.caption) + } + } + .contextMenu(menuItems: { + Button("dupliquer") { + + } + Button("éditer") { + + } + Button("effacer") { + + } + }) + .swipeActions { + Button(role: .destructive) { + courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id }) + } label: { + LabelDelete() + } + } + } + } header: { + Text("Terrain #\(key + 1)") + } + .headerProminence(.increased) + } + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showingPopover = true + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } + } + } + .onDisappear { + tournament.courtsUnavailability = courtsUnavailability + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Créneaux") + .popover(isPresented: $showingPopover) { + NavigationStack { + Form { + Section { + CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: 3) + } + + Section { + DatePicker("Début", selection: $startDate) + DatePicker("Fin", selection: $endDate) + } footer: { + Button("jour entier") { + startDate = startDate.startOfDay + endDate = endDate.endOfDay() + } + .buttonStyle(.borderless) + .underline() + } + } + .toolbar { + Button("Valider") { + let dateInterval = DateInterval(startDate: startDate, endDate: endDate) + var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]() + courtUnavailability.append(dateInterval) + courtsUnavailability[courtIndex] = courtUnavailability + showingPopover = false + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Nouveau créneau") + } + .onAppear { + UIDatePicker.appearance().minuteInterval = 5 + } + .onDisappear { + UIDatePicker.appearance().minuteInterval = 1 + } + } + } +} + +struct CourtPicker: View { + let title: String + @Binding var selection: Int + let maxCourt: Int + + var body: some View { + Picker(title, selection: $selection) { + ForEach(0.. 1 { +// ForEach(0.. some View { VStack(alignment: .leading) { ImportedPlayerView(player: player) + HStack { + Text(player.isImported() ? "importé" : "non importé") + Text(player.formattedLicense().isLicenseNumber ? "valide" : "non valide") + } HStack { Menu { if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") { diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index 392c64a..3bfa6e2 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -10,6 +10,7 @@ import SwiftUI struct ImportedPlayerView: View { let player: PlayerHolder var index: Int? = nil + var showFemaleInMaleAssimilation: Bool = false var body: some View { VStack(alignment: .leading) { @@ -39,7 +40,7 @@ struct ImportedPlayerView: View { .font(.title3) if let rank = player.getRank() { Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) - .font(.caption) + .font(.caption) } } @@ -58,6 +59,19 @@ struct ImportedPlayerView: View { } } + if let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank(), showFemaleInMaleAssimilation { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + } + Text(")").font(.title3) + } + HStack { Text(player.formattedLicense()) if let computedAge = player.computedAge { diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 7b72993..318d7b9 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -34,12 +34,13 @@ struct SelectablePlayerListView: View { return URL.importDateFormatter.date(from: lastDataSource) } - init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection // self.searchText = searchField ?? "" self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction let searchViewModel = SearchViewModel() + searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation searchViewModel.searchText = searchField ?? "" searchViewModel.isPresented = allowSelection != 0 searchViewModel.user = user @@ -292,7 +293,7 @@ struct MySearchView: View { let array = Array(searchViewModel.selectedPlayers) Section { ForEach(array) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .onDelete { indexSet in for index in indexSet { @@ -307,7 +308,7 @@ struct MySearchView: View { } else { Section { ForEach(players, id: \.self) { player in - ImportedPlayerView(player: player, index: nil) + ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -326,7 +327,7 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } .buttonStyle(.plain) } @@ -339,7 +340,7 @@ struct MySearchView: View { } else { Section { ForEach(players) { player in - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } header: { if players.isEmpty == false { @@ -356,13 +357,13 @@ struct MySearchView: View { Button { searchViewModel.selectedPlayers.insert(player) } label: { - ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil) + ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) .contentShape(Rectangle()) } .frame(maxWidth: .infinity) .buttonStyle(.plain) } else { - ImportedPlayerView(player: player) + ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation) } } } header: { diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 9eeb438..39f7751 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -40,6 +40,14 @@ struct FileImportView: View { Label("beach-padel.app.fft.fr", systemImage: "tennisball") } + Picker(selection: $fileProvider) { + ForEach(FileImportManager.FileProvider.allCases) { + Text($0.localizedLabel).tag($0) + } + } label: { + Text("Source du fichier") + } + Button { convertingFile = false isShowing.toggle() @@ -160,7 +168,7 @@ struct FileImportView: View { } } } - .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in + .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in switch results { case .success(let fileurls): @@ -267,8 +275,9 @@ struct FileImportView: View { Section { HStack { VStack(alignment: .leading) { - Text(team.playerOne.playerLabel()) - Text(team.playerTwo.playerLabel()) + ForEach(team.players.sorted(by: \.weight)) { + Text($0.playerLabel()) + } } Spacer() HStack { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index dee982e..52273e7 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -76,7 +76,7 @@ struct InscriptionManagerView: View { selectionSearchField = nil }) { NavigationStack { - SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in + SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in selectionSearchField = nil players.forEach { player in let newPlayer = PlayerRegistration(importedPlayer: player) diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index a1b6e89..b7fae0a 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -61,11 +61,15 @@ struct TournamentCellView: View { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id + //todo + //newTournament.umpireMail() + //newTournament.jsonData = jsonData newTournament.tournamentLevel = build.level newTournament.tournamentCategory = build.category newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration - newTournament.startDate = federalTournament.startDate + newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) + newTournament.setupFederalSettings() try? dataStore.tournaments.addOrUpdate(instance: newTournament) } } label: {