multistore
Razmig Sarkissian 2 years ago
parent 18c744b34a
commit 509a1e3423
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 5
      PadelClub/Data/Event.swift
  3. 28
      PadelClub/Data/PlayerRegistration.swift
  4. 90
      PadelClub/Data/TeamRegistration.swift
  5. 146
      PadelClub/Data/Tournament.swift
  6. 27
      PadelClub/Extensions/MySortDescriptor.swift
  7. 39
      PadelClub/Extensions/Sequence+Extensions.swift
  8. 10
      PadelClub/Manager/FileImportManager.swift
  9. 2
      PadelClub/Manager/PadelRule.swift
  10. 3
      PadelClub/Manager/SourceFileManager.swift
  11. 6
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  12. 47
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  13. 45
      PadelClub/Views/Tournament/FileImportView.swift
  14. 44
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  15. 791
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -100,6 +100,7 @@
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; };
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; };
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -159,6 +160,7 @@
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; }; FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; };
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; }; FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.swift */; };
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; }; FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; }; FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; };
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; };
@ -308,6 +310,7 @@
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreSheetView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -369,6 +372,7 @@
FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; }; FFD784012B91C1B4000F62A6 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = "<group>"; };
FFD784032B91C280000F62A6 /* EmptyActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActivityView.swift; sourceTree = "<group>"; }; FFD784032B91C280000F62A6 /* EmptyActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyActivityView.swift; sourceTree = "<group>"; };
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; }; FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; }; FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; };
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; };
@ -742,6 +746,7 @@
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */, FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */,
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@ -872,6 +877,7 @@
FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */, FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */,
FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */, FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */,
FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */, FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */,
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1140,6 +1146,7 @@
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
@ -1178,6 +1185,7 @@
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */, FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */,
C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */,

@ -32,6 +32,11 @@ class Event: ModelObject, Storable {
self.roundFormat = roundFormat self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat self.loserRoundFormat = loserRoundFormat
} }
var clubObject: Club? {
guard let club else { return nil }
return Store.main.findById(club)
}
var tournaments: [Tournament] { var tournaments: [Tournament] {
Store.main.filter { $0.event == self.id } Store.main.filter { $0.event == self.id }

@ -33,8 +33,9 @@ class PlayerRegistration: ModelObject, Storable {
var birthdate: String? var birthdate: String?
var weight: Int = 0 var weight: Int = 0
var source: PlayerDataSource?
internal init(teamRegistration: String = "", firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int) { internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) {
self.teamRegistration = teamRegistration self.teamRegistration = teamRegistration
self.firstName = firstName self.firstName = firstName
self.lastName = lastName self.lastName = lastName
@ -43,6 +44,7 @@ class PlayerRegistration: ModelObject, Storable {
self.registrationType = registrationType self.registrationType = registrationType
self.registrationDate = registrationDate self.registrationDate = registrationDate
self.sex = sex self.sex = sex
self.source = source
} }
internal init(importedPlayer: ImportedPlayer) { internal init(importedPlayer: ImportedPlayer) {
@ -57,6 +59,7 @@ class PlayerRegistration: ModelObject, Storable {
self.clubName = importedPlayer.clubName self.clubName = importedPlayer.clubName
self.ligueName = importedPlayer.ligueName self.ligueName = importedPlayer.ligueName
self.assimilation = importedPlayer.assimilation self.assimilation = importedPlayer.assimilation
self.source = .frenchFederation
} }
internal init(federalData: [String], sex: Int, sexUnknown: Bool) { internal init(federalData: [String], sex: Int, sexUnknown: Bool) {
@ -68,7 +71,7 @@ class PlayerRegistration: ModelObject, Storable {
rank = Int(federalData[5]) rank = Int(federalData[5])
email = federalData[6] email = federalData[6]
phoneNumber = federalData[7] phoneNumber = federalData[7]
// manuallyCreated = false source = .beachPadel
if sexUnknown { if sexUnknown {
if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) { if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) {
self.sex = 0 self.sex = 0
@ -82,6 +85,9 @@ class PlayerRegistration: ModelObject, Storable {
} }
} }
func pasteData() -> String {
[firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ")
}
func contains(_ searchField: String) -> Bool { func contains(_ searchField: String) -> Bool {
firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField) firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField)
@ -116,7 +122,17 @@ class PlayerRegistration: ModelObject, Storable {
} }
func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized switch displayStyle {
case .wide:
lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
case .short:
lastName.trimmed.capitalized + " " + firstName.trimmed.prefix(1).capitalized + "."
}
}
@objc
var canonicalName: String {
playerLabel().folding(options: .diacriticInsensitive, locale: .current).lowercased()
} }
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
@ -219,9 +235,15 @@ class PlayerRegistration: ModelObject, Storable {
case _phoneNumber = "phoneNumber" case _phoneNumber = "phoneNumber"
case _email = "email" case _email = "email"
case _weight = "weight" case _weight = "weight"
case _source = "source"
} }
enum PlayerDataSource: Int, Codable {
case frenchFederation
case beachPadel
}
enum PaymentType: Int, CaseIterable, Identifiable { enum PaymentType: Int, CaseIterable, Identifiable {
var id: Self { var id: Self {
self self

@ -30,6 +30,7 @@ class TeamRegistration: ModelObject, Storable {
var wildCardGroupStage: Bool = false var wildCardGroupStage: Bool = false
var category: Int? var category: Int?
var weight: Int = 0 var weight: Int = 0
var lockWeight: Int?
internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) { internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, category: Int? = nil) {
self.tournament = tournament self.tournament = tournament
@ -46,6 +47,18 @@ class TeamRegistration: ModelObject, Storable {
self.category = category self.category = category
} }
var initialWeight: Int {
lockWeight ?? weight
}
func isImported() -> Bool {
unsortedPlayers().allSatisfy({ $0.source == .beachPadel })
}
func isWildCard() -> Bool {
wildCardBracket || wildCardGroupStage
}
var tournamentCategory: TournamentCategory { var tournamentCategory: TournamentCategory {
get { get {
TournamentCategory(rawValue: category ?? 0) ?? .men TournamentCategory(rawValue: category ?? 0) ?? .men
@ -55,12 +68,33 @@ class TeamRegistration: ModelObject, Storable {
} }
} }
@objc
var canonicalName: String {
players().map { $0.canonicalName }.joined(separator: " ")
}
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
guard let codeClubOrClubName else { return true }
return unsortedPlayers().anySatisfy({
$0.clubName?.contains(codeClubOrClubName) == true || $0.clubName?.contains(codeClubOrClubName) == true
})
}
func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String { func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String {
unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") switch displayStyle {
case .wide:
unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ")
case .short:
unsortedPlayers().map { $0.playerLabel(.wide) }.joined(separator: "\n")
}
} }
func index(in teams: [TeamRegistration]) -> Int? {
teams.firstIndex(where: { $0.id == id })
}
func formattedSeed(in teams: [TeamRegistration]) -> String { func formattedSeed(in teams: [TeamRegistration]) -> String {
if let index = teams.firstIndex(where: { $0.id == id }) { if let index = index(in: teams) {
return "#\(index + 1)" return "#\(index + 1)"
} else { } else {
return "###" return "###"
@ -87,21 +121,36 @@ class TeamRegistration: ModelObject, Storable {
groupStagePosition ?? -1 groupStagePosition ?? -1
} }
func updatePlayers(_ players: Set<PlayerRegistration>) { func resetPositions() {
self.unsortedPlayers().forEach { player in groupStage = nil
if players.contains(player) == false { groupStagePosition = nil
try? DataStore.shared.playerRegistrations.delete(instance: player) bracketPosition = nil
} }
func pasteData() -> String {
[name, playersPasteData(), formattedInscriptionDate()].compactMap({ $0 }).joined(separator: "\n")
}
func formattedInscriptionDate() -> String? {
if let registrationDate {
return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
} }
}
func playersPasteData() -> String {
players().map { $0.pasteData() }.joined(separator: "\n")
}
func updatePlayers(_ players: Set<PlayerRegistration>) {
try? DataStore.shared.playerRegistrations.delete(contentOfs: unsortedPlayers())
setWeight(from: Array(players)) setWeight(from: Array(players))
players.forEach { player in players.forEach { player in
player.teamRegistration = id player.teamRegistration = id
try? DataStore.shared.playerRegistrations.addOrUpdate(instance: player) }
}
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: self)
} }
func qualified() -> Bool { func qualified() -> Bool {
@ -157,7 +206,7 @@ class TeamRegistration: ModelObject, Storable {
func missingPlayerType() -> [Int] { func missingPlayerType() -> [Int] {
let players = unsortedPlayers() let players = unsortedPlayers()
if players.count < 2 { return [] } if players.count >= 2 { return [] }
let s = players.map { $0.sex } let s = players.map { $0.sex }
var missing = mandatoryPlayerType() var missing = mandatoryPlayerType()
s.forEach { i in s.forEach { i in
@ -198,5 +247,20 @@ class TeamRegistration: ModelObject, Storable {
case _category = "category" case _category = "category"
case _weight = "weight" case _weight = "weight"
case _walkOut = "walkOut" case _walkOut = "walkOut"
case _lockWeight = "lockWeight"
}
}
extension TeamRegistration: Hashable {
static func == (lhs: TeamRegistration, rhs: TeamRegistration) -> Bool {
lhs.id == rhs.id
} }
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
enum TeamDataSource: Int, Codable {
case beachPadel
} }

@ -28,7 +28,7 @@ class Tournament : ModelObject, Storable {
var rankSourceDate: Date? var rankSourceDate: Date?
var dayDuration: Int var dayDuration: Int
var teamCount: Int var teamCount: Int
var teamSorting: Int var teamSorting: TeamSortingType
var federalCategory: Int var federalCategory: Int
var federalLevelCategory: Int var federalLevelCategory: Int
var federalAgeCategory: Int var federalAgeCategory: Int
@ -50,7 +50,7 @@ class Tournament : ModelObject, Storable {
@ObservationIgnored @ObservationIgnored
var undoManager: Int = 0 var undoManager: Int = 0
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) { internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) {
self.event = event self.event = event
self.creator = creator self.creator = creator
self.name = name self.name = name
@ -66,7 +66,7 @@ class Tournament : ModelObject, Storable {
self.rankSourceDate = rankSourceDate self.rankSourceDate = rankSourceDate
self.dayDuration = dayDuration self.dayDuration = dayDuration
self.teamCount = teamCount self.teamCount = teamCount
self.teamSorting = teamSorting.rawValue //self.teamSorting = teamSorting.rawValue
self.federalCategory = federalCategory.rawValue self.federalCategory = federalCategory.rawValue
self.federalLevelCategory = federalLevelCategory.rawValue self.federalLevelCategory = federalLevelCategory.rawValue
self.federalAgeCategory = federalAgeCategory.rawValue self.federalAgeCategory = federalAgeCategory.rawValue
@ -81,12 +81,27 @@ class Tournament : ModelObject, Storable {
self.entryFee = entryFee self.entryFee = entryFee
self.maleUnrankedValue = maleUnrankedValue self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue self.femaleUnrankedValue = femaleUnrankedValue
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
} }
enum State { enum State {
case initial case initial
case build case build
} }
var eventObject: Event? {
guard let event else { return nil }
return Store.main.findById(event)
}
func pasteDataForImporting() -> String {
let selectedSortedTeams = selectedSortedTeams()
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams).compactMap { $0.pasteData() }).joined(separator: "\n\n")
}
func club() -> Club? {
eventObject?.clubObject
}
func hasEnded() -> Bool { func hasEnded() -> Bool {
endDate != nil endDate != nil
@ -107,6 +122,74 @@ class Tournament : ModelObject, Storable {
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index) Store.main.filter { $0.tournament == self.id }.sorted(by: \.index)
} }
var clubName: String? {
nil
}
func sortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
return teams + waitingListTeams(in: teams)
}
func selectedSortedTeams() -> [TeamRegistration] {
let start = Date()
var _sortedTeams : [TeamRegistration] = []
let _teams = unsortedTeams().filter({ $0.walkOut == false })
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false}
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
var bracketSeeds = min(teamCount, _completeTeams.count) - groupStageCount * teamsPerGroupStage - wcBracket.count
var groupStageTeamCount = groupStageCount * teamsPerGroupStage - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
if prioritizeClubMembers {
let bracketTeams = (_completeTeams.filter { $0.hasMemberOfClub(clubName) } + _completeTeams.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeamsNoFiltering = Set(_completeTeams).subtracting(bracketTeams)
let groupStageTeams = (groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) } + groupStageTeamsNoFiltering.filter { $0.hasMemberOfClub(clubName) == false }.sorted(using: defaultSorting, order: .ascending)).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
} else {
let bracketTeams = _completeTeams.prefix(bracketSeeds).sorted(using: _currentSelectionSorting, order: .ascending) + wcBracket
let groupStageTeams = Set(_completeTeams).subtracting(bracketTeams).sorted(using: defaultSorting, order: .ascending).prefix(groupStageTeamCount).sorted(using: _currentSelectionSorting, order: .ascending) + wcGroupStage
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
}
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print(id, title(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams
}
func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] {
Set(unsortedTeams()).subtracting(teams).sorted(using: _defaultSorting(), order: .ascending)
}
func bracketCut() -> Int {
max(0, teamCount - groupStageCut())
}
func groupStageCut() -> Int {
groupStageCount * teamsPerGroupStage
}
func cutLabel(index: Int) -> String {
if index < bracketCut() {
return "Tableau"
} else if index - bracketCut() < groupStageCut() {
return "Poule"
} else {
return "Liste d'attente"
}
}
func unsortedTeams() -> [TeamRegistration] { func unsortedTeams() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id } Store.main.filter { $0.tournament == self.id }
} }
@ -176,10 +259,30 @@ class Tournament : ModelObject, Storable {
2 2
} }
func importTeams(_ teams: [FileImportManager.TeamHolder], keepPreviousData: Bool = false) { func importTeams(_ teams: [FileImportManager.TeamHolder]) {
var teamsToImport = [TeamRegistration]()
teams.forEach { team in teams.forEach { team in
addTeam(Set([team.playerOne, team.playerTwo])) if let previousTeam = team.previousTeam {
previousTeam.updatePlayers(team.players)
teamsToImport.append(previousTeam)
} else {
let newTeam = addTeam(team.players)
teamsToImport.append(newTeam)
}
} }
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teamsToImport)
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players })
}
func lockRegistration() {
closedRegistrationDate = Date()
let teams = unsortedTeams()
teams.forEach { team in
team.lockWeight = team.weight
}
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
} }
func updateWeights() { func updateWeights() {
@ -213,7 +316,7 @@ class Tournament : ModelObject, Storable {
// if inscriptionClosed == false { // if inscriptionClosed == false {
// orderedEntries.forEach { entrant in // orderedEntries.forEach { entrant in
// entrant.initialRank = entrant.updatedRank // entrant.weightAtRegistration = entrant.updatedRank
// } // }
// } // }
} }
@ -350,7 +453,7 @@ class Tournament : ModelObject, Storable {
func setBrackets(randomize: Bool) { func setBrackets(randomize: Bool) {
let groupStages = groupStages() let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = Int(teamsPerBracket) // let teamsPerBracket = teamsPerBracket
if groupStageCount != numberOfBracketsAsInt { if groupStageCount != numberOfBracketsAsInt {
buildGroupStages() buildGroupStages()
return return
@ -380,26 +483,16 @@ class Tournament : ModelObject, Storable {
entryFee == nil || entryFee == 0 entryFee == nil || entryFee == 0
} }
func addTeam(_ players: Set<PlayerRegistration>) { func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: Date()) let team = TeamRegistration(tournament: id, registrationDate: Date())
team.tournamentCategory = tournamentCategory team.tournamentCategory = tournamentCategory
team.setWeight(from: Array(players)) team.setWeight(from: Array(players))
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: team)
players.forEach { player in players.forEach { player in
player.teamRegistration = team.id player.teamRegistration = team.id
} }
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players) return team
} }
var teamSortingType: TeamSortingType {
get {
TeamSortingType(rawValue: teamSorting) ?? tournamentLevel.defaultTeamSortingType
}
set {
teamSorting = newValue.rawValue
}
}
var matchFormat: MatchFormat { var matchFormat: MatchFormat {
get { get {
MatchFormat(rawValue: roundFormat ?? 0) ?? .defaultFormatForMatchType(.bracket) MatchFormat(rawValue: roundFormat ?? 0) ?? .defaultFormatForMatchType(.bracket)
@ -456,7 +549,7 @@ class Tournament : ModelObject, Storable {
} }
set { set {
federalLevelCategory = newValue.rawValue federalLevelCategory = newValue.rawValue
teamSortingType = newValue.defaultTeamSortingType teamSorting = newValue.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat() groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1) loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1) matchFormat = roundSmartMatchFormat(1)
@ -501,7 +594,18 @@ class Tournament : ModelObject, Storable {
return matchFormat return matchFormat
} }
} }
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting {
case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\.canonicalName)]
case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\.canonicalName)]
}
}
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.canonicalName)]
override func deleteDependencies() throws { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.unsortedTeams()) try Store.main.deleteDependencies(items: self.unsortedTeams())
try Store.main.deleteDependencies(items: self.groupStages()) try Store.main.deleteDependencies(items: self.groupStages())

@ -0,0 +1,27 @@
//
// MySortDescriptor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 26/03/2024.
//
import Foundation
struct MySortDescriptor<Value> {
var comparator: (Value, Value) -> ComparisonResult
}
extension MySortDescriptor {
static func keyPath<T: Comparable>(_ keyPath: KeyPath<Value, T>) -> Self {
Self { rootA, rootB in
let valueA = rootA[keyPath: keyPath]
let valueB = rootB[keyPath: keyPath]
guard valueA != valueB else {
return .orderedSame
}
return valueA < valueB ? .orderedAscending : .orderedDescending
}
}
}

@ -37,3 +37,42 @@ extension Sequence {
} }
} }
} }
enum SortOrder {
case ascending
case descending
}
extension Sequence {
func sorted(using descriptors: [MySortDescriptor<Element>],
order: SortOrder) -> [Element] {
sorted { valueA, valueB in
for descriptor in descriptors {
let result = descriptor.comparator(valueA, valueB)
switch result {
case .orderedSame:
// Keep iterating if the two elements are equal,
// since that'll let the next descriptor determine
// the sort order:
break
case .orderedAscending:
return order == .ascending
case .orderedDescending:
return order == .descending
}
}
// If no descriptor was able to determine the sort
// order, we'll default to false (similar to when
// using the '<' operator with the built-in API):
return false
}
}
}
extension Sequence {
func sorted(using descriptors: MySortDescriptor<Element>...) -> [Element] {
sorted(using: descriptors, order: .ascending)
}
}

@ -72,8 +72,16 @@ class FileImportManager {
self.weight = playerOne.weight + playerTwo.weight self.weight = playerOne.weight + playerTwo.weight
} }
var players: Set<PlayerRegistration> {
Set([playerOne, playerTwo])
}
func index(in teams: [TeamHolder]) -> Int? {
teams.firstIndex(where: { $0.id == id })
}
func formattedSeed(in teams: [TeamHolder]) -> String { func formattedSeed(in teams: [TeamHolder]) -> String {
if let index = teams.firstIndex(where: { $0.id == id }) { if let index = index(in: teams) {
return "#\(index + 1)" return "#\(index + 1)"
} else { } else {
return "###" return "###"

@ -1327,7 +1327,7 @@ enum EventType: Int, CaseIterable, Identifiable {
} }
} }
enum TeamSortingType: Int, Identifiable, CaseIterable, Hashable { enum TeamSortingType: Int, Identifiable, CaseIterable, Hashable, Codable {
case rank = 1 case rank = 1
case inscriptionDate = 2 case inscriptionDate = 2

@ -9,7 +9,8 @@ import Foundation
class SourceFileManager { class SourceFileManager {
static let shared = SourceFileManager() static let shared = SourceFileManager()
static let beachPadel = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")!
var lastDataSource: String? { var lastDataSource: String? {
UserDefaults.standard.string(forKey: "lastDataSource") UserDefaults.standard.string(forKey: "lastDataSource")
} }

@ -16,7 +16,11 @@ struct EventListView: View {
ForEach(tournaments) { tournament in ForEach(tournaments) { tournament in
NavigationLink(value: tournament) { NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament) HStack {
TournamentCellView(tournament: tournament)
Spacer()
Text(tournament.sortedTeams().count.formatted())
}
} }
.contextMenu { .contextMenu {
Button { Button {

@ -0,0 +1,47 @@
/*
See the LICENSE.txt file for this samples licensing information.
Abstract:
A view that gets displayed when the learn more action buttons is tapped.
*/
import SwiftUI
struct LearnMoreSheetView: View {
@Environment(\.dismiss) private var dismiss
var tournament: Tournament
var body: some View {
VStack(spacing: 20) {
Text("Pourquoi cette étape ?")
.font(.title)
Text("""
Pour terminer la préparation de votre tournoi et pouvoir commencer à convoquer vos joueurs, vous devez inscrire les paires que vous avez préparé dans Padel Club sur le site beach-padel.app.fft.fr.
Padel Club ne peut pas, pour l'instant, faire cette manipulation automatiquement.
Par contre, vous pouvez exporter les paires que vous avez préparé en un simple fichier texte vous permettant ainsi d'accélérer un peu plus la saisie sur le site fédéral.
Une fois vos que vos paires seront inscrites sur beach-padel.app.fft.fr, vous pourrez les importer à nouveau dans Padel Club en un instant, vous donnant accès aux emails et téléphones des joueurs dans le but de les convoquer.
""")
.foregroundStyle(.secondary)
ShareLink(item: tournament.pasteDataForImporting()) {
HStack {
Spacer()
Text("Exporter les inscriptions")
Spacer()
}
}
.buttonStyle(.borderedProminent)
Button("J'ai compris") {
dismiss()
}
}
.padding([.leading, .trailing], 40)
}
}

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
struct FileImportView: View { struct FileImportView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -54,7 +55,7 @@ struct FileImportView: View {
} }
} }
if filteredTeams.isEmpty == false { if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false {
Section { Section {
ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in
LabeledContent { LabeledContent {
@ -224,11 +225,26 @@ struct FileImportView: View {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .bottomBar) {
Button { Button {
// tournament.updateTournamentEntriesWith(teams: filteredTeams, viewContext: viewContext) if selectedOptions.contains(.deleteBeforeImport) { // remove all previous teams
// save() try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
tournament.importTeams(filteredTeams) }
if selectedOptions.contains(.notFoundAreWalkOut) {
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
unfound.forEach { team in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = true
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: unfound)
}
tournament.importTeams(filteredTeams)
dismiss() dismiss()
} label: { } label: {
Text("Valider") Text("Valider")
@ -262,38 +278,25 @@ struct FileImportView: View {
} }
enum TeamImportStrategy: CaseIterable { enum TeamImportStrategy: CaseIterable {
case keepPreviousData
case notFoundAreWalkOut case notFoundAreWalkOut
case deleteBeforeImport case deleteBeforeImport
case updatePosition
case updatePositionWithinBlock
func titleLabel() -> String { func titleLabel() -> String {
switch self { switch self {
case .keepPreviousData:
"Gardez les données existantes"
case .notFoundAreWalkOut: case .notFoundAreWalkOut:
"Mettre les équipes manquantes WO" "Mettre les équipes manquantes WO"
case .deleteBeforeImport: case .deleteBeforeImport:
"Effacer avant d'importer" "Effacer avant d'importer"
case .updatePosition:
"Modifier les positions"
case .updatePositionWithinBlock:
"Modidier les positions par bloc"
} }
} }
func descriptionLabel() -> String { func descriptionLabel() -> String {
switch self { switch self {
case .keepPreviousData:
"Si l'équipe déjà présente, garde la date d'inscription"
case .notFoundAreWalkOut: case .notFoundAreWalkOut:
"Si une équipe déjà présente n'est pas dans la nouvelle liste, elle sera mise à WO" "Si une équipe déjà présente n'est pas dans la nouvelle liste, elle sera mise à WO"
case .deleteBeforeImport: case .deleteBeforeImport:
"Écrase les données précédentes avant d'importer" "Supprime toutes les équipes avant d'importer"
case .updatePosition: // case .lockWeight:
"Mets à jour les positions si changement de poids d'équipe" // "Permets de déplacer les équipes avec leur nouveaux classements sans les déplacer entre les blocs (p500+)"
case .updatePositionWithinBlock:
"Mets à jour les positions seulement au sein du tableau et des poules séparement"
} }
} }
} }

@ -11,44 +11,50 @@ struct UpdateSourceRankDateView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Binding var currentRankSourceDate: Date? @Binding var currentRankSourceDate: Date?
@Binding var confirmUpdateRank: Bool @Binding var confirmUpdateRank: Bool
@State private var forceRefreshLockWeight: Bool = false
@State private var updatingRank = false @State private var updatingRank = false
var tournament: Tournament var tournament: Tournament
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List { List {
let suffix = (false ? "Les incriptions sont closes. Les équipes ne pourront pas être déplacé entre les poules et le tableau. Seule leur position au sein des poules et du tableau, respectivement, seront modifiée." : "Les inscriptions sont toujours ouvertes. Les équipes pourront être déplacé entre les poules et le tableau.") let suffix = (tournament.inscriptionClosed() ? "Les incriptions sont closes. Les équipes ne pourront pas être déplacé entre les poules et le tableau. Seule leur position au sein des poules et du tableau, respectivement, seront modifiée." : "Les inscriptions sont toujours ouvertes. Les équipes pourront être déplacé entre les poules et le tableau.")
Section { Section {
Text("Vous êtes sur le point de mettre à jour les rangs des équipes, cela affectera leur position." + "\n" + suffix) Text("Vous êtes sur le point de mettre à jour les rangs des équipes." + "\n" + suffix)
}
if tournament.inscriptionClosed() {
Section {
Toggle(isOn: $forceRefreshLockWeight) {
Text("Ne pas en tenir compte")
}
}
} }
RowButtonView(title: "Valider") { RowButtonView(title: "Valider") {
updatingRank = true updatingRank = true
//buildMoveArray()
Task { Task {
do { do {
try await tournament.updateRank(to: currentRankSourceDate) try await tournament.updateRank(to: currentRankSourceDate)
await MainActor.run { await MainActor.run {
if tournament.state() == .build { tournament.unsortedPlayers().forEach { player in
//manageEntriesMovement() player.setWeight(in: tournament)
} else { }
//save()
tournament.unsortedPlayers().forEach { player in try? dataStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers())
player.setWeight(in: tournament)
} tournament.unsortedTeams().forEach { team in
team.setWeight(from: team.players())
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers()) if forceRefreshLockWeight {
team.lockWeight = team.weight
tournament.unsortedTeams().forEach { team in
team.setWeight(from: team.players())
} }
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try? dataStore.tournaments.addOrUpdate(instance: tournament)
} }
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try? dataStore.tournaments.addOrUpdate(instance: tournament)
updatingRank = false updatingRank = false
confirmUpdateRank = false confirmUpdateRank = false
} }

@ -22,13 +22,13 @@ struct InscriptionManagerView: View {
@State private var presentPlayerSearch: Bool = false @State private var presentPlayerSearch: Bool = false
@State private var presentPlayerCreation: Bool = false @State private var presentPlayerCreation: Bool = false
@State private var presentImportView: Bool = false @State private var presentImportView: Bool = false
@State private var isLearningMore: Bool = false
@State private var createdPlayers: Set<PlayerRegistration> = Set() @State private var createdPlayers: Set<PlayerRegistration> = Set()
@State private var createdPlayerIds: Set<String> = Set() @State private var createdPlayerIds: Set<String> = Set()
@State private var editedTeam: TeamRegistration? @State private var editedTeam: TeamRegistration?
@State private var pasteString: String? @State private var pasteString: String?
@State private var currentRankSourceDate: Date? @State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false @State private var confirmUpdateRank = false
@State private var updatingRank = false
@State private var selectionSearchField: String? @State private var selectionSearchField: String?
let slideToDeleteTip = SlideToDeleteTip() let slideToDeleteTip = SlideToDeleteTip()
@ -38,10 +38,13 @@ struct InscriptionManagerView: View {
let searchTip = InscriptionManagerSearchInputTip() let searchTip = InscriptionManagerSearchInputTip()
let createTip = InscriptionManagerCreateInputTip() let createTip = InscriptionManagerCreateInputTip()
let rankUpdateTip = InscriptionManagerRankUpdateTip() let rankUpdateTip = InscriptionManagerRankUpdateTip()
let padelBeachExportTip = PadelBeachExportTip()
let padelBeachImportTip = PadelBeachImportTip()
let categoryOption: PlayerFilterOption let categoryOption: PlayerFilterOption
let filterable: Bool let filterable: Bool
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
@ -54,290 +57,11 @@ struct InscriptionManagerView: View {
filterable = true filterable = true
} }
} }
private func _searchSource() -> String? {
selectionSearchField ?? pasteString
}
private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? {
let text = pasteField.canonicalVersion
let nameComponents = text.components(separatedBy: .whitespacesAndNewlines).compactMap { $0.isEmpty ? nil : $0 }.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if _filterOption() == .male {
andPredicates.append(NSPredicate(format: "male == YES"))
} else if _filterOption() == .female {
andPredicates.append(NSPredicate(format: "male == NO"))
}
if let mostRecentDate {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {
orPredicates = nameComponents.pairs().map {
return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) }
} else {
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }
}
let matches = text.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)])
}
return predicate
}
private func _currentSelection() -> Set<PlayerRegistration> {
var currentSelection = Set<PlayerRegistration>()
createdPlayerIds.compactMap { id in
fetchPlayers.first(where: { id == $0.license })
}.forEach { player in
let player = PlayerRegistration(importedPlayer: player)
player.setWeight(in: tournament)
currentSelection.insert(player)
}
createdPlayerIds.compactMap { id in
createdPlayers.first(where: { id == $0.id })
}.forEach {
currentSelection.insert($0)
}
return currentSelection
}
private func _createTeam() {
tournament.addTeam(_currentSelection())
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
}
private func _updateTeam() {
editedTeam?.updatePlayers(_currentSelection())
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
editedTeam = nil
}
private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) {
Section {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) {
PlayerView(player: p).tag(p.id)
}
if let p = fetchPlayers.first(where: { $0.license == id }) {
ImportedPlayerView(player: p).tag(p.license!)
}
}
// ForEach(createdPlayers.sorted(by: \.computedRank)) { player in
// PlayerView(player: player).tag(player.id)
// }
}
if editedTeam == nil {
if createdPlayerIds.isEmpty {
RowButtonView(title: "Bloquer une place") {
_createTeam()
}
} else {
RowButtonView(title: "Ajouter l'équipe") {
_createTeam()
}
}
} else {
RowButtonView(title: "Modifier l'équipe") {
_updateTeam()
}
}
if let pasteString {
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
.buttonStyle(.borderless)
}
}
if fetchPlayers.isEmpty {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("Aucun joueur classé n'a été trouvé dans ce message.")
} actions: {
RowButtonView(title: "Créer un joueur non classé") {
presentPlayerCreation = true
}
RowButtonView(title: "Effacer cette recherche") {
self.pasteString = nil
}
}
} else {
Section {
ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
}
}
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2 {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
}
var count: Int {
return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count
}
var hitTarget: Int {
if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 {
if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 }
} else {
return 2
}
return 1
}
private func _teamRegisteredView() -> some View {
List {
Section {
_rankHandlerView()
let duplicates = tournament.duplicates()
DisclosureGroup {
if duplicates.isEmpty == false {
ForEach(duplicates) { player in
PlayerView(player: player)
}
}
} label: {
LabeledContent {
Text(duplicates.count.formatted())
} label: {
Text("Doublons")
}
}
} header: {
Text("Informations")
}
if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false {
Section {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
}
Section {
TipView(slideToDeleteTip)
.tipStyle(tint: nil)
}
let unfilteredTeams = tournament.teams()
let teams = searchField.isEmpty ? unfilteredTeams : unfilteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("\(searchField) est introuvable dans les équipes inscrites.")
} actions: {
RowButtonView(title: "Modifier la recherche") {
searchField = ""
presentSearch = true
}
RowButtonView(title: "Créer une équipe") {
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: tournament.rankSourceDate)
pasteString = searchField
}
}
}
RowButtonView(title: "D'accord") {
searchField = ""
presentSearch = false
}
}
}
ForEach(teams) { team in
Section {
TeamRowView(team: team)
} header: {
HStack {
Text("Équipe " + team.formattedSeed(in: unfilteredTeams))
Spacer()
Text(team.weight.formatted())
}
} footer: {
HStack {
Spacer()
Menu {
Button("Éditer") {
editedTeam = team
team.unsortedPlayers().forEach { player in
createdPlayers.insert(player)
createdPlayerIds.insert(player.id)
}
}
Divider()
Button(role: .destructive) {
try? dataStore.teamRegistrations.delete(instance: team)
} label: {
LabelDelete()
}
} label: {
LabelOptions().labelStyle(.titleOnly)
}
}
}
.headerProminence(.increased)
}
}
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet)
.autocorrectionDisabled()
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
_managementView() _managementView()
if createdPlayerIds.isEmpty == false || pasteString != nil || editedTeam != nil { if _isEditingTeam() {
_buildingTeamView() _buildingTeamView()
} else if tournament.unsortedTeams().isEmpty { } else if tournament.unsortedTeams().isEmpty {
_inscriptionTipsView() _inscriptionTipsView()
@ -345,6 +69,9 @@ struct InscriptionManagerView: View {
_teamRegisteredView() _teamRegisteredView()
} }
} }
.sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament)
}
.sheet(isPresented: $presentPlayerSearch, onDismiss: { .sheet(isPresented: $presentPlayerSearch, onDismiss: {
selectionSearchField = nil selectionSearchField = nil
}) { }) {
@ -375,43 +102,46 @@ struct InscriptionManagerView: View {
FileImportView(fileContent: nil) FileImportView(fileContent: nil)
} }
} }
.onChange(of: tournament.prioritizeClubMembers) {
_save()
}
.onChange(of: tournament.teamSorting) {
_save()
}
.onChange(of: currentRankSourceDate) { .onChange(of: currentRankSourceDate) {
// if let currentRankSourceDate, tournament.currentRankSourceDate != currentRankSourceDate { if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
// confirmUpdateRank = true confirmUpdateRank = true
// } }
confirmUpdateRank = true
} }
.sheet(isPresented: $confirmUpdateRank, onDismiss: { .sheet(isPresented: $confirmUpdateRank, onDismiss: {
currentRankSourceDate = tournament.rankSourceDate currentRankSourceDate = tournament.rankSourceDate
}) { }) {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
} }
.toolbar { .toolbar {
if createdPlayerIds.isEmpty == false { if _isEditingTeam() {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
pasteString = nil
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
} }
} }
} } else {
if editedTeam == nil {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Menu { Menu {
if tournament.inscriptionClosed() == false { if tournament.inscriptionClosed() == false {
Menu { Menu {
//sortingTypePickerView _sortingTypePickerView()
} label: { } label: {
Text("Méthode de sélection") Text("Méthode de sélection")
Text(tournament.teamSortingType.localizedLabel()) Text(tournament.teamSorting.localizedLabel())
} }
Divider() Divider()
rankingDateSourcePickerView(showDateInLabel: true) rankingDateSourcePickerView(showDateInLabel: true)
if tournament.teamSortingType == .inscriptionDate { if tournament.teamSorting == .inscriptionDate {
Divider() Divider()
//prioritizeClubMembersButton _prioritizeClubMembersButton()
} }
Divider() Divider()
Button { Button {
@ -425,19 +155,16 @@ struct InscriptionManagerView: View {
Label("Clôturer", systemImage: "lock") Label("Clôturer", systemImage: "lock")
} }
Divider() Divider()
// ShareLink(item: tournament.pasteDataForImporting) { ShareLink(item: tournament.pasteDataForImporting()) {
// Text("Exporter les paires") Text("Exporter les paires")
// } }
Button { Button {
presentImportView = true presentImportView = true
} label: { } label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down") Label("Importer beach-padel", systemImage: "square.and.arrow.down")
} }
if let url = URL(string: "beach-padel.app.fft.fr") { Link(destination: SourceFileManager.beachPadel) {
Link(destination: url) { Label("beach-padel.app.fft.fr", systemImage: "safari")
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} }
} else { } else {
Button { Button {
@ -457,12 +184,89 @@ struct InscriptionManagerView: View {
} }
} }
} }
.navigationBarBackButtonHidden(createdPlayerIds.isEmpty == false) .navigationBarBackButtonHidden(_isEditingTeam())
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions") .navigationTitle("Inscriptions")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
}
private func _isEditingTeam() -> Bool {
createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil
}
private func _teamRegisteredView() -> some View {
List {
Section {
_rankHandlerView()
let duplicates = tournament.duplicates()
DisclosureGroup {
if duplicates.isEmpty == false {
ForEach(duplicates) { player in
PlayerView(player: player)
}
}
} label: {
LabeledContent {
Text(duplicates.count.formatted())
} label: {
Text("Doublons")
}
}
} header: {
Text("Informations")
}
_relatedTips()
let unfilteredTeams = tournament.sortedTeams()
let teams = searchField.isEmpty ? unfilteredTeams : unfilteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("\(searchField) est introuvable dans les équipes inscrites.")
} actions: {
RowButtonView(title: "Modifier la recherche") {
searchField = ""
presentSearch = true
}
RowButtonView(title: "Créer une équipe") {
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = searchField
}
}
}
RowButtonView(title: "D'accord") {
searchField = ""
presentSearch = false
}
}
}
ForEach(teams) { team in
let teamIndex = team.index(in: unfilteredTeams)
Section {
TeamRowView(team: team)
} header: {
_teamHeaderView(team, teamIndex: teamIndex)
} footer: {
_teamFooterView(team)
}
.headerProminence(.increased)
}
}
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet)
.autocorrectionDisabled()
} }
@MainActor
private func _managementView() -> some View { private func _managementView() -> some View {
HStack { HStack {
Button { Button {
@ -481,12 +285,8 @@ struct InscriptionManagerView: View {
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 { fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
await MainActor.run() { pasteString = first
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: tournament.rankSourceDate)
pasteString = first
}
}
} }
Button { Button {
@ -517,9 +317,6 @@ struct InscriptionManagerView: View {
if currentRankSourceDate == nil { if currentRankSourceDate == nil {
Text("inconnu").tag(nil as Date?) Text("inconnu").tag(nil as Date?)
} }
let dates = Array(Set(SourceFileManager.shared.allFilesSortedByDate(tournament.tournamentCategory.rankingDataSourceMale).map({ $0.dateFromPath }))).sorted().reversed()
ForEach(dates, id: \.self) { date in ForEach(dates, id: \.self) { date in
Text(date.monthYearFormatted).tag(date as Date?) Text(date.monthYearFormatted).tag(date as Date?)
} }
@ -562,13 +359,15 @@ struct InscriptionManagerView: View {
} }
@ViewBuilder @ViewBuilder
func _inscriptionTipsView() -> some View { private func _inscriptionTipsView() -> some View {
List { List {
Section { Section {
TipView(fileTip) { action in TipView(fileTip) { action in
if action.id == "website" { if action.id == "website" {
UIApplication.shared.open(SourceFileManager.beachPadel)
} else if action.id == "add-team-file" { } else if action.id == "add-team-file" {
presentImportView = true
} }
} }
.tipStyle(tint: nil) .tipStyle(tint: nil)
@ -609,7 +408,7 @@ struct InscriptionManagerView: View {
} }
@ViewBuilder @ViewBuilder
func _rankHandlerView() -> some View { private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false { if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
Section { Section {
TipView(rankUpdateTip) { action in TipView(rankUpdateTip) { action in
@ -623,7 +422,359 @@ struct InscriptionManagerView: View {
} }
} }
func _save() { @ViewBuilder
private func _relatedTips() -> some View {
if pasteString?.isEmpty == true
&& createdPlayerIds.isEmpty
&& tournament.unsortedTeams().count >= tournament.teamCount
&& tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
Section {
TipView(padelBeachExportTip) { action in
if action.id == "more-info-export" {
isLearningMore = true
}
if action.id == "padel-beach" {
UIApplication.shared.open(SourceFileManager.beachPadel)
}
}
.tipStyle(tint: nil)
}
Section {
TipView(padelBeachImportTip) { action in
if action.id == "more-info-import" {
presentImportView = true
}
}
.tipStyle(tint: nil)
}
}
if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false {
Section {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
}
Section {
TipView(slideToDeleteTip)
.tipStyle(tint: nil)
}
}
private func _searchSource() -> String? {
selectionSearchField ?? pasteString
}
private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? {
let text = pasteField.canonicalVersion
let nameComponents = text.components(separatedBy: .whitespacesAndNewlines).compactMap { $0.isEmpty ? nil : $0 }.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if _filterOption() == .male {
andPredicates.append(NSPredicate(format: "male == YES"))
} else if _filterOption() == .female {
andPredicates.append(NSPredicate(format: "male == NO"))
}
if let mostRecentDate {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {
orPredicates = nameComponents.pairs().map {
return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) }
} else {
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }
}
let matches = text.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)])
}
return predicate
}
private func _currentSelection() -> Set<PlayerRegistration> {
var currentSelection = Set<PlayerRegistration>()
createdPlayerIds.compactMap { id in
fetchPlayers.first(where: { id == $0.license })
}.forEach { player in
let player = PlayerRegistration(importedPlayer: player)
player.setWeight(in: tournament)
currentSelection.insert(player)
}
createdPlayerIds.compactMap { id in
createdPlayers.first(where: { id == $0.id })
}.forEach {
currentSelection.insert($0)
}
return currentSelection
}
private func _createTeam() {
let players = _currentSelection()
let team = tournament.addTeam(players)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
}
private func _updateTeam() {
guard let editedTeam else { return }
let players = _currentSelection()
editedTeam.updatePlayers(players)
try? dataStore.teamRegistrations.addOrUpdate(instance: editedTeam)
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
self.editedTeam = nil
}
private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) {
Section {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) {
PlayerView(player: p).tag(p.id)
}
if let p = fetchPlayers.first(where: { $0.license == id }) {
ImportedPlayerView(player: p).tag(p.license!)
}
}
}
if editedTeam == nil {
if createdPlayerIds.isEmpty {
RowButtonView(title: "Bloquer une place") {
_createTeam()
}
} else {
RowButtonView(title: "Ajouter l'équipe") {
_createTeam()
}
}
} else {
RowButtonView(title: "Modifier l'équipe") {
_updateTeam()
}
}
if let pasteString {
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
.buttonStyle(.borderless)
}
}
if fetchPlayers.isEmpty {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("Aucun joueur classé n'a été trouvé dans ce message.")
} actions: {
RowButtonView(title: "Créer un joueur non classé") {
presentPlayerCreation = true
}
RowButtonView(title: "Effacer cette recherche") {
self.pasteString = nil
}
}
} else {
Section {
ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
}
}
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2 {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
}
private var count: Int {
return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count
}
private var hitTarget: Int {
if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 {
if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 }
} else {
return 2
}
return 1
}
@ViewBuilder
private func _sortingTypePickerView() -> some View {
@Bindable var tournament = tournament
Picker(selection: $tournament.teamSorting) {
ForEach(TeamSortingType.allCases) {
Text($0.localizedLabel()).tag($0)
}
} label: {
}
}
@ViewBuilder
private func _prioritizeClubMembersButton() -> some View {
@Bindable var tournament = tournament
if let federalClub = tournament.club() {
Menu {
Picker(selection: $tournament.prioritizeClubMembers) {
Text("Oui").tag(true)
Text("Non").tag(false)
} label: {
}
.labelsHidden()
} label: {
Text("Membres prioritaires")
Text(federalClub.acronym)
}
Divider()
} else if let event = tournament.eventObject {
NavigationLink {
ClubSearchView()
} label: {
Text("Identifier le club")
}
Divider()
}
}
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View {
HStack {
if let teamIndex {
Text("#" + (teamIndex + 1).formatted())
}
if team.unsortedPlayers().isEmpty == false {
Text(team.weight.formatted())
}
if team.isWildCard() {
Text("wildcard").italic().font(.caption)
}
Spacer()
if team.walkOut {
Text("WO")
} else if let teamIndex {
Text(tournament.cutLabel(index: teamIndex))
}
}
}
private func _teamFooterView(_ team: TeamRegistration) -> some View {
HStack {
if let formattedRegistrationDate = team.formattedInscriptionDate() {
Text(formattedRegistrationDate).font(.caption).foregroundStyle(.secondary)
}
Spacer()
_teamMenuOptionView(team)
}
}
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu {
Section {
Button("Éditer les joueurs") {
editedTeam = team
team.unsortedPlayers().forEach { player in
createdPlayers.insert(player)
createdPlayerIds.insert(player.id)
}
}
Divider()
Toggle(isOn: .init(get: {
return team.wildCardBracket
}, set: { value in
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle")
}
Toggle(isOn: .init(get: {
return team.wildCardGroupStage
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.walkOut = false
team.wildCardGroupStage = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle")
}
Divider()
Toggle(isOn: .init(get: {
return team.walkOut
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle")
}
Divider()
Button(role: .destructive) {
try? dataStore.teamRegistrations.delete(instance: team)
} label: {
LabelDelete()
}
} header: {
Text(team.teamLabel(.short))
}
} label: {
LabelOptions().labelStyle(.titleOnly)
.font(.caption)
}
}
private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament) try? dataStore.tournaments.addOrUpdate(instance: tournament)
} }
} }

Loading…
Cancel
Save