Laurent 2 years ago
commit 52ba7dac15
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  3. 2
      PadelClub/Data/DataStore.swift
  4. 28
      PadelClub/Data/Federal/FederalPlayer.swift
  5. 4
      PadelClub/Data/Federal/PlayerHolder.swift
  6. 200
      PadelClub/Data/MatchScheduler.swift
  7. 20
      PadelClub/Data/MonthData.swift
  8. 11
      PadelClub/Data/Tournament.swift
  9. 64
      PadelClub/Utils/FileImportManager.swift
  10. 19
      PadelClub/Utils/Network/NetworkManager.swift
  11. 2
      PadelClub/Views/Cashier/CashierView.swift
  12. 2
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  13. 2
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  14. 5
      PadelClub/Views/Navigation/MainView.swift
  15. 4
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  16. 118
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  17. 2
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  18. 2
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  19. 2
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  20. 175
      PadelClub/Views/Planning/PlanningSettingsView.swift
  21. 2
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  22. 2
      PadelClub/Views/Planning/SchedulerView.swift
  23. 27
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  24. 8
      PadelClub/Views/Shared/ImportedPlayerView.swift
  25. 29
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  26. 54
      PadelClub/Views/Tournament/FileImportView.swift
  27. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  28. 8
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -693,6 +693,7 @@
FF8F263E2BAD7D5C00650388 /* Event.swift */, FF8F263E2BAD7D5C00650388 /* Event.swift */,
FF025AE82BD1307E00A86CF8 /* MonthData.swift */, FF025AE82BD1307E00A86CF8 /* MonthData.swift */,
FF1DC5522BAB354A00FD8220 /* MockData.swift */, FF1DC5522BAB354A00FD8220 /* MockData.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
@ -961,7 +962,6 @@
FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */, FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */,
FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */, FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */,
FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */, FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
); );
path = Toolbox; path = Toolbox;
sourceTree = "<group>"; sourceTree = "<group>";
@ -970,6 +970,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */, FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
); );
path = Umpire; path = Umpire;
sourceTree = "<group>"; sourceTree = "<group>";
@ -990,7 +991,6 @@
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */, FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */,
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */, FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */,
); );
path = ViewModel; path = ViewModel;

@ -24,11 +24,11 @@ extension ImportedPlayer: PlayerHolder {
} }
func getFirstName() -> String { func getFirstName() -> String {
self.firstName ?? "prénom inconnu" self.firstName ?? ""
} }
func getLastName() -> String { func getLastName() -> String {
self.lastName ?? "nom inconnu" self.lastName ?? ""
} }
func formattedLicense() -> String { func formattedLicense() -> String {

@ -42,6 +42,7 @@ class DataStore: ObservableObject {
fileprivate(set) var teamScores: StoredCollection<TeamScore> fileprivate(set) var teamScores: StoredCollection<TeamScore>
fileprivate(set) var monthData: StoredCollection<MonthData> fileprivate(set) var monthData: StoredCollection<MonthData>
fileprivate(set) var dateIntervals: StoredCollection<DateInterval> fileprivate(set) var dateIntervals: StoredCollection<DateInterval>
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler>
fileprivate(set) var userStorage: StoredSingleton<User> fileprivate(set) var userStorage: StoredSingleton<User>
@ -81,6 +82,7 @@ class DataStore: ObservableObject {
self.matches = store.registerCollection(synchronized: synchronized, indexed: indexed) self.matches = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.monthData = store.registerCollection(synchronized: false, indexed: indexed) self.monthData = store.registerCollection(synchronized: false, indexed: indexed)
self.dateIntervals = store.registerCollection(synchronized: synchronized, indexed: indexed) self.dateIntervals = store.registerCollection(synchronized: synchronized, indexed: indexed)
self.matchSchedulers = store.registerCollection(synchronized: false, indexed: indexed)
self.userStorage = store.registerObject(synchronized: synchronized) self.userStorage = store.registerObject(synchronized: synchronized)

@ -7,7 +7,7 @@
import Foundation import Foundation
struct FederalPlayer: Decodable { class FederalPlayer: Decodable {
var rank: Int var rank: Int
var lastName: String var lastName: String
var firstName: String var firstName: String
@ -27,7 +27,7 @@ struct FederalPlayer: Decodable {
let code, codeFov: String let code, codeFov: String
} }
init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case nom case nom
case prenom case prenom
@ -77,7 +77,6 @@ struct FederalPlayer: Decodable {
tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois) tournamentCount = try? container.decodeIfPresent(Int.self, forKey: .nombreDeTournois)
let assimile = try container.decode(Bool.self, forKey: .assimile) let assimile = try container.decode(Bool.self, forKey: .assimile)
assimilation = assimile ? "Oui" : "Non" assimilation = assimile ? "Oui" : "Non"
fullNameCanonical = _lastName.canonicalVersion + " " + _firstName.canonicalVersion
} }
@ -109,8 +108,6 @@ struct FederalPlayer: Decodable {
return modifiedString return modifiedString
} }
var fullNameCanonical: String
/* /*
;RANG;NOM;PRENOM;Nationalité;N° Licence;POINTS;Assimilation;NB. DE TOURNOIS JOUES;LIGUE;CODE CLUB;CLUB; ;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 $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
})) }))
// print(result) //print(result)
if result.count < 11 { if result.count < 11 {
return nil return nil
} }
@ -151,7 +148,6 @@ struct FederalPlayer: Decodable {
lastName = result[1] lastName = result[1]
firstName = result[2] firstName = result[2]
fullNameCanonical = result[1].canonicalVersion + " " + result[2].canonicalVersion
country = result[3] country = result[3]
license = result[4] license = result[4]
@ -173,7 +169,19 @@ struct FederalPlayer: Decodable {
club = result[10] 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 context = PersistenceController.shared.localContainer.newBackgroundContext()
let lastPlayerFetch = ImportedPlayer.fetchRequest() let lastPlayerFetch = ImportedPlayer.fetchRequest()
lastPlayerFetch.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: false)] lastPlayerFetch.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: false)]
@ -182,7 +190,7 @@ struct FederalPlayer: Decodable {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)]) predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)])
} }
lastPlayerFetch.predicate = predicate lastPlayerFetch.predicate = predicate
let count = try? context.count(for: lastPlayerFetch)
do { do {
if let lr = try context.fetch(lastPlayerFetch).first?.rank { if let lr = try context.fetch(lastPlayerFetch).first?.rank {
let fetch = ImportedPlayer.fetchRequest() let fetch = ImportedPlayer.fetchRequest()
@ -193,7 +201,7 @@ struct FederalPlayer: Decodable {
fetch.predicate = rankPredicate fetch.predicate = rankPredicate
let lastPlayersCount = try context.count(for: fetch) let lastPlayersCount = try context.count(for: fetch)
return Int(lr) + Int(lastPlayersCount) - 1 return (Int(lr) + Int(lastPlayersCount) - 1, count)
} }
} catch { } catch {
print("ImportedPlayer.fetchRequest", error) print("ImportedPlayer.fetchRequest", error)

@ -29,4 +29,8 @@ extension PlayerHolder {
var isAssimilated: Bool { var isAssimilated: Bool {
assimilation == "Oui" assimilation == "Oui"
} }
func isAnonymous() -> Bool {
getFirstName().isEmpty && getLastName().isEmpty
}
} }

@ -7,91 +7,71 @@
import Foundation import Foundation
import LeStorage import LeStorage
import SwiftUI
struct GroupStageTimeMatch {
let matchID: String @Observable
let rotationIndex: Int class MatchScheduler : ModelObject, Storable {
var courtIndex: Int static func resourceName() -> String { return "match-scheduler" }
let groupIndex: Int static func requestsRequiresToken() -> Bool { true }
}
private(set) var id: String = Store.randomId()
struct TimeMatch { var tournament: String
let matchID: String var timeDifferenceLimit: Int
let rotationIndex: Int var loserBracketRotationDifference: Int
var courtIndex: Int var upperBracketRotationDifference: Int
var startDate: Date var accountUpperBracketBreakTime: Bool
var durationLeft: Int //in minutes var accountLoserBracketBreakTime: Bool
var minimumBreakTime: Int //in minutes var randomizeCourts: Bool
var rotationDifferenceIsImportant: Bool
func estimatedEndDate(includeBreakTime: Bool) -> Date { var shouldHandleUpperRoundSlice: Bool
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) var shouldEndRoundBeforeStartingNext: Bool
return startDate.addingTimeInterval(minutesToAdd * 60.0)
} init(tournament: String,
} timeDifferenceLimit: Int = 5,
loserBracketRotationDifference: Int = 0,
struct GroupStageMatchDispatcher { upperBracketRotationDifference: Int = 1,
let timedMatches: [GroupStageTimeMatch] accountUpperBracketBreakTime: Bool = true,
let freeCourtPerRotation: [Int: [Int]] accountLoserBracketBreakTime: Bool = false,
let rotationCount: Int randomizeCourts: Bool = true,
let groupLastRotation: [Int: Int] rotationDifferenceIsImportant: Bool = false,
} shouldHandleUpperRoundSlice: Bool = true,
shouldEndRoundBeforeStartingNext: Bool = true) {
struct MatchDispatcher { self.tournament = tournament
let timedMatches: [TimeMatch] self.timeDifferenceLimit = timeDifferenceLimit
let freeCourtPerRotation: [Int: [Int]] self.loserBracketRotationDifference = loserBracketRotationDifference
let rotationCount: Int self.upperBracketRotationDifference = upperBracketRotationDifference
} self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
extension Match { self.randomizeCourts = randomizeCourts
func teamIds() -> [String] { self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
return teams().map { $0.id } self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
} self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
func containsTeamId(_ id: String) -> Bool {
teamIds().contains(id)
}
}
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<MatchSchedulerOption> = 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)
} }
func shouldHandleUpperRoundSlice() -> Bool { enum CodingKeys: String, CodingKey {
options.contains(.shouldHandleUpperRoundSlice) 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"
} }
func accountLoserBracketBreakTime() -> Bool { var courtsUnavailability: [DateInterval]? {
options.contains(.accountLoserBracketBreakTime) tournamentObject()?.eventObject()?.courtsUnavailability
} }
func accountUpperBracketBreakTime() -> Bool { var additionalEstimationDuration : Int {
options.contains(.accountUpperBracketBreakTime) tournamentObject()?.additionalEstimationDuration ?? 0
} }
func randomizeCourts() -> Bool { func tournamentObject() -> Tournament? {
options.contains(.randomizeCourts) Store.main.findById(tournament)
}
func rotationDifferenceIsImportant() -> Bool {
options.contains(.rotationDifferenceIsImportant)
} }
@discardableResult @discardableResult
@ -99,7 +79,6 @@ class MatchScheduler {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
let matches = groupStages.flatMap({ $0._matches() }) let matches = groupStages.flatMap({ $0._matches() })
matches.forEach({ matches.forEach({
@ -197,7 +176,7 @@ class MatchScheduler {
var organizedSlots = [GroupStageTimeMatch]() var organizedSlots = [GroupStageTimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
for j in 0..<matches.count { for j in 0..<matches.count {
@ -254,11 +233,11 @@ class MatchScheduler {
var includeBreakTime = false var includeBreakTime = false
if accountLoserBracketBreakTime() && roundObject.isLoserBracket() { if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
includeBreakTime = true includeBreakTime = true
} }
if accountUpperBracketBreakTime() && roundObject.isLoserBracket() == false { if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false {
includeBreakTime = true includeBreakTime = true
} }
@ -269,7 +248,7 @@ class MatchScheduler {
} }
if targetedStartDate >= minimumPossibleEndDate { if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant() { if rotationDifferenceIsImportant {
return previousMatchIsInPreviousRotation return previousMatchIsInPreviousRotation
} else { } else {
return true return true
@ -375,14 +354,15 @@ class MatchScheduler {
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
print("difference w break", differenceWithBreak) print("difference w break", differenceWithBreak)
print("difference w/o break", differenceWithoutBreak) print("difference w/o break", differenceWithoutBreak)
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
var difference = differenceWithBreak var difference = differenceWithBreak
if differenceWithBreak <= 0 { if differenceWithBreak <= 0 {
difference = differenceWithoutBreak difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit { } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
} }
if difference > timeDifferenceLimit { if difference > timeDifferenceLimitInSeconds {
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index)
}) })
freeCourtPerRotation[rotationIndex] = courts freeCourtPerRotation[rotationIndex] = courts
@ -399,7 +379,7 @@ class MatchScheduler {
var organizedSlots = [TimeMatch]() var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex)) var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex))
for j in 0..<matches.count { for j in 0..<matches.count {
@ -430,7 +410,7 @@ class MatchScheduler {
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
if shouldHandleUpperRoundSlice() { if shouldHandleUpperRoundSlice {
let roundMatchesCount = roundObject.playedMatches().count let roundMatchesCount = roundObject.playedMatches().count
print("shouldHandleUpperRoundSlice \(roundMatchesCount)") print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
if roundObject.parent == nil && roundMatchesCount > courts.count { if roundObject.parent == nil && roundMatchesCount > courts.count {
@ -502,14 +482,13 @@ class MatchScheduler {
} }
func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) { func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) {
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
let upperRounds = tournament.rounds() let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
var rounds = [Round]() var rounds = [Round]()
if shouldEndRoundBeforeStartingNext() { if shouldEndRoundBeforeStartingNext {
rounds = upperRounds.flatMap { rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren() [$0] + $0.loserRoundsAndChildren()
} }
@ -607,8 +586,51 @@ class MatchScheduler {
} }
func updateSchedule(tournament: Tournament) { func updateSchedule(tournament: Tournament) {
courtsUnavailability = tournament.eventObject()?.courtsUnavailability
let lastDate = updateGroupStageSchedule(tournament: tournament) let lastDate = updateGroupStageSchedule(tournament: tournament)
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) 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)
}
}

@ -21,21 +21,32 @@ class MonthData : ModelObject, Storable {
var maleUnrankedValue: Int? = nil var maleUnrankedValue: Int? = nil
var femaleUnrankedValue: Int? = nil var femaleUnrankedValue: Int? = nil
var maleCount: Int? = nil
var femaleCount: Int? = nil
var anonymousCount: Int? = nil
init(monthKey: String) { init(monthKey: String) {
self.monthKey = monthKey self.monthKey = monthKey
self.creationDate = Date() self.creationDate = Date()
} }
func total() -> Int {
(maleCount ?? 0) + (femaleCount ?? 0)
}
static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async {
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true)
let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false)
let anonymousCount = await FederalPlayer.anonymousCount(mostRecentDateAvailable: mostRecentDateAvailable)
await MainActor.run { await MainActor.run {
if let lastDataSource = DataStore.shared.appSettings.lastDataSource { if let lastDataSource = DataStore.shared.appSettings.lastDataSource {
let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource) let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource)
currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked?.0
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked currentMonthData.maleCount = lastDataSourceMaleUnranked?.1
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked?.0
currentMonthData.femaleCount = lastDataSourceFemaleUnranked?.1
currentMonthData.anonymousCount = anonymousCount
try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData)
} }
} }
@ -50,5 +61,8 @@ class MonthData : ModelObject, Storable {
case _creationDate = "creationDate" case _creationDate = "creationDate"
case _maleUnrankedValue = "maleUnrankedValue" case _maleUnrankedValue = "maleUnrankedValue"
case _femaleUnrankedValue = "femaleUnrankedValue" case _femaleUnrankedValue = "femaleUnrankedValue"
case _maleCount = "maleCount"
case _femaleCount = "femaleCount"
case _anonymousCount = "anonymousCount"
} }
} }

@ -562,7 +562,7 @@ class Tournament : ModelObject, Storable {
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting() let defaultSorting : [MySortDescriptor<TeamRegistration>] = _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) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
@ -1302,6 +1302,15 @@ class Tournament : ModelObject, Storable {
try Store.main.deleteDependencies(items: self.unsortedTeams()) try Store.main.deleteDependencies(items: self.unsortedTeams())
try Store.main.deleteDependencies(items: self.groupStages()) try Store.main.deleteDependencies(items: self.groupStages())
try Store.main.deleteDependencies(items: self.rounds()) 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? { func currentMonthData() -> MonthData? {

@ -11,6 +11,31 @@ import LeStorage
class FileImportManager { class FileImportManager {
static let shared = 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 { func foundInWomenData(license: String?) -> Bool {
guard let license = license?.strippedLicense else { guard let license = license?.strippedLicense else {
return false return false
@ -68,11 +93,25 @@ class FileImportManager {
let previousTeam: TeamRegistration? let previousTeam: TeamRegistration?
var registrationDate: Date? = nil 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.players = Set(players)
self.tournamentCategory = tournamentCategory self.tournamentCategory = tournamentCategory
self.previousTeam = previousTeam 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
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,+)
}
self.registrationDate = registrationDate self.registrationDate = registrationDate
} }
@ -214,7 +253,7 @@ class FileImportManager {
playerOne.setComputedRank(in: tournament) playerOne.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament) 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) results.append(team)
} }
} }
@ -261,7 +300,7 @@ class FileImportManager {
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament) 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) results.append(team)
} }
} }
@ -270,11 +309,18 @@ class FileImportManager {
} }
private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] { private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n\n") 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] = [] var results: [TeamHolder] = []
let fetchRequest = ImportedPlayer.fetchRequest() let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext let federalContext = PersistenceController.shared.localContainer.viewContext
lines.removeAll(where: { $0.contains("Liste d'attente")})
lines.forEach { team in lines.forEach { team in
let data = team.components(separatedBy: "\n") let data = team.components(separatedBy: "\n")
let players = team.licencesFound() let players = team.licencesFound()
@ -287,12 +333,12 @@ class FileImportManager {
}) })
if let registeredPlayers, registeredPlayers.isEmpty == false { if let registeredPlayers, registeredPlayers.isEmpty == false {
var registrationDate: Date? { var registrationDate: Date? {
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") { if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "Inscrit le ", with: "") {
return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute()) return dateFormatter.date(from: registrationDateData)
} }
return nil 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) results.append(team)
} }
} }

@ -36,6 +36,18 @@ class NetworkManager {
let task = try await URLSession.shared.download(for: request) let task = try await URLSession.shared.download(for: request)
if let urlResponse = task.1 as? HTTPURLResponse { if let urlResponse = task.1 as? HTTPURLResponse {
if urlResponse.statusCode == 200 { 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) try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
print("dl rank data ok", lastDateString, fileName) print("dl rank data ok", lastDateString, fileName)
} else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" { } 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
}
} }

@ -159,7 +159,7 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View { 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 { private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {

@ -17,7 +17,7 @@ struct GroupStageTeamView: View {
List { List {
Section { Section {
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
} }
} }

@ -31,7 +31,7 @@ struct MatchTeamDetailView: View {
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View {
Section { Section {
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
} }
} header: { } header: {
TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil)

@ -59,6 +59,11 @@ struct MainView: View {
// PadelClubView() // PadelClubView()
// .tabItem(for: .padelClub) // .tabItem(for: .padelClub)
} }
.id(Store.main.currentUserUUID)
.onChange(of: Store.main.currentUserUUID) {
navigation.tournament = nil
navigation.path.removeLast(navigation.path.count)
}
.environmentObject(dataStore) .environmentObject(dataStore)
.task { .task {
await self._checkSourceFileAvailability() await self._checkSourceFileAvailability()

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct TournamentOrganizerView: View { struct TournamentOrganizerView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -46,6 +47,9 @@ struct TournamentOrganizerView: View {
} }
} }
} }
.onChange(of: Store.main.currentUserUUID) {
selectedTournamentId = nil
}
} }
} }

@ -26,7 +26,6 @@ struct PadelClubView: View {
animation: .default) animation: .default)
private var players: FetchedResults<ImportedPlayer> private var players: FetchedResults<ImportedPlayer>
var _mostRecentDateAvailable: Date? { var _mostRecentDateAvailable: Date? {
SourceFileManager.shared.mostRecentDateAvailable SourceFileManager.shared.mostRecentDateAvailable
} }
@ -38,6 +37,37 @@ struct PadelClubView: View {
var body: some View { var body: some View {
List { 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 { if let _lastDataSourceDate {
Section { Section {
LabeledContent { LabeledContent {
@ -46,6 +76,11 @@ struct PadelClubView: View {
Text(_lastDataSourceDate.monthYearFormatted) Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé") Text("Classement mensuel utilisé")
} }
.contextMenu {
Button("Ré-importer") {
_startImporting()
}
}
} }
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) { if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) {
@ -55,31 +90,6 @@ 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)
}
}
}
}
#endif
} }
if importingFiles { if importingFiles {
@ -99,24 +109,62 @@ struct PadelClubView: View {
let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed() let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed()
ForEach(monthData) { monthData in ForEach(monthData) { monthData in
Section { 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 { LabeledContent {
if let maleUnrankedValue = monthData.maleUnrankedValue { if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted()) Text(maleUnrankedValue.formatted())
} }
} label: { } label: {
Text("Messieurs")
Text("Rang d'un non classé") Text("Rang d'un non classé")
Text("Messieurs")
} }
LabeledContent { LabeledContent {
if let femaleUnrankedValue = monthData.femaleUnrankedValue { if let femaleUnrankedValue = monthData.femaleUnrankedValue {
Text(femaleUnrankedValue.formatted()) Text(femaleUnrankedValue.formatted())
} }
} label: { } label: {
Text("Dames")
Text("Rang d'une non classée") Text("Rang d'une non classée")
Text("Dames")
} }
} header: { } 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)
}
}
}
}
} }
} }
} }
@ -129,13 +177,20 @@ struct PadelClubView: View {
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Source des données fédérales") .navigationTitle("Données fédérales")
} }
@ViewBuilder @ViewBuilder
func _activityStatus() -> some View { func _activityStatus() -> some View {
if checkingFiles || importingFiles { 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 { } else if let _mostRecentDateAvailable {
if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast { if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation") Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation")
@ -168,6 +223,7 @@ struct PadelClubView: View {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate) await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
} }
importingFiles = false importingFiles = false
viewContext.refreshAllObjects()
} }
} }
} }

@ -66,7 +66,7 @@ struct LoserRoundScheduleEditorView: View {
// _save() // _save()
let loserRounds = upperRound.loserRounds().filter { $0.isDisabled() == false } 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 loserRounds.first?.startDate = startDate
_save() _save()
} }

@ -59,7 +59,7 @@ struct LoserRoundStepScheduleEditorView: View {
} }
private func _updateSchedule() async { 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 upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate round.startDate = startDate
}) })

@ -33,7 +33,7 @@ struct MatchScheduleEditorView: View {
} }
private func _updateSchedule() async { 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)
} }
} }

@ -10,36 +10,25 @@ import LeStorage
struct PlanningSettingsView: View { struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament @Bindable var tournament: Tournament
@State private var randomCourtDistribution: Bool @Bindable var matchScheduler: MatchScheduler
@State private var groupStageCourtCount: Int @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 isScheduling: Bool = false
@State private var schedulingDone: Bool = false @State private var schedulingDone: Bool = false
@State private var showOptions: Bool = false @State private var showOptions: Bool = false
@State private var shouldEndBeforeStartNext: Bool = true
init(tournament: Tournament) { init(tournament: Tournament) {
self.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._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 { var body: some View {
@Bindable var tournament = tournament
List { List {
SubscriptionInfoView() SubscriptionInfoView()
@ -80,10 +69,6 @@ struct PlanningSettingsView: View {
await _setupSchedule() await _setupSchedule()
schedulingDone = true schedulingDone = true
} }
if showOptions {
_optionsView()
}
} footer: { } footer: {
Button { Button {
showOptions.toggle() showOptions.toggle()
@ -94,22 +79,37 @@ struct PlanningSettingsView: View {
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
if showOptions {
_optionsView()
}
Section { Section {
RowButtonView("Supprimer tous les horaires", role: .destructive) { RowButtonView("Supprimer tous les horaires", role: .destructive) {
let allMatches = tournament.allMatches() do {
allMatches.forEach({ $0.startDate = nil }) let allMatches = tournament.allMatches()
try? dataStore.matches.addOrUpdate(contentOfs: allMatches) allMatches.forEach({ $0.startDate = nil })
try dataStore.matches.addOrUpdate(contentOfs: allMatches)
let allGroupStages = tournament.groupStages()
allGroupStages.forEach({ $0.startDate = nil }) let allGroupStages = tournament.groupStages()
try? dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages) allGroupStages.forEach({ $0.startDate = nil })
try dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages)
let allRounds = tournament.allRounds()
allRounds.forEach({ $0.startDate = nil }) let allRounds = tournament.allRounds()
try? dataStore.rounds.addOrUpdate(contentOfs: 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) { .overlay(alignment: .bottom) {
if schedulingDone { if schedulingDone {
Label("Horaires mis à jour", systemImage: "checkmark.circle.fill") Label("Horaires mis à jour", systemImage: "checkmark.circle.fill")
@ -137,87 +137,70 @@ struct PlanningSettingsView: View {
@ViewBuilder @ViewBuilder
private func _optionsView() -> some View { 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) { Section {
Text("Finir une manche et les matchs de classements avant de continuer") Toggle(isOn: $matchScheduler.randomizeCourts) {
} Text("Distribuer les terrains au hasard")
}
Toggle(isOn: $upperBracketBreakTime) {
Text("Tableau : tenir compte des pauses")
}
Toggle(isOn: $loserBracketBreakTime) {
Text("Classement : tenir compte des pauses")
}
Toggle(isOn: $rotationDifferenceIsImportant) {
Text("Forcer un créneau supplémentaire entre 2 phases")
}
LabeledContent { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
StepperView(count: $upperBracketRotationDifference, minimum: 0, maximum: 2) Text("Équilibrer les matchs d'une manche sur plusieurs tours")
} label: { }
Text("Tableau")
}
.disabled(rotationDifferenceIsImportant == false)
LabeledContent { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) {
StepperView(count: $loserBracketRotationDifference, minimum: 0, maximum: 2) Text("Finir une manche, classement inclus avant de continuer")
} label: { }
Text("Classement")
} }
.disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit
}
private func _setupSchedule() async { Section {
Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) {
let matchScheduler = MatchScheduler.shared Text("Tenir compte des pauses")
Text("Tableau")
matchScheduler.options.removeAll() }
if shouldEndBeforeStartNext {
matchScheduler.options.insert(.shouldEndRoundBeforeStartingNext)
}
if randomCourtDistribution { Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) {
matchScheduler.options.insert(.randomizeCourts) Text("Tenir compte des pauses")
Text("Classement")
}
} }
if shouldHandleUpperRoundSlice { Section {
matchScheduler.options.insert(.shouldHandleUpperRoundSlice) Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
} Text("Forcer un créneau supplémentaire entre 2 phases")
}
if upperBracketBreakTime { LabeledContent {
matchScheduler.options.insert(.accountUpperBracketBreakTime) StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2)
} } label: {
Text("Tableau")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
if loserBracketBreakTime { LabeledContent {
matchScheduler.options.insert(.accountLoserBracketBreakTime) StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2)
} label: {
Text("Classement")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
} }
if rotationDifferenceIsImportant { Section {
matchScheduler.options.insert(.rotationDifferenceIsImportant) LabeledContent {
StepperView(count: $matchScheduler.timeDifferenceLimit, step: 5)
} label: {
Text("Optimisation des créneaux")
Text("Si libre plus de \(matchScheduler.timeDifferenceLimit) minutes")
}
} }
}
matchScheduler.loserBracketRotationDifference = loserBracketRotationDifference private func _setupSchedule() async {
matchScheduler.upperBracketRotationDifference = upperBracketRotationDifference
matchScheduler.timeDifferenceLimit = timeDifferenceLimit
matchScheduler.updateSchedule(tournament: tournament) matchScheduler.updateSchedule(tournament: tournament)
} }
private func _save() { private func _save() {
do { do {
try dataStore.matchSchedulers.addOrUpdate(instance: matchScheduler)
try dataStore.tournaments.addOrUpdate(instance: tournament) try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)

@ -54,7 +54,7 @@ struct RoundScheduleEditorView: View {
} }
private func _updateSchedule() async { 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 round.startDate = startDate
_save() _save()
} }

@ -32,7 +32,7 @@ struct SchedulerView: View {
case .scheduleGroupStage: case .scheduleGroupStage:
MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) { MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) {
Task { Task {
MatchScheduler.shared.updateSchedule(tournament: tournament) tournament.matchScheduler()?.updateSchedule(tournament: tournament)
} }
} }
.onChange(of: tournament.groupStageMatchFormat) { .onChange(of: tournament.groupStageMatchFormat) {

@ -12,13 +12,16 @@ struct EditablePlayerView: View {
enum PlayerEditingOption { enum PlayerEditingOption {
case payment case payment
case licenceId case licenceId
case name
} }
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var player: PlayerRegistration @Bindable var player: PlayerRegistration
var editingOptions: [PlayerEditingOption] var editingOptions: [PlayerEditingOption]
@State private var editedLicenceId = "" @State private var editedLicenceId = ""
@State private var shouldPresentLicenceIdEdition: Bool = false @State private var shouldPresentLicenceIdEdition: Bool = false
@State private var presentLastNameUpdate: Bool = false
@State private var presentFirstNameUpdate: Bool = false
var body: some View { var body: some View {
computedPlayerView(player) computedPlayerView(player)
@ -30,6 +33,19 @@ struct EditablePlayerView: View {
try? dataStore.playerRegistrations.addOrUpdate(instance: player) 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 // 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) { if editingOptions.contains(.licenceId) {
Divider() Divider()
if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil {

@ -15,8 +15,12 @@ struct ImportedPlayerView: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { HStack {
Text(player.getLastName().capitalized) if player.isAnonymous() {
Text(player.getFirstName().capitalized) Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil { if index == nil {
Text(player.male ? "" : "") Text(player.male ? "" : "")
} }

@ -19,20 +19,31 @@ struct TeamHeaderView: View {
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View {
HStack { HStack {
if let teamIndex { 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 { if team.unsortedPlayers().isEmpty == false {
Text(team.weight.formatted()) VStack(alignment: .leading, spacing: 0) {
} Text("poids").font(.caption)
if team.isWildCard() { Text(team.weight.formatted())
Text("wildcard").italic().font(.caption) }
} }
Spacer() Spacer()
if team.walkOut { VStack(alignment: .trailing, spacing: 0) {
Text("WO") if team.walkOut {
} else if let teamIndex, let tournament { Text("").font(.caption)
Text(tournament.cutLabel(index: teamIndex)) 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))
}
} }
} }
} }

@ -14,9 +14,9 @@ struct FileImportView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
let fileContent: String?
let notFoundAreWalkOutTip = NotFoundAreWalkOutTip() let notFoundAreWalkOutTip = NotFoundAreWalkOutTip()
@State private var fileContent: String?
@State private var teams: [FileImportManager.TeamHolder] = [] @State private var teams: [FileImportManager.TeamHolder] = []
@State private var isShowing = false @State private var isShowing = false
@State private var didImport = false @State private var didImport = false
@ -40,24 +40,29 @@ struct FileImportView: View {
convertingFile = false convertingFile = false
isShowing.toggle() 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: { } footer: {
if fileProvider == .frenchFederation { if fileProvider == .frenchFederation {
let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))"
Text(.init(footerString)) 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 { // 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 .fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results { switch results {
@ -178,16 +176,11 @@ struct FileImportView: View {
teams.removeAll() teams.removeAll()
Task { Task {
do { do {
var fileContent: String?
if selectedFile.lastPathComponent.hasSuffix("xls") { if selectedFile.lastPathComponent.hasSuffix("xls") {
fileContent = try await CloudConvert.manager.uploadFile(selectedFile) fileContent = try await CloudConvert.manager.uploadFile(selectedFile)
} else { } else {
fileContent = try String(contentsOf: selectedFile) fileContent = try String(contentsOf: selectedFile)
} }
if let fileContent {
await _startImport(fileContent: fileContent)
}
selectedFile.stopAccessingSecurityScopedResource() selectedFile.stopAccessingSecurityScopedResource()
} catch { } catch {
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
@ -204,10 +197,7 @@ struct FileImportView: View {
.onOpenURL { url in .onOpenURL { url in
do { do {
let fileContent = try String(contentsOf: url) fileContent = try String(contentsOf: url)
Task {
await _startImport(fileContent: fileContent)
}
} catch { } catch {
errorMessage = error.localizedDescription errorMessage = error.localizedDescription
} }
@ -256,6 +246,10 @@ struct FileImportView: View {
errorMessage = nil errorMessage = nil
teams.removeAll() 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) self.teams = await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider)
await MainActor.run { await MainActor.run {
convertingFile = false convertingFile = false
@ -302,7 +296,7 @@ struct FileImportView: View {
} }
#Preview { #Preview {
FileImportView(fileContent: nil) FileImportView()
.environment(Tournament.mock()) .environment(Tournament.mock())
} }

@ -104,7 +104,7 @@ struct InscriptionManagerView: View {
} }
.sheet(isPresented: $presentImportView) { .sheet(isPresented: $presentImportView) {
NavigationStack { NavigationStack {
FileImportView(fileContent: nil) FileImportView()
} }
.tint(.master) .tint(.master)
} }

@ -69,7 +69,9 @@ struct TournamentCellView: View {
} }
Spacer() Spacer()
if let tournament = tournament as? Tournament, displayStyle == .wide { 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 { } else if let federalTournament = tournament as? FederalTournament {
Button { Button {
_createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build) _createOrShow(federalTournament: federalTournament, existingTournament: existingTournament, build: build)
@ -89,7 +91,9 @@ struct TournamentCellView: View {
Text(tournament.durationLabel()) Text(tournament.durationLabel())
Spacer() Spacer()
if let tournament = tournament as? Tournament { if let tournament = tournament as? Tournament {
Text("équipes") let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted()
let word = hasStarted ? "équipes" : "inscriptions"
Text(word)
} }
} }
Text(tournament.subtitleLabel()) Text(tournament.subtitleLabel())

Loading…
Cancel
Save