diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f9b5129..688bcbc 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,11 +3134,12 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 11; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3157,7 +3158,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3178,10 +3179,11 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 11; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; + GCC_OPTIMIZATION_LEVEL = 0; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; @@ -3200,7 +3202,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3293,7 +3295,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3315,7 +3317,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3335,7 +3337,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 9; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3356,7 +3358,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.15; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index ab7d66f..b7679f3 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -122,6 +122,10 @@ extension ImportedPlayer: PlayerHolder { func getProgression() -> Int { return Int(progression) } + + func getComputedRank() -> Int? { + nil + } } fileprivate extension Int { diff --git a/PadelClub/Data/Federal/PlayerHolder.swift b/PadelClub/Data/Federal/PlayerHolder.swift index 07c6860..72949e9 100644 --- a/PadelClub/Data/Federal/PlayerHolder.swift +++ b/PadelClub/Data/Federal/PlayerHolder.swift @@ -27,6 +27,7 @@ protocol PlayerHolder { func isNotFromCurrentDate() -> Bool func getBirthYear() -> Int? func getProgression() -> Int + func getComputedRank() -> Int? } extension PlayerHolder { diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index ee20f67..09eab0a 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -52,7 +52,7 @@ final class GroupStage: ModelObject, Storable { // MARK: - Computed dependencies func _matches() -> [Match] { - return self.tournamentStore.matches.filter { $0.groupStage == self.id } + return self.tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) // Store.main.filter { $0.groupStage == self.id } } @@ -150,6 +150,15 @@ final class GroupStage: ModelObject, Storable { } catch { Logger.error(error) } + + if tournament.groupStagesAreOver(), tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { + tournament.endDate = Date() + do { + try DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } } } @@ -262,16 +271,20 @@ final class GroupStage: ModelObject, Storable { case 4: return [2, 3, 1, 4, 5, 0] case 5: - return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] -// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0] +// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] + return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: - return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] - //return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] + return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: return [] } } + func indexOf(_ matchIndex: Int) -> Int { + _matchOrder().firstIndex(of: matchIndex) ?? matchIndex + } + private func _matchUp(for matchIndex: Int) -> [Int] { Array((0.. String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) contre #\(index2 + 1)" + return "#\(index + 1) vs #\(index2 + 1)" } else { return "--" } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 1e8c918..5476029 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -408,7 +408,7 @@ defer { } func next() -> Match? { - let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index } + let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index && $0.disabled == false } return matches.sorted(by: \.index).first } @@ -435,6 +435,10 @@ defer { else { return nil } } + func roundAndMatchTitle() -> String { + [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") + } + func topPreviousRoundMatchIndex() -> Int { return index * 2 + 1 } @@ -470,8 +474,11 @@ defer { } var computedOrder: Int { + if let groupStageObject { + return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) + } guard let roundObject else { return index } - return roundObject.isLoserBracket() ? roundObject.index * 100 + indexInRound() : roundObject.index * 1000 + indexInRound() + return roundObject.isLoserBracket() ? (roundObject.index + 1) * 1000 + indexInRound() : (roundObject.index + 1) * 10000 + indexInRound() } func previousMatches() -> [Match] { diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 6c71fe4..8a0abce 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -179,30 +179,39 @@ final class MatchScheduler : ModelObject, Storable { // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 - // Use zip and flatMap to flatten matches in the desired order + // Flatten matches in a round-robin order by cycling through each group let flattenedMatches = (0.. 0 { @@ -216,28 +225,42 @@ final class MatchScheduler : ModelObject, Storable { } (0..= numberOfCourtsAvailablePerRotation - courtsUnavailable.count { + + if courtsUnavailable.contains(courtIndex) { + print("Court \(courtIndex) is unavailable at \(rotationStartDate)") + return false + } + + let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) + if !teamsAvailable { + print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") return false - } else { - return teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true } + + print("Match \(match.roundAndMatchTitle()) is available for court \(courtIndex) at \(rotationStartDate)") + return true }) { let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index) + + print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") + slots.append(timeMatch) teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) rotationMatches.removeAll(where: { $0.id == first.id }) - availableMatchs.removeAll(where: { $0.id == first.id }) + availableMatches.removeAll(where: { $0.id == first.id }) + if let index = first.groupStageObject?.index { groupLastRotation[index] = rotationIndex } } else { + print("No available matches for court \(courtIndex) in rotation \(rotationIndex), adding to free court list") freeCourtPerRotation[rotationIndex]!.append(courtIndex) } } @@ -245,6 +268,9 @@ final class MatchScheduler : ModelObject, Storable { rotationIndex += 1 } + print("All matches scheduled. Total rotations: \(rotationIndex)") + + // Organize slots and ensure courts are randomized or sorted var organizedSlots = [GroupStageTimeMatch]() for i in 0.. Int { if loserBracket { @@ -270,70 +301,96 @@ final class MatchScheduler : ModelObject, Storable { } func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { - print(roundObject.roundTitle(), match.matchTitle()) - + print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)") + if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate { - print("can't start \(targetedStartDate) earlier than \(roundStartDate)") + print("Cannot start at \(targetedStartDate), earlier than round start date \(roundStartDate)") if targetedStartDate == minimumTargetedEndDate { + print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)") minimumTargetedEndDate = roundStartDate } else { + print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)") minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) } + print("Returning false: Match cannot start earlier than the round start date.") return false } let previousMatches = roundObject.precedentMatches(ofMatch: match) - if previousMatches.isEmpty { return true } + if previousMatches.isEmpty { + print("No ancestors matches for this match, returning true. (eg beginning of tournament 1st bracket") + return true + } - let previousMatchSlots = slots.filter({ slot in - previousMatches.map { $0.id }.contains(slot.matchID) - }) + let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) } if previousMatchSlots.isEmpty { - if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) { + if previousMatches.filter({ !$0.disabled }).allSatisfy({ $0.startDate != nil }) { + print("All previous matches have start dates, returning true.") return true } + print("Some previous matches are pending, returning false.") return false } - if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count { - if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) { + if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count { + if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) { + print("Some previous matches started, returning true.") return true } + print("Not enough previous matches have started, returning false.") return false } - + var includeBreakTime = false - if accountLoserBracketBreakTime && roundObject.isLoserBracket() { includeBreakTime = true + print("Including break time for loser bracket.") } - - if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false { + + if accountUpperBracketBreakTime && !roundObject.isLoserBracket() { includeBreakTime = true + print("Including break time for upper bracket.") } - let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex }) - - guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: includeBreakTime) }).max() else { + let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy { + $0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex + } + + if previousMatchIsInPreviousRotation { + print("All previous matches are from earlier rotations, returning true.") + } else { + print("Some previous matches are from the current rotation.") + } + + guard let minimumPossibleEndDate = previousMatchSlots.map({ + $0.estimatedEndDate(includeBreakTime: includeBreakTime) + }).max() else { + print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).") return previousMatchIsInPreviousRotation } if targetedStartDate >= minimumPossibleEndDate { if rotationDifferenceIsImportant { + print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).") return previousMatchIsInPreviousRotation } else { + print("Targeted start date is after the minimum possible end date, returning true.") return true } } else { if targetedStartDate == minimumTargetedEndDate { + print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") minimumTargetedEndDate = minimumPossibleEndDate } else { + print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) } + print("Targeted start date is before the minimum possible end date, returning false.") return false } } + func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() @@ -369,13 +426,15 @@ final class MatchScheduler : ModelObject, Storable { } func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { - var slots = [TimeMatch]() var _startDate: Date? var rotationIndex = 0 var availableMatchs = flattenedMatches.filter({ $0.startDate == nil }) let courtsUnavailability = courtsUnavailability var issueFound: Bool = false + + // Log start of the function + print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in if _startDate == nil { @@ -388,24 +447,21 @@ final class MatchScheduler : ModelObject, Storable { let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime) slots.append(timeMatch) } - - if slots.isEmpty == false { + + if !slots.isEmpty { rotationIndex += 1 } var freeCourtPerRotation = [Int: [Int]]() - let availableCourt = numberOfCourtsAvailablePerRotation - var courts = initialCourts ?? (0.. 0 - while availableMatchs.count > 0 && issueFound == false { + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate - + if shouldStartAtDispatcherDate { rotationStartDate = dispatcherStartDate shouldStartAtDispatcherDate = false @@ -413,23 +469,28 @@ final class MatchScheduler : ModelObject, Storable { courts = rotationIndex == 0 ? courts : (0.. 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 { - print("scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it") - let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }) + // Log courts availability and start date + print("Courts available at rotation \(rotationIndex): \(courts)") + print("Rotation start date: \(rotationStartDate)") + + // Check for court availability and break time conflicts + if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { + print("Handling break time conflicts or waiting for free courts") + let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) - let noBreakAlreadyTested = previousRotationSlots.anySatisfy({ $0.startDate == previousEndDateNoBreak }) + let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } + if let previousEndDate, let previousEndDateNoBreak { let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate) let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) - print("difference w break", differenceWithBreak) - print("difference w/o break", differenceWithoutBreak) + print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)") + let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) var difference = differenceWithBreak + if differenceWithBreak <= 0 { difference = differenceWithoutBreak } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { @@ -437,148 +498,127 @@ final class MatchScheduler : ModelObject, Storable { } if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { - courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) - }) + courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) freeCourtPerRotation[rotationIndex] = courts courts = freeCourtPreviousRotation rotationStartDate = rotationStartDate.addingTimeInterval(-difference) } } - } else if let first = availableMatchs.first { - let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration) + } else if let firstMatch = availableMatchs.first { + let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { - print("issue") + print("Issue: All courts unavailable in this rotation") issueFound = true } else { courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) } } + // Dispatch courts and schedule matches dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) rotationIndex += 1 } - + + // Organize matches in slots var organizedSlots = [TimeMatch]() for i in 0..= availableCourts - courtsUnavailable.count { + let duration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) + + let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) + + if courtsUnavailable.contains(courtPosition) { + print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).") return false } let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) - let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0 + if !canBePlayed { + print("Returning false: Match \(match.roundAndMatchTitle()) can't be played due to constraints.") + return false + } + let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0 let roundMatchesCount = roundObject.playedMatches().count - - if shouldHandleUpperRoundSlice { - print("shouldHandleUpperRoundSlice \(roundMatchesCount)") - if roundObject.parent == nil && roundMatchesCount > courts.count { - print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") - if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { - print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))") - return false - } - } - } - //if all is ok, we do a final check to see if the first - let indexInRound = match.indexInRound() - - print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)") - if roundObject.parent == nil && roundObject.index > 0, indexInRound == 0, let nextMatch = match.next() { - guard courtPosition < courts.count - 1, courts.count > 1 else { - print("next match and this match can not be played at the same time, returning false") + if shouldHandleUpperRoundSlice { + if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { + print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).") return false } - if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { - - print("next match and this match can be played, returning true") - - return true - } } - - //not adding a last match of a 4-match round (final not included obviously) - print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)") + let indexInRound = match.indexInRound() - if shouldTryToFillUpCourtsAvailable == false { - if roundMatchesCount <= 4 && currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.parent == nil && ((courts.count > 1 && courtPosition >= courts.count - 1) || courts.count == 1 && availableCourts > 1) { - print("we return false") + if roundObject.parent == nil && roundObject.index > 0 && indexInRound == 0, let nextMatch = match.next() { + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") + return true + } else { + print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") return false } } - + print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).") return canBePlayed }) { - print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate) + print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)") + + matchPerRound[firstMatch.roundObject!.id, default: 0] += 1 + + let timeMatch = TimeMatch( + matchID: firstMatch.id, + rotationIndex: rotationIndex, + courtIndex: courtIndex, + startDate: rotationStartDate, + durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), + minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime + ) - if first.roundObject!.parent == nil { - if let roundIndex = matchPerRound[first.roundObject!.id] { - matchPerRound[first.roundObject!.id] = roundIndex + 1 - } else { - matchPerRound[first.roundObject!.id] = 1 - } - } - let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime) slots.append(timeMatch) - availableMatchs.removeAll(where: { $0.id == first.id }) + availableMatchs.removeAll(where: { $0.id == firstMatch.id }) } else { - freeCourtPerRotation[rotationIndex]!.append(courtIndex) + print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.") + freeCourtPerRotation[rotationIndex]?.append(courtIndex) } + } - - if freeCourtPerRotation[rotationIndex]!.count == availableCourts { - print("no match found to be put in this rotation, check if we can put anything to another date") - freeCourtPerRotation[rotationIndex] = [] - let courtsUsed = getNextEarliestAvailableDate(from: slots) - var freeCourts: [Int] = [] - if courtsUsed.isEmpty { - freeCourts = (0.. Bool { let upperRounds: [Round] = tournament.rounds() @@ -586,16 +626,20 @@ final class MatchScheduler : ModelObject, Storable { var rounds = [Round]() + if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() { + rounds.append(groupStageLoserBracketRound) + } + if shouldEndRoundBeforeStartingNext { - rounds = upperRounds.flatMap { + rounds.append(contentsOf: upperRounds.flatMap { [$0] + $0.loserRoundsAndChildren() - } + }) } else { - rounds = upperRounds.map { + rounds.append(contentsOf: upperRounds.map { $0 } + upperRounds.flatMap { $0.loserRoundsAndChildren() - } + }) } let flattenedMatches = rounds.flatMap { round in diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 28c1cff..2e966ec 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -220,11 +220,7 @@ final class PlayerRegistration: ModelObject, Storable { return "non classé" + (isMalePlayer() ? "" : "e") } } - - func getRank() -> Int { - computedRank - } - + @MainActor func updateRank(from sources: [CSVParser], lastRank: Int) async throws { if let dataFound = try await history(from: sources) { @@ -586,4 +582,8 @@ extension PlayerRegistration: PlayerHolder { func getProgression() -> Int { 0 } + + func getComputedRank() -> Int? { + computedRank + } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 3ed4dc1..67a84e4 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -208,6 +208,7 @@ final class TeamRegistration: ModelObject, Storable { } func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String { + if let name { return name } return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ") } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 130c2f9..5cec3c6 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1493,6 +1493,13 @@ defer { //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } + func groupStageLoserBracketAreOver() -> Bool { + guard let groupStageLoserBracket = groupStageLoserBracket() else { + return true + } + return groupStageLoserBracket.hasEnded() + } + fileprivate func _paymentMethodMessage() -> String? { return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods } @@ -1524,13 +1531,27 @@ defer { return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) } + func presenceStatus() -> Double { + let selectedPlayers = selectedPlayers() + if selectedPlayers.isEmpty { return 0 } + return Double(selectedPlayers.filter { $0.hasArrived }.count) / Double(selectedPlayers.count) + } + typealias TournamentStatus = (label:String, completion: String) func cashierStatus() async -> TournamentStatus { let selectedPlayers = selectedPlayers() - let paid = selectedPlayers.filter({ $0.hasPaid() }) + var filteredPlayers = [PlayerRegistration]() + var wording = "" + if isFree() { + wording = "présent" + filteredPlayers = selectedPlayers.filter({ $0.hasArrived }) + } else { + wording = "encaissé" + filteredPlayers = selectedPlayers.filter({ $0.hasPaid() }) + } // let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés" - let label = "\(paid.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs encaissés" - let completion = (Double(paid.count) / Double(selectedPlayers.count)) + let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)" + let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) } @@ -2219,7 +2240,7 @@ extension Tournament: FederalTournamentHolder { } extension Tournament: TournamentBuildHolder { - func buildHolderTitle() -> String { + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { tournamentTitle(.short) } diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index f61f0dd..a275e03 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -278,9 +278,9 @@ class FileImportManager { FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior } - let resultOne = Array(dataOne.dropFirst(3).dropLast()) - let resultTwo = Array(dataTwo.dropFirst(3).dropLast()) - let sexUnknown: Bool = (resultOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (resultTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) + let resultOne = Array(dataOne.dropFirst(3).dropLast(3)) + let resultTwo = Array(dataTwo.dropFirst(3).dropLast(3)) + let sexUnknown: Bool = (dataOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (dataTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) var sexPlayerOne : Int { switch tournamentCategory { diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 802d741..de58eb2 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -30,7 +30,7 @@ protocol TournamentBuildHolder: Identifiable { var category: TournamentCategory { get } var level: TournamentLevel { get } var age: FederalTournamentAge { get } - func buildHolderTitle() -> String + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String } struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { @@ -43,29 +43,29 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { // var japFirstName: String? = nil // var japLastName: String? = nil - func buildHolderTitle() -> String { - computedLabel + func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { + computedLabel(displayStyle) } var identifier: String { level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() } - var computedLabel: String { - if age == .senior { return localizedLabel() } - return localizedLabel() + " " + localizedAge + func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + if age == .senior { return localizedLabel(displayStyle) } + return localizedLabel(displayStyle) + " " + localizedAge(displayStyle) } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { - level.localizedLevelLabel() + category.localizedLabel(.short) + level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } - var localizedTitle: String { - level.localizedLevelLabel() + " " + category.localizedLabel() + func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String { + level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } - var localizedAge: String { - age.tournamentDescriptionLabel + func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { + age.localizedLabel(displayStyle) } } diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index d4d6d24..71c0cf1 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -97,6 +97,33 @@ class FederalDataViewModel { }) } + func countForTournamentBuilds(from tournaments: [any FederalTournamentHolder]) -> Int { + tournaments.filter({ tournament in + (selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!)) + && + (dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod)) + && + (dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration)) + }) + .flatMap { $0.tournaments } + .filter { + (levels.isEmpty || levels.contains($0.level)) + && + (categories.isEmpty || categories.contains($0.category)) + && + (ageCategories.isEmpty || ageCategories.contains($0.age)) + } + .count + } + + func buildIsValid(_ build: any TournamentBuildHolder) -> Bool { + (levels.isEmpty || levels.contains(build.level)) + && + (categories.isEmpty || categories.contains(build.category)) + && + (ageCategories.isEmpty || ageCategories.contains(build.age)) + } + func isTournamentValidForFilters(_ tournament: Tournament) -> Bool { if tournament.isDeleted { return false } let firstPart = (levels.isEmpty || levels.contains(tournament.level)) diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index 71eacb7..ef094a0 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -124,7 +124,7 @@ struct MenuWarningView: View { @ViewBuilder func _teamActionView(_ team: TeamRegistration) -> some View { - Menu("Toute l'équipe") { + Menu(team.name ?? "Toute l'équipe") { let players = team.players() _actionView(players: players) } diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index 32992c1..074a67e 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -89,7 +89,8 @@ struct CashierDetailView: View { let showTournamentTitle: Bool @State private var earnings: Double? = nil @State private var paidCompletion: Double? = nil - + @State private var presence: Double? = nil + var body: some View { Section { LabeledContent { @@ -99,9 +100,15 @@ struct CashierDetailView: View { ProgressView() } } label: { - Text("Encaissement") - if let paidCompletion { - Text(paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + Text(tournament.isFree() ? "Présence" : "Encaissement") + if tournament.isFree() { + if let presence { + Text(presence.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } + } else { + if let paidCompletion { + Text(paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) + } } } CashierDetailDisclosureView(tournament: tournament) @@ -119,6 +126,10 @@ struct CashierDetailView: View { if paidCompletion == nil { paidCompletion = tournament.paidCompletion() } + + if presence == nil { + presence = tournament.presenceStatus() + } } } } diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index 908465f..ed5e63b 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -25,14 +25,12 @@ struct CashierSettingsView: View { var body: some View { List { Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { + RowButtonView("Tout le monde est arrivé", role: .destructive) { for tournament in self.tournaments { let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift - } + player.hasArrived = true } do { try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) @@ -43,29 +41,72 @@ struct CashierSettingsView: View { } } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") + Text("Indique tous les joueurs sont là") } - + Section { - RowButtonView("Personne n'a réglé", role: .destructive) { + RowButtonView("Personne n'est là", role: .destructive) { + for tournament in self.tournaments { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) players.forEach { player in - player.paymentType = nil + player.hasArrived = false } do { - try store.playerRegistrations.addOrUpdate(contentOfs: players) + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) } catch { Logger.error(error) } } + } } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") + Text("Indique qu'aucun joueur n'est arrivé") } + if tournaments.count > 1 || tournaments.first?.isFree() == false { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + + for tournament in self.tournaments { + let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift + } + } + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + + } + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") + } + + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + for tournament in self.tournaments { + let store = tournament.tournamentStore + + let players = tournament.selectedPlayers() + players.forEach { player in + player.paymentType = nil + } + do { + try store.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") + } + } } } } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index ce35f7f..b022259 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -57,6 +57,7 @@ class CashierViewModel: ObservableObject { let id: UUID = UUID() @Published var sortOption: SortOption = .callDate @Published var filterOption: FilterOption = .all + @Published var presenceFilterOption: PresenceFilterOption = .all @Published var sortOrder: SortOrder = .ascending @Published var searchText: String = "" @Published var isSearching: Bool = false @@ -69,9 +70,14 @@ class CashierViewModel: ObservableObject { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { if searchText.isEmpty == false { - sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) && player.contains(searchText) + sortOption.shouldDisplayPlayer(player) + && filterOption.shouldDisplayPlayer(player) + && presenceFilterOption.shouldDisplayPlayer(player) + && player.contains(searchText) } else { - sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) + sortOption.shouldDisplayPlayer(player) + && filterOption.shouldDisplayPlayer(player) + && presenceFilterOption.shouldDisplayPlayer(player) } } @@ -183,6 +189,37 @@ class CashierViewModel: ObservableObject { } } + enum PresenceFilterOption: Int, Identifiable, CaseIterable { + case all + case hasArrived + case hasNotArrived + + var id: Int { self.rawValue } + + func localizedLabel() -> String { + switch self { + case .all: + return "Tous" + case .hasArrived: + return "Présent" + case .hasNotArrived: + return "Absent" + } + } + + func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { + switch self { + case .all: + return true + case .hasArrived: + return player.hasArrived + case .hasNotArrived: + return player.hasArrived == false + + } + } + } + } struct CashierView: View { @@ -201,16 +238,42 @@ struct CashierView: View { _players = .init(wrappedValue: teams.flatMap({ $0.unsortedPlayers() })) } + private func _isFree() -> Bool { + if tournaments.count == 1 { + return tournaments.first?.isFree() == true + } else { + return false + } + } + + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if _isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + var body: some View { List { if cashierViewModel.isSearching == false { Section { - Picker(selection: $cashierViewModel.filterOption) { - ForEach(CashierViewModel.FilterOption.allCases) { filterOption in + Picker(selection: $cashierViewModel.presenceFilterOption) { + ForEach(CashierViewModel.PresenceFilterOption.allCases) { filterOption in Text(filterOption.localizedLabel()).tag(filterOption) } } label: { - Text("Statut du règlement") + Text("Présence") + } + + if _isFree() == false { + Picker(selection: $cashierViewModel.filterOption) { + ForEach(CashierViewModel.FilterOption.allCases) { filterOption in + Text(filterOption.localizedLabel()).tag(filterOption) + } + } label: { + Text("Statut du règlement") + } } Picker(selection: $cashierViewModel.sortOption) { @@ -239,12 +302,12 @@ struct CashierView: View { switch cashierViewModel.sortOption { case .teamRank: - TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1) + TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: - PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1) + PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .callDate: let _teams = teams.filter({ $0.callDate != nil }) - TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1) + TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) } } .onAppear { @@ -279,11 +342,12 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let players: [PlayerRegistration] let displayTournamentTitle: Bool - + let editingOptions: [EditablePlayerView.PlayerEditingOption] + var body: some View { ForEach(players) { player in Section { - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } header: { if displayTournamentTitle, let tournamentTitle = player.tournament()?.tournamentTitle() { Text(tournamentTitle) @@ -301,6 +365,7 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let teams: [TeamRegistration] let displayTournamentTitle: Bool + let editingOptions: [EditablePlayerView.PlayerEditingOption] var body: some View { ForEach(teams) { team in @@ -308,11 +373,17 @@ struct CashierView: View { if players.isEmpty == false { Section { ForEach(players) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { - if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { - Text(tournamentTitle) + HStack { + if let name = team.name { + Text(name) + } + if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Spacer() + Text(tournamentTitle) + } } } footer: { if let callDate = team.callDate { @@ -329,6 +400,7 @@ struct CashierView: View { @EnvironmentObject var cashierViewModel: CashierViewModel let teams: [TeamRegistration] let displayTournamentTitle: Bool + let editingOptions: [EditablePlayerView.PlayerEditingOption] var body: some View { let groupedTeams = Dictionary(grouping: teams) { team in @@ -343,10 +415,15 @@ struct CashierView: View { if players.isEmpty == false { Section { ForEach(players) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { + if let name = team.name { + Text(name) + } + if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { + Spacer() Text(tournamentTitle) } } footer: { diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index fc022f1..9c5378c 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -385,7 +385,7 @@ struct ClubSearchView: View { LabeledContent { Text(club.distance(from: locationManager.location)) } label: { - Text(club.nom) + Text(club.nom).lineLimit(1) Text(club.ville).font(.caption) } } diff --git a/PadelClub/Views/Components/FooterButtonView.swift b/PadelClub/Views/Components/FooterButtonView.swift index a710b18..7730639 100644 --- a/PadelClub/Views/Components/FooterButtonView.swift +++ b/PadelClub/Views/Components/FooterButtonView.swift @@ -11,13 +11,15 @@ fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire c struct FooterButtonView: View { var role: ButtonRole? = nil + var systemImage: String? = nil let title: String let confirmationMessage: String let action: () -> () @State private var askConfirmation: Bool = false - init(_ title: String, role: ButtonRole? = nil, confirmationMessage: String? = nil, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, confirmationMessage: String? = nil, action: @escaping () -> Void) { self.title = title + self.systemImage = systemImage self.action = action self.role = role self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage @@ -31,8 +33,16 @@ struct FooterButtonView: View { action() } } label: { - Text(title) - .underline() + if let systemImage { + HStack { + Text(title) + .underline() + Image(systemName: systemImage).font(.caption) + } + } else { + Text(title) + .underline() + } } .buttonStyle(.borderless) .confirmationDialog("Confirmation", diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 0d87d01..af1b072 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -37,12 +37,22 @@ struct GroupStageTeamView: View { } } + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if tournament.isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + var body: some View { List { Section { + if let name = team.name { + Text(name).foregroundStyle(.secondary) + } ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) - .environmentObject(tournament.tournamentStore) + EditablePlayerView(player: player, editingOptions: _editingOptions()) } } diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 467a377..e1c0543 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -125,15 +125,16 @@ struct GroupStageView: View { HStack { VStack(alignment: .leading) { if let teamName = team.name { - Text(teamName).foregroundStyle(.secondary) - } - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1) - .overlay { - if player.hasArrived && team.isHere() == false { - Color.green.opacity(0.6) + Text(teamName).font(.title3) + } else { + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .overlay { + if player.hasArrived && team.isHere() == false { + Color.green.opacity(0.6) + } } - } + } } } Spacer() diff --git a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift index b01901b..0ab01e9 100644 --- a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift +++ b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift @@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View { Section { Picker(selection: $selectedPlayer) { HStack { - Text("Toute l'équipe") + Text(team.name ?? "Toute l'équipe") Spacer() Text(team.weight.formatted()).bold() } diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index d969252..8034631 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -93,7 +93,7 @@ struct MatchDateView: View { .foregroundStyle(Color.master) .underline() } else { - Text("en attente") + Text("démarrer") .foregroundStyle(Color.master) .underline() } diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index 435f181..cf952d0 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -32,13 +32,26 @@ struct MatchTeamDetailView: View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { ForEach(team.players()) { player in - EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) + EditablePlayerView(player: player, editingOptions: _editingOptions()) } } header: { TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) } } + private func _isFree() -> Bool { + let tournament = match.currentTournament() + return tournament?.isFree() == true + } + + private func _editingOptions() -> [EditablePlayerView.PlayerEditingOption] { + if _isFree() { + return [.licenceId, .name, .presence] + } else { + return [.licenceId, .name, .payment] + } + } + } //#Preview { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 444aa94..5f5b278 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -57,19 +57,21 @@ struct PlayerBlockView: View { } if let name = team?.name { - Text(name).foregroundStyle(.secondary) - } - ForEach(names, id: \.self) { name in - Text(name).lineLimit(1) + Text(name).font(.title3) + } else { + ForEach(names, id: \.self) { name in + Text(name).lineLimit(1) + } } } else { ZStack(alignment: .leading) { VStack { if let name = team?.name { - Text(name).foregroundStyle(.secondary) + Text(name).font(.title3) + } else { + Text("longLabelPlayerOne").lineLimit(1) + Text("longLabelPlayerTwo").lineLimit(1) } - Text("longLabelPlayerOne").lineLimit(1) - Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index bf7c291..235e648 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -107,30 +107,31 @@ struct MatchDetailView: View { } } - let players = self.match.teams().flatMap { $0.players() } - let unpaid = players.filter({ $0.hasPaid() == false }) - - if unpaid.isEmpty == false { - Section { - DisclosureGroup { - ForEach(unpaid) { player in + if self.match.currentTournament()?.isFree() == false { + let players = self.match.teams().flatMap { $0.players() } + let unpaid = players.filter({ $0.hasPaid() == false }) + + if unpaid.isEmpty == false { + Section { + DisclosureGroup { + ForEach(unpaid) { player in + LabeledContent { + PlayerPayView(player: player) + .environmentObject(tournamentStore) + } label: { + Text(player.playerLabel()) + } + } + } label: { LabeledContent { - PlayerPayView(player: player) - .environmentObject(tournamentStore) + Text(unpaid.count.formatted() + " / " + players.count.formatted()) } label: { - Text(player.playerLabel()) + Text("Encaissement manquant") } } - } label: { - LabeledContent { - Text(unpaid.count.formatted() + " / " + players.count.formatted()) - } label: { - Text("Encaissement manquant") - } } } } - menuView } .sheet(isPresented: $showDetails) { @@ -423,9 +424,9 @@ struct MatchDetailView: View { let rotationDuration = match.getDuration() Picker(selection: $startDateSetup) { if match.isReady() { + Text("Tout de suite").tag(MatchDateSetup.now) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) - Text("Tout de suite").tag(MatchDateSetup.now) } Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) @@ -464,11 +465,7 @@ struct MatchDetailView: View { Text("Au hasard parmi les libres").tag(MatchFieldSetup.random) Text("Au hasard").tag(MatchFieldSetup.fullRandom) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) - if let club = match.currentTournament()?.club() { - ForEach(0.. URL? { + do { + let filePath = try Club.storageDirectoryPath() + return try Zip.quickZipFiles([filePath], fileName: "backup") // Zip + } catch { + Logger.error(error) + return nil + } + } } //#Preview { diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 96302fd..25cc794 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -40,6 +40,7 @@ struct PlanningByCourtView: View { var body: some View { List { _byCourtView() + .id(selectedCourt) } .overlay { if matches.allSatisfy({ $0.startDate == nil }) { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 645faf0..230b748 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -240,9 +240,9 @@ struct PlanningSettingsView: View { let value = tournament.getGroupStageChunkValue() if parallelType == false { if value > 1 { - Text("\(value.formatted()) poules commenceront en parallèle") + Text("\(value.formatted()) poules en parallèle") } else { - Text("une poule sera jouer à la fois") + Text("une poule sera jouée à la fois") } } } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index e490a08..9dd42da 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -16,6 +16,23 @@ struct PlanningView: View { @State private var timeSlots: [Date:[Match]] @State private var days: [Date] @State private var keys: [Date] + @State private var filterOption: PlanningFilterOption = .byDefault + + enum PlanningFilterOption: Int, CaseIterable, Identifiable { + var id: Int { self.rawValue } + + case byDefault + case byCourt + + func localizedPlanningLabel() -> String { + switch self { + case .byCourt: + return "Par terrain" + case .byDefault: + return "Par défaut" + } + } + } init(matches: [Match], selectedScheduleDestination: Binding) { self.matches = matches @@ -30,6 +47,24 @@ struct PlanningView: View { List { _bySlotView() } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) + } label: { + Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant(filterOption == .byCourt ? .fill : .none) + } + } + }) .overlay { if matches.allSatisfy({ $0.startDate == nil }) { ContentUnavailableView { @@ -53,7 +88,7 @@ struct PlanningView: View { ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in if let _matches = timeSlots[key] { DisclosureGroup { - ForEach(_matches) { match in + ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in NavigationLink { MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) } label: { @@ -98,7 +133,14 @@ struct PlanningView: View { Text(self._formattedMatchCount(matches.count)) } label: { Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", ")) + let names = matches.sorted(by: \.computedOrder) + .compactMap({ $0.roundTitle() }) + .reduce(into: [String]()) { uniqueNames, name in + if !uniqueNames.contains(name) { + uniqueNames.append(name) + } + } + Text(names.joined(separator: ", ")) } } diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index 130b127..b2955fb 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -14,6 +14,7 @@ struct EditablePlayerView: View { case payment case licenceId case name + case presence } @EnvironmentObject var dataStore: DataStore @@ -77,6 +78,13 @@ struct EditablePlayerView: View { Logger.error(error) } } + .onChange(of: player.hasArrived) { + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } } @ViewBuilder @@ -91,11 +99,6 @@ struct EditablePlayerView: View { Menu { Button { player.hasArrived.toggle() - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) - } catch { - Logger.error(error) - } } label: { Label("Présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle") } @@ -172,6 +175,11 @@ struct EditablePlayerView: View { if editingOptions.contains(.payment) { Spacer() PlayerPayView(player: player) + } else if editingOptions.contains(.presence) { + Spacer() + FooterButtonView(player.hasArrived ? "Présent" : "Sur place ?", role: player.hasArrived ? nil : .cancel, systemImage: player.hasArrived ? "checkmark" : nil) { + player.hasArrived.toggle() + } } } } diff --git a/PadelClub/Views/Player/Components/PlayerPopoverView.swift b/PadelClub/Views/Player/Components/PlayerPopoverView.swift index f1dbe6e..3e8ddcf 100644 --- a/PadelClub/Views/Player/Components/PlayerPopoverView.swift +++ b/PadelClub/Views/Player/Components/PlayerPopoverView.swift @@ -31,7 +31,7 @@ struct PlayerPopoverView: View { @State private var source: String? - init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { + init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { if let source { let words = source.components(separatedBy: .whitespaces) if words.isEmpty == false { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 12bda95..37aa4c5 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -259,7 +259,7 @@ struct RoundView: View { #if DEBUG Spacer() - Text(match.teamScores.count.formatted()) + Text(match.index.formatted() + " " + match.teamScores.count.formatted()) #endif } } footer: { diff --git a/PadelClub/Views/Shared/ImportedPlayerView.swift b/PadelClub/Views/Shared/ImportedPlayerView.swift index fd99bf8..6de85dd 100644 --- a/PadelClub/Views/Shared/ImportedPlayerView.swift +++ b/PadelClub/Views/Shared/ImportedPlayerView.swift @@ -12,6 +12,9 @@ struct ImportedPlayerView: View { var index: Int? = nil var showFemaleInMaleAssimilation: Bool = false var showProgression: Bool = false + var isAnimation: Bool { + player.getComputedRank() == 0 + } var body: some View { VStack(alignment: .leading) { @@ -39,74 +42,76 @@ struct ImportedPlayerView: View { } .font(.title3) .lineLimit(1) - HStack { - HStack(alignment: .top, spacing: 0) { - Text(player.formattedRank()).italic(player.isAssimilated) - .font(.title3) - .background { - if player.isNotFromCurrentDate() { - UnderlineView() + if isAnimation == false { + HStack { + HStack(alignment: .top, spacing: 0) { + Text(player.formattedRank()).italic(player.isAssimilated) + .font(.title3) + .background { + if player.isNotFromCurrentDate() { + UnderlineView() + } } + if let rank = player.getRank() { + Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) + .font(.caption) + } + } + + if showProgression, player.getProgression() != 0 { + HStack(alignment: .top, spacing: 2) { + Text("(") + Text(player.getProgression().formatted(.number.sign(strategy: .always()))) + .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) + Text(")") + }.font(.title3) + } + + if let pts = player.getPoints(), pts > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(pts.formatted()).font(.title3) + Text(" pts").font(.caption) + } + } + + if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { + HStack(alignment: .lastTextBaseline, spacing: 0) { + Text(tournamentPlayed.formatted()).font(.title3) + Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) } - if let rank = player.getRank() { - Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated) - .font(.caption) } } + .lineLimit(1) - if showProgression, player.getProgression() != 0 { + if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { HStack(alignment: .top, spacing: 2) { Text("(") - Text(player.getProgression().formatted(.number.sign(strategy: .always()))) - .foregroundStyle(player.getProgressionColor(progression: player.getProgression())) - Text(")") - }.font(.title3) - } - - if let pts = player.getPoints(), pts > 0 { - HStack(alignment: .lastTextBaseline, spacing: 0) { - Text(pts.formatted()).font(.title3) - Text(" pts").font(.caption) + Text(assimilatedAsMaleRank.formatted()) + VStack(alignment: .leading, spacing: 0) { + Text("équivalence") + Text("messieurs") + } + .font(.caption) + Text(")").font(.title3) } } - - if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { - HStack(alignment: .lastTextBaseline, spacing: 0) { - Text(tournamentPlayed.formatted()).font(.title3) - Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) + + HStack { + Text(player.formattedLicense()) + if let computedAge = player.computedAge { + Text(computedAge.formatted() + " ans") } } - } - .lineLimit(1) - - if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { - HStack(alignment: .top, spacing: 2) { - Text("(") - Text(assimilatedAsMaleRank.formatted()) - VStack(alignment: .leading, spacing: 0) { - Text("équivalence") - Text("messieurs") - } - .font(.caption) - Text(")").font(.title3) + .font(.caption) + if let clubName = player.clubName { + Text(clubName) + .font(.caption) } - } - - HStack { - Text(player.formattedLicense()) - if let computedAge = player.computedAge { - Text(computedAge.formatted() + " ans") + if let ligueName = player.ligueName { + Text(ligueName) + .font(.caption) } } - .font(.caption) - if let clubName = player.clubName { - Text(clubName) - .font(.caption) - } - if let ligueName = player.ligueName { - Text(ligueName) - .font(.caption) - } } } } diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 533d4bc..458d4e3 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -17,10 +17,6 @@ struct TeamRowView: View { TeamWeightView(team: team, teamPosition: teamPosition) } label: { VStack(alignment: .leading) { - if let name = team.name { - Text(name).foregroundStyle(.secondary) - } - if let groupStage = team.groupStageObject() { HStack { Text(groupStage.groupStageTitle()) @@ -32,13 +28,20 @@ struct TeamRowView: View { Text(round.roundTitle(.wide)) } - if team.players().isEmpty == false { - ForEach(team.players()) { player in - Text(player.playerLabel()) + if let name = team.name { + Text(name).font(.title3) + if team.players().isEmpty { + Text("Aucun joueur") } } else { - Text("Place réservée") - Text("Place réservée") + if team.players().isEmpty == false { + ForEach(team.players()) { player in + Text(player.playerLabel()) + } + } else { + Text("Place réservée") + Text("Place réservée") + } } } if displayCallDate { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index 2353007..b965fb5 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -54,9 +54,15 @@ enum CashierDestination: Identifiable, Selectable, Equatable { case .summary: return nil case .groupStage(let groupStage): + if groupStage.tournamentObject()?.isFree() == true { + return groupStage.unsortedPlayers().filter({ $0.hasArrived == false }).count + } return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count case .bracket(let round): let playerRegistrations: [PlayerRegistration] = round.seeds().flatMap { $0.unsortedPlayers() } + if round.tournamentObject()?.isFree() == true { + return playerRegistrations.filter({ $0.hasArrived == false }).count + } return playerRegistrations.filter({ $0.hasPaid() == false }).count case .all(_): return nil @@ -156,7 +162,7 @@ struct TournamentCashierView: View { .environmentObject(cashierViewModel) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Encaissement") + .navigationTitle(tournament.isFree() ? "Présence" : "Encaissement") } } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 94a7353..62c22d1 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -11,6 +11,7 @@ import LeStorage struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore @Environment(NavigationViewModel.self) private var navigation + @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel let tournament: FederalTournamentHolder // let color: Color = .black @@ -23,11 +24,17 @@ struct TournamentCellView: View { var body: some View { ForEach(tournament.tournaments, id: \.id) { build in - if navigation.agendaDestination == .around, let federalTournament = tournament as? FederalTournament { - NavigationLink { - TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) - } label: { - _buildView(build, existingTournament: event?.existingBuild(build)) + if let federalTournament = tournament as? FederalTournament { + if federalDataViewModel.isFederalTournamentValidForFilters(federalTournament, build: build) { + if navigation.agendaDestination == .around { + NavigationLink { + TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) + } label: { + _buildView(build, existingTournament: event?.existingBuild(build)) + } + } else { + _buildView(build, existingTournament: event?.existingBuild(build)) + } } } else { _buildView(build, existingTournament: event?.existingBuild(build)) diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 75c2cdd..7aa4836 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -191,7 +191,7 @@ struct TournamentBuildView: View { ProgressView() } } label: { - Text("Encaissement") + Text(tournament.isFree() ? "Présence" : "Encaissement") if let tournamentStatus { Text(tournamentStatus.label).lineLimit(1) } else {