Merge branch 'main'

Conflicts:
	PadelClub/Data/Match.swift
	PadelClub/Data/Tournament.swift
	PadelClub/Views/Navigation/MainView.swift
	PadelClub/Views/Tournament/Screen/TournamentRankView.swift
multistore
Razmig Sarkissian 1 year ago
commit 51b9102c4f
  1. 14
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/AppSettings.swift
  3. 2
      PadelClub/Data/Club.swift
  4. 2
      PadelClub/Data/Court.swift
  5. 2
      PadelClub/Data/DateInterval.swift
  6. 2
      PadelClub/Data/Event.swift
  7. 2
      PadelClub/Data/GroupStage.swift
  8. 33
      PadelClub/Data/Match.swift
  9. 2
      PadelClub/Data/MatchScheduler.swift
  10. 2
      PadelClub/Data/MonthData.swift
  11. 18
      PadelClub/Data/PlayerRegistration.swift
  12. 25
      PadelClub/Data/Round.swift
  13. 8
      PadelClub/Data/TeamRegistration.swift
  14. 2
      PadelClub/Data/TeamScore.swift
  15. 47
      PadelClub/Data/Tournament.swift
  16. 19
      PadelClub/Utils/FileImportManager.swift
  17. 30
      PadelClub/Utils/HtmlGenerator.swift
  18. 32
      PadelClub/Utils/HtmlService.swift
  19. 1
      PadelClub/Utils/URLs.swift
  20. 6
      PadelClub/ViewModel/SeedInterval.swift
  21. 2
      PadelClub/Views/Calling/GroupStageCallingView.swift
  22. 23
      PadelClub/Views/Club/ClubSearchView.swift
  23. 6
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  24. 85
      PadelClub/Views/GroupStage/LoserGroupStageSettingsView.swift
  25. 4
      PadelClub/Views/Match/MatchSetupView.swift
  26. 2
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  27. 2
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  28. 13
      PadelClub/Views/Round/LoserRoundView.swift
  29. 11
      PadelClub/Views/Round/LoserRoundsView.swift
  30. 26
      PadelClub/Views/Team/TeamPickerView.swift
  31. 4
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  32. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  33. 34
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  34. 205
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  35. 7
      PadelClub/Views/Tournament/TournamentBuildView.swift
  36. 3
      PadelClubTests/ServerDataTests.swift

@ -77,6 +77,7 @@
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; };
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -407,6 +408,7 @@
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserGroupStageSettingsView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -1146,6 +1148,7 @@
FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */,
FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */,
FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */, FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */,
FF135BF82C2FCB8300C9247A /* LoserGroupStageSettingsView.swift */,
FF9AC3932BE3625D00C2E883 /* Components */, FF9AC3932BE3625D00C2E883 /* Components */,
FF9AC3922BE3625200C2E883 /* Shared */, FF9AC3922BE3625200C2E883 /* Shared */,
); );
@ -1532,6 +1535,7 @@
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */,
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */, FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */,
FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */, FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */,
FF135BF92C2FCB8300C9247A /* LoserGroupStageSettingsView.swift in Sources */,
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */,
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */, FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */,
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */, FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */,
@ -1886,13 +1890,14 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 76; CURRENT_PROJECT_VERSION = 78;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_MODULE_VERIFIER = YES; ENABLE_MODULE_VERIFIER = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -1910,10 +1915,12 @@
); );
MARKETING_VERSION = 0.1; MARKETING_VERSION = 0.1;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_SWIFT_FLAGS = ""; OTHER_SWIFT_FLAGS = "-Onone";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -1926,7 +1933,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 76; CURRENT_PROJECT_VERSION = 78;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1952,6 +1959,7 @@
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50"; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI import SwiftUI
@Observable @Observable
class AppSettings: MicroStorable { final class AppSettings: MicroStorable {
var lastDataSource: String? = nil var lastDataSource: String? = nil
var didCreateAccount: Bool = false var didCreateAccount: Bool = false

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage import LeStorage
@Observable @Observable
class Club : ModelObject, Storable, Hashable { final class Club : ModelObject, Storable, Hashable {
static func resourceName() -> String { return "clubs" } static func resourceName() -> String { return "clubs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] } static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage import LeStorage
@Observable @Observable
class Court : ModelObject, Storable, Hashable { final class Court : ModelObject, Storable, Hashable {
static func resourceName() -> String { return "courts" } static func resourceName() -> String { return "courts" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage import LeStorage
@Observable @Observable
class DateInterval: ModelObject, Storable { final class DateInterval: ModelObject, Storable {
static func resourceName() -> String { return "date-intervals" } static func resourceName() -> String { return "date-intervals" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI import SwiftUI
@Observable @Observable
class Event: ModelObject, Storable { final class Event: ModelObject, Storable {
static func resourceName() -> String { return "events" } static func resourceName() -> String { return "events" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -11,7 +11,7 @@ import Algorithms
import SwiftUI import SwiftUI
@Observable @Observable
class GroupStage: ModelObject, Storable { final class GroupStage: ModelObject, Storable {
static func resourceName() -> String { "group-stages" } static func resourceName() -> String { "group-stages" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true } static func filterByStoreIdentifier() -> Bool { return true }

@ -9,7 +9,7 @@ import Foundation
import LeStorage import LeStorage
@Observable @Observable
class Match: ModelObject, Storable { final class Match: ModelObject, Storable {
static func resourceName() -> String { "matches" } static func resourceName() -> String { "matches" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true } static func filterByStoreIdentifier() -> Bool { return true }
@ -220,25 +220,10 @@ defer {
func teamWillBeWalkOut(_ team: TeamRegistration) { func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch() resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil }) let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
existingTeamScore.walkOut = 1
do { do {
try self.tournamentStore.teamScores.delete(contentOfs: previousScores) try DataStore.shared.teamScores.addOrUpdate(instance: existingTeamScore)
} catch {
Logger.error(error)
}
if let existingTeamScore = teamScore(ofTeam: team) {
do {
try self.tournamentStore.teamScores.delete(instance: existingTeamScore)
} catch {
Logger.error(error)
}
}
let teamScoreWalkout = TeamScore(match: id, team: team)
teamScoreWalkout.walkOut = 1
do {
try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreWalkout)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -271,7 +256,15 @@ defer {
let matchIndex = index let matchIndex = index
let position = matchIndex * 2 + teamPosition.rawValue let position = matchIndex * 2 + teamPosition.rawValue
let teamScoreLuckyLoser = TeamScore(match: id, team: team)
let previousScores = teamScores.filter({ $0.luckyLoser == position })
do {
try DataStore.shared.teamScores.delete(contentOfs: previousScores)
} catch {
Logger.error(error)
}
let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
teamScoreLuckyLoser.luckyLoser = position teamScoreLuckyLoser.luckyLoser = position
do { do {
try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI import SwiftUI
@Observable @Observable
class MatchScheduler : ModelObject, Storable { final class MatchScheduler : ModelObject, Storable {
static func resourceName() -> String { return "match-scheduler" } static func resourceName() -> String { return "match-scheduler" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -10,7 +10,7 @@ import SwiftUI
import LeStorage import LeStorage
@Observable @Observable
class MonthData : ModelObject, Storable { final class MonthData : ModelObject, Storable {
static func resourceName() -> String { return "month-data" } static func resourceName() -> String { return "month-data" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -9,7 +9,7 @@ import Foundation
import LeStorage import LeStorage
@Observable @Observable
class PlayerRegistration: ModelObject, Storable { final class PlayerRegistration: ModelObject, Storable {
static func resourceName() -> String { "player-registrations" } static func resourceName() -> String { "player-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true } static func filterByStoreIdentifier() -> Bool { return true }
@ -75,13 +75,21 @@ class PlayerRegistration: ModelObject, Storable {
self.source = .frenchFederation self.source = .frenchFederation
} }
internal init(federalData: [String], sex: Int, sexUnknown: Bool) { internal init?(federalData: [String], sex: Int, sexUnknown: Bool) {
lastName = federalData[0].trimmed.uppercased() let _lastName = federalData[0].trimmed.uppercased()
firstName = federalData[1].trimmed.capitalized let _firstName = federalData[1].trimmed.capitalized
if _lastName.isEmpty && _firstName.isEmpty { return nil }
lastName = _lastName
firstName = _firstName
birthdate = federalData[2] birthdate = federalData[2]
licenceId = federalData[3] licenceId = federalData[3]
clubName = federalData[4] clubName = federalData[4]
rank = Int(federalData[5]) let stringRank = federalData[5]
if stringRank.isEmpty {
rank = nil
} else {
rank = Int(stringRank)
}
let _email = federalData[6] let _email = federalData[6]
if _email.isEmpty == false { if _email.isEmpty == false {
self.email = _email self.email = _email

@ -10,7 +10,7 @@ import LeStorage
import SwiftUI import SwiftUI
@Observable @Observable
class Round: ModelObject, Storable { final class Round: ModelObject, Storable {
static func resourceName() -> String { "rounds" } static func resourceName() -> String { "rounds" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true } static func filterByStoreIdentifier() -> Bool { return true }
@ -582,6 +582,29 @@ defer {
loserRounds().forEach { round in loserRounds().forEach { round in
round.buildLoserBracket() round.buildLoserBracket()
} }
/*
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
matches.forEach {
$0.name = $0.roundObject?.roundTitle()
}
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
*/
} }
var parentRound: Round? { var parentRound: Round? {

@ -9,7 +9,7 @@ import Foundation
import LeStorage import LeStorage
@Observable @Observable
class TeamRegistration: ModelObject, Storable { final class TeamRegistration: ModelObject, Storable {
static func resourceName() -> String { "team-registrations" } static func resourceName() -> String { "team-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true } static func filterByStoreIdentifier() -> Bool { return true }
@ -207,8 +207,12 @@ class TeamRegistration: ModelObject, Storable {
} }
func includes(players: [PlayerRegistration]) -> Bool { func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false }
return players.allSatisfy { player in return players.allSatisfy { player in
includes(player: player) unsortedPlayers.anySatisfy { _player in
_player.isSameAs(player)
}
} }
} }

@ -9,7 +9,7 @@ import Foundation
import LeStorage import LeStorage
@Observable @Observable
class TeamScore: ModelObject, Storable { final class TeamScore: ModelObject, Storable {
static func resourceName() -> String { "team-scores" } static func resourceName() -> String { "team-scores" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }

@ -9,7 +9,7 @@ import Foundation
import LeStorage import LeStorage
@Observable @Observable
class Tournament : ModelObject, Storable { final class Tournament : ModelObject, Storable {
static func resourceName() -> String { "tournaments" } static func resourceName() -> String { "tournaments" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }
@ -55,6 +55,7 @@ class Tournament : ModelObject, Storable {
var hideTeamsWeight: Bool = false var hideTeamsWeight: Bool = false
var publishTournament: Bool = false var publishTournament: Bool = false
var hidePointsEarned: Bool = false var hidePointsEarned: Bool = false
var publishRankings: Bool = false
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -102,9 +103,10 @@ class Tournament : ModelObject, Storable {
case _hideTeamsWeight = "hideTeamsWeight" case _hideTeamsWeight = "hideTeamsWeight"
case _publishTournament = "publishTournament" case _publishTournament = "publishTournament"
case _hidePointsEarned = "hidePointsEarned" case _hidePointsEarned = "hidePointsEarned"
case _publishRankings = "publishRankings"
} }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false) { internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false) {
self.event = event self.event = event
self.name = name self.name = name
self.startDate = startDate self.startDate = startDate
@ -141,6 +143,7 @@ class Tournament : ModelObject, Storable {
self.hideTeamsWeight = hideTeamsWeight self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -184,6 +187,7 @@ class Tournament : ModelObject, Storable {
hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
} }
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -298,6 +302,7 @@ class Tournament : ModelObject, Storable {
try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight) try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
try container.encode(publishTournament, forKey: ._publishTournament) try container.encode(publishTournament, forKey: ._publishTournament)
try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
try container.encode(publishRankings, forKey: ._publishRankings)
} }
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws { fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -487,8 +492,7 @@ class Tournament : ModelObject, Storable {
} }
func courtUsed() -> [Int] { func courtUsed() -> [Int] {
#if DEBUG //DEBUGING TIME
#if DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
@ -1104,6 +1108,17 @@ defer {
return selected.sorted(by: \.finalRanking!, order: .ascending) return selected.sorted(by: \.finalRanking!, order: .ascending)
} }
private func _removeStrings(from dictionary: inout [Int: [String]], stringsToRemove: [String]) {
for key in dictionary.keys {
if var stringArray = dictionary[key] {
// Remove all instances of each string in stringsToRemove
stringArray.removeAll { stringsToRemove.contains($0) }
dictionary[key] = stringArray
}
}
}
func finalRanking() async -> [Int: [String]] { func finalRanking() async -> [Int: [String]] {
var teams: [Int: [String]] = [:] var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>() var ids: Set<String> = Set<String>()
@ -1119,6 +1134,14 @@ defer {
} }
let others: [Round] = rounds.flatMap { round in let others: [Round] = rounds.flatMap { round in
let losers = round.losers()
let minimumFinalPosition = round.seedInterval()?.last ?? teamCount
if teams[minimumFinalPosition] == nil {
teams[minimumFinalPosition] = losers.map { $0.id }
} else {
teams[minimumFinalPosition]?.append(contentsOf: losers.map { $0.id })
}
print("round", round.roundTitle()) print("round", round.roundTitle())
let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false } let rounds = round.loserRoundsAndChildren().filter { $0.isRankDisabled() == false && $0.hasNextRound() == false }
print(rounds.count, rounds.map { $0.roundTitle() }) print(rounds.count, rounds.map { $0.roundTitle() })
@ -1137,14 +1160,21 @@ defer {
print("losers", losers.count) print("losers", losers.count)
if winners.isEmpty { if winners.isEmpty {
let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false }) let disabledIds = playedMatches.flatMap({ $0.teamScores.compactMap({ $0.teamRegistration }) }).filter({ ids.contains($0) == false })
if disabledIds.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: disabledIds)
teams[interval.computedLast] = disabledIds teams[interval.computedLast] = disabledIds
let teamNames : [String] = disabledIds.compactMap { let teamNames : [String] = disabledIds.compactMap {
let t : TeamRegistration? = Store.main.findById($0) let t : TeamRegistration? = Store.main.findById($0)
return t return t
}.map { $0.canonicalName } }.map { $0.canonicalName }
print("winners.isEmpty", "\(interval.computedLast) : ", teamNames) print("winners.isEmpty", "\(interval.computedLast) : ", teamNames)
disabledIds.forEach { ids.insert($0) } disabledIds.forEach {
ids.insert($0)
}
}
} else { } else {
if winners.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: winners)
teams[interval.computedFirst + winners.count - 1] = winners teams[interval.computedFirst + winners.count - 1] = winners
let teamNames : [String] = winners.compactMap { let teamNames : [String] = winners.compactMap {
let t: TeamRegistration? = Store.main.findById($0) let t: TeamRegistration? = Store.main.findById($0)
@ -1152,6 +1182,10 @@ defer {
}.map { $0.canonicalName } }.map { $0.canonicalName }
print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames) print("winners", "\(interval.computedFirst + winners.count - 1) : ", teamNames)
winners.forEach { ids.insert($0) } winners.forEach { ids.insert($0) }
}
if losers.isEmpty == false {
_removeStrings(from: &teams, stringsToRemove: losers)
teams[interval.computedLast] = losers teams[interval.computedLast] = losers
let loserTeamNames : [String] = losers.compactMap { let loserTeamNames : [String] = losers.compactMap {
let t: TeamRegistration? = Store.main.findById($0) let t: TeamRegistration? = Store.main.findById($0)
@ -1162,6 +1196,7 @@ defer {
} }
} }
} }
}
let groupStages = groupStages() let groupStages = groupStages()
let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified let baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified
@ -1882,7 +1917,7 @@ defer {
let selected = selectedSortedTeams() let selected = selectedSortedTeams()
let allTeams = unsortedTeams() let allTeams = unsortedTeams()
let seedCount = max(selected.count - groupStageSpots(), 0) let seedCount = max(selected.count - groupStageSpots(), 0)
let newGroup = selected.prefix(seedCount) let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
let selectedIds = newGroup.map { $0.id } let selectedIds = newGroup.map { $0.id }
let groupIds = currentGroup.map { $0.id } let groupIds = currentGroup.map { $0.id }

@ -270,15 +270,19 @@ class FileImportManager {
} }
if tournamentCategory == tournament.tournamentCategory { if tournamentCategory == tournament.tournamentCategory {
let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne.setComputedRank(in: tournament) playerOne?.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament) playerTwo?.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament)
let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
results.append(team) results.append(team)
} }
} }
} }
} }
}
return results return results
} else { } else {
lines.dropFirst().forEach { line in lines.dropFirst().forEach { line in
@ -320,15 +324,18 @@ class FileImportManager {
} }
let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne.setComputedRank(in: tournament) playerOne?.setComputedRank(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setComputedRank(in: tournament) playerTwo?.setComputedRank(in: tournament)
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]), tournament: tournament) let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
results.append(team) results.append(team)
} }
} }
} }
}
return results return results
} }
} }

@ -31,10 +31,19 @@ class HtmlGenerator: ObservableObject {
@Published var height: CGFloat = 0 @Published var height: CGFloat = 0
private var webView: WKWebView = WKWebView() private var webView: WKWebView = WKWebView()
private var groupStageDone: Int = 0 private var groupStageDone: Int = 0
@Published var landscape: Bool = false
var baseWidth: CGFloat {
landscape ? 842 : 595
}
var baseHeight: CGFloat {
landscape ? 595 : 842
}
var estimatedPageCount: Int { var estimatedPageCount: Int {
if let zoomLevel { if let zoomLevel {
let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel))
let numberOfPageInWidth = Int(width / pageSize.width) + 1 let numberOfPageInWidth = Int(width / pageSize.width) + 1
let numberOfPageInHeight = Int(height / pageSize.height) + 1 let numberOfPageInHeight = Int(height / pageSize.height) + 1
return numberOfPageInWidth * numberOfPageInHeight return numberOfPageInWidth * numberOfPageInHeight
@ -50,19 +59,21 @@ class HtmlGenerator: ObservableObject {
func generateWebView(webView: WKWebView) { func generateWebView(webView: WKWebView) {
self.webView = webView self.webView = webView
self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
print("evaluateJavaScript", "readystage", complete, error)
if complete != nil { if complete != nil {
self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
print("evaluateJavaScript", "height", height, error)
self.height = height as! CGFloat self.height = height as! CGFloat
})
self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in
print("evaluateJavaScript", "width", width, error)
self.width = width as! CGFloat self.width = width as! CGFloat
})
}
if self.completionHandler != nil { if self.completionHandler != nil {
self.buildPDF() self.buildPDF()
} }
}) })
})
}
})
} }
func generateGroupStage(webView: WKWebView) { func generateGroupStage(webView: WKWebView) {
@ -75,7 +86,7 @@ class HtmlGenerator: ObservableObject {
print("bracket", width, height) print("bracket", width, height)
let config = WKPDFConfiguration() let config = WKPDFConfiguration()
config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(width))) config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(height)))
webView.createPDF(configuration: config){ result in webView.createPDF(configuration: config){ result in
switch result{ switch result{
case .success(let data): case .success(let data):
@ -111,7 +122,7 @@ class HtmlGenerator: ObservableObject {
try? FileManager.default.removeItem(at: pdfURL!) try? FileManager.default.removeItem(at: pdfURL!)
print("buildPDF", width, height, zoomLevel ?? 0) print("buildPDF", width, height, zoomLevel ?? 0)
if let zoomLevel { if let zoomLevel {
let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) let pageSize = CGSize(width: baseWidth * (1 + zoomLevel), height: baseHeight * (1 + zoomLevel))
let numberOfPageInWidth = Int(width / pageSize.width) + 1 let numberOfPageInWidth = Int(width / pageSize.width) + 1
let numberOfPageInHeight = Int(height / pageSize.height) + 1 let numberOfPageInHeight = Int(height / pageSize.height) + 1
for w in 0..<numberOfPageInWidth { for w in 0..<numberOfPageInWidth {
@ -159,6 +170,11 @@ class HtmlGenerator: ObservableObject {
HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false)
} }
func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withScore: false)
}
var pdfURL: URL? { var pdfURL: URL? {
guard let pdfFolderURL = getFilePath() else { guard let pdfFolderURL = getFilePath() else {
return nil return nil

@ -10,7 +10,8 @@ import Foundation
enum HtmlService { enum HtmlService {
case template(tournament: Tournament) case template(tournament: Tournament)
case bracket(tournament: Tournament, roundIndex: Int) case bracket(round: Round)
case loserBracket(upperRound: Round)
case match(match: Match) case match(match: Match)
case player(entrant: TeamRegistration) case player(entrant: TeamRegistration)
case hiddenPlayer case hiddenPlayer
@ -26,7 +27,7 @@ enum HtmlService {
var fileName: String { var fileName: String {
switch self { switch self {
case .template: case .template, .loserBracket:
return "tournament-template" return "tournament-template"
case .bracket: case .bracket:
return "bracket-template" return "bracket-template"
@ -191,23 +192,42 @@ enum HtmlService {
} }
template = template.replacingOccurrences(of: "{{matchDescription}}", with: "") template = template.replacingOccurrences(of: "{{matchDescription}}", with: "")
return template return template
case .bracket(let tournament, let roundIndex): case .bracket(let round):
var template = "" var template = ""
var bracket = "" var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
for (_, match) in round._matches().enumerated() { for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
} }
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
}
return bracket return bracket
case .loserBracket(let upperRound):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle())
var brackets = ""
for round in upperRound.loserRounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore))
}
var winnerName = ""
let winner = """
<ul class="round" scope="last">
<li class="spacer">&nbsp;</li>
<li class="game game-top winner">\(winnerName)</li>
<li class="spacer">&nbsp;</li>
</ul>
<ul class="main" style="visibility:hidden">
</ul>
"""
brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
return template
case .template(let tournament): case .template(let tournament):
var template = html var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short)) template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short))
var brackets = "" var brackets = ""
for round in tournament.rounds() { for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore)) brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withScore: withScore))
} }
var winnerName = "" var winnerName = ""

@ -14,6 +14,7 @@ enum URLs: String, Identifiable {
case api = "https://xlr.alwaysdata.net/roads/" case api = "https://xlr.alwaysdata.net/roads/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app" //case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr"
case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf" case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf"
var id: String { return self.rawValue } var id: String { return self.rawValue }

@ -37,9 +37,15 @@ struct SeedInterval: Hashable, Comparable {
func chunks() -> [SeedInterval]? { func chunks() -> [SeedInterval]? {
if dimension > 3 { if dimension > 3 {
let split = dimension / 2 let split = dimension / 2
if split%2 == 0 {
let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce) let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce)
let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce) let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce)
return [firstHalf, secondHalf] return [firstHalf, secondHalf]
} else {
let firstHalf = SeedInterval(first: first, last: first + split, reduce: reduce)
let secondHalf = SeedInterval(first: first + split + 1, last: last, reduce: reduce)
return [firstHalf, secondHalf]
}
} else { } else {
return nil return nil
} }

@ -85,7 +85,7 @@ struct GroupStageCallingView: View {
} }
} }
.overlay { .overlay {
if groupStage.startDate == nil { if groupStage.startDate == nil && groupStage._matches().filter({ $0.startDate != nil }).isEmpty {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: { } description: {

@ -27,6 +27,7 @@ struct ClubSearchView: View {
@State private var searchPresented: Bool = false @State private var searchPresented: Bool = false
@State private var showingSettingsAlert = false @State private var showingSettingsAlert = false
@State private var newClub: Club? @State private var newClub: Club?
@State private var error: Error?
var presentClubCreationView: Binding<Bool> { Binding( var presentClubCreationView: Binding<Bool> { Binding(
get: { newClub != nil }, get: { newClub != nil },
@ -59,7 +60,7 @@ struct ClubSearchView: View {
searching = false searching = false
searchAttempted = true searchAttempted = true
} }
error = nil
clubMarkers = [] clubMarkers = []
guard let city = locationManager.city else { return } guard let city = locationManager.city else { return }
let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location) let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location)
@ -70,6 +71,8 @@ struct ClubSearchView: View {
} }
} catch { } catch {
print("getclubs", error) print("getclubs", error)
self.error = error
Logger.error(error)
} }
} }
@ -143,12 +146,20 @@ struct ClubSearchView: View {
} else if clubMarkers.isEmpty && searching == false && searchPresented == false { } else if clubMarkers.isEmpty && searching == false && searchPresented == false {
ContentUnavailableView { ContentUnavailableView {
if searchAttempted { if searchAttempted {
if error != nil {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} else {
Label("Aucun club trouvé", systemImage: "mappin.slash") Label("Aucun club trouvé", systemImage: "mappin.slash")
}
} else { } else {
Label("Recherche de club", systemImage: "location.circle") Label("Recherche de club", systemImage: "location.circle")
} }
} description: { } description: {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") if searchAttempted && error != nil {
Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.")
} else {
Text("Padel Club recherche via Tenup un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
}
} actions: { } actions: {
if locationManager.manager.authorizationStatus != .restricted { if locationManager.manager.authorizationStatus != .restricted {
RowButtonView("Chercher autour de moi") { RowButtonView("Chercher autour de moi") {
@ -161,6 +172,13 @@ struct ClubSearchView: View {
} }
} }
} }
if error != nil {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
}
RowButtonView("Chercher une ville ou un code postal") { RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true searchPresented = true
} }
@ -343,6 +361,7 @@ struct ClubSearchView: View {
private func _resetSearch() { private func _resetSearch() {
searchAttempted = false searchAttempted = false
error = nil
debouncableViewModel.debouncableText = "" debouncableViewModel.debouncableText = ""
searchedCity = "" searchedCity = ""
locationManager.city = nil locationManager.city = nil

@ -82,6 +82,12 @@ struct GroupStageSettingsView: View {
} }
#endif #endif
// NavigationLink {
// LoserGroupStageSettingsView(tournament: tournament)
// } label: {
// Text("Match de perdant de poules")
// }
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false { if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false {
Section { Section {
menuBuildAllGroupStages menuBuildAllGroupStages

@ -0,0 +1,85 @@
//
// LoserGroupStageSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/06/2024.
//
import SwiftUI
extension Round {
var isGroupStageLoserBracket: Bool {
return false
}
}
extension Tournament {
func groupStageLoserBrackets() -> [Round] {
[]
}
func removeGroupStageLoserBrackets() {
}
}
struct LoserGroupStageSettingsView: View {
var tournament: Tournament
@State private var loserGroupStageBracketType: Int? = nil
@State private var losers : Set<TeamRegistration> = Set()
@Environment(\.editMode) private var editMode
var body: some View {
List(selection: $losers) {
if tournament.groupStageLoserBrackets().isEmpty == false {
//for each all rounds without parent and loserGroupStage, ability to delete them
Section {
RowButtonView("Effacer", role: .destructive) {
tournament.removeGroupStageLoserBrackets()
}
}
}
if self.editMode?.wrappedValue == .active {
Section {
//rajouter + toolbar valider / cancel
ForEach(tournament.groupStageTeams().filter({ $0.qualified == false })) { team in
TeamRowView(team: team).tag(team)
}
} header: {
Text("Sélection des perdants de poules")
}
} else {
Section {
RowButtonView("Ajouter un match de perdant") {
self.editMode?.wrappedValue = .active
}
} footer: {
Text("Permet d'ajouter un match de perdant de poules.")
}
}
}
.toolbar {
if self.editMode?.wrappedValue == .active {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
self.editMode?.wrappedValue = .inactive
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Valider") {
self.editMode?.wrappedValue = .inactive
//tournament.createGroupStageLoserBracket()
}
}
}
}
.navigationTitle("Match de perdant de poules")
.navigationBarBackButtonHidden(self.editMode?.wrappedValue == .active)
.navigationBarTitleDisplayMode(.inline)
.toolbar(.visible, for: .navigationBar)
.headerProminence(.increased)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -80,14 +80,14 @@ struct MatchSetupView: View {
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(groupStagePosition: nil, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
if walkOutSpot { if walkOutSpot || team.bracketPosition != nil {
match.setLuckyLoser(team: team, teamPosition: teamPosition) match.setLuckyLoser(team: team, teamPosition: teamPosition)
do { do {
try tournamentStore.matches.addOrUpdate(instance: match) try tournamentStore.matches.addOrUpdate(instance: match)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} else { } else if team.bracketPosition == nil {
team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
do { do {
try tournamentStore.matches.addOrUpdate(instance: match) try tournamentStore.matches.addOrUpdate(instance: match)

@ -102,7 +102,7 @@ struct ActivityView: View {
.overlay { .overlay {
if let error, navigation.agendaDestination == .tenup { if let error, navigation.agendaDestination == .tenup {
ContentUnavailableView { ContentUnavailableView {
Label("Erreur", systemImage: "exclamationmark") Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: { } description: {
Text(error.localizedDescription) Text(error.localizedDescription)
} actions: { } actions: {

@ -128,7 +128,7 @@ struct CourtAvailabilitySettingsView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible") .navigationTitle("Créneau indisponible")
.popover(isPresented: $showingPopover) { .sheet(isPresented: $showingPopover) {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct LoserRoundView: View { struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -22,7 +23,7 @@ struct LoserRoundView: View {
print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func _roundDisabled", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return loserBracket.allMatches.allSatisfy({ $0.disabled == false }) return loserBracket.allMatches.allSatisfy({ $0.disabled == true })
} }
private func _matches(loserRoundId: String?) -> [Match] { private func _matches(loserRoundId: String?) -> [Match] {
@ -87,6 +88,16 @@ struct LoserRoundView: View {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.wrappedValue.toggle() isEditingTournamentSeed.wrappedValue.toggle()
if isEditingTournamentSeed.wrappedValue == false {
let allRoundMatches = loserBracket.allMatches
allRoundMatches.forEach({ $0.name = $0.roundTitle() })
do {
try DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches)
} catch {
Logger.error(error)
}
}
} }
} }
} }

@ -177,13 +177,12 @@ struct LoserRoundsView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: false) GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: destinations, nilDestinationIsValid: true)
if let selectedRound { switch selectedRound {
case .some(let selectedRound):
LoserRoundView(loserBracket: selectedRound) LoserRoundView(loserBracket: selectedRound)
} else { default:
Section { LoserRoundSettingsView()
ContentUnavailableView("Aucun tour à jouer", systemImage: "tennisball", description: Text("Il il n'y a aucun tour de match de classement prévu."))
}
} }
} }
.environment(\.isEditingTournamentSeed, $isEditingTournamentSeed) .environment(\.isEditingTournamentSeed, $isEditingTournamentSeed)

@ -11,12 +11,20 @@ struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore @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
@State private var confirmTeam: TeamRegistration?
@State private var presentTeamPickerView: Bool = false @State private var presentTeamPickerView: Bool = false
@State private var searchField: String = "" @State private var searchField: String = ""
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
var confirmationRequest: Binding<Bool> {
Binding {
confirmTeam != nil
} set: { _ in
}
}
var body: some View { var body: some View {
Button { Button {
presentTeamPickerView = true presentTeamPickerView = true
@ -86,12 +94,30 @@ struct TeamPickerView: View {
Button { Button {
teamPicked(team) teamPicked(team)
presentTeamPickerView = false presentTeamPickerView = false
// if team.inRound() {
// confirmTeam = team
// } else {
// teamPicked(team)
// presentTeamPickerView = false
// }
} label: { } label: {
TeamRowView(team: team) TeamRowView(team: team)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!)
// presentTeamPickerView = false
// }
//
// Button("Annuler", role: .cancel) {
// confirmTeam = nil
// }
// } message: {
// Text("Vous êtes sur le point de retirer cette équipe du tableau pour le replacer, cela effacera les résultats des matchs déjà joués par cette équipe dans le tableau.")
// }
} }
} }
} }

@ -106,9 +106,9 @@ struct InscriptionInfoView: View {
} }
.listRowView(color: .red) .listRowView(color: .red)
} header: { } header: {
Text("Équipes non sélectionnées") Text("Équipes ne devant plus être sélectionnées")
} footer: { } footer: {
Text("Il s'agit des équipes déjà placé en poule ou tableau qui sont actuellement en attente à cause de l'arrivée d'une nouvelle équipe ou une modification de classement.") Text("Il s'agit des équipes précédement placées en poule ou tableau mais qui sont finalement maintenant en attente suite à l'arrivée d'une nouvelle équipe plus forte ou une modification de classement.")
} }
Section { Section {

@ -530,7 +530,7 @@ struct InscriptionManagerView: View {
RowButtonView("Créer une équipe") { RowButtonView("Créer une équipe") {
Task { Task {
await MainActor.run() { await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption()) fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = searchField pasteString = searchField

@ -28,7 +28,6 @@ struct PrintSettingsView: View {
// Toggle(isOn: $generator.displayHeads, label: { // Toggle(isOn: $generator.displayHeads, label: {
// Text("Afficher les têtes de séries") // Text("Afficher les têtes de séries")
// }) // })
Toggle(isOn: $generator.displayRank, label: { Toggle(isOn: $generator.displayRank, label: {
Text("Afficher le classement du joueur") Text("Afficher le classement du joueur")
}) })
@ -57,6 +56,17 @@ struct PrintSettingsView: View {
} label: { } label: {
Text("Zoom") Text("Zoom")
} }
.onChange(of: generator.zoomLevel) {
if generator.zoomLevel == nil {
generator.landscape = false
}
}
if generator.zoomLevel != nil {
Toggle(isOn: $generator.landscape, label: {
Text("Format paysage")
})
}
HStack { HStack {
Text("Nombre de page A4 à imprimer") Text("Nombre de page A4 à imprimer")
@ -106,11 +116,23 @@ struct PrintSettingsView: View {
Section { Section {
NavigationLink { NavigationLink {
WebViewPreview(bracket: true) WebViewPreview()
.environmentObject(generator) .environmentObject(generator)
} label: { } label: {
Text("Aperçu du tableau") Text("Aperçu du tableau")
} }
//
// ForEach(tournament.rounds()) { round in
// if round.index > 0 {
// NavigationLink {
// WebViewPreview(round: round)
// .environmentObject(generator)
// } label: {
// Text("Aperçu \(round.correspondingLoserRoundTitle())")
// }
// }
// }
//
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
NavigationLink { NavigationLink {
WebViewPreview(groupStage: groupStage) WebViewPreview(groupStage: groupStage)
@ -260,13 +282,13 @@ struct WebView: UIViewRepresentable {
struct WebViewPreview: View { struct WebViewPreview: View {
@EnvironmentObject var generator: HtmlGenerator @EnvironmentObject var generator: HtmlGenerator
let bracket: Bool
let groupStage: GroupStage? let groupStage: GroupStage?
let round: Round?
@State private var html: String? @State private var html: String?
init(bracket: Bool = false, groupStage: GroupStage? = nil) { init(groupStage: GroupStage? = nil, round: Round? = nil) {
self.bracket = bracket self.round = round
self.groupStage = groupStage self.groupStage = groupStage
} }
@ -280,6 +302,8 @@ struct WebViewPreview: View {
.onAppear { .onAppear {
if let groupStage { if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false) html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)
} else if let round {
html = generator.generateLoserBracketHtml(upperRound: round)
} else { } else {
html = generator.generateHtml() html = generator.generateHtml()
} }

@ -11,6 +11,7 @@ import LeStorage
struct TournamentRankView: View { struct TournamentRankView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@State private var rankings: [Int: [TeamRegistration]] = [:] @State private var rankings: [Int: [TeamRegistration]] = [:]
@State private var calculating = false @State private var calculating = false
@ -33,63 +34,53 @@ struct TournamentRankView: View {
var body: some View { var body: some View {
List { List {
@Bindable var tournament = tournament @Bindable var tournament = tournament
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) let rankingsCalculated = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if editMode?.wrappedValue.isEditing == false {
Section { Section {
LabeledContent { MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
if let matchesLeft { MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)
Text(matchesLeft.count.formatted())
} else {
ProgressView()
}
} label: {
Text("Matchs restant")
}
LabeledContent { Toggle(isOn: $tournament.hidePointsEarned) {
if let runningMatches { Text("Masquer les points gagnés")
Text(runningMatches.count.formatted()) }
} else { .onChange(of: tournament.hidePointsEarned) {
ProgressView() do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
} }
} label: {
Text("Matchs en cours")
} }
LabeledContent { Toggle(isOn: $tournament.publishRankings) {
if rankingPublished { Text("Publier sur Padel Club")
Image(systemName: "checkmark") if let url = tournament.shareURL(.rankings) {
.foregroundStyle(.green) Link(destination: url) {
} else { Text("Accéder à la page")
Image(systemName: "xmark")
.foregroundStyle(.logoRed)
} }
} label: {
Text("Classement publié")
} }
Toggle(isOn: $tournament.hidePointsEarned) {
Text("Masquer les points gagnés")
} }
.onChange(of: tournament.hidePointsEarned) { .onChange(of: tournament.publishRankings) {
do { do {
try dataStore.tournaments.addOrUpdate(instance: tournament) try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} }
if rankingPublished == false {
RowButtonView("Publier le classement", role: .destructive) {
_publishRankings()
} }
} else {
RowButtonView("Re-publier le classement", role: .destructive) {
_publishRankings()
} }
if (editMode?.wrappedValue.isEditing == true || rankingsCalculated == false) && calculating == false {
Section {
RowButtonView(rankingsCalculated ? "Re-calculer le classement" : "Calculer", role: .destructive) {
await _calculateRankings()
}
} footer: {
if rankingsCalculated {
Text("Vos éditions seront perdus.")
} }
} }
if rankingPublished { if rankingsCalculated {
Section { Section {
RowButtonView("Supprimer le classement", role: .destructive) { RowButtonView("Supprimer le classement", role: .destructive) {
tournament.unsortedTeams().forEach { team in tournament.unsortedTeams().forEach { team in
@ -98,32 +89,16 @@ struct TournamentRankView: View {
} }
_save() _save()
} }
} footer: { }
Text(.init("Masque également le classement sur le site [Padel Club](\(URLs.main.rawValue))"))
} }
} }
if rankingPublished {
let teamsRanked = tournament.teamsRanked()
if calculating == false && rankingsCalculated && teamsRanked.isEmpty == false {
Section { Section {
ForEach(tournament.teamsRanked()) { team in ForEach(teamsRanked) { team in
let key = team.finalRanking ?? 0 if let key = team.finalRanking {
Button {
selectedTeam = team
} label: {
TeamRankCellView(team: team, key: key)
.frame(maxWidth: .infinity)
}
.contentShape(Rectangle())
.buttonStyle(.plain)
}
} footer: {
Text("Vous pouvez appuyer sur une ligne pour éditer manuellement le classement calculé par Padel Club.")
}
} else {
let keys = rankings.keys.sorted()
ForEach(keys, id: \.self) { key in
if let rankedTeams = rankings[key] {
ForEach(rankedTeams) { team in
TeamRankCellView(team: team, key: key) TeamRankCellView(team: team, key: key)
} }
} }
@ -165,12 +140,10 @@ struct TournamentRankView: View {
} }
}) })
.onAppear { .onAppear {
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil }) let rankingPublished = tournament.selectedSortedTeams().anySatisfy({ $0.finalRanking != nil })
if rankingPublished == false { if rankingPublished == false {
calculating = true
Task { Task {
await _calculateRankings() await _calculateRankings()
calculating = false
} }
} }
} }
@ -179,19 +152,41 @@ struct TournamentRankView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if let url = tournament.shareURL(.rankings) { EditButton()
_actionForURL(url)
}
} }
} }
} }
struct TeamRankCellView: View { struct TeamRankCellView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration @EnvironmentObject var dataStore: DataStore
let key: Int @State private var isEditingTeam: Bool = false
@Bindable var team: TeamRegistration
@State var key: Int
var body: some View { var body: some View {
VStack(spacing: 0) {
if editMode?.wrappedValue.isEditing == true {
if key > 1 {
Button {
key -= 1
team.finalRanking = key
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
} label: {
Label("descendre", systemImage: "chevron.compact.up").labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
}
}
Button {
isEditingTeam = true
} label: {
HStack { HStack {
VStack(alignment: .trailing) { VStack(alignment: .trailing) {
VStack(alignment: .trailing, spacing: -8.0) { VStack(alignment: .trailing, spacing: -8.0) {
@ -249,7 +244,6 @@ struct TournamentRankView: View {
} }
} }
} }
if tournament.isAnimation() == false && key > 0 { if tournament.isAnimation() == false && key > 0 {
Spacer() Spacer()
VStack(alignment: .trailing) { VStack(alignment: .trailing) {
@ -260,22 +254,55 @@ struct TournamentRankView: View {
} }
} }
} }
.frame(maxWidth: .infinity)
}
.contentShape(Rectangle())
.buttonStyle(.plain)
if editMode?.wrappedValue.isEditing == true {
Button {
key += 1
team.finalRanking = key
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
} label: {
Label("descendre", systemImage: "chevron.compact.down").labelStyle(.iconOnly)
}
.buttonStyle(.bordered)
} }
} }
.alert("Position", isPresented: $isEditingTeam) {
TextField("Position", value: $team.finalRanking, format: .number)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
private func _publishRankings() { Button("Valider") {
rankings.keys.sorted().forEach { rank in team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount)
if let rankedTeams = rankings[rank] { do {
rankedTeams.forEach { team in try dataStore.teamRegistrations.addOrUpdate(instance: team)
team.finalRanking = rank } catch {
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount) Logger.error(error)
}
isEditingTeam = false
}
Button("Annuler", role: .cancel) {
isEditingTeam = false
} }
} }
} }
_save()
} }
private func _calculateRankings() async { private func _calculateRankings() async {
await MainActor.run {
calculating = true
}
let finalRanks = await tournament.finalRanking() let finalRanks = await tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in finalRanks.keys.sorted().forEach { rank in
if let rankedTeamIds = finalRanks[rank] { if let rankedTeamIds = finalRanks[rank] {
@ -283,27 +310,21 @@ struct TournamentRankView: View {
self.rankings[rank] = teams self.rankings[rank] = teams
} }
} }
}
@ViewBuilder
private func _actionForURL(_ url: URL, removeSource: Bool = false) -> some View {
Menu {
Button {
UIApplication.shared.open(url)
} label: {
Label("Voir", systemImage: "safari")
}
ShareLink(item: url) { await MainActor.run {
Label("Partager le lien", systemImage: "link") rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = tournament.isAnimation() ? nil : tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount)
} }
} label: {
Image(systemName: "square.and.arrow.up")
} }
.frame(maxWidth: .infinity)
.buttonStyle(.borderless)
} }
_save()
calculating = false
}
}
private func _save() { private func _save() {
do { do {

@ -89,12 +89,17 @@ struct TournamentBuildView: View {
Section { Section {
#if DEBUG
NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes")
}
#else
if tournament.hasEnded() { if tournament.hasEnded() {
NavigationLink(value: Screen.rankings) { NavigationLink(value: Screen.rankings) {
Text("Classement final des équipes") Text("Classement final des équipes")
} }
} }
#endif
if state == .running || state == .finished { if state == .running || state == .finished {
TournamentInscriptionView(tournament: tournament) TournamentInscriptionView(tournament: tournament)
TournamentBroadcastRowView(tournament: tournament) TournamentBroadcastRowView(tournament: tournament)

@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase {
return return
} }
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true) let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true)
let t = try await Store.main.service().post(tournament) let t = try await Store.main.service().post(tournament)
assert(t.event == tournament.event) assert(t.event == tournament.event)
@ -138,6 +138,7 @@ final class ServerDataTests: XCTestCase {
assert(t.hideTeamsWeight == tournament.hideTeamsWeight) assert(t.hideTeamsWeight == tournament.hideTeamsWeight)
assert(t.publishTournament == tournament.publishTournament) assert(t.publishTournament == tournament.publishTournament)
assert(t.hidePointsEarned == tournament.hidePointsEarned) assert(t.hidePointsEarned == tournament.hidePointsEarned)
assert(t.publishRankings == tournament.publishRankings)
} }
func testGroupStage() async throws { func testGroupStage() async throws {

Loading…
Cancel
Save