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. 86
      PadelClub/Data/TeamRegistration.swift
  5. 144
      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. 1
      PadelClub/Manager/SourceFileManager.swift
  11. 6
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  12. 47
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  13. 43
      PadelClub/Views/Tournament/FileImportView.swift
  14. 38
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  15. 787
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -100,6 +100,7 @@
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; };
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.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 */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.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 */; };
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784032B91C280000F62A6 /* EmptyActivityView.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 */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.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>"; };
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>"; };
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>"; };
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>"; };
@ -369,6 +372,7 @@
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>"; };
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>"; };
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>"; };
@ -742,6 +746,7 @@
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */,
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -872,6 +877,7 @@
FF6EC9032B9479F500EA7F5A /* Sequence+Extensions.swift */,
FF6EC9082B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift */,
FF6EC90A2B947AC000EA7F5A /* Array+Extensions.swift */,
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1140,6 +1146,7 @@
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
@ -1178,6 +1185,7 @@
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */,
C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */,

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

@ -33,8 +33,9 @@ class PlayerRegistration: ModelObject, Storable {
var birthdate: String?
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.firstName = firstName
self.lastName = lastName
@ -43,6 +44,7 @@ class PlayerRegistration: ModelObject, Storable {
self.registrationType = registrationType
self.registrationDate = registrationDate
self.sex = sex
self.source = source
}
internal init(importedPlayer: ImportedPlayer) {
@ -57,6 +59,7 @@ class PlayerRegistration: ModelObject, Storable {
self.clubName = importedPlayer.clubName
self.ligueName = importedPlayer.ligueName
self.assimilation = importedPlayer.assimilation
self.source = .frenchFederation
}
internal init(federalData: [String], sex: Int, sexUnknown: Bool) {
@ -68,7 +71,7 @@ class PlayerRegistration: ModelObject, Storable {
rank = Int(federalData[5])
email = federalData[6]
phoneNumber = federalData[7]
// manuallyCreated = false
source = .beachPadel
if sexUnknown {
if sex == 1 && FileImportManager.shared.foundInWomenData(license: federalData[3]) {
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 {
firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField)
@ -116,7 +122,17 @@ class PlayerRegistration: ModelObject, Storable {
}
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 {
@ -219,7 +235,13 @@ class PlayerRegistration: ModelObject, Storable {
case _phoneNumber = "phoneNumber"
case _email = "email"
case _weight = "weight"
case _source = "source"
}
enum PlayerDataSource: Int, Codable {
case frenchFederation
case beachPadel
}
enum PaymentType: Int, CaseIterable, Identifiable {

@ -30,6 +30,7 @@ class TeamRegistration: ModelObject, Storable {
var wildCardGroupStage: Bool = false
var category: Int?
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) {
self.tournament = tournament
@ -46,6 +47,18 @@ class TeamRegistration: ModelObject, Storable {
self.category = category
}
var initialWeight: Int {
lockWeight ?? weight
}
func isImported() -> Bool {
unsortedPlayers().allSatisfy({ $0.source == .beachPadel })
}
func isWildCard() -> Bool {
wildCardBracket || wildCardGroupStage
}
var tournamentCategory: TournamentCategory {
get {
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 {
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 {
if let index = teams.firstIndex(where: { $0.id == id }) {
if let index = index(in: teams) {
return "#\(index + 1)"
} else {
return "###"
@ -87,21 +121,36 @@ class TeamRegistration: ModelObject, Storable {
groupStagePosition ?? -1
}
func updatePlayers(_ players: Set<PlayerRegistration>) {
self.unsortedPlayers().forEach { player in
if players.contains(player) == false {
try? DataStore.shared.playerRegistrations.delete(instance: player)
}
func resetPositions() {
groupStage = nil
groupStagePosition = nil
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))
players.forEach { player in
player.teamRegistration = id
try? DataStore.shared.playerRegistrations.addOrUpdate(instance: player)
}
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: self)
}
func qualified() -> Bool {
@ -157,7 +206,7 @@ class TeamRegistration: ModelObject, Storable {
func missingPlayerType() -> [Int] {
let players = unsortedPlayers()
if players.count < 2 { return [] }
if players.count >= 2 { return [] }
let s = players.map { $0.sex }
var missing = mandatoryPlayerType()
s.forEach { i in
@ -198,5 +247,20 @@ class TeamRegistration: ModelObject, Storable {
case _category = "category"
case _weight = "weight"
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 dayDuration: Int
var teamCount: Int
var teamSorting: Int
var teamSorting: TeamSortingType
var federalCategory: Int
var federalLevelCategory: Int
var federalAgeCategory: Int
@ -50,7 +50,7 @@ class Tournament : ModelObject, Storable {
@ObservationIgnored
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.creator = creator
self.name = name
@ -66,7 +66,7 @@ class Tournament : ModelObject, Storable {
self.rankSourceDate = rankSourceDate
self.dayDuration = dayDuration
self.teamCount = teamCount
self.teamSorting = teamSorting.rawValue
//self.teamSorting = teamSorting.rawValue
self.federalCategory = federalCategory.rawValue
self.federalLevelCategory = federalLevelCategory.rawValue
self.federalAgeCategory = federalAgeCategory.rawValue
@ -81,6 +81,7 @@ class Tournament : ModelObject, Storable {
self.entryFee = entryFee
self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
}
enum State {
@ -88,6 +89,20 @@ class Tournament : ModelObject, Storable {
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 {
endDate != nil
}
@ -107,6 +122,74 @@ class Tournament : ModelObject, Storable {
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] {
Store.main.filter { $0.tournament == self.id }
}
@ -176,10 +259,30 @@ class Tournament : ModelObject, Storable {
2
}
func importTeams(_ teams: [FileImportManager.TeamHolder], keepPreviousData: Bool = false) {
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
var teamsToImport = [TeamRegistration]()
teams.forEach { team in
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
addTeam(Set([team.playerOne, team.playerTwo]))
team.lockWeight = team.weight
}
try? DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
}
func updateWeights() {
@ -213,7 +316,7 @@ class Tournament : ModelObject, Storable {
// if inscriptionClosed == false {
// orderedEntries.forEach { entrant in
// entrant.initialRank = entrant.updatedRank
// entrant.weightAtRegistration = entrant.updatedRank
// }
// }
}
@ -350,7 +453,7 @@ class Tournament : ModelObject, Storable {
func setBrackets(randomize: Bool) {
let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = Int(teamsPerBracket)
// let teamsPerBracket = teamsPerBracket
if groupStageCount != numberOfBracketsAsInt {
buildGroupStages()
return
@ -380,24 +483,14 @@ class Tournament : ModelObject, Storable {
entryFee == nil || entryFee == 0
}
func addTeam(_ players: Set<PlayerRegistration>) {
func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: Date())
team.tournamentCategory = tournamentCategory
team.setWeight(from: Array(players))
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: team)
players.forEach { player in
player.teamRegistration = team.id
}
try? DataStore.shared.playerRegistrations.addOrUpdate(contentOfs: players)
}
var teamSortingType: TeamSortingType {
get {
TeamSortingType(rawValue: teamSorting) ?? tournamentLevel.defaultTeamSortingType
}
set {
teamSorting = newValue.rawValue
}
return team
}
var matchFormat: MatchFormat {
@ -456,7 +549,7 @@ class Tournament : ModelObject, Storable {
}
set {
federalLevelCategory = newValue.rawValue
teamSortingType = newValue.defaultTeamSortingType
teamSorting = newValue.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
@ -502,6 +595,17 @@ class Tournament : ModelObject, Storable {
}
}
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 {
try Store.main.deleteDependencies(items: self.unsortedTeams())
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
}
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 {
if let index = teams.firstIndex(where: { $0.id == id }) {
if let index = index(in: teams) {
return "#\(index + 1)"
} else {
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 inscriptionDate = 2

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

@ -16,7 +16,11 @@ struct EventListView: View {
ForEach(tournaments) { tournament in
NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament)
HStack {
TournamentCellView(tournament: tournament)
Spacer()
Text(tournament.sortedTeams().count.formatted())
}
}
.contextMenu {
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
struct FileImportView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@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 {
ForEach(TeamImportStrategy.allCases, id: \.self) { strategy in
LabeledContent {
@ -224,11 +225,26 @@ struct FileImportView: View {
ToolbarItem(placement: .bottomBar) {
Button {
// tournament.updateTournamentEntriesWith(teams: filteredTeams, viewContext: viewContext)
// save()
tournament.importTeams(filteredTeams)
if selectedOptions.contains(.deleteBeforeImport) { // remove all previous teams
try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
}
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()
} label: {
Text("Valider")
@ -262,38 +278,25 @@ struct FileImportView: View {
}
enum TeamImportStrategy: CaseIterable {
case keepPreviousData
case notFoundAreWalkOut
case deleteBeforeImport
case updatePosition
case updatePositionWithinBlock
func titleLabel() -> String {
switch self {
case .keepPreviousData:
"Gardez les données existantes"
case .notFoundAreWalkOut:
"Mettre les équipes manquantes WO"
case .deleteBeforeImport:
"Effacer avant d'importer"
case .updatePosition:
"Modifier les positions"
case .updatePositionWithinBlock:
"Modidier les positions par bloc"
}
}
func descriptionLabel() -> String {
switch self {
case .keepPreviousData:
"Si l'équipe déjà présente, garde la date d'inscription"
case .notFoundAreWalkOut:
"Si une équipe déjà présente n'est pas dans la nouvelle liste, elle sera mise à WO"
case .deleteBeforeImport:
"Écrase les données précédentes avant d'importer"
case .updatePosition:
"Mets à jour les positions si changement de poids d'équipe"
case .updatePositionWithinBlock:
"Mets à jour les positions seulement au sein du tableau et des poules séparement"
"Supprime toutes les équipes avant d'importer"
// case .lockWeight:
// "Permets de déplacer les équipes avec leur nouveaux classements sans les déplacer entre les blocs (p500+)"
}
}
}

@ -11,43 +11,49 @@ struct UpdateSourceRankDateView: View {
@EnvironmentObject var dataStore: DataStore
@Binding var currentRankSourceDate: Date?
@Binding var confirmUpdateRank: Bool
@State private var forceRefreshLockWeight: Bool = false
@State private var updatingRank = false
var tournament: Tournament
var body: some View {
NavigationStack {
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 {
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") {
updatingRank = true
//buildMoveArray()
Task {
do {
try await tournament.updateRank(to: currentRankSourceDate)
await MainActor.run {
if tournament.state() == .build {
//manageEntriesMovement()
} else {
//save()
tournament.unsortedPlayers().forEach { player in
player.setWeight(in: tournament)
}
tournament.unsortedPlayers().forEach { player in
player.setWeight(in: tournament)
}
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers())
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers())
tournament.unsortedTeams().forEach { team in
team.setWeight(from: team.players())
tournament.unsortedTeams().forEach { team in
team.setWeight(from: team.players())
if forceRefreshLockWeight {
team.lockWeight = team.weight
}
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
try? dataStore.tournaments.addOrUpdate(instance: tournament)
}
try? dataStore.tournaments.addOrUpdate(instance: tournament)
updatingRank = false
confirmUpdateRank = false

@ -22,13 +22,13 @@ struct InscriptionManagerView: View {
@State private var presentPlayerSearch: Bool = false
@State private var presentPlayerCreation: Bool = false
@State private var presentImportView: Bool = false
@State private var isLearningMore: Bool = false
@State private var createdPlayers: Set<PlayerRegistration> = Set()
@State private var createdPlayerIds: Set<String> = Set()
@State private var editedTeam: TeamRegistration?
@State private var pasteString: String?
@State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false
@State private var updatingRank = false
@State private var selectionSearchField: String?
let slideToDeleteTip = SlideToDeleteTip()
@ -38,9 +38,12 @@ struct InscriptionManagerView: View {
let searchTip = InscriptionManagerSearchInputTip()
let createTip = InscriptionManagerCreateInputTip()
let rankUpdateTip = InscriptionManagerRankUpdateTip()
let padelBeachExportTip = PadelBeachExportTip()
let padelBeachImportTip = PadelBeachImportTip()
let categoryOption: PlayerFilterOption
let filterable: Bool
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
init(tournament: Tournament) {
self.tournament = tournament
@ -55,289 +58,10 @@ struct InscriptionManagerView: View {
}
}
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 {
VStack(spacing: 0) {
_managementView()
if createdPlayerIds.isEmpty == false || pasteString != nil || editedTeam != nil {
if _isEditingTeam() {
_buildingTeamView()
} else if tournament.unsortedTeams().isEmpty {
_inscriptionTipsView()
@ -345,6 +69,9 @@ struct InscriptionManagerView: View {
_teamRegisteredView()
}
}
.sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament)
}
.sheet(isPresented: $presentPlayerSearch, onDismiss: {
selectionSearchField = nil
}) {
@ -375,43 +102,46 @@ struct InscriptionManagerView: View {
FileImportView(fileContent: nil)
}
}
.onChange(of: tournament.prioritizeClubMembers) {
_save()
}
.onChange(of: tournament.teamSorting) {
_save()
}
.onChange(of: currentRankSourceDate) {
// if let currentRankSourceDate, tournament.currentRankSourceDate != currentRankSourceDate {
// confirmUpdateRank = true
// }
confirmUpdateRank = true
if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
confirmUpdateRank = true
}
}
.sheet(isPresented: $confirmUpdateRank, onDismiss: {
currentRankSourceDate = tournament.rankSourceDate
}) {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
}
.toolbar {
if createdPlayerIds.isEmpty == false {
if _isEditingTeam() {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
pasteString = nil
createdPlayers.removeAll()
createdPlayerIds.removeAll()
}
}
}
if editedTeam == nil {
} else {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
if tournament.inscriptionClosed() == false {
Menu {
//sortingTypePickerView
_sortingTypePickerView()
} label: {
Text("Méthode de sélection")
Text(tournament.teamSortingType.localizedLabel())
Text(tournament.teamSorting.localizedLabel())
}
Divider()
rankingDateSourcePickerView(showDateInLabel: true)
if tournament.teamSortingType == .inscriptionDate {
if tournament.teamSorting == .inscriptionDate {
Divider()
//prioritizeClubMembersButton
_prioritizeClubMembersButton()
}
Divider()
Button {
@ -425,19 +155,16 @@ struct InscriptionManagerView: View {
Label("Clôturer", systemImage: "lock")
}
Divider()
// ShareLink(item: tournament.pasteDataForImporting) {
// Text("Exporter les paires")
// }
ShareLink(item: tournament.pasteDataForImporting()) {
Text("Exporter les paires")
}
Button {
presentImportView = true
} label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down")
}
if let url = URL(string: "beach-padel.app.fft.fr") {
Link(destination: url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
Link(destination: SourceFileManager.beachPadel) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} else {
Button {
@ -457,12 +184,89 @@ struct InscriptionManagerView: View {
}
}
}
.navigationBarBackButtonHidden(createdPlayerIds.isEmpty == false)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(_isEditingTeam())
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions")
.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 {
HStack {
Button {
@ -481,12 +285,8 @@ struct InscriptionManagerView: View {
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: tournament.rankSourceDate)
pasteString = first
}
}
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = first
}
Button {
@ -517,9 +317,6 @@ struct InscriptionManagerView: View {
if currentRankSourceDate == nil {
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
Text(date.monthYearFormatted).tag(date as Date?)
}
@ -562,13 +359,15 @@ struct InscriptionManagerView: View {
}
@ViewBuilder
func _inscriptionTipsView() -> some View {
private func _inscriptionTipsView() -> some View {
List {
Section {
TipView(fileTip) { action in
if action.id == "website" {
UIApplication.shared.open(SourceFileManager.beachPadel)
} else if action.id == "add-team-file" {
presentImportView = true
}
}
.tipStyle(tint: nil)
@ -609,7 +408,7 @@ struct InscriptionManagerView: View {
}
@ViewBuilder
func _rankHandlerView() -> some View {
private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
Section {
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)
}
}

Loading…
Cancel
Save