Merge branch 'retrieve'

sync2
Raz 1 year ago
commit 0161203e84
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 66
      PadelClub/Data/AppSettings.swift
  3. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  4. 17
      PadelClub/Data/Federal/FederalTournament.swift
  5. 4
      PadelClub/Data/PlayerRegistration.swift
  6. 72
      PadelClub/Data/Round.swift
  7. 13
      PadelClub/Data/Tournament.swift
  8. 41
      PadelClub/Data/User.swift
  9. 3
      PadelClub/Utils/ContactManager.swift
  10. 4
      PadelClub/Views/Club/ClubSearchView.swift
  11. 2
      PadelClub/Views/Club/ClubsView.swift
  12. 2
      PadelClub/Views/Components/Labels.swift
  13. 1
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  14. 3
      PadelClub/Views/Match/MatchSetupView.swift
  15. 6
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  16. 102
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  17. 130
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  18. 16
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  19. 104
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  20. 1
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  21. 2
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  22. 13
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  23. 4
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  24. 1
      PadelClub/Views/Planning/PlanningByCourtView.swift
  25. 1
      PadelClub/Views/Planning/PlanningView.swift
  26. 10
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  27. 2
      PadelClub/Views/Player/PlayerDetailView.swift
  28. 27
      PadelClub/Views/Round/LoserRoundSettingsView.swift
  29. 591
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  30. 4
      PadelClub/Views/Team/EditingTeamView.swift
  31. 10
      PadelClub/Views/Team/TeamPickerView.swift
  32. 16
      PadelClub/Views/Tournament/FileImportView.swift
  33. 136
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  34. 47
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  35. 9
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  36. 2
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  37. 4
      PadelClub/Views/Tournament/TournamentInitView.swift

@ -3126,7 +3126,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 = 5; CURRENT_PROJECT_VERSION = 1;
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\"";
@ -3136,6 +3136,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3148,13 +3149,14 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -3168,7 +3170,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 = 5; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3177,6 +3179,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3189,13 +3192,14 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };

@ -16,8 +16,48 @@ final class AppSettings: MicroStorable {
var didCreateAccount: Bool = false var didCreateAccount: Bool = false
var didRegisterAccount: Bool = false var didRegisterAccount: Bool = false
//search tournament stuff
var tournamentCategories: Set<TournamentCategory.ID>
var tournamentLevels: Set<TournamentLevel.ID>
var tournamentAges: Set<FederalTournamentAge.ID>
var tournamentTypes: Set<FederalTournamentType.ID>
var startDate: Date
var endDate: Date
var city: String
var distance: Double
var sortingOption: String
var nationalCup: Bool
var dayDuration: Int?
var dayPeriod: DayPeriod
func resetSearch() {
tournamentAges = Set()
tournamentTypes = Set()
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
nationalCup = false
dayDuration = nil
dayPeriod = .all
}
required init() { required init() {
tournamentAges = Set()
tournamentTypes = Set()
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
nationalCup = false
dayDuration = nil
dayPeriod = .all
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -25,11 +65,35 @@ final class AppSettings: MicroStorable {
lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource) lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource)
didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false
didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false
tournamentCategories = try container.decodeIfPresent(Set<TournamentCategory.ID>.self, forKey: ._tournamentCategories) ?? Set()
tournamentLevels = try container.decodeIfPresent(Set<TournamentLevel.ID>.self, forKey: ._tournamentLevels) ?? Set()
tournamentAges = try container.decodeIfPresent(Set<FederalTournamentAge.ID>.self, forKey: ._tournamentAges) ?? Set()
tournamentTypes = try container.decodeIfPresent(Set<FederalTournamentType.ID>.self, forKey: ._tournamentTypes) ?? Set()
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date()
endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Calendar.current.date(byAdding: .month, value: 3, to: Date())!
city = try container.decodeIfPresent(String.self, forKey: ._city) ?? ""
distance = try container.decodeIfPresent(Double.self, forKey: ._distance) ?? 30
sortingOption = try container.decodeIfPresent(String.self, forKey: ._sortingOption) ?? "dateDebut+asc"
nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false
dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration)
dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case _lastDataSource = "lastDataSource" case _lastDataSource = "lastDataSource"
case _didCreateAccount = "didCreateAccount" case _didCreateAccount = "didCreateAccount"
case _didRegisterAccount = "didRegisterAccount" case _didRegisterAccount = "didRegisterAccount"
case _tournamentCategories = "tournamentCategories"
case _tournamentLevels = "tournamentLevels"
case _tournamentAges = "tournamentAges"
case _tournamentTypes = "tournamentTypes"
case _startDate = "startDate"
case _endDate = "endDate"
case _city = "city"
case _distance = "distance"
case _sortingOption = "sortingOption"
case _nationalCup = "nationalCup"
case _dayDuration = "dayDuration"
case _dayPeriod = "dayPeriod"
} }
} }

@ -70,6 +70,10 @@ extension ImportedPlayer: PlayerHolder {
} }
} }
func contains(_ searchField: String) -> Bool {
firstName?.localizedCaseInsensitiveContains(searchField) == true || lastName?.localizedCaseInsensitiveContains(searchField) == true
}
func hitForSearch(_ searchText: String) -> Int { func hitForSearch(_ searchText: String) -> Int {
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")

@ -7,8 +7,8 @@ import Foundation
import CoreLocation import CoreLocation
import LeStorage import LeStorage
enum DayPeriod: CaseIterable, Identifiable { enum DayPeriod: Int, CaseIterable, Identifiable, Codable {
var id: Self { self } var id: Int { self.rawValue }
case all case all
case weekend case weekend
@ -151,6 +151,19 @@ struct FederalTournament: Identifiable, Codable {
[libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n" [libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n"
} }
var sharePartnerMessage: String {
["Je nous ai inscris au tournoi suivant : ",
libelle,
dateDebut?.formatted(date: .complete, time: .omitted),
"message preparé par Padel Club",
URLs.appStore.rawValue
].compactMap({$0}).joined(separator: "\n") + "\n"
}
func calendarNoteMessage() -> String {
[jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: "\n")
}
var japMessage: String { var japMessage: String {
[nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";") [nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";")
} }

@ -160,8 +160,8 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func isSameAs(_ player: PlayerRegistration) -> Bool { func isSameAs(_ player: PlayerRegistration) -> Bool {
firstName.localizedCaseInsensitiveCompare(player.firstName) == .orderedSame && firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame &&
lastName.localizedCaseInsensitiveCompare(player.lastName) == .orderedSame lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame
} }
func tournament() -> Tournament? { func tournament() -> Tournament? {

@ -23,14 +23,16 @@ final class Round: ModelObject, Storable {
private(set) var format: MatchFormat? private(set) var format: MatchFormat?
var startDate: Date? var startDate: Date?
var groupStageLoserBracket: Bool = false var groupStageLoserBracket: Bool = false
var loserBracketMode: LoserBracketMode = .automatic
internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false) {
internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) {
self.tournament = tournament self.tournament = tournament
self.index = index self.index = index
self.parent = parent self.parent = parent
self.format = matchFormat self.format = matchFormat
self.startDate = startDate self.startDate = startDate
self.groupStageLoserBracket = groupStageLoserBracket self.groupStageLoserBracket = groupStageLoserBracket
self.loserBracketMode = loserBracketMode
} }
// MARK: - Computed dependencies // MARK: - Computed dependencies
@ -153,7 +155,12 @@ final class Round: ModelObject, Storable {
let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } let teamIds: [String] = self._matches().compactMap { $0.losingTeamId }
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
} }
func winners() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.winningTeamId }
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
}
func teams() -> [TeamRegistration] { func teams() -> [TeamRegistration] {
return playedMatches().flatMap({ $0.teams() }) return playedMatches().flatMap({ $0.teams() })
} }
@ -186,13 +193,13 @@ defer {
case .two: case .two:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
return luckyLoser.team return luckyLoser.team
} else if groupStageLoserBracket == false, let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) {
if let teamId = previousMatch.winningTeamId { if let teamId = previousMatch.winningTeamId {
return self.tournamentStore.teamRegistrations.findById(teamId) return self.tournamentStore.teamRegistrations.findById(teamId)
} else if previousMatch.disabled { } else if previousMatch.disabled {
return previousMatch.teams().first return previousMatch.teams().first
} }
} else if groupStageLoserBracket == false, let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId {
return tournamentStore.findById(parent) return tournamentStore.findById(parent)
} }
} }
@ -208,8 +215,13 @@ defer {
print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let parentRound = parentRound
if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { if isLoserBracket(), previousRound == nil, let upperBracketTopMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2) {
return upperBracketTopMatch return upperBracketTopMatch
} }
return nil return nil
@ -224,8 +236,13 @@ defer {
} }
#endif #endif
let parentRound = parentRound
if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { if isLoserBracket(), previousRound == nil, let upperBracketBottomMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
return upperBracketBottomMatch return upperBracketBottomMatch
} }
return nil return nil
@ -420,6 +437,10 @@ defer {
return _matches().filter({ $0.disabled }) return _matches().filter({ $0.disabled })
} }
func allLoserRoundMatches() -> [Match] {
loserRoundsAndChildren().flatMap({ $0._matches() })
}
var theoryCumulativeMatchCount: Int { var theoryCumulativeMatchCount: Int {
var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index) var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index)
if let parentRound { if let parentRound {
@ -576,7 +597,11 @@ defer {
func deleteLoserBracket() { func deleteLoserBracket() {
do { do {
try self.tournamentStore.rounds.delete(contentOfs: loserRounds()) let loserRounds = loserRounds()
for loserRound in loserRounds {
try loserRound.deleteDependencies()
}
try self.tournamentStore.rounds.delete(contentOfs: loserRounds)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -681,6 +706,7 @@ defer {
case _format = "format" case _format = "format"
case _startDate = "startDate" case _startDate = "startDate"
case _groupStageLoserBracket = "groupStageLoserBracket" case _groupStageLoserBracket = "groupStageLoserBracket"
case _loserBracketMode = "loserBracketMode"
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -692,6 +718,7 @@ defer {
format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false
loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -701,7 +728,8 @@ defer {
try container.encode(tournament, forKey: ._tournament) try container.encode(tournament, forKey: ._tournament)
try container.encode(index, forKey: ._index) try container.encode(index, forKey: ._index)
try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket) try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket)
try container.encode(loserBracketMode, forKey: ._loserBracketMode)
if let parent = parent { if let parent = parent {
try container.encode(parent, forKey: ._parent) try container.encode(parent, forKey: ._parent)
} else { } else {
@ -776,3 +804,29 @@ extension Round: Selectable, Equatable {
return hasEnded() ? .checkmark : nil return hasEnded() ? .checkmark : nil
} }
} }
enum LoserBracketMode: Int, CaseIterable, Identifiable, Codable {
var id: Int { self.rawValue }
case automatic
case manual
func localizedLoserBracketMode() -> String {
switch self {
case .automatic:
"Automatique"
case .manual:
"Manuelle"
}
}
func localizedLoserBracketModeDescription() -> String {
switch self {
case .automatic:
"Les perdants du tableau principal sont placés à leur place prévue."
case .manual:
"Aucun placement automatique n'est fait. Vous devez choisir les perdants qui se jouent."
}
}
}

@ -57,7 +57,8 @@ final class Tournament : ModelObject, Storable {
var publishTournament: Bool = false var publishTournament: Bool = false
var hidePointsEarned: Bool = false var hidePointsEarned: Bool = false
var publishRankings: Bool = false var publishRankings: Bool = false
var loserBracketMode: LoserBracketMode = .automatic
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -105,9 +106,10 @@ final class Tournament : ModelObject, Storable {
case _publishTournament = "publishTournament" case _publishTournament = "publishTournament"
case _hidePointsEarned = "hidePointsEarned" case _hidePointsEarned = "hidePointsEarned"
case _publishRankings = "publishRankings" case _publishRankings = "publishRankings"
case _loserBracketMode = "loserBracketMode"
} }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) { internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) {
self.event = event self.event = event
self.name = name self.name = name
self.startDate = startDate self.startDate = startDate
@ -145,6 +147,7 @@ final class Tournament : ModelObject, Storable {
self.publishTournament = publishTournament self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -189,6 +192,7 @@ final class Tournament : ModelObject, Storable {
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
} }
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -1633,7 +1637,7 @@ defer {
let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount()) let roundCount = RoundRule.numberOfRounds(forTeams: bracketTeamCount())
let rounds = (0..<roundCount).map { //index 0 is the final let rounds = (0..<roundCount).map { //index 0 is the final
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0)) return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
} }
do { do {
@ -2181,7 +2185,7 @@ extension Tournament {
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//creator: DataStore.shared.user?.id //creator: DataStore.shared.user?.id
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge) return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode)
} }
static func fake() -> Tournament { static func fake() -> Tournament {
@ -2189,3 +2193,4 @@ extension Tournament {
} }
} }

@ -43,16 +43,18 @@ class User: ModelObject, UserBase, Storable {
var bracketMatchFormatPreference: MatchFormat? var bracketMatchFormatPreference: MatchFormat?
var groupStageMatchFormatPreference: MatchFormat? var groupStageMatchFormatPreference: MatchFormat?
var loserBracketMatchFormatPreference: MatchFormat? var loserBracketMatchFormatPreference: MatchFormat?
var loserBracketMode: LoserBracketMode = .automatic
var deviceId: String? var deviceId: String?
init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?) { init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) {
self.username = username self.username = username
self.firstName = firstName self.firstName = firstName
self.lastName = lastName self.lastName = lastName
self.email = email self.email = email
self.phone = phone self.phone = phone
self.country = country self.country = country
self.loserBracketMode = loserBracketMode
} }
public func uuid() throws -> UUID { public func uuid() throws -> UUID {
@ -139,8 +141,42 @@ class User: ModelObject, UserBase, Storable {
case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference"
case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
case _deviceId = "deviceId" case _deviceId = "deviceId"
case _loserBracketMode = "loserBracketMode"
} }
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Required properties
id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId()
username = try container.decode(String.self, forKey: ._username)
email = try container.decode(String.self, forKey: ._email)
firstName = try container.decode(String.self, forKey: ._firstName)
lastName = try container.decode(String.self, forKey: ._lastName)
// Optional properties
clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? []
umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode)
licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId)
phone = try container.decodeIfPresent(String.self, forKey: ._phone)
country = try container.decodeIfPresent(String.self, forKey: ._country)
// Summons-related properties
summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody)
summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature)
summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods)
summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false
summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false
summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false
// Match-related properties
matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration)
bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference)
groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference)
loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference)
loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
}
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
@ -228,6 +264,7 @@ class User: ModelObject, UserBase, Storable {
try container.encodeNil(forKey: ._deviceId) try container.encodeNil(forKey: ._deviceId)
} }
try container.encode(loserBracketMode, forKey: ._loserBracketMode)
} }
static func placeHolder() -> User { static func placeHolder() -> User {

@ -15,6 +15,9 @@ enum ContactManagerError: LocalizedError {
case mailNotSent //no network no error case mailNotSent //no network no error
case messageFailed case messageFailed
case messageNotSent //no network no error case messageNotSent //no network no error
case calendarAccessDenied
case calendarEventSaveFailed
case noCalendarAvailable
} }
enum ContactType: Identifiable { enum ContactType: Identifiable {

@ -140,7 +140,6 @@ struct ClubSearchView: View {
RowButtonView("D'accord") { RowButtonView("D'accord") {
locationManager.lastError = nil locationManager.lastError = nil
} }
.padding(.horizontal)
} }
} else if clubMarkers.isEmpty == false && searching == false && _filteredClubs().isEmpty { } else if clubMarkers.isEmpty == false && searching == false && _filteredClubs().isEmpty {
ContentUnavailableView.search(text: searchedCity) ContentUnavailableView.search(text: searchedCity)
@ -172,7 +171,6 @@ struct ClubSearchView: View {
locationManager.requestLocation() locationManager.requestLocation()
} }
} }
.padding(.horizontal)
} }
if error != nil { if error != nil {
@ -184,13 +182,11 @@ struct ClubSearchView: View {
RowButtonView("Chercher une ville ou un code postal") { RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true searchPresented = true
} }
.padding(.horizontal)
if searchAttempted { if searchAttempted {
RowButtonView("Créer un club manuellement") { RowButtonView("Créer un club manuellement") {
newClub = club ?? Club.newEmptyInstance() newClub = club ?? Club.newEmptyInstance()
} }
.padding(.horizontal)
} }
} }
} }

@ -159,11 +159,9 @@ struct ClubsView: View {
RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") { RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") {
newClub = Club.newEmptyInstance() newClub = Club.newEmptyInstance()
} }
.padding(.horizontal)
RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") { RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") {
presentClubSearchView = true presentClubSearchView = true
} }
.padding(.horizontal)
} }
} }

@ -21,7 +21,7 @@ struct LabelStructure: View {
struct LabelSettings: View { struct LabelSettings: View {
var body: some View { var body: some View {
Label("Réglages", systemImage: "slider.horizontal.3").labelStyle(.titleOnly) Label("Réglages du tournoi", systemImage: "slider.horizontal.3").labelStyle(.titleOnly)
} }
} }

@ -81,7 +81,6 @@ struct LoserBracketFromGroupStageView: View {
isEditingLoserBracketGroupStage = true isEditingLoserBracketGroupStage = true
_addNewMatch() _addNewMatch()
} }
.padding(.horizontal)
} }
} }
} }

@ -63,7 +63,7 @@ struct MatchSetupView: View {
} }
HStack { HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition) match.setLuckyLoser(team: team, teamPosition: teamPosition)
@ -86,6 +86,7 @@ struct MatchSetupView: View {
} }
} }
}) })
.environment(match)
if matchTypeContext == .bracket, let tournament = match.currentTournament() { if matchTypeContext == .bracket, let tournament = match.currentTournament() {
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
let availableSeedGroups = tournament.availableSeedGroups() let availableSeedGroups = tournament.availableSeedGroups()

@ -122,7 +122,6 @@ struct ActivityView: View {
RowButtonView("D'accord.") { RowButtonView("D'accord.") {
self.error = nil self.error = nil
} }
.padding(.horizontal)
} }
} else if isGatheringFederalTournaments { } else if isGatheringFederalTournaments {
ProgressView() ProgressView()
@ -139,11 +138,9 @@ struct ActivityView: View {
FooterButtonView("supprimer vos filtres") { FooterButtonView("supprimer vos filtres") {
federalDataViewModel.removeFilters() federalDataViewModel.removeFilters()
} }
.padding(.horizontal)
FooterButtonView("modifier vos filtres") { FooterButtonView("modifier vos filtres") {
presentFilterView = true presentFilterView = true
} }
.padding(.horizontal)
} }
} else { } else {
_dataEmptyView() _dataEmptyView()
@ -403,13 +400,10 @@ struct ActivityView: View {
RowButtonView("Créer un nouvel événement") { RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
} }
.padding(.horizontal)
RowButtonView("Importer via Tenup") { RowButtonView("Importer via Tenup") {
navigation.agendaDestination = .tenup navigation.agendaDestination = .tenup
} }
.padding(.horizontal)
SupportButtonView(contentIsUnavailable: true) SupportButtonView(contentIsUnavailable: true)
.padding(.horizontal)
} }
} }

@ -10,6 +10,7 @@ import CoreLocation
import CoreLocationUI import CoreLocationUI
struct TournamentLookUpView: View { struct TournamentLookUpView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel @Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager() @StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -18,19 +19,8 @@ struct TournamentLookUpView: View {
@State var page: Int = 0 @State var page: Int = 0
@State var total: Int = 0 @State var total: Int = 0
@State private var tournamentCategories = Set<TournamentCategory.ID>()
@State private var tournamentLevels = Set<TournamentLevel.ID>()
@State private var tournamentAges = Set<FederalTournamentAge.ID>()
@State private var tournamentTypes = Set<FederalTournamentType.ID>()
@State private var searching: Bool = false @State private var searching: Bool = false
@State private var startDate: Date = Date()
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
@AppStorage("lastCity") private var city: String = ""
@State private var ligue: String = ""
@State private var distance: Double = 30
@State private var sortingOption: String = "dateDebut+asc"
@State private var requestedToGetAllPages: Bool = false @State private var requestedToGetAllPages: Bool = false
@State private var nationalCup: Bool = false
@State private var revealSearchParameters: Bool = true @State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false @State private var presentAlert: Bool = false
@ -61,14 +51,18 @@ struct TournamentLookUpView: View {
presentAlert = false presentAlert = false
} }
}, message: { }, message: {
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.") if dataStore.appSettings.city.isEmpty {
Text("Il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
} else {
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.")
}
}) })
.toolbarBackground(.visible, for: .bottomBar, .navigationBar) .toolbarBackground(.visible, for: .bottomBar, .navigationBar)
.navigationTitle("Chercher un tournoi") .navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city) { .onChange(of: locationManager.city) {
if let newValue = locationManager.city, city.isEmpty { if let newValue = locationManager.city, dataStore.appSettings.city.isEmpty {
city = newValue dataStore.appSettings.city = newValue
} }
} }
.toolbarTitleDisplayMode(.large) .toolbarTitleDisplayMode(.large)
@ -113,15 +107,9 @@ struct TournamentLookUpView: View {
#endif #endif
Button(role: .destructive) { Button(role: .destructive) {
tournamentLevels = Set() dataStore.appSettings.resetSearch()
tournamentCategories = Set()
city = ""
locationManager.location = nil locationManager.location = nil
locationManager.city = nil locationManager.city = nil
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
revealSearchParameters = true revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = [] federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0 federalDataViewModel.searchAttemptCount = 0
@ -151,6 +139,7 @@ struct TournamentLookUpView: View {
} }
private func runSearch() { private func runSearch() {
dataStore.appSettingsStorage.write()
revealSearchParameters = false revealSearchParameters = false
total = 0 total = 0
page = 0 page = 0
@ -158,6 +147,9 @@ struct TournamentLookUpView: View {
searching = true searching = true
requestedToGetAllPages = false requestedToGetAllPages = false
federalDataViewModel.searchAttemptCount += 1 federalDataViewModel.searchAttemptCount += 1
federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod
federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration
Task { Task {
await getNewPage() await getNewPage()
searching = false searching = false
@ -170,7 +162,7 @@ struct TournamentLookUpView: View {
} }
private var distanceLimit: Measurement<UnitLength> { private var distanceLimit: Measurement<UnitLength> {
distanceLimit(distance: distance) distanceLimit(distance: dataStore.appSettings.distance)
} }
private func distanceLimit(distance: Double) -> Measurement<UnitLength> { private func distanceLimit(distance: Double) -> Measurement<UnitLength> {
@ -178,19 +170,19 @@ struct TournamentLookUpView: View {
} }
private var categories: [TournamentCategory] { private var categories: [TournamentCategory] {
tournamentCategories.compactMap { TournamentCategory(rawValue: $0) } dataStore.appSettings.tournamentCategories.compactMap { TournamentCategory(rawValue: $0) }
} }
private var levels: [TournamentLevel] { private var levels: [TournamentLevel] {
tournamentLevels.compactMap { TournamentLevel(rawValue: $0) } dataStore.appSettings.tournamentLevels.compactMap { TournamentLevel(rawValue: $0) }
} }
private var ages: [FederalTournamentAge] { private var ages: [FederalTournamentAge] {
tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) } dataStore.appSettings.tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) }
} }
private var types: [FederalTournamentType] { private var types: [FederalTournamentType] {
tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) } dataStore.appSettings.tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) }
} }
func getNewPage() async { func getNewPage() async {
@ -198,7 +190,7 @@ struct TournamentLookUpView: View {
if NetworkFederalService.shared.formId.isEmpty { if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm() await getNewBuildForm()
} else { } else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: nationalCup) let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup)
let resultCommand = commands.first(where: { $0.results != nil }) let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items { if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in newTournaments.forEach { ft in
@ -253,15 +245,9 @@ struct TournamentLookUpView: View {
} }
} }
Button { Button {
tournamentLevels = Set() dataStore.appSettings.resetSearch()
tournamentCategories = Set()
city = ""
locationManager.location = nil locationManager.location = nil
locationManager.city = nil locationManager.city = nil
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
revealSearchParameters = true revealSearchParameters = true
} label: { } label: {
Label("Ré-initialiser la recherche", systemImage: "xmark.circle") Label("Ré-initialiser la recherche", systemImage: "xmark.circle")
@ -271,12 +257,12 @@ struct TournamentLookUpView: View {
@ViewBuilder @ViewBuilder
var searchParametersView: some View { var searchParametersView: some View {
@Bindable var federalDataViewModel = federalDataViewModel @Bindable var appSettings = dataStore.appSettings
Section { Section {
DatePicker("Début", selection: $startDate, displayedComponents: .date) DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date)
DatePicker("Fin", selection: $endDate, displayedComponents: .date) DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date)
Picker(selection: $federalDataViewModel.dayDuration) { Picker(selection: $appSettings.dayDuration) {
Text("aucune").tag(nil as Int?) Text("Aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?) Text(1.formatted()).tag(1 as Int?)
Text(2.formatted()).tag(2 as Int?) Text(2.formatted()).tag(2 as Int?)
Text(3.formatted()).tag(3 as Int?) Text(3.formatted()).tag(3 as Int?)
@ -284,16 +270,16 @@ struct TournamentLookUpView: View {
Text("Durée max (en jours)") Text("Durée max (en jours)")
} }
Picker(selection: $federalDataViewModel.dayPeriod) { Picker(selection: $appSettings.dayPeriod) {
ForEach(DayPeriod.allCases) { ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel()).tag($0) Text($0.localizedDayPeriodLabel().capitalized).tag($0)
} }
} label: { } label: {
Text("En semaine ou week-end") Text("En semaine ou week-end")
} }
HStack { HStack {
TextField("Ville", text: $city) TextField("Ville", text: $appSettings.city)
if let city = locationManager.city { if let city = locationManager.city {
Divider() Divider()
Text(city).italic() Text(city).italic()
@ -311,7 +297,7 @@ struct TournamentLookUpView: View {
} }
} }
Picker(selection: $distance) { Picker(selection: $appSettings.distance) {
Text(distanceLimit(distance:30).formatted()).tag(30.0) Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0) Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0) Text(distanceLimit(distance:60).formatted()).tag(60.0)
@ -324,7 +310,7 @@ struct TournamentLookUpView: View {
Text("Distance max") Text("Distance max")
} }
Picker(selection: $sortingOption) { Picker(selection: $appSettings.sortingOption) {
Text("Distance").tag("_DIST_") Text("Distance").tag("_DIST_")
Text("Date de début").tag("dateDebut+asc") Text("Date de début").tag("dateDebut+asc")
Text("Date de fin").tag("dateFin+asc") Text("Date de fin").tag("dateFin+asc")
@ -333,7 +319,7 @@ struct TournamentLookUpView: View {
} }
NavigationLink { NavigationLink {
List(TournamentCategory.allCases, selection: $tournamentCategories) { type in List([TournamentCategory.men, TournamentCategory.women, TournamentCategory.mix], selection: $appSettings.tournamentCategories) { type in
Text(type.localizedLabel()) Text(type.localizedLabel())
} }
.navigationTitle("Catégories") .navigationTitle("Catégories")
@ -348,7 +334,7 @@ struct TournamentLookUpView: View {
} }
NavigationLink { NavigationLink {
List(TournamentLevel.allCases, selection: $tournamentLevels) { type in List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in
Text(type.localizedLabel()) Text(type.localizedLabel())
} }
.navigationTitle("Niveaux") .navigationTitle("Niveaux")
@ -363,7 +349,7 @@ struct TournamentLookUpView: View {
} }
NavigationLink { NavigationLink {
List(FederalTournamentAge.allCases, selection: $tournamentAges) { type in List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in
Text(type.localizedLabel()) Text(type.localizedLabel())
} }
.navigationTitle("Limites d'âge") .navigationTitle("Limites d'âge")
@ -372,7 +358,7 @@ struct TournamentLookUpView: View {
HStack { HStack {
Text("Limite d'âge") Text("Limite d'âge")
Spacer() Spacer()
if tournamentAges.isEmpty || tournamentAges.count == FederalTournamentAge.allCases.count { if dataStore.appSettings.tournamentAges.isEmpty || dataStore.appSettings.tournamentAges.count == FederalTournamentAge.allCases.count {
Text("Tous les âges") Text("Tous les âges")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
@ -383,7 +369,7 @@ struct TournamentLookUpView: View {
} }
NavigationLink { NavigationLink {
List(FederalTournamentType.allCases, selection: $tournamentTypes) { type in List(FederalTournamentType.allCases, selection: $appSettings.tournamentTypes) { type in
Text(type.localizedLabel()) Text(type.localizedLabel())
} }
.navigationTitle("Types de tournoi") .navigationTitle("Types de tournoi")
@ -392,7 +378,7 @@ struct TournamentLookUpView: View {
HStack { HStack {
Text("Type de tournoi") Text("Type de tournoi")
Spacer() Spacer()
if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count { if dataStore.appSettings.tournamentTypes.isEmpty || dataStore.appSettings.tournamentTypes.count == FederalTournamentType.allCases.count {
Text("Tous les types") Text("Tous les types")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
@ -402,7 +388,7 @@ struct TournamentLookUpView: View {
} }
} }
Picker(selection: $nationalCup) { Picker(selection: $appSettings.nationalCup) {
Text("N'importe").tag(false) Text("N'importe").tag(false)
Text("Uniquement").tag(true) Text("Uniquement").tag(true)
} label: { } label: {
@ -416,7 +402,7 @@ struct TournamentLookUpView: View {
} }
var categoriesLabel: some View { var categoriesLabel: some View {
if tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count { if dataStore.appSettings.tournamentCategories.isEmpty || dataStore.appSettings.tournamentCategories.count == TournamentCategory.allCases.count {
Text("Toutes les catégories") Text("Toutes les catégories")
} else { } else {
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", ")) Text(categories.map({ $0.localizedLabel() }).joined(separator: ", "))
@ -424,7 +410,7 @@ struct TournamentLookUpView: View {
} }
var levelsLabel: some View { var levelsLabel: some View {
if tournamentLevels.isEmpty || tournamentLevels.count == TournamentLevel.allCases.count { if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count {
Text("Tous les niveaux") Text("Tous les niveaux")
} else { } else {
Text(levels.map({ $0.localizedLabel() }).joined(separator: ", ")) Text(levels.map({ $0.localizedLabel() }).joined(separator: ", "))
@ -437,8 +423,8 @@ struct TournamentLookUpView: View {
HStack { HStack {
Text("Lieu") Text("Lieu")
Spacer() Spacer()
Text(city) Text(dataStore.appSettings.city)
if distance >= 3000 { if dataStore.appSettings.distance >= 3000 {
Text("sans limite de distance") Text("sans limite de distance")
} else { } else {
Text("à moins de " + distanceLimit.formatted()) Text("à moins de " + distanceLimit.formatted())
@ -448,9 +434,9 @@ struct TournamentLookUpView: View {
Text("Période") Text("Période")
Spacer() Spacer()
Text("Du") Text("Du")
Text(startDate.twoDigitsYearFormatted) Text(dataStore.appSettings.startDate.twoDigitsYearFormatted)
Text("Au") Text("Au")
Text(endDate.twoDigitsYearFormatted) Text(dataStore.appSettings.endDate.twoDigitsYearFormatted)
} }
HStack { HStack {
Text("Niveau") Text("Niveau")
@ -472,7 +458,7 @@ struct TournamentLookUpView: View {
} }
var sortingOptionLabel: String { var sortingOptionLabel: String {
switch sortingOption { switch dataStore.appSettings.sortingOption {
case "_DIST_": return "Distance" case "_DIST_": return "Distance"
case "dateDebut+asc": return "Date de début" case "dateDebut+asc": return "Date de début"
case "dateFin+asc": return "Date de fin" case "dateFin+asc": return "Date de fin"

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import EventKit
struct TournamentSubscriptionView: View { struct TournamentSubscriptionView: View {
@EnvironmentObject var networkMonitor: NetworkMonitor @EnvironmentObject var networkMonitor: NetworkMonitor
@ -17,6 +18,8 @@ struct TournamentSubscriptionView: View {
@State private var selectedPlayers: [ImportedPlayer] @State private var selectedPlayers: [ImportedPlayer]
@State private var contactType: ContactType? = nil @State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var didSendMessage: Bool = false
@State private var didSaveInCalendar: Bool = false
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) { init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) {
self.federalTournament = federalTournament self.federalTournament = federalTournament
@ -25,8 +28,79 @@ struct TournamentSubscriptionView: View {
_selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 })) _selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 }))
} }
func hasPartner() -> Bool {
selectedPlayers.count == 2
}
func inscriptionSent() -> Bool {
didSendMessage
}
func addEvent() {
let eventStore = EKEventStore()
eventStore.requestWriteOnlyAccessToEvents { (granted, error) in
if granted && error == nil {
print("Access granted")
let startDate = federalTournament.startDate
let endDate = federalTournament.dateFin ?? federalTournament.startDate.endOfDay()
addEventToCalendar(title: messageSubject, startDate: startDate, endDate: endDate)
didSaveInCalendar = true
} else {
print("Access denied or error occurred: \(String(describing: error?.localizedDescription))")
sentError = .calendarAccessDenied
}
}
}
func addEventToCalendar(title: String, startDate: Date, endDate: Date) {
let eventStore = EKEventStore()
if eventStore.defaultCalendarForNewEvents == nil {
sentError = .noCalendarAvailable
return
}
eventStore.requestWriteOnlyAccessToEvents { (granted, error) in
if granted && error == nil {
let event = EKEvent(eventStore: eventStore)
event.title = title
event.isAllDay = true
event.startDate = startDate
event.endDate = endDate
event.calendar = eventStore.defaultCalendarForNewEvents
event.notes = noteCalendar
event.location = federalTournament.clubLabel()
do {
try eventStore.save(event, span: .thisEvent)
didSaveInCalendar = true
print("Event saved")
} catch let error {
print("Failed to save event: \(error.localizedDescription)")
sentError = .calendarEventSaveFailed
}
} else {
print("Access denied or error occurred: \(String(describing: error?.localizedDescription))")
sentError = .calendarAccessDenied
}
}
}
var body: some View { var body: some View {
List { List {
if didSaveInCalendar {
Section {
LabeledContent {
Image(systemName: "checkmark").foregroundStyle(.green)
} label: {
Text("Le tournoi a bien été ajouté dans votre calendrier par défaut")
let eventStore = EKEventStore()
if let defaultCalendarForNewEvents = eventStore.defaultCalendarForNewEvents {
Text(defaultCalendarForNewEvents.title)
}
}
}
}
Section { Section {
LabeledContent("Tournoi") { LabeledContent("Tournoi") {
Text(federalTournament.libelle ?? "Tournoi") Text(federalTournament.libelle ?? "Tournoi")
@ -121,6 +195,17 @@ struct TournamentSubscriptionView: View {
} }
.toolbar(content: { .toolbar(content: {
Menu { Menu {
ShareLink(item: federalTournament.sharePartnerMessage) {
Label("Prévenir votre partenaire", systemImage: "person.2")
}
Button("Ajouter à votre agenda") {
addEvent()
}
ShareLink(item: federalTournament.shareMessage) {
Label("Partager les infos", systemImage: "info")
}
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) { Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) {
Label("Voir sur Tenup", systemImage: "tennisball") Label("Voir sur Tenup", systemImage: "tennisball")
} }
@ -134,6 +219,13 @@ struct TournamentSubscriptionView: View {
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") { Button("OK") {
} }
if sentError == .calendarAccessDenied || sentError == .noCalendarAvailable {
Button("Voir vos réglages") {
openAppSettings()
}
}
} message: { } message: {
Text(_networkErrorMessage) Text(_networkErrorMessage)
} }
@ -150,6 +242,8 @@ struct TournamentSubscriptionView: View {
case .sent: case .sent:
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.sentError = .messageNotSent self.sentError = .messageNotSent
} else {
self.didSendMessage = true
} }
@unknown default: @unknown default:
break break
@ -167,6 +261,8 @@ struct TournamentSubscriptionView: View {
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
self.sentError = .mailNotSent self.sentError = .mailNotSent
} else {
self.didSendMessage = true
} }
@unknown default: @unknown default:
break break
@ -188,12 +284,19 @@ 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 !\nVotre tournoi n'est pas encore dessus ? \(URLs.main.rawValue)", "Téléchargez l'app : \(URLs.appStore.rawValue)", "En savoir plus : \(URLs.appDescription.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n" 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 !"].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
var messageBodyShort: String { var messageBodyShort: String {
let body = [[build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " "), federalTournament.computedStartDate, teamsString].compacted().joined(separator: "\n") + "\n" let bonjourOuBonsoir = Date().timeOfDay.hello
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].compactMap { $0 }.joined(separator: "\n") + "\n"
return body
}
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"
return body return body
} }
@ -227,6 +330,29 @@ struct TournamentSubscriptionView: View {
if sentError == .mailFailed { if sentError == .mailFailed {
errors.append("Le mail n'a pas été envoyé") errors.append("Le mail n'a pas été envoyé")
} }
if sentError == .calendarAccessDenied {
errors.append("Padel Club n'a pas accès à votre calendrier")
}
if sentError == .calendarEventSaveFailed {
errors.append("Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier")
}
if sentError == .noCalendarAvailable {
errors.append("Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi")
}
return errors.joined(separator: "\n") return errors.joined(separator: "\n")
} }
func openAppSettings() {
if let appSettings = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)
}
}
}
} }

@ -121,6 +121,22 @@ struct UmpireView: View {
} }
Section {
@Bindable var user = dataStore.user
Picker(selection: $user.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: user.loserBracketMode) {
dataStore.saveUser()
}
} header: {
Text("Matchs de classement")
}
Section { Section {
NavigationLink { NavigationLink {
GlobalSettingsView() GlobalSettingsView()

@ -93,6 +93,7 @@ struct DatePickingView: View {
} }
struct MatchFormatPickingView: View { struct MatchFormatPickingView: View {
var title: String? = nil
@Binding var matchFormat: MatchFormat @Binding var matchFormat: MatchFormat
var validateAction: (() async -> ()) var validateAction: (() async -> ())
@ -110,6 +111,10 @@ struct MatchFormatPickingView: View {
confirmScheduleUpdate = false confirmScheduleUpdate = false
} }
} }
} header: {
if let title {
Text(title)
}
} footer: { } footer: {
if confirmScheduleUpdate && updatingInProgress == false { if confirmScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier les horaires") { FooterButtonView("non, ne pas modifier les horaires") {
@ -125,6 +130,105 @@ struct MatchFormatPickingView: View {
} }
struct DatePickingViewWithFormat: View {
@Binding var matchFormat: MatchFormat
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: ((Bool) async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmScheduleUpdate {
RowButtonView("Sauver et modifier la suite") {
updatingInProgress = true
await validateAction(true)
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
HStack {
FooterButtonView("sauver sans modifier la suite") {
Task {
await validateAction(false)
confirmScheduleUpdate = false
}
}
Text("ou")
FooterButtonView("annuler") {
confirmScheduleUpdate = false
}
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
.onChange(of: startDate) {
confirmScheduleUpdate = true
}
}
}
struct GroupStageDatePickingView: View { struct GroupStageDatePickingView: View {
let title: String let title: String
@Binding var startDate: Date @Binding var startDate: Date

@ -114,7 +114,6 @@ struct CourtAvailabilitySettingsView: View {
endDate = tournament.startDate.addingTimeInterval(5400) endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
.padding(.horizontal)
} }
} }
} }

@ -33,7 +33,7 @@ struct LoserRoundScheduleEditorView: View {
var body: some View { var body: some View {
List { List {
MatchFormatPickingView(matchFormat: $matchFormat) { MatchFormatPickingView(title: "Format des tours par défault", matchFormat: $matchFormat) {
await _updateSchedule() await _updateSchedule()
} }

@ -38,8 +38,11 @@ struct LoserRoundStepScheduleEditorView: View {
var body: some View { var body: some View {
@Bindable var round = round @Bindable var round = round
DatePickingView(title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { DatePickingViewWithFormat(matchFormat: $round.matchFormat, title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { update in
await _updateSchedule() for match in matches {
match.matchFormat = round.matchFormat
}
await _updateSchedule(update: update)
} }
Section { Section {
@ -64,8 +67,10 @@ struct LoserRoundStepScheduleEditorView: View {
} }
} }
private func _updateSchedule() async { private func _updateSchedule(update: Bool) async {
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) if update {
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
}
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate round.startDate = startDate
}) })

@ -27,6 +27,10 @@ struct MatchScheduleEditorView: View {
} }
var body: some View { var body: some View {
MatchFormatPickingView(matchFormat: $match.matchFormat) {
await _updateSchedule()
}
DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule() await _updateSchedule()
} }

@ -51,7 +51,6 @@ struct PlanningByCourtView: View {
RowButtonView("Horaire intelligent") { RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil selectedScheduleDestination = nil
} }
.padding(.horizontal)
} }
} }
} }

@ -40,7 +40,6 @@ struct PlanningView: View {
RowButtonView("Horaire intelligent") { RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil selectedScheduleDestination = nil
} }
.padding(.horizontal)
} }
} }
} }

@ -99,7 +99,7 @@ struct PlayerPopoverView: View {
.textInputAutocapitalization(.words) .textInputAutocapitalization(.words)
.focused($firstNameIsFocused) .focused($firstNameIsFocused)
.onSubmit { .onSubmit {
firstName = firstName.trimmed firstName = firstName.trimmedMultiline
lastNameIsFocused = true lastNameIsFocused = true
} }
.fixedSize() .fixedSize()
@ -114,7 +114,7 @@ struct PlayerPopoverView: View {
.keyboardType(.alphabet) .keyboardType(.alphabet)
.focused($lastNameIsFocused) .focused($lastNameIsFocused)
.onSubmit { .onSubmit {
lastName = lastName.trimmed lastName = lastName.trimmedMultiline
licenseIsFocused = true licenseIsFocused = true
} }
.fixedSize() .fixedSize()
@ -128,7 +128,7 @@ struct PlayerPopoverView: View {
.keyboardType(.numberPad) .keyboardType(.numberPad)
.submitLabel(.next) .submitLabel(.next)
.onSubmit { .onSubmit {
license = license.trimmed license = license.trimmedMultiline
if requiredField.contains(.license) { if requiredField.contains(.license) {
if license.isLicenseNumber { if license.isLicenseNumber {
amountIsFocused = true amountIsFocused = true
@ -206,7 +206,7 @@ struct PlayerPopoverView: View {
Spacer() Spacer()
Button("Confirmer") { Button("Confirmer") {
if licenseIsFocused { if licenseIsFocused {
license = license.trimmed license = license.trimmedMultiline
if requiredField.contains(.license) { if requiredField.contains(.license) {
if license.isLicenseNumber { if license.isLicenseNumber {
amountIsFocused = true amountIsFocused = true
@ -251,7 +251,7 @@ struct PlayerPopoverView: View {
} }
func createManualPlayer() { func createManualPlayer() {
let playerRegistration = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: license.trimmed.isEmpty ? nil : license, rank: rank, sex: PlayerRegistration.PlayerSexType(rawValue: sex)) let playerRegistration = PlayerRegistration(firstName: firstName.trimmedMultiline, lastName: lastName.trimmedMultiline, licenceId: license.trimmedMultiline.isEmpty ? nil : license, rank: rank, sex: PlayerRegistration.PlayerSexType(rawValue: sex))
self.creationCompletionHandler(playerRegistration) self.creationCompletionHandler(playerRegistration)
} }

@ -41,6 +41,7 @@ struct PlayerDetailView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit(of: .text) { .onSubmit(of: .text) {
player.lastName = player.lastName.trimmedMultiline
_save() _save()
} }
} label: { } label: {
@ -53,6 +54,7 @@ struct PlayerDetailView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit(of: .text) { .onSubmit(of: .text) {
player.firstName = player.firstName.trimmedMultiline
_save() _save()
} }
} label: { } label: {

@ -22,6 +22,28 @@ struct LoserRoundSettingsView: View {
} }
} }
Section {
@Bindable var round: Round = upperBracketRound.round
Picker(selection: $round.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: round.loserBracketMode) {
do {
try self.tournament.tournamentStore.rounds.addOrUpdate(instance: upperBracketRound.round)
} catch {
Logger.error(error)
}
}
} header: {
Text("Matchs de classement")
} footer: {
Text(upperBracketRound.round.loserBracketMode.localizedLoserBracketModeDescription())
}
Section { Section {
RowButtonView("Synchroniser les noms des matchs") { RowButtonView("Synchroniser les noms des matchs") {
let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches
@ -48,6 +70,11 @@ struct LoserRoundSettingsView: View {
upperBracketRound.round.disabledMatches().forEach { match in upperBracketRound.round.disabledMatches().forEach { match in
match.disableMatch() match.disableMatch()
} }
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches())
} catch {
Logger.error(error)
}
} }
} }
.disabled(upperBracketRound.round.loserRounds().isEmpty == false) .disabled(upperBracketRound.round.loserRounds().isEmpty == false)

@ -172,7 +172,7 @@ struct SelectablePlayerListView: View {
} }
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
.toolbarBackground(.visible, for: .bottomBar) //.toolbarBackground(.visible, for: .bottomBar)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
@ -358,13 +358,109 @@ struct MySearchView: View {
@ViewBuilder @ViewBuilder
var playersView: some View { var playersView: some View {
let showProgression = true
let showFemaleInMaleAssimilation = searchViewModel.showFemaleInMaleAssimilation
if searchViewModel.allowMultipleSelection { if searchViewModel.allowMultipleSelection {
List(selection: $searchViewModel.selectedPlayers) { List(selection: $searchViewModel.selectedPlayers) {
if searchViewModel.filterSelectionEnabled { if searchViewModel.filterSelectionEnabled {
let array = Array(searchViewModel.selectedPlayers) let array = Array(searchViewModel.selectedPlayers)
Section { Section {
ForEach(array) { player in ForEach(array) { player in
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) let index : Int? = nil
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.onDelete { indexSet in .onDelete { indexSet in
for index in indexSet { for index in indexSet {
@ -379,7 +475,102 @@ struct MySearchView: View {
} else { } else {
Section { Section {
ForEach(players, id: \.self) { player in ForEach(players, id: \.self) { player in
ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) let index : Int? = nil
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -395,10 +586,105 @@ struct MySearchView: View {
if searchViewModel.allowSingleSelection { if searchViewModel.allowSingleSelection {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
let index : Int? = nil
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -410,9 +696,104 @@ struct MySearchView: View {
.id(UUID()) .id(UUID())
} else { } else {
Section { Section {
ForEach(players.indices, id: \.self) { index in ForEach(players.indices, id: \.self) { playerIndex in
let player = players[index] let player = players[playerIndex]
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil
VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -423,19 +804,205 @@ struct MySearchView: View {
} }
} else { } else {
Section { Section {
ForEach(players.indices, id: \.self) { index in ForEach(players.indices, id: \.self) { playerIndex in
let player = players[index] let player = players[playerIndex]
let index: Int? = searchViewModel.showIndex() ? (playerIndex + 1) : nil
if searchViewModel.allowSingleSelection { if searchViewModel.allowSingleSelection {
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) VStack(alignment: .leading) {
.contentShape(Rectangle()) HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} else { } else {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation, showProgression: true) VStack(alignment: .leading) {
HStack {
if player.isAnonymous() {
Text("Joueur Anonyme")
} else {
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
}
if index == nil {
Text(player.male ? "" : "")
}
Spacer()
if let index {
HStack(alignment: .top, spacing: 0) {
Text(index.formatted())
.foregroundStyle(.secondary)
.font(.title3)
Text(index.ordinalFormattedSuffix())
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
.background {
if player.isNotFromCurrentDate() {
UnderlineView()
}
}
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if showProgression, player.getProgression() != 0 {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(player.getProgression().formatted(.number.sign(strategy: .always())))
.foregroundStyle(player.getProgressionColor(progression: player.getProgression()))
Text(")")
}.font(.title3)
}
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
.lineLimit(1)
if showFemaleInMaleAssimilation, let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank() {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
} }
} }
} header: { } header: {

@ -147,7 +147,7 @@ struct EditingTeamView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.submitLabel(.done) .submitLabel(.done)
.onSubmit(of: .text) { .onSubmit(of: .text) {
let trimmed = name.trimmed let trimmed = name.trimmedMultiline
if trimmed.isEmpty { if trimmed.isEmpty {
team.name = nil team.name = nil
} else { } else {
@ -243,7 +243,7 @@ struct EditingTeamView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $editedTeam) { editedTeam in .fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack { NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam) AddTeamView(tournament: tournament, editedTeam: editedTeam)
} }

@ -18,6 +18,7 @@ struct TeamPickerView: View {
var shouldConfirm: Bool = false var shouldConfirm: Bool = false
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var round: Round? = nil
var matchTypeContext: MatchType = .bracket var matchTypeContext: MatchType = .bracket
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
@ -39,6 +40,14 @@ struct TeamPickerView: View {
.sheet(isPresented: $presentTeamPickerView) { .sheet(isPresented: $presentTeamPickerView) {
NavigationStack { NavigationStack {
List { List {
if matchTypeContext == .loserBracket, let losers = round?.parentRound?.losers() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Perdant du tour précédent")
}
if matchTypeContext == .loserBracket, let losers = round?.previousRound()?.winners() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Gagnant du tour précédent")
}
if let groupStagePosition, let replacementRangeExtended = tournament.replacementRangeExtended(groupStagePosition: groupStagePosition) { if let groupStagePosition, let replacementRangeExtended = tournament.replacementRangeExtended(groupStagePosition: groupStagePosition) {
Section { Section {
GroupStageTeamReplacementView.TeamRangeView(teamRange: replacementRangeExtended, playerWeight: 0) GroupStageTeamReplacementView.TeamRangeView(teamRange: replacementRangeExtended, playerWeight: 0)
@ -130,6 +139,7 @@ struct TeamPickerView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
.listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) { // Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!) // teamPicked(confirmTeam!)

@ -311,6 +311,11 @@ struct FileImportView: View {
} label: { } label: {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
} }
LabeledContent {
Text(_filteredTeams.count.formatted())
} label: {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
}
} footer: { } footer: {
if previousTeams.isEmpty == false { if previousTeams.isEmpty == false {
Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed) Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed)
@ -442,11 +447,16 @@ struct FileImportView: View {
.disabled(validationInProgress) .disabled(validationInProgress)
} }
private func _getUnfound(tournament: Tournament, fromTeams filteredTeams: [FileImportManager.TeamHolder]) -> Set<TeamRegistration> {
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
return unfound
}
private func _validate(tournament: Tournament) async { private func _validate(tournament: Tournament) async {
let filteredTeams = filteredTeams(tournament: tournament) let filteredTeams = filteredTeams(tournament: tournament)
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams)) let unfound = _getUnfound(tournament: tournament, fromTeams: filteredTeams)
unfound.forEach { team in unfound.forEach { team in
team.resetPositions() team.resetPositions()
team.wildCardBracket = false team.wildCardBracket = false
@ -455,7 +465,7 @@ struct FileImportView: View {
} }
do { do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unfound) try tournament.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unfound)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }

@ -20,7 +20,11 @@ struct AddTeamView: View {
var tournament: Tournament var tournament: Tournament
var cancelShouldDismiss: Bool = false var cancelShouldDismiss: Bool = false
enum FocusField: Hashable {
case pasteField
}
@FocusState private var focusedField: FocusField?
@State private var searchField: String = "" @State private var searchField: String = ""
@State private var presentSearch: Bool = false @State private var presentSearch: Bool = false
@State private var presentPlayerSearch: Bool = false @State private var presentPlayerSearch: Bool = false
@ -36,7 +40,8 @@ struct AddTeamView: View {
@State private var confirmDuplicate: Bool = false @State private var confirmDuplicate: Bool = false
@State private var homonyms: [PlayerRegistration] = [] @State private var homonyms: [PlayerRegistration] = []
@State private var confirmHomonym: Bool = false @State private var confirmHomonym: Bool = false
@State private var editableTextField: String = ""
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
@ -61,13 +66,32 @@ struct AddTeamView: View {
_pasteString = .init(wrappedValue: pasteString) _pasteString = .init(wrappedValue: pasteString)
_fetchPlayers = FetchRequest<ImportedPlayer>(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) _fetchPlayers = FetchRequest<ImportedPlayer>(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption))
_autoSelect = .init(wrappedValue: true) _autoSelect = .init(wrappedValue: true)
_editableTextField = .init(wrappedValue: pasteString)
cancelShouldDismiss = true cancelShouldDismiss = true
} }
} }
var body: some View { var body: some View {
_buildingTeamView() if pasteString != nil, fetchPlayers.isEmpty == false {
.navigationBarBackButtonHidden(true) computedBody
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher dans les résultats"))
} else {
computedBody
}
}
var computedBody: some View {
List(selection: $createdPlayerIds) {
_buildingTeamView()
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.alert("Présence d'homonyme", isPresented: $confirmHomonym) { .alert("Présence d'homonyme", isPresented: $confirmHomonym) {
Button("Créer l'équipe quand même") { Button("Créer l'équipe quand même") {
_createTeam(checkDuplicates: false, checkHomonym: false) _createTeam(checkDuplicates: false, checkHomonym: false)
@ -121,7 +145,6 @@ struct AddTeamView: View {
} }
.tint(.master) .tint(.master)
} }
.navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe")
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
@ -130,27 +153,23 @@ struct AddTeamView: View {
} }
if pasteString == nil { if pasteString == nil {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return } guard let first = strings.first else { return }
Task { handlePasteString(first)
await MainActor.run {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
autoSelect = true
}
}
} }
.foregroundStyle(.master) .foregroundStyle(.master)
.labelStyle(.iconOnly) .labelStyle(.titleAndIcon)
.buttonBorderShape(.capsule) .buttonBorderShape(.capsule)
} }
} }
} }
.navigationBarBackButtonHidden(_isEditingTeam()) .navigationBarBackButtonHidden(true)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe")
.environment(\.editMode, Binding.constant(EditMode.active))
} }
private func _isEditingTeam() -> Bool { private func _isEditingTeam() -> Bool {
@ -275,6 +294,8 @@ struct AddTeamView: View {
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
editableTextField = ""
if team.players().count > 1 { if team.players().count > 1 {
dismiss() dismiss()
} }
@ -302,28 +323,50 @@ struct AddTeamView: View {
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
editableTextField = ""
self.editedTeam = nil self.editedTeam = nil
if editedTeam.players().count > 1 { if editedTeam.players().count > 1 {
dismiss() dismiss()
} }
} }
@ViewBuilder
private func _buildingTeamView() -> some View { private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) {
if let pasteString { if let pasteString {
Section { Section {
Text(pasteString) TextEditor(text: $editableTextField)
.frame(minHeight: 120, maxHeight: .infinity)
.focused($focusedField, equals: .pasteField)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Fermer", role: .cancel) {
self.editableTextField = pasteString
self.focusedField = nil
}
Spacer()
Button("Chercher") {
self.handlePasteString(editableTextField)
self.focusedField = nil
}
.buttonStyle(.bordered)
}
}
} header: {
Text("Contenu du presse-papier")
} footer: { } footer: {
HStack { HStack {
Text("contenu du presse-papier") FooterButtonView("éditer") {
self.focusedField = .pasteField
}
Spacer() Spacer()
Button("effacer", role: .destructive) { FooterButtonView("effacer", role: .destructive) {
self.focusedField = nil
self.editableTextField = ""
self.pasteString = nil self.pasteString = nil
self.createdPlayers.removeAll() self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll() self.createdPlayerIds.removeAll()
} }
.buttonStyle(.borderless)
} }
} }
} }
@ -387,6 +430,8 @@ struct AddTeamView: View {
Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count))
} }
} }
// } else {
// Text("Préparation de l'équipe")
} }
} }
@ -404,33 +449,16 @@ struct AddTeamView: View {
RowButtonView("Effacer cette recherche") { RowButtonView("Effacer cette recherche") {
self.pasteString = nil self.pasteString = nil
self.editableTextField = ""
} }
} }
} else { } else {
Section { _listOfPlayers(pasteString: pasteString)
let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
ForEach(sortedPlayers) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
} }
} else { } else {
_managementView() _managementView()
} }
}
.headerProminence(.increased)
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
} }
private var count: Int { private var count: Int {
@ -453,4 +481,32 @@ struct AddTeamView: View {
Logger.error(error) Logger.error(error)
} }
} }
private func handlePasteString(_ first: String) {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
editableTextField = first
autoSelect = true
}
}
}
@ViewBuilder
private func _listOfPlayers(pasteString: String) -> some View {
let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }).sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
Section {
ForEach(sortedPlayers) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(sortedPlayers.count.formatted() + " résultat" + sortedPlayers.count.pluralSuffix)
}
}
} }

@ -11,7 +11,7 @@ import LeStorage
struct TournamentGeneralSettingsView: View { struct TournamentGeneralSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament @Bindable var tournament: Tournament
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
@ -24,7 +24,7 @@ struct TournamentGeneralSettingsView: View {
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
Form { Form {
Section { Section {
TournamentDatePickerView() TournamentDatePickerView()
TournamentDurationManagerView() TournamentDurationManagerView()
@ -34,6 +34,43 @@ struct TournamentGeneralSettingsView: View {
TournamentLevelPickerView() TournamentLevelPickerView()
} }
Section {
Picker(selection: $tournament.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: tournament.loserBracketMode) {
_save()
let rounds = tournament.rounds()
rounds.forEach { round in
round.loserBracketMode = tournament.loserBracketMode
}
do {
try self.tournament.tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
}
}
} header: {
Text("Matchs de classement")
} footer: {
if dataStore.user.loserBracketMode != tournament.loserBracketMode {
_footerView()
.onTapGesture(perform: {
self.dataStore.user.loserBracketMode = tournament.loserBracketMode
self.dataStore.saveUser()
})
} else {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
}
}
Section { Section {
LabeledContent { LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
@ -118,4 +155,10 @@ struct TournamentGeneralSettingsView: View {
Logger.error(error) Logger.error(error)
} }
} }
private func _footerView() -> some View {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
+
Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue)
}
} }

@ -224,14 +224,11 @@ struct InscriptionManagerView: View {
RowButtonView("Ajouter une équipe") { RowButtonView("Ajouter une équipe") {
presentAddTeamView = true presentAddTeamView = true
} }
.padding(.horizontal)
RowButtonView("Importer un fichier") { RowButtonView("Importer un fichier") {
presentImportView = true presentImportView = true
} }
.padding(.horizontal)
} }
.padding()
} }
} }
} }
@ -327,7 +324,7 @@ struct InscriptionManagerView: View {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $presentAddTeamView, onDismiss: { .fullScreenCover(isPresented: $presentAddTeamView, onDismiss: {
_setHash() _setHash()
}) { }) {
NavigationStack { NavigationStack {
@ -335,7 +332,7 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $editedTeam, onDismiss: { .fullScreenCover(item: $editedTeam, onDismiss: {
_setHash() _setHash()
}) { editedTeam in }) { editedTeam in
NavigationStack { NavigationStack {
@ -343,7 +340,7 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $pasteString, onDismiss: { .fullScreenCover(item: $pasteString, onDismiss: {
_setHash() _setHash()
}) { pasteString in }) { pasteString in
NavigationStack { NavigationStack {

@ -74,7 +74,7 @@ struct TournamentSettingsView: View {
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Réglages") .navigationTitle("Réglages du tournoi")
} }
} }

@ -40,7 +40,7 @@ struct TournamentInitView: View {
Image(systemName: "checkmark").foregroundStyle(.green) Image(systemName: "checkmark").foregroundStyle(.green)
} }
} label: { } label: {
LabelStructure() Text("Structure")
Text(tournament.structureDescriptionLocalizedLabel()) Text(tournament.structureDescriptionLocalizedLabel())
} }
} }
@ -49,7 +49,7 @@ struct TournamentInitView: View {
LabeledContent { LabeledContent {
Text(tournament.localizedTournamentType()) Text(tournament.localizedTournamentType())
} label: { } label: {
LabelSettings() Text("Réglages du tournoi")
Text("Formats, terrains, prix et plus") Text("Formats, terrains, prix et plus")
} }
} }

Loading…
Cancel
Save