Merge branch 'main'

sync2
Raz 1 year ago
commit 6ac26eb1e4
  1. 18
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  3. 1
      PadelClub/Data/Federal/PlayerHolder.swift
  4. 25
      PadelClub/Data/GroupStage.swift
  5. 11
      PadelClub/Data/Match.swift
  6. 294
      PadelClub/Data/MatchScheduler.swift
  7. 8
      PadelClub/Data/PlayerRegistration.swift
  8. 1
      PadelClub/Data/TeamRegistration.swift
  9. 29
      PadelClub/Data/Tournament.swift
  10. 6
      PadelClub/Utils/FileImportManager.swift
  11. 22
      PadelClub/Utils/PadelRule.swift
  12. 27
      PadelClub/ViewModel/FederalDataViewModel.swift
  13. 2
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  14. 17
      PadelClub/Views/Cashier/CashierDetailView.swift
  15. 65
      PadelClub/Views/Cashier/CashierSettingsView.swift
  16. 103
      PadelClub/Views/Cashier/CashierView.swift
  17. 2
      PadelClub/Views/Club/ClubSearchView.swift
  18. 16
      PadelClub/Views/Components/FooterButtonView.swift
  19. 14
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  20. 17
      PadelClub/Views/GroupStage/GroupStageView.swift
  21. 2
      PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift
  22. 2
      PadelClub/Views/Match/Components/MatchDateView.swift
  23. 15
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  24. 16
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  25. 43
      PadelClub/Views/Match/MatchDetailView.swift
  26. 2
      PadelClub/Views/Match/MatchSummaryView.swift
  27. 8
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  28. 4
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  29. 10
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  30. 27
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  31. 1
      PadelClub/Views/Planning/PlanningByCourtView.swift
  32. 4
      PadelClub/Views/Planning/PlanningSettingsView.swift
  33. 46
      PadelClub/Views/Planning/PlanningView.swift
  34. 18
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  35. 2
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  36. 2
      PadelClub/Views/Round/RoundView.swift
  37. 111
      PadelClub/Views/Shared/ImportedPlayerView.swift
  38. 21
      PadelClub/Views/Team/TeamRowView.swift
  39. 8
      PadelClub/Views/Tournament/Screen/TournamentCashierView.swift
  40. 17
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  41. 2
      PadelClub/Views/Tournament/TournamentBuildView.swift

@ -3134,11 +3134,12 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3157,7 +3158,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3178,10 +3179,11 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 11;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3200,7 +3202,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3293,7 +3295,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3315,7 +3317,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3335,7 +3337,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3356,7 +3358,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.15;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -122,6 +122,10 @@ extension ImportedPlayer: PlayerHolder {
func getProgression() -> Int { func getProgression() -> Int {
return Int(progression) return Int(progression)
} }
func getComputedRank() -> Int? {
nil
}
} }
fileprivate extension Int { fileprivate extension Int {

@ -27,6 +27,7 @@ protocol PlayerHolder {
func isNotFromCurrentDate() -> Bool func isNotFromCurrentDate() -> Bool
func getBirthYear() -> Int? func getBirthYear() -> Int?
func getProgression() -> Int func getProgression() -> Int
func getComputedRank() -> Int?
} }
extension PlayerHolder { extension PlayerHolder {

@ -52,7 +52,7 @@ final class GroupStage: ModelObject, Storable {
// MARK: - Computed dependencies // MARK: - Computed dependencies
func _matches() -> [Match] { 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 } // Store.main.filter { $0.groupStage == self.id }
} }
@ -150,6 +150,15 @@ final class GroupStage: ModelObject, Storable {
} catch { } catch {
Logger.error(error) 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: case 4:
return [2, 3, 1, 4, 5, 0] return [2, 3, 1, 4, 5, 0]
case 5: case 5:
return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] // return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9]
// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0] return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6: case 6:
return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 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] return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
default: default:
return [] return []
} }
} }
func indexOf(_ matchIndex: Int) -> Int {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex
}
private func _matchUp(for matchIndex: Int) -> [Int] { private func _matchUp(for matchIndex: Int) -> [Int] {
Array((0..<size).combinations(ofCount: 2))[safe: matchIndex] ?? [] Array((0..<size).combinations(ofCount: 2))[safe: matchIndex] ?? []
} }
@ -279,7 +292,7 @@ final class GroupStage: ModelObject, Storable {
func localizedMatchUpLabel(for matchIndex: Int) -> String { func localizedMatchUpLabel(for matchIndex: Int) -> String {
let matchUp = _matchUp(for: matchIndex) let matchUp = _matchUp(for: matchIndex)
if let index = matchUp.first, let index2 = matchUp.last { if let index = matchUp.first, let index2 = matchUp.last {
return "#\(index + 1) contre #\(index2 + 1)" return "#\(index + 1) vs #\(index2 + 1)"
} else { } else {
return "--" return "--"
} }

@ -408,7 +408,7 @@ defer {
} }
func next() -> Match? { 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 return matches.sorted(by: \.index).first
} }
@ -435,6 +435,10 @@ defer {
else { return nil } else { return nil }
} }
func roundAndMatchTitle() -> String {
[roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ")
}
func topPreviousRoundMatchIndex() -> Int { func topPreviousRoundMatchIndex() -> Int {
return index * 2 + 1 return index * 2 + 1
} }
@ -470,8 +474,11 @@ defer {
} }
var computedOrder: Int { var computedOrder: Int {
if let groupStageObject {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
}
guard let roundObject else { return 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] { func previousMatches() -> [Match] {

@ -179,30 +179,39 @@ final class MatchScheduler : ModelObject, Storable {
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 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..<maxMatchesCount).flatMap { index in let flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in _groupStages.compactMap { group in
// Use optional subscript to safely access matches // Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches() let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil return playedMatches.indices.contains(index) ? playedMatches[index] : nil
} }
} }
var slots = [GroupStageTimeMatch]() var slots = [GroupStageTimeMatch]()
var availableMatchs = flattenedMatches var availableMatches = flattenedMatches
var rotationIndex = 0 var rotationIndex = 0
var teamsPerRotation = [Int: [String]]() var teamsPerRotation = [Int: [String]]() // Tracks teams assigned to each rotation
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]() // Tracks free courts per rotation
var groupLastRotation = [Int: Int]() var groupLastRotation = [Int: Int]() // Tracks the last rotation each group was involved in
let courtsUnavailability = courtsUnavailability let courtsUnavailability = courtsUnavailability
while slots.count < flattenedMatches.count { while slots.count < flattenedMatches.count {
print("Starting rotation \(rotationIndex) with \(availableMatches.count) matches left")
teamsPerRotation[rotationIndex] = [] teamsPerRotation[rotationIndex] = []
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) }
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }
.map { ($0.groupIndex, 1) }
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatchs.filter({ match in var rotationMatches = Array(availableMatches.filter({ match in
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true // Check if all teams from the match are not already scheduled in the current rotation
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) })
if !teamsAvailable {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
}
return teamsAvailable
}).prefix(numberOfCourtsAvailablePerRotation)) }).prefix(numberOfCourtsAvailablePerRotation))
if rotationIndex > 0 { if rotationIndex > 0 {
@ -216,28 +225,42 @@ final class MatchScheduler : ModelObject, Storable {
} }
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in (0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)")
if let first = rotationMatches.first(where: { match in if let first = rotationMatches.first(where: { match in
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let timeIntervalToAdd = (Double(rotationIndex)) * Double(estimatedDuration) * 60 let timeIntervalToAdd = Double(rotationIndex) * Double(estimatedDuration) * 60
let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd) let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)
if courtIndex >= numberOfCourtsAvailablePerRotation - courtsUnavailable.count {
if courtsUnavailable.contains(courtIndex) {
print("Court \(courtIndex) is unavailable at \(rotationStartDate)")
return false return false
} else {
return teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
} }
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) })
if !teamsAvailable {
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
return false
}
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) 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) slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
rotationMatches.removeAll(where: { $0.id == first.id }) 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 { if let index = first.groupStageObject?.index {
groupLastRotation[index] = rotationIndex groupLastRotation[index] = rotationIndex
} }
} else { } else {
print("No available matches for court \(courtIndex) in rotation \(rotationIndex), adding to free court list")
freeCourtPerRotation[rotationIndex]!.append(courtIndex) freeCourtPerRotation[rotationIndex]!.append(courtIndex)
} }
} }
@ -245,6 +268,9 @@ final class MatchScheduler : ModelObject, Storable {
rotationIndex += 1 rotationIndex += 1
} }
print("All matches scheduled. Total rotations: \(rotationIndex)")
// Organize slots and ensure courts are randomized or sorted
var organizedSlots = [GroupStageTimeMatch]() var organizedSlots = [GroupStageTimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted: [Int] = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted: [Int] = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
@ -257,10 +283,15 @@ final class MatchScheduler : ModelObject, Storable {
} }
} }
return GroupStageMatchDispatcher(
return GroupStageMatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation) timedMatches: organizedSlots,
freeCourtPerRotation: freeCourtPerRotation,
rotationCount: rotationIndex,
groupLastRotation: groupLastRotation
)
} }
func rotationDifference(loserBracket: Bool) -> Int { func rotationDifference(loserBracket: Bool) -> Int {
if loserBracket { if loserBracket {
return loserBracketRotationDifference return loserBracketRotationDifference
@ -270,71 +301,97 @@ final class MatchScheduler : ModelObject, Storable {
} }
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool { 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 { 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 { if targetedStartDate == minimumTargetedEndDate {
print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)")
minimumTargetedEndDate = roundStartDate minimumTargetedEndDate = roundStartDate
} else { } else {
print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate) minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
} }
print("Returning false: Match cannot start earlier than the round start date.")
return false return false
} }
let previousMatches = roundObject.precedentMatches(ofMatch: match) 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 let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) }
previousMatches.map { $0.id }.contains(slot.matchID)
})
if previousMatchSlots.isEmpty { 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 return true
} }
print("Some previous matches are pending, returning false.")
return false return false
} }
if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count { if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count {
if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) { if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) {
print("Some previous matches started, returning true.")
return true return true
} }
print("Not enough previous matches have started, returning false.")
return false return false
} }
var includeBreakTime = false var includeBreakTime = false
if accountLoserBracketBreakTime && roundObject.isLoserBracket() { if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
includeBreakTime = true includeBreakTime = true
print("Including break time for loser bracket.")
} }
if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false { if accountUpperBracketBreakTime && !roundObject.isLoserBracket() {
includeBreakTime = true includeBreakTime = true
print("Including break time for upper bracket.")
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy {
$0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex
} }
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 { guard let minimumPossibleEndDate = previousMatchSlots.map({
$0.estimatedEndDate(includeBreakTime: includeBreakTime)
}).max() else {
print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).")
return previousMatchIsInPreviousRotation return previousMatchIsInPreviousRotation
} }
if targetedStartDate >= minimumPossibleEndDate { if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant { if rotationDifferenceIsImportant {
print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).")
return previousMatchIsInPreviousRotation return previousMatchIsInPreviousRotation
} else { } else {
print("Targeted start date is after the minimum possible end date, returning true.")
return true return true
} }
} else { } else {
if targetedStartDate == minimumTargetedEndDate { if targetedStartDate == minimumTargetedEndDate {
print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)")
minimumTargetedEndDate = minimumPossibleEndDate minimumTargetedEndDate = minimumPossibleEndDate
} else { } else {
print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
} }
print("Targeted start date is before the minimum possible end date, returning false.")
return false return false
} }
} }
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
} }
@ -369,7 +426,6 @@ final class MatchScheduler : ModelObject, Storable {
} }
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]() var slots = [TimeMatch]()
var _startDate: Date? var _startDate: Date?
var rotationIndex = 0 var rotationIndex = 0
@ -377,6 +433,9 @@ final class MatchScheduler : ModelObject, Storable {
let courtsUnavailability = courtsUnavailability let courtsUnavailability = courtsUnavailability
var issueFound: Bool = false 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 flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
if _startDate == nil { if _startDate == nil {
_startDate = match.startDate _startDate = match.startDate
@ -389,19 +448,16 @@ final class MatchScheduler : ModelObject, Storable {
slots.append(timeMatch) slots.append(timeMatch)
} }
if slots.isEmpty == false { if !slots.isEmpty {
rotationIndex += 1 rotationIndex += 1
} }
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]()
let availableCourt = numberOfCourtsAvailablePerRotation let availableCourt = numberOfCourtsAvailablePerRotation
var courts = initialCourts ?? (0..<availableCourt).map { $0 } var courts = initialCourts ?? (0..<availableCourt).map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0 var shouldStartAtDispatcherDate = rotationIndex > 0
while availableMatchs.count > 0 && issueFound == false { while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 {
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
@ -413,23 +469,28 @@ final class MatchScheduler : ModelObject, Storable {
courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 } courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 }
} }
courts.sort() courts.sort()
print("courts available at rotation \(rotationIndex)", courts)
print("rotationStartDate", rotationStartDate)
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], freeCourtPreviousRotation.count > 0 { // Log courts availability and start date
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") print("Courts available at rotation \(rotationIndex): \(courts)")
let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }) 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 previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) 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 { if let previousEndDate, let previousEndDateNoBreak {
let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate) let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
print("difference w break", differenceWithBreak) print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)")
print("difference w/o break", differenceWithoutBreak)
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
var difference = differenceWithBreak var difference = differenceWithBreak
if differenceWithBreak <= 0 { if differenceWithBreak <= 0 {
difference = differenceWithoutBreak difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
@ -437,34 +498,35 @@ final class MatchScheduler : ModelObject, Storable {
} }
if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate {
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) courts.removeAll(where: { freeCourtPreviousRotation.contains($0) })
})
freeCourtPerRotation[rotationIndex] = courts freeCourtPerRotation[rotationIndex] = courts
courts = freeCourtPreviousRotation courts = freeCourtPreviousRotation
rotationStartDate = rotationStartDate.addingTimeInterval(-difference) rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
} }
} }
} else if let first = availableMatchs.first { } else if let firstMatch = availableMatchs.first {
let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration) let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { if courtsUnavailable.count == numberOfCourtsAvailablePerRotation {
print("issue") print("Issue: All courts unavailable in this rotation")
issueFound = true issueFound = true
} else { } else {
courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) 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) dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
rotationIndex += 1 rotationIndex += 1
} }
// Organize matches in slots
var organizedSlots = [TimeMatch]() var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex { for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() let courtsSorted = slots.filter { $0.rotationIndex == i }.map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex)) var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.courtIndex))
for j in 0..<matches.count { for j in 0..<matches.count {
matches[j].courtIndex = courts[j] matches[j].courtIndex = courts[j]
@ -472,110 +534,88 @@ final class MatchScheduler : ModelObject, Storable {
} }
} }
print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches")
return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
} }
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) { func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) {
var matchPerRound = [String: Int]() var matchPerRound = [String: Int]()
var minimumTargetedEndDate: Date = rotationStartDate var minimumTargetedEndDate = rotationStartDate
print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
// Log dispatch attempt
print("Dispatching courts for rotation \(rotationIndex) with start date \(rotationStartDate) and available courts \(courts.sorted())")
for (courtPosition, courtIndex) in courts.sorted().enumerated() { for (courtPosition, courtIndex) in courts.sorted().enumerated() {
if let first = availableMatchs.first(where: { match in if let firstMatch = availableMatchs.first(where: { match in
print("trying to find a match for \(courtIndex) in \(rotationIndex)") print("Trying to find a match for court \(courtIndex) in rotation \(rotationIndex)")
let roundObject = match.roundObject! let roundObject = match.roundObject!
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability) let duration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
print("courtsUnavailable \(courtsUnavailable)")
if courtPosition >= availableCourts - courtsUnavailable.count { 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 return false
} }
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) 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 let roundMatchesCount = roundObject.playedMatches().count
if shouldHandleUpperRoundSlice { if shouldHandleUpperRoundSlice {
print("shouldHandleUpperRoundSlice \(roundMatchesCount)") if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
if roundObject.parent == nil && roundMatchesCount > courts.count { print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).")
print("roundMatchesCount \(roundMatchesCount) > \(courts.count)") return false
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() 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() {
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) {
guard courtPosition < courts.count - 1, courts.count > 1 else { print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).")
print("next match and this match can not be played at the same time, returning false")
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 return true
} } else {
} print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).")
//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)")
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")
return false return false
} }
} }
print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).")
return canBePlayed return canBePlayed
}) { }) {
print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate) print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)")
if first.roundObject!.parent == nil { matchPerRound[firstMatch.roundObject!.id, default: 0] += 1
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 })
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
}
if freeCourtPerRotation[rotationIndex]!.count == availableCourts { let timeMatch = TimeMatch(
print("no match found to be put in this rotation, check if we can put anything to another date") matchID: firstMatch.id,
freeCourtPerRotation[rotationIndex] = [] rotationIndex: rotationIndex,
let courtsUsed = getNextEarliestAvailableDate(from: slots) courtIndex: courtIndex,
var freeCourts: [Int] = [] startDate: rotationStartDate,
if courtsUsed.isEmpty { durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration),
freeCourts = (0..<availableCourts).map { $0 } minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime
)
slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == firstMatch.id })
} else { } else {
freeCourts = courtsUsed.filter { (courtIndex, availableDate) in print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.")
availableDate <= minimumTargetedEndDate freeCourtPerRotation[rotationIndex]?.append(courtIndex)
}.sorted(by: \.1).map { $0.0 }
} }
}
if let first = availableMatchs.first { if freeCourtPerRotation[rotationIndex]?.count == availableCourts {
let duration = first.matchFormat.getEstimatedDuration(additionalEstimationDuration) print("All courts in rotation \(rotationIndex) are free")
let courtsUnavailable = courtsUnavailable(startDate: minimumTargetedEndDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.count < availableCourts {
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
}
}
} }
} }
@ -586,16 +626,20 @@ final class MatchScheduler : ModelObject, Storable {
var rounds = [Round]() var rounds = [Round]()
if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() {
rounds.append(groupStageLoserBracketRound)
}
if shouldEndRoundBeforeStartingNext { if shouldEndRoundBeforeStartingNext {
rounds = upperRounds.flatMap { rounds.append(contentsOf: upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren() [$0] + $0.loserRoundsAndChildren()
} })
} else { } else {
rounds = upperRounds.map { rounds.append(contentsOf: upperRounds.map {
$0 $0
} + upperRounds.flatMap { } + upperRounds.flatMap {
$0.loserRoundsAndChildren() $0.loserRoundsAndChildren()
} })
} }
let flattenedMatches = rounds.flatMap { round in let flattenedMatches = rounds.flatMap { round in

@ -221,10 +221,6 @@ final class PlayerRegistration: ModelObject, Storable {
} }
} }
func getRank() -> Int {
computedRank
}
@MainActor @MainActor
func updateRank(from sources: [CSVParser], lastRank: Int) async throws { func updateRank(from sources: [CSVParser], lastRank: Int) async throws {
if let dataFound = try await history(from: sources) { if let dataFound = try await history(from: sources) {
@ -586,4 +582,8 @@ extension PlayerRegistration: PlayerHolder {
func getProgression() -> Int { func getProgression() -> Int {
0 0
} }
func getComputedRank() -> Int? {
computedRank
}
} }

@ -208,6 +208,7 @@ final class TeamRegistration: ModelObject, Storable {
} }
func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String { func teamLabel(_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false) -> String {
if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ") return players().map { $0.playerLabel(displayStyle) }.joined(separator: twoLines ? "\n" : " & ")
} }

@ -1493,6 +1493,13 @@ defer {
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified //return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
} }
func groupStageLoserBracketAreOver() -> Bool {
guard let groupStageLoserBracket = groupStageLoserBracket() else {
return true
}
return groupStageLoserBracket.hasEnded()
}
fileprivate func _paymentMethodMessage() -> String? { fileprivate func _paymentMethodMessage() -> String? {
return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods return DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods
} }
@ -1524,13 +1531,27 @@ defer {
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) 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) typealias TournamentStatus = (label:String, completion: String)
func cashierStatus() async -> TournamentStatus { func cashierStatus() async -> TournamentStatus {
let selectedPlayers = selectedPlayers() 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 label = "\(paid.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs encaissés" let label = "\(filteredPlayers.count.formatted()) / \(selectedPlayers.count.formatted()) joueurs \(wording)\(filteredPlayers.count.pluralSuffix)"
let completion = (Double(paid.count) / Double(selectedPlayers.count)) let completion = (Double(filteredPlayers.count) / Double(selectedPlayers.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
@ -2219,7 +2240,7 @@ extension Tournament: FederalTournamentHolder {
} }
extension Tournament: TournamentBuildHolder { extension Tournament: TournamentBuildHolder {
func buildHolderTitle() -> String { func buildHolderTitle(_ displayStyle: DisplayStyle) -> String {
tournamentTitle(.short) tournamentTitle(.short)
} }

@ -278,9 +278,9 @@ class FileImportManager {
FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior FederalTournamentAge.allCases.first(where: { $0.importingRawValue.canonicalVersion == ageCategory.canonicalVersion }) ?? .senior
} }
let resultOne = Array(dataOne.dropFirst(3).dropLast()) let resultOne = Array(dataOne.dropFirst(3).dropLast(3))
let resultTwo = Array(dataTwo.dropFirst(3).dropLast()) let resultTwo = Array(dataTwo.dropFirst(3).dropLast(3))
let sexUnknown: Bool = (resultOne.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) || (resultTwo.last?.hasPrefix(FileImportManager.FFT_ASSIMILATION_WOMAN_IN_MAN) == true) 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 { var sexPlayerOne : Int {
switch tournamentCategory { switch tournamentCategory {

@ -30,7 +30,7 @@ protocol TournamentBuildHolder: Identifiable {
var category: TournamentCategory { get } var category: TournamentCategory { get }
var level: TournamentLevel { get } var level: TournamentLevel { get }
var age: FederalTournamentAge { get } var age: FederalTournamentAge { get }
func buildHolderTitle() -> String func buildHolderTitle(_ displayStyle: DisplayStyle) -> String
} }
struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
@ -43,29 +43,29 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
// var japFirstName: String? = nil // var japFirstName: String? = nil
// var japLastName: String? = nil // var japLastName: String? = nil
func buildHolderTitle() -> String { func buildHolderTitle(_ displayStyle: DisplayStyle) -> String {
computedLabel computedLabel(displayStyle)
} }
var identifier: String { var identifier: String {
level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel()
} }
var computedLabel: String { func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if age == .senior { return localizedLabel() } if age == .senior { return localizedLabel(displayStyle) }
return localizedLabel() + " " + localizedAge return localizedLabel(displayStyle) + " " + localizedAge(displayStyle)
} }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel() + category.localizedLabel(.short) level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle)
} }
var localizedTitle: String { func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLevelLabel() + " " + category.localizedLabel() level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle)
} }
var localizedAge: String { func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String {
age.tournamentDescriptionLabel age.localizedLabel(displayStyle)
} }
} }

@ -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 { func isTournamentValidForFilters(_ tournament: Tournament) -> Bool {
if tournament.isDeleted { return false } if tournament.isDeleted { return false }
let firstPart = (levels.isEmpty || levels.contains(tournament.level)) let firstPart = (levels.isEmpty || levels.contains(tournament.level))

@ -124,7 +124,7 @@ struct MenuWarningView: View {
@ViewBuilder @ViewBuilder
func _teamActionView(_ team: TeamRegistration) -> some View { func _teamActionView(_ team: TeamRegistration) -> some View {
Menu("Toute l'équipe") { Menu(team.name ?? "Toute l'équipe") {
let players = team.players() let players = team.players()
_actionView(players: players) _actionView(players: players)
} }

@ -89,6 +89,7 @@ struct CashierDetailView: View {
let showTournamentTitle: Bool let showTournamentTitle: Bool
@State private var earnings: Double? = nil @State private var earnings: Double? = nil
@State private var paidCompletion: Double? = nil @State private var paidCompletion: Double? = nil
@State private var presence: Double? = nil
var body: some View { var body: some View {
Section { Section {
@ -99,9 +100,15 @@ struct CashierDetailView: View {
ProgressView() ProgressView()
} }
} label: { } label: {
Text("Encaissement") Text(tournament.isFree() ? "Présence" : "Encaissement")
if let paidCompletion { if tournament.isFree() {
Text(paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary) 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) CashierDetailDisclosureView(tournament: tournament)
@ -119,6 +126,10 @@ struct CashierDetailView: View {
if paidCompletion == nil { if paidCompletion == nil {
paidCompletion = tournament.paidCompletion() paidCompletion = tournament.paidCompletion()
} }
if presence == nil {
presence = tournament.presenceStatus()
}
} }
} }
} }

@ -25,14 +25,12 @@ struct CashierSettingsView: View {
var body: some View { var body: some View {
List { List {
Section { Section {
RowButtonView("Tout le monde a réglé", role: .destructive) { RowButtonView("Tout le monde est arrivé", role: .destructive) {
for tournament in self.tournaments { for tournament in self.tournaments {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in players.forEach { player in
if player.hasPaid() == false { player.hasArrived = true
player.paymentType = .gift
}
} }
do { do {
try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
@ -43,29 +41,72 @@ struct CashierSettingsView: View {
} }
} footer: { } footer: {
Text("Passe tous les joueurs qui n'ont pas réglé en offert") Text("Indique tous les joueurs sont là")
} }
Section { 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() for tournament in self.tournaments {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in players.forEach { player in
player.paymentType = nil player.hasArrived = false
} }
do { do {
try store.playerRegistrations.addOrUpdate(contentOfs: players) try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} }
} }
} footer: { } 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")
}
}
} }
} }
} }

@ -57,6 +57,7 @@ class CashierViewModel: ObservableObject {
let id: UUID = UUID() let id: UUID = UUID()
@Published var sortOption: SortOption = .callDate @Published var sortOption: SortOption = .callDate
@Published var filterOption: FilterOption = .all @Published var filterOption: FilterOption = .all
@Published var presenceFilterOption: PresenceFilterOption = .all
@Published var sortOrder: SortOrder = .ascending @Published var sortOrder: SortOrder = .ascending
@Published var searchText: String = "" @Published var searchText: String = ""
@Published var isSearching: Bool = false @Published var isSearching: Bool = false
@ -69,9 +70,14 @@ class CashierViewModel: ObservableObject {
func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false { 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 { } 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 { struct CashierView: View {
@ -201,16 +238,42 @@ struct CashierView: View {
_players = .init(wrappedValue: teams.flatMap({ $0.unsortedPlayers() })) _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 { var body: some View {
List { List {
if cashierViewModel.isSearching == false { if cashierViewModel.isSearching == false {
Section { Section {
Picker(selection: $cashierViewModel.filterOption) { Picker(selection: $cashierViewModel.presenceFilterOption) {
ForEach(CashierViewModel.FilterOption.allCases) { filterOption in ForEach(CashierViewModel.PresenceFilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption) Text(filterOption.localizedLabel()).tag(filterOption)
} }
} label: { } 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) { Picker(selection: $cashierViewModel.sortOption) {
@ -239,12 +302,12 @@ struct CashierView: View {
switch cashierViewModel.sortOption { switch cashierViewModel.sortOption {
case .teamRank: case .teamRank:
TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1) TeamRankView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age:
PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1) PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
case .callDate: case .callDate:
let _teams = teams.filter({ $0.callDate != nil }) let _teams = teams.filter({ $0.callDate != nil })
TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1) TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
} }
} }
.onAppear { .onAppear {
@ -279,11 +342,12 @@ struct CashierView: View {
@EnvironmentObject var cashierViewModel: CashierViewModel @EnvironmentObject var cashierViewModel: CashierViewModel
let players: [PlayerRegistration] let players: [PlayerRegistration]
let displayTournamentTitle: Bool let displayTournamentTitle: Bool
let editingOptions: [EditablePlayerView.PlayerEditingOption]
var body: some View { var body: some View {
ForEach(players) { player in ForEach(players) { player in
Section { Section {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) EditablePlayerView(player: player, editingOptions: editingOptions)
} header: { } header: {
if displayTournamentTitle, let tournamentTitle = player.tournament()?.tournamentTitle() { if displayTournamentTitle, let tournamentTitle = player.tournament()?.tournamentTitle() {
Text(tournamentTitle) Text(tournamentTitle)
@ -301,6 +365,7 @@ struct CashierView: View {
@EnvironmentObject var cashierViewModel: CashierViewModel @EnvironmentObject var cashierViewModel: CashierViewModel
let teams: [TeamRegistration] let teams: [TeamRegistration]
let displayTournamentTitle: Bool let displayTournamentTitle: Bool
let editingOptions: [EditablePlayerView.PlayerEditingOption]
var body: some View { var body: some View {
ForEach(teams) { team in ForEach(teams) { team in
@ -308,11 +373,17 @@ struct CashierView: View {
if players.isEmpty == false { if players.isEmpty == false {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) EditablePlayerView(player: player, editingOptions: editingOptions)
} }
} header: { } header: {
if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { HStack {
Text(tournamentTitle) if let name = team.name {
Text(name)
}
if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Spacer()
Text(tournamentTitle)
}
} }
} footer: { } footer: {
if let callDate = team.callDate { if let callDate = team.callDate {
@ -329,6 +400,7 @@ struct CashierView: View {
@EnvironmentObject var cashierViewModel: CashierViewModel @EnvironmentObject var cashierViewModel: CashierViewModel
let teams: [TeamRegistration] let teams: [TeamRegistration]
let displayTournamentTitle: Bool let displayTournamentTitle: Bool
let editingOptions: [EditablePlayerView.PlayerEditingOption]
var body: some View { var body: some View {
let groupedTeams = Dictionary(grouping: teams) { team in let groupedTeams = Dictionary(grouping: teams) { team in
@ -343,10 +415,15 @@ struct CashierView: View {
if players.isEmpty == false { if players.isEmpty == false {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) EditablePlayerView(player: player, editingOptions: editingOptions)
} }
} header: { } header: {
if let name = team.name {
Text(name)
}
if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Spacer()
Text(tournamentTitle) Text(tournamentTitle)
} }
} footer: { } footer: {

@ -385,7 +385,7 @@ struct ClubSearchView: View {
LabeledContent { LabeledContent {
Text(club.distance(from: locationManager.location)) Text(club.distance(from: locationManager.location))
} label: { } label: {
Text(club.nom) Text(club.nom).lineLimit(1)
Text(club.ville).font(.caption) Text(club.ville).font(.caption)
} }
} }

@ -11,13 +11,15 @@ fileprivate let defaultConfirmationMessage = "Êtes-vous sûr de vouloir faire c
struct FooterButtonView: View { struct FooterButtonView: View {
var role: ButtonRole? = nil var role: ButtonRole? = nil
var systemImage: String? = nil
let title: String let title: String
let confirmationMessage: String let confirmationMessage: String
let action: () -> () let action: () -> ()
@State private var askConfirmation: Bool = false @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.title = title
self.systemImage = systemImage
self.action = action self.action = action
self.role = role self.role = role
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
@ -31,8 +33,16 @@ struct FooterButtonView: View {
action() action()
} }
} label: { } label: {
Text(title) if let systemImage {
.underline() HStack {
Text(title)
.underline()
Image(systemName: systemImage).font(.caption)
}
} else {
Text(title)
.underline()
}
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
.confirmationDialog("Confirmation", .confirmationDialog("Confirmation",

@ -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 { var body: some View {
List { List {
Section { Section {
if let name = team.name {
Text(name).foregroundStyle(.secondary)
}
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) EditablePlayerView(player: player, editingOptions: _editingOptions())
.environmentObject(tournament.tournamentStore)
} }
} }

@ -125,15 +125,16 @@ struct GroupStageView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let teamName = team.name { if let teamName = team.name {
Text(teamName).foregroundStyle(.secondary) Text(teamName).font(.title3)
} } else {
ForEach(team.players()) { player in ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1) Text(player.playerLabel()).lineLimit(1)
.overlay { .overlay {
if player.hasArrived && team.isHere() == false { if player.hasArrived && team.isHere() == false {
Color.green.opacity(0.6) Color.green.opacity(0.6)
}
} }
} }
} }
} }
Spacer() Spacer()

@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View {
Section { Section {
Picker(selection: $selectedPlayer) { Picker(selection: $selectedPlayer) {
HStack { HStack {
Text("Toute l'équipe") Text(team.name ?? "Toute l'équipe")
Spacer() Spacer()
Text(team.weight.formatted()).bold() Text(team.weight.formatted()).bold()
} }

@ -93,7 +93,7 @@ struct MatchDateView: View {
.foregroundStyle(Color.master) .foregroundStyle(Color.master)
.underline() .underline()
} else { } else {
Text("en attente") Text("démarrer")
.foregroundStyle(Color.master) .foregroundStyle(Color.master)
.underline() .underline()
} }

@ -32,13 +32,26 @@ struct MatchTeamDetailView: View {
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View {
Section { Section {
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment]) EditablePlayerView(player: player, editingOptions: _editingOptions())
} }
} header: { } header: {
TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) 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 { //#Preview {

@ -57,19 +57,21 @@ struct PlayerBlockView: View {
} }
if let name = team?.name { if let name = team?.name {
Text(name).foregroundStyle(.secondary) Text(name).font(.title3)
} } else {
ForEach(names, id: \.self) { name in ForEach(names, id: \.self) { name in
Text(name).lineLimit(1) Text(name).lineLimit(1)
}
} }
} else { } else {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
VStack { VStack {
if let name = team?.name { 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) .opacity(0)
Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1)

@ -107,30 +107,31 @@ struct MatchDetailView: View {
} }
} }
let players = self.match.teams().flatMap { $0.players() } if self.match.currentTournament()?.isFree() == false {
let unpaid = players.filter({ $0.hasPaid() == false }) let players = self.match.teams().flatMap { $0.players() }
let unpaid = players.filter({ $0.hasPaid() == false })
if unpaid.isEmpty == false {
Section { if unpaid.isEmpty == false {
DisclosureGroup { Section {
ForEach(unpaid) { player in DisclosureGroup {
ForEach(unpaid) { player in
LabeledContent {
PlayerPayView(player: player)
.environmentObject(tournamentStore)
} label: {
Text(player.playerLabel())
}
}
} label: {
LabeledContent { LabeledContent {
PlayerPayView(player: player) Text(unpaid.count.formatted() + " / " + players.count.formatted())
.environmentObject(tournamentStore)
} label: { } label: {
Text(player.playerLabel()) Text("Encaissement manquant")
} }
} }
} label: {
LabeledContent {
Text(unpaid.count.formatted() + " / " + players.count.formatted())
} label: {
Text("Encaissement manquant")
}
} }
} }
} }
menuView menuView
} }
.sheet(isPresented: $showDetails) { .sheet(isPresented: $showDetails) {
@ -423,9 +424,9 @@ struct MatchDetailView: View {
let rotationDuration = match.getDuration() let rotationDuration = match.getDuration()
Picker(selection: $startDateSetup) { Picker(selection: $startDateSetup) {
if match.isReady() { if match.isReady() {
Text("Tout de suite").tag(MatchDateSetup.now)
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) 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("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration))
Text("Prochaine 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 parmi les libres").tag(MatchFieldSetup.random)
Text("Au hasard").tag(MatchFieldSetup.fullRandom) Text("Au hasard").tag(MatchFieldSetup.fullRandom)
//Text("Premier disponible").tag(MatchFieldSetup.firstAvailable) //Text("Premier disponible").tag(MatchFieldSetup.firstAvailable)
if let club = match.currentTournament()?.club() { if let tournament = match.currentTournament() {
ForEach(0..<club.courtCount, id: \.self) { courtIndex in
Text(club.courtName(atIndex: courtIndex)) .tag(MatchFieldSetup.field(courtIndex))
}
} else if let tournament = match.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Text(tournament.courtName(atIndex: courtIndex)) .tag(MatchFieldSetup.field(courtIndex)) Text(tournament.courtName(atIndex: courtIndex)) .tag(MatchFieldSetup.field(courtIndex))
} }

@ -57,7 +57,7 @@ struct MatchSummaryView: View {
} }
} }
Spacer() Spacer()
if let courtName, matchViewStyle != .feedStyle { if let courtName {
Spacer() Spacer()
Text(courtName) Text(courtName)
.foregroundStyle(.gray) .foregroundStyle(.gray)

@ -93,11 +93,11 @@ struct CalendarView: View {
if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) { if federalDataViewModel.isFederalTournamentValidForFilters(tournament, build: build) {
if navigation.agendaDestination == .around { if navigation.agendaDestination == .around {
NavigationLink(build.buildHolderTitle()) { NavigationLink(build.buildHolderTitle(.wide)) {
TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user) TournamentSubscriptionView(federalTournament: tournament, build: build, user: dataStore.user)
} }
} else { } else {
Button(build.buildHolderTitle()) { Button(build.buildHolderTitle(.wide)) {
_createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build) _createOrShow(federalTournament: tournament, existingTournament: event(forTournament: tournament)?.existingBuild(build), build: build)
} }
} }
@ -144,7 +144,9 @@ struct CalendarView: View {
let filteredTournaments = tournaments let filteredTournaments = tournaments
let mappedItems = filteredTournaments.flatMap { tournamentHolder in let mappedItems = filteredTournaments.flatMap { tournamentHolder in
(0..<tournamentHolder.dayDuration).map({ dayDuration in (0..<tournamentHolder.dayDuration).map({ dayDuration in
(tournamentHolder.startDate.dayInt + dayDuration, tournamentHolder.tournaments.count) (tournamentHolder.startDate.dayInt + dayDuration, tournamentHolder.tournaments.filter({ build in
federalDataViewModel.buildIsValid(build)
}).count)
}) })
} }
counts = Dictionary(mappedItems, uniquingKeysWith: +) counts = Dictionary(mappedItems, uniquingKeysWith: +)

@ -33,7 +33,7 @@ struct EventListView: View {
HStack { HStack {
Text(section.monthYearFormatted) Text(section.monthYearFormatted)
Spacer() Spacer()
let count = _tournaments.map { $0.tournaments.count }.reduce(0,+) let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }
@ -52,7 +52,7 @@ struct EventListView: View {
HStack { HStack {
Text(section.monthYearFormatted) Text(section.monthYearFormatted)
Spacer() Spacer()
let count = _tournaments.map { $0.tournaments.count }.reduce(0,+) let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
Text("\(count.formatted()) tournoi" + count.pluralSuffix) Text("\(count.formatted()) tournoi" + count.pluralSuffix)
} }
} }

@ -96,7 +96,7 @@ struct TournamentSubscriptionView: View {
Text(federalTournament.clubLabel()) Text(federalTournament.clubLabel())
} }
LabeledContent("Épreuve") { LabeledContent("Épreuve") {
Text(build.buildHolderTitle()) Text(build.buildHolderTitle(.wide))
} }
LabeledContent("JAP") { LabeledContent("JAP") {
@ -292,24 +292,24 @@ struct TournamentSubscriptionView: View {
var messageBody: String { var messageBody: String {
let bonjourOuBonsoir = Date().timeOfDay.hello let bonjourOuBonsoir = Date().timeOfDay.hello
let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye
let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\n\(URLs.appStore.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n" let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(.wide), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\n\(URLs.appStore.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
var messageBodyShort: String { var messageBodyShort: String {
let bonjourOuBonsoir = Date().timeOfDay.hello let bonjourOuBonsoir = Date().timeOfDay.hello
let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye
let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\n\(URLs.appStore.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n" let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(.wide), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\n\(URLs.appStore.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
var noteCalendar: String { var noteCalendar: String {
let body = [[build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, federalTournament.calendarNoteMessage()].compactMap { $0 }.joined(separator: "\n") + "\n" let body = [[build.buildHolderTitle(.wide), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, federalTournament.calendarNoteMessage()].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
var messageSubject: String { var messageSubject: String {
let subject = [build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " ") let subject = [build.buildHolderTitle(.wide), federalTournament.clubLabel()].compacted().joined(separator: " ")
return subject return subject
} }

@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
import Zip
struct ToolboxView: View { struct ToolboxView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -210,14 +211,38 @@ struct ToolboxView: View {
} }
.navigationTitle(TabDestination.toolbox.title) .navigationTitle(TabDestination.toolbox.title)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarLeading) {
Link(destination: URLs.appStore.url) { Link(destination: URLs.appStore.url) {
Text("v\(PadelClubApp.appVersion)") Text("v\(PadelClubApp.appVersion)")
} }
} }
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
if let zip = _getZip() {
ShareLink(item: zip) {
Label("Mes données", systemImage: "server.rack")
}
}
} label: {
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
}
}
} }
} }
} }
private func _getZip() -> URL? {
do {
let filePath = try Club.storageDirectoryPath()
return try Zip.quickZipFiles([filePath], fileName: "backup") // Zip
} catch {
Logger.error(error)
return nil
}
}
} }
//#Preview { //#Preview {

@ -40,6 +40,7 @@ struct PlanningByCourtView: View {
var body: some View { var body: some View {
List { List {
_byCourtView() _byCourtView()
.id(selectedCourt)
} }
.overlay { .overlay {
if matches.allSatisfy({ $0.startDate == nil }) { if matches.allSatisfy({ $0.startDate == nil }) {

@ -240,9 +240,9 @@ struct PlanningSettingsView: View {
let value = tournament.getGroupStageChunkValue() let value = tournament.getGroupStageChunkValue()
if parallelType == false { if parallelType == false {
if value > 1 { if value > 1 {
Text("\(value.formatted()) poules commenceront en parallèle") Text("\(value.formatted()) poules en parallèle")
} else { } else {
Text("une poule sera jouer à la fois") Text("une poule sera jouée à la fois")
} }
} }
} }

@ -16,6 +16,23 @@ struct PlanningView: View {
@State private var timeSlots: [Date:[Match]] @State private var timeSlots: [Date:[Match]]
@State private var days: [Date] @State private var days: [Date]
@State private var keys: [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<ScheduleDestination?>) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.matches = matches self.matches = matches
@ -30,6 +47,24 @@ struct PlanningView: View {
List { List {
_bySlotView() _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 { .overlay {
if matches.allSatisfy({ $0.startDate == nil }) { if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView { ContentUnavailableView {
@ -53,7 +88,7 @@ struct PlanningView: View {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] { if let _matches = timeSlots[key] {
DisclosureGroup { DisclosureGroup {
ForEach(_matches) { match in ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in
NavigationLink { NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: { } label: {
@ -98,7 +133,14 @@ struct PlanningView: View {
Text(self._formattedMatchCount(matches.count)) Text(self._formattedMatchCount(matches.count))
} label: { } label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) 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: ", "))
} }
} }

@ -14,6 +14,7 @@ struct EditablePlayerView: View {
case payment case payment
case licenceId case licenceId
case name case name
case presence
} }
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -77,6 +78,13 @@ struct EditablePlayerView: View {
Logger.error(error) Logger.error(error)
} }
} }
.onChange(of: player.hasArrived) {
do {
try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player)
} catch {
Logger.error(error)
}
}
} }
@ViewBuilder @ViewBuilder
@ -91,11 +99,6 @@ struct EditablePlayerView: View {
Menu { Menu {
Button { Button {
player.hasArrived.toggle() player.hasArrived.toggle()
do {
try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player)
} catch {
Logger.error(error)
}
} label: { } label: {
Label("Présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle") Label("Présent", systemImage: player.hasArrived ? "checkmark.circle" : "circle")
} }
@ -172,6 +175,11 @@ struct EditablePlayerView: View {
if editingOptions.contains(.payment) { if editingOptions.contains(.payment) {
Spacer() Spacer()
PlayerPayView(player: player) 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()
}
} }
} }
} }

@ -31,7 +31,7 @@ struct PlayerPopoverView: View {
@State private var source: String? @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 { if let source {
let words = source.components(separatedBy: .whitespaces) let words = source.components(separatedBy: .whitespaces)
if words.isEmpty == false { if words.isEmpty == false {

@ -259,7 +259,7 @@ struct RoundView: View {
#if DEBUG #if DEBUG
Spacer() Spacer()
Text(match.teamScores.count.formatted()) Text(match.index.formatted() + " " + match.teamScores.count.formatted())
#endif #endif
} }
} footer: { } footer: {

@ -12,6 +12,9 @@ struct ImportedPlayerView: View {
var index: Int? = nil var index: Int? = nil
var showFemaleInMaleAssimilation: Bool = false var showFemaleInMaleAssimilation: Bool = false
var showProgression: Bool = false var showProgression: Bool = false
var isAnimation: Bool {
player.getComputedRank() == 0
}
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -39,73 +42,75 @@ struct ImportedPlayerView: View {
} }
.font(.title3) .font(.title3)
.lineLimit(1) .lineLimit(1)
HStack { if isAnimation == false {
HStack(alignment: .top, spacing: 0) { HStack {
Text(player.formattedRank()).italic(player.isAssimilated) HStack(alignment: .top, spacing: 0) {
.font(.title3) Text(player.formattedRank()).italic(player.isAssimilated)
.background { .font(.title3)
if player.isNotFromCurrentDate() { .background {
UnderlineView() if player.isNotFromCurrentDate() {
UnderlineView()
}
} }
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
} }
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
} }
}
if showProgression, player.getProgression() != 0 { if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) { HStack(alignment: .top, spacing: 2) {
Text("(") Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always()))) Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression())) .foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")") Text(")")
}.font(.title3) }.font(.title3)
} }
if let pts = player.getPoints(), pts > 0 { if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) { HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3) Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption) Text(" pts").font(.caption)
}
} }
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 { if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) { HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3) Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption) Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
} }
} }
} .lineLimit(1)
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() { if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) { HStack(alignment: .top, spacing: 2) {
Text("(") Text("(")
Text(assimilatedAsMaleRank.formatted()) Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("équivalence") Text("équivalence")
Text("messieurs") Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
} }
.font(.caption)
Text(")").font(.title3)
} }
}
HStack { HStack {
Text(player.formattedLicense()) Text(player.formattedLicense())
if let computedAge = player.computedAge { if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans") Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
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)
} }
} }
} }

@ -17,10 +17,6 @@ struct TeamRowView: View {
TeamWeightView(team: team, teamPosition: teamPosition) TeamWeightView(team: team, teamPosition: teamPosition)
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let name = team.name {
Text(name).foregroundStyle(.secondary)
}
if let groupStage = team.groupStageObject() { if let groupStage = team.groupStageObject() {
HStack { HStack {
Text(groupStage.groupStageTitle()) Text(groupStage.groupStageTitle())
@ -32,13 +28,20 @@ struct TeamRowView: View {
Text(round.roundTitle(.wide)) Text(round.roundTitle(.wide))
} }
if team.players().isEmpty == false { if let name = team.name {
ForEach(team.players()) { player in Text(name).font(.title3)
Text(player.playerLabel()) if team.players().isEmpty {
Text("Aucun joueur")
} }
} else { } else {
Text("Place réservée") if team.players().isEmpty == false {
Text("Place réservée") ForEach(team.players()) { player in
Text(player.playerLabel())
}
} else {
Text("Place réservée")
Text("Place réservée")
}
} }
} }
if displayCallDate { if displayCallDate {

@ -54,9 +54,15 @@ enum CashierDestination: Identifiable, Selectable, Equatable {
case .summary: case .summary:
return nil return nil
case .groupStage(let groupStage): 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 return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count
case .bracket(let round): case .bracket(let round):
let playerRegistrations: [PlayerRegistration] = round.seeds().flatMap { $0.unsortedPlayers() } 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 return playerRegistrations.filter({ $0.hasPaid() == false }).count
case .all(_): case .all(_):
return nil return nil
@ -156,7 +162,7 @@ struct TournamentCashierView: View {
.environmentObject(cashierViewModel) .environmentObject(cashierViewModel)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Encaissement") .navigationTitle(tournament.isFree() ? "Présence" : "Encaissement")
} }
} }

@ -11,6 +11,7 @@ import LeStorage
struct TournamentCellView: View { struct TournamentCellView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation @Environment(NavigationViewModel.self) private var navigation
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
let tournament: FederalTournamentHolder let tournament: FederalTournamentHolder
// let color: Color = .black // let color: Color = .black
@ -23,11 +24,17 @@ struct TournamentCellView: View {
var body: some View { var body: some View {
ForEach(tournament.tournaments, id: \.id) { build in ForEach(tournament.tournaments, id: \.id) { build in
if navigation.agendaDestination == .around, let federalTournament = tournament as? FederalTournament { if let federalTournament = tournament as? FederalTournament {
NavigationLink { if federalDataViewModel.isFederalTournamentValidForFilters(federalTournament, build: build) {
TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user) if navigation.agendaDestination == .around {
} label: { NavigationLink {
_buildView(build, existingTournament: event?.existingBuild(build)) TournamentSubscriptionView(federalTournament: federalTournament, build: build, user: dataStore.user)
} label: {
_buildView(build, existingTournament: event?.existingBuild(build))
}
} else {
_buildView(build, existingTournament: event?.existingBuild(build))
}
} }
} else { } else {
_buildView(build, existingTournament: event?.existingBuild(build)) _buildView(build, existingTournament: event?.existingBuild(build))

@ -191,7 +191,7 @@ struct TournamentBuildView: View {
ProgressView() ProgressView()
} }
} label: { } label: {
Text("Encaissement") Text(tournament.isFree() ? "Présence" : "Encaissement")
if let tournamentStatus { if let tournamentStatus {
Text(tournamentStatus.label).lineLimit(1) Text(tournamentStatus.label).lineLimit(1)
} else { } else {

Loading…
Cancel
Save