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. 26
      PadelClub/Data/Federal/FederalPlayer.swift
  5. 4
      PadelClub/Data/Federal/PlayerHolder.swift
  6. 216
      PadelClub/Data/MatchScheduler.swift
  7. 20
      PadelClub/Data/MonthData.swift
  8. 11
      PadelClub/Data/Tournament.swift
  9. 62
      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. 104
      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. 127
      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. 4
      PadelClub/Views/Shared/ImportedPlayerView.swift
  25. 15
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  26. 42
      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 */,
FF025AE82BD1307E00A86CF8 /* MonthData.swift */,
FF1DC5522BAB354A00FD8220 /* MockData.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
@ -961,7 +962,6 @@
FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */,
FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */,
FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
);
path = Toolbox;
sourceTree = "<group>";
@ -970,6 +970,7 @@
isa = PBXGroup;
children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
);
path = Umpire;
sourceTree = "<group>";
@ -990,7 +991,6 @@
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */,
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */,
);
path = ViewModel;

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

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

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

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

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

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

@ -562,7 +562,7 @@ class Tournament : ModelObject, Storable {
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)
@ -1302,6 +1302,15 @@ class Tournament : ModelObject, Storable {
try Store.main.deleteDependencies(items: self.unsortedTeams())
try Store.main.deleteDependencies(items: self.groupStages())
try Store.main.deleteDependencies(items: self.rounds())
try Store.main.deleteDependencies(items: self._matchSchedulers())
}
private func _matchSchedulers() -> [MatchScheduler] {
Store.main.filter(isIncluded: { $0.id == self.id })
}
func matchScheduler() -> MatchScheduler? {
_matchSchedulers().first
}
func currentMonthData() -> MonthData? {

@ -11,6 +11,31 @@ import LeStorage
class FileImportManager {
static let shared = FileImportManager()
func updatePlayers(isMale: Bool, players: inout [FederalPlayer]) {
guard let mostRecentDateAvailable = URL.importDateFormatter.date(from: "05-2024") else { return }
let replacements: [(Character, Character)] = [("Á", "ç"), ("", "à"), ("Ù", "ô"), ("Ë", "è"), ("Ó", "î"), ("Î", "ë"), ("", "É"), ("Ô", "ï"), ("È", "é"), ("«", "Ç"), ("»", "È")]
var playersLeft = players
SourceFileManager.shared.allFilesSortedByDate(isMale).filter({ $0.dateFromPath.isEarlierThan(mostRecentDateAvailable) }).forEach({ url in
if playersLeft.isEmpty == false {
let federalPlayers = readCSV(inputFile: url)
let replacementsCharacters = url.dateFromPath.monthYearFormatted != "04-2024" ? [] : replacements
playersLeft.forEach { importedPlayer in
if let federalPlayer = federalPlayers.first(where: { $0.license == importedPlayer.license }) {
var lastName = federalPlayer.lastName
lastName.replace(characters: replacementsCharacters)
var firstName = federalPlayer.firstName
firstName.replace(characters: replacementsCharacters)
importedPlayer.lastName = lastName
importedPlayer.firstName = firstName
}
}
playersLeft.removeAll(where: { $0.lastName.isEmpty == false })
}
})
}
func foundInWomenData(license: String?) -> Bool {
guard let license = license?.strippedLicense else {
return false
@ -68,11 +93,25 @@ class FileImportManager {
let previousTeam: TeamRegistration?
var registrationDate: Date? = nil
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) {
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil, tournament: Tournament) {
self.players = Set(players)
self.tournamentCategory = tournamentCategory
self.previousTeam = previousTeam
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
}
@ -214,7 +253,7 @@ class FileImportManager {
playerOne.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament)
results.append(team)
}
}
@ -261,7 +300,7 @@ class FileImportManager {
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament)
results.append(team)
}
}
@ -270,11 +309,18 @@ class FileImportManager {
}
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] = []
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.removeAll(where: { $0.contains("Liste d'attente")})
lines.forEach { team in
let data = team.components(separatedBy: "\n")
let players = team.licencesFound()
@ -287,12 +333,12 @@ class FileImportManager {
})
if let registeredPlayers, registeredPlayers.isEmpty == false {
var registrationDate: Date? {
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") {
return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute())
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "Inscrit le ", with: "") {
return dateFormatter.date(from: registrationDateData)
}
return nil
}
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)
}
}

@ -36,6 +36,18 @@ class NetworkManager {
let task = try await URLSession.shared.download(for: request)
if let urlResponse = task.1 as? HTTPURLResponse {
if urlResponse.statusCode == 200 {
//todo à voir si on en a besoin, permet de re-télécharger un csv si on détecte qu'il a été mis à jour
// if FileManager.default.fileExists(atPath: destinationFileUrl.path()) {
// if let creationDate = try checkFileCreationDate(filePath: task.0.path()), let previousCreationDate = try checkFileCreationDate(filePath: destinationFileUrl.path()) {
// print("File creation date:", creationDate)
// print("File previous creation date:", previousCreationDate)
// if previousCreationDate.isEarlierThan(creationDate) {
// try FileManager.default.removeItem(at: destinationFileUrl)
// }
// }
// }
try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
print("dl rank data ok", lastDateString, fileName)
} else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" {
@ -44,4 +56,11 @@ class NetworkManager {
}
}
}
func checkFileCreationDate(filePath: String) throws -> Date? {
let fileManager = FileManager.default
let attributes = try fileManager.attributesOfItem(atPath: filePath)
return attributes[.creationDate] as? Date
}
}

@ -159,7 +159,7 @@ struct CashierView: View {
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment])
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {

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

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

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

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

@ -26,7 +26,6 @@ struct PadelClubView: View {
animation: .default)
private var players: FetchedResults<ImportedPlayer>
var _mostRecentDateAvailable: Date? {
SourceFileManager.shared.mostRecentDateAvailable
}
@ -38,24 +37,6 @@ struct PadelClubView: View {
var body: some View {
List {
if let _lastDataSourceDate {
Section {
LabeledContent {
Image(systemName: "checkmark")
} label: {
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
}
}
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) {
Section {
RowButtonView("Importer \(URL.importDateFormatter.string(from: mostRecentDateAvailable))") {
_startImporting()
}
}
}
#if targetEnvironment(simulator)
/*
["36435", "BOUNOUA", "walid", "France", "3311600", "15,00", "Non", "2", "AUVERGNE RHONE-ALPES", "50 73 0046", "CHAMBERY TC"]
@ -71,15 +52,44 @@ struct PadelClubView: View {
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)
var anonymousPlayers = players.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
let okPlayers = players.filter { $0.firstName.isEmpty == false && $0.lastName.isEmpty == false }
print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count)
SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch {
Logger.error(error)
}
}
}
}
#endif
if let _lastDataSourceDate {
Section {
LabeledContent {
Image(systemName: "checkmark")
} label: {
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
}
.contextMenu {
Button("Ré-importer") {
_startImporting()
}
}
}
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, _lastDataSourceDate.isEarlierThan(mostRecentDateAvailable) {
Section {
RowButtonView("Importer \(URL.importDateFormatter.string(from: mostRecentDateAvailable))") {
_startImporting()
}
}
}
}
if importingFiles {
@ -99,24 +109,62 @@ struct PadelClubView: View {
let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed()
ForEach(monthData) { monthData in
Section {
LabeledContent {
if let maleCount = monthData.maleCount {
Text(maleCount.formatted())
}
} label: {
Text("Messieurs")
}
LabeledContent {
if let femaleCount = monthData.femaleCount {
Text(femaleCount.formatted())
}
} label: {
Text("Dames")
}
LabeledContent {
if let anonymousCount = monthData.anonymousCount {
Text(anonymousCount.formatted())
}
} label: {
Text("Joueurs anonymes")
}
LabeledContent {
if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted())
}
} label: {
Text("Messieurs")
Text("Rang d'un non classé")
Text("Messieurs")
}
LabeledContent {
if let femaleUnrankedValue = monthData.femaleUnrankedValue {
Text(femaleUnrankedValue.formatted())
}
} label: {
Text("Dames")
Text("Rang d'une non classée")
Text("Dames")
}
} header: {
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)
.navigationTitle("Source des données fédérales")
.navigationTitle("Données fédérales")
}
@ViewBuilder
func _activityStatus() -> some View {
if checkingFiles || importingFiles {
HStack(spacing: 20) {
ProgressView()
if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable {
if mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast {
Text("import " + mostRecentDateAvailable.monthYearFormatted)
}
}
}
} else if let _mostRecentDateAvailable {
if _mostRecentDateAvailable > _lastDataSourceDate ?? .distantPast {
Text(_mostRecentDateAvailable.monthYearFormatted + " disponible à l'importation")
@ -168,6 +223,7 @@ struct PadelClubView: View {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}
importingFiles = false
viewContext.refreshAllObjects()
}
}
}

@ -66,7 +66,7 @@ struct LoserRoundScheduleEditorView: View {
// _save()
let loserRounds = upperRound.loserRounds().filter { $0.isDisabled() == false }
MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: loserRounds.first?.id, fromMatchId: nil, startDate: startDate)
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: loserRounds.first?.id, fromMatchId: nil, startDate: startDate)
loserRounds.first?.startDate = startDate
_save()
}

@ -59,7 +59,7 @@ struct LoserRoundStepScheduleEditorView: View {
}
private func _updateSchedule() async {
MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate
})

@ -33,7 +33,7 @@ struct MatchScheduleEditorView: View {
}
private func _updateSchedule() async {
MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate)
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate)
}
}

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

@ -54,7 +54,7 @@ struct RoundScheduleEditorView: View {
}
private func _updateSchedule() async {
MatchScheduler.shared.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
round.startDate = startDate
_save()
}

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

@ -12,13 +12,16 @@ struct EditablePlayerView: View {
enum PlayerEditingOption {
case payment
case licenceId
case name
}
@EnvironmentObject var dataStore: DataStore
var player: PlayerRegistration
@Bindable var player: PlayerRegistration
var editingOptions: [PlayerEditingOption]
@State private var editedLicenceId = ""
@State private var shouldPresentLicenceIdEdition: Bool = false
@State private var presentLastNameUpdate: Bool = false
@State private var presentFirstNameUpdate: Bool = false
var body: some View {
computedPlayerView(player)
@ -30,6 +33,19 @@ struct EditablePlayerView: View {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
}
}
.alert("Prénom", isPresented: $presentFirstNameUpdate) {
TextField("Prénom", text: $player.firstName)
.onSubmit {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
}
}
.alert("Nom", isPresented: $presentLastNameUpdate) {
TextField("Nom", text: $player.lastName)
.onSubmit {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
}
}
}
// TODO: Guard
@ -61,6 +77,15 @@ struct EditablePlayerView: View {
}
}
if editingOptions.contains(.name) {
Divider()
Button("Modifier le prénom") {
presentFirstNameUpdate = true
}
Button("Modifier le nom") {
presentLastNameUpdate = true
}
}
if editingOptions.contains(.licenceId) {
Divider()
if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil {

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

@ -19,24 +19,35 @@ struct TeamHeaderView: View {
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View {
HStack {
if let teamIndex {
VStack(alignment: .leading, spacing: 0) {
Text("rang").font(.caption)
Text("#" + (teamIndex + 1).formatted())
}
}
if team.unsortedPlayers().isEmpty == false {
VStack(alignment: .leading, spacing: 0) {
Text("poids").font(.caption)
Text(team.weight.formatted())
}
if team.isWildCard() {
Text("wildcard").italic().font(.caption)
}
Spacer()
VStack(alignment: .trailing, spacing: 0) {
if team.walkOut {
Text("").font(.caption)
Text("WO")
} else if let teamIndex, let tournament {
if team.isWildCard() {
Text("wildcard").font(.caption).italic()
} else {
Text("").font(.caption)
}
Text(tournament.cutLabel(index: teamIndex))
}
}
}
}
}
#Preview {
TeamHeaderView(team: TeamRegistration.mock(), teamIndex: 1, tournament: nil)

@ -14,9 +14,9 @@ struct FileImportView: View {
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
let fileContent: String?
let notFoundAreWalkOutTip = NotFoundAreWalkOutTip()
@State private var fileContent: String?
@State private var teams: [FileImportManager.TeamHolder] = []
@State private var isShowing = false
@State private var didImport = false
@ -40,14 +40,8 @@ struct FileImportView: View {
convertingFile = false
isShowing.toggle()
}
} footer: {
if fileProvider == .frenchFederation {
let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))"
Text(.init(footerString))
}
}
if fileContent != nil {
Section {
Picker(selection: $fileProvider) {
ForEach(FileImportManager.FileProvider.allCases) {
@ -56,6 +50,17 @@ struct FileImportView: View {
} label: {
Text("Source du fichier")
}
RowButtonView("Démarrer l'importation") {
if let fileContent {
await _startImport(fileContent: fileContent)
}
}
.disabled(fileContent == nil)
} footer: {
if fileProvider == .frenchFederation {
let footerString = "Fichier provenant de [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))"
Text(.init(footerString))
}
}
}
@ -160,13 +165,6 @@ struct FileImportView: View {
}
}
}
.onAppear {
if let fileContent {
Task {
await _startImport(fileContent: fileContent)
}
}
}
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results {
@ -178,16 +176,11 @@ struct FileImportView: View {
teams.removeAll()
Task {
do {
var fileContent: String?
if selectedFile.lastPathComponent.hasSuffix("xls") {
fileContent = try await CloudConvert.manager.uploadFile(selectedFile)
} else {
fileContent = try String(contentsOf: selectedFile)
}
if let fileContent {
await _startImport(fileContent: fileContent)
}
selectedFile.stopAccessingSecurityScopedResource()
} catch {
errorMessage = error.localizedDescription
@ -204,10 +197,7 @@ struct FileImportView: View {
.onOpenURL { url in
do {
let fileContent = try String(contentsOf: url)
Task {
await _startImport(fileContent: fileContent)
}
fileContent = try String(contentsOf: url)
} catch {
errorMessage = error.localizedDescription
}
@ -256,6 +246,10 @@ struct FileImportView: View {
errorMessage = nil
teams.removeAll()
}
if let rankSourceDate = tournament.rankSourceDate, tournament.unrankValue(for: false) == nil || tournament.unrankValue(for: true) == nil {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: rankSourceDate)
}
self.teams = await FileImportManager.shared.createTeams(from: fileContent, tournament: tournament, fileProvider: fileProvider)
await MainActor.run {
convertingFile = false
@ -302,7 +296,7 @@ struct FileImportView: View {
}
#Preview {
FileImportView(fileContent: nil)
FileImportView()
.environment(Tournament.mock())
}

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

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

Loading…
Cancel
Save