Laurent 1 year ago
commit a85da54f96
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 7
      PadelClub/Data/Coredata/Persistence.swift
  3. 6
      PadelClub/Data/GroupStage.swift
  4. 13
      PadelClub/Data/Match.swift
  5. 4
      PadelClub/Data/PlayerRegistration.swift
  6. 10
      PadelClub/Data/Round.swift
  7. 52
      PadelClub/Data/Tournament.swift
  8. 12
      PadelClub/Extensions/String+Extensions.swift
  9. 1
      PadelClub/PadelClubApp.swift
  10. 14
      PadelClub/Utils/ContactManager.swift
  11. 4
      PadelClub/Utils/FileImportManager.swift
  12. 50
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  13. 30
      PadelClub/Views/Calling/CallSettingsView.swift
  14. 8
      PadelClub/Views/Calling/CallView.swift
  15. 10
      PadelClub/Views/Calling/SendToAllView.swift
  16. 2
      PadelClub/Views/Cashier/CashierView.swift
  17. 5
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  18. 136
      PadelClub/Views/Match/MatchDetailView.swift
  19. 4
      PadelClub/Views/Match/MatchSummaryView.swift
  20. 2
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  21. 39
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  22. 2
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  23. 43
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  24. 124
      PadelClub/Views/Planning/SchedulerView.swift
  25. 33
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  26. 3
      PadelClub/Views/Player/PlayerDetailView.swift
  27. 6
      PadelClub/Views/Round/RoundSettingsView.swift
  28. 2
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  29. 56
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  30. 46
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  31. 2
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  32. 2
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  33. 2
      PadelClub/Views/Tournament/TournamentInscriptionView.swift
  34. 2
      PadelClub/Views/Tournament/TournamentView.swift
  35. 5
      PadelClubTests/ServerDataTests.swift

@ -1919,7 +1919,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15; CURRENT_PROJECT_VERSION = 21;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1957,7 +1957,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 15; CURRENT_PROJECT_VERSION = 21;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -155,15 +155,16 @@ class PersistenceController: NSObject {
importedPlayer.country?.replace(characters: replacementsCharacters) importedPlayer.country?.replace(characters: replacementsCharacters)
} }
importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0) importedPlayer.tournamentCount = Int64(data.tournamentCount ?? 0)
importedPlayer.lastName = data.lastName importedPlayer.lastName = data.lastName.trimmed.uppercased()
if fixApril2024 { if fixApril2024 {
importedPlayer.lastName?.replace(characters: replacementsCharacters) importedPlayer.lastName?.replace(characters: replacementsCharacters)
} }
importedPlayer.firstName = data.firstName importedPlayer.firstName = data.firstName.trimmed.capitalized
if fixApril2024 { if fixApril2024 {
importedPlayer.firstName?.replace(characters: replacementsCharacters) importedPlayer.firstName?.replace(characters: replacementsCharacters)
} }
importedPlayer.fullName = data.firstName + " " + data.lastName
importedPlayer.fullName = data.firstName.trimmed.capitalized + " " + data.lastName.trimmed.uppercased()
if fixApril2024 { if fixApril2024 {
importedPlayer.fullName?.replace(characters: replacementsCharacters) importedPlayer.fullName?.replace(characters: replacementsCharacters)
} }

@ -304,7 +304,11 @@ class GroupStage: ModelObject, Storable {
playedMatches.forEach { match in playedMatches.forEach { match in
match.matchFormat = matchFormat match.matchFormat = matchFormat
} }
try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) do {
try DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
} catch {
Logger.error(error)
}
} }
override func deleteDependencies() throws { override func deleteDependencies() throws {

@ -168,6 +168,19 @@ class Match: ModelObject, Storable {
servingTeamId = nil servingTeamId = nil
} }
func resetScores() {
if hasEnded() == false {
teamScores.forEach({ $0.score = nil })
do {
try DataStore.shared.teamScores.addOrUpdate(contentOfs: teamScores)
} catch {
Logger.error(error)
}
} else {
updateTeamScores()
}
}
func teamWillBeWalkOut(_ team: TeamRegistration) { func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch() resetMatch()
let previousScores = teamScores.filter({ $0.luckyLoser != nil }) let previousScores = teamScores.filter({ $0.luckyLoser != nil })

@ -60,8 +60,8 @@ class PlayerRegistration: ModelObject, Storable {
internal init(importedPlayer: ImportedPlayer) { internal init(importedPlayer: ImportedPlayer) {
self.teamRegistration = "" self.teamRegistration = ""
self.firstName = importedPlayer.firstName ?? "" self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized
self.lastName = importedPlayer.lastName ?? "" self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased()
self.licenceId = importedPlayer.license ?? nil self.licenceId = importedPlayer.license ?? nil
self.rank = Int(importedPlayer.rank) self.rank = Int(importedPlayer.rank)
self.sex = importedPlayer.male ? .male : .female self.sex = importedPlayer.male ? .male : .female

@ -369,9 +369,9 @@ class Round: ModelObject, Storable {
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if parent != nil { if parent != nil {
return seedInterval()?.localizedLabel(displayStyle) ?? "Pas trouvé" return seedInterval()?.localizedLabel(displayStyle) ?? "Round pas trouvé"
} }
return RoundRule.roundName(fromRoundIndex: index) return RoundRule.roundName(fromRoundIndex: index, displayStyle: displayStyle)
} }
func updateTournamentState() { func updateTournamentState() {
@ -467,7 +467,11 @@ class Round: ModelObject, Storable {
playedMatches.forEach { match in playedMatches.forEach { match in
match.matchFormat = updatedMatchFormat match.matchFormat = updatedMatchFormat
} }
try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches) do {
try DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
} catch {
Logger.error(error)
}
} }
override func deleteDependencies() throws { override func deleteDependencies() throws {

@ -48,8 +48,6 @@ class Tournament : ModelObject, Storable {
var publishSummons: Bool = false var publishSummons: Bool = false
var publishGroupStages: Bool = false var publishGroupStages: Bool = false
var publishBrackets: Bool = false var publishBrackets: Bool = false
//local
var shouldVerifyGroupStage: Bool = false var shouldVerifyGroupStage: Bool = false
var shouldVerifyBracket: Bool = false var shouldVerifyBracket: Bool = false
@ -94,9 +92,11 @@ class Tournament : ModelObject, Storable {
case _publishSummons = "publishSummons" case _publishSummons = "publishSummons"
case _publishGroupStages = "publishGroupStages" case _publishGroupStages = "publishGroupStages"
case _publishBrackets = "publishBrackets" case _publishBrackets = "publishBrackets"
case _shouldVerifyGroupStage = "shouldVerifyGroupStage"
case _shouldVerifyBracket = "shouldVerifyBracket"
} }
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) { 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) {
self.event = event self.event = event
self.name = name self.name = name
self.startDate = startDate self.startDate = startDate
@ -128,6 +128,8 @@ class Tournament : ModelObject, Storable {
self.publishSummons = publishSummons self.publishSummons = publishSummons
self.publishBrackets = publishBrackets self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages self.publishGroupStages = publishGroupStages
self.shouldVerifyBracket = shouldVerifyBracket
self.shouldVerifyGroupStage = shouldVerifyGroupStage
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -166,7 +168,8 @@ class Tournament : ModelObject, Storable {
publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false
publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false
publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false
shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false
shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false
} }
fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
@ -276,6 +279,8 @@ class Tournament : ModelObject, Storable {
try container.encode(publishSummons, forKey: ._publishSummons) try container.encode(publishSummons, forKey: ._publishSummons)
try container.encode(publishBrackets, forKey: ._publishBrackets) try container.encode(publishBrackets, forKey: ._publishBrackets)
try container.encode(publishGroupStages, forKey: ._publishGroupStages) try container.encode(publishGroupStages, forKey: ._publishGroupStages)
try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket)
try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage)
} }
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws { fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -338,6 +343,14 @@ class Tournament : ModelObject, Storable {
case finished case finished
} }
func eventLabel() -> String {
if let event = eventObject(), let name = event.name {
return name
} else {
return ""
}
}
func publishedTeamsDate() -> Date { func publishedTeamsDate() -> Date {
startDate startDate
} }
@ -749,7 +762,15 @@ class Tournament : ModelObject, Storable {
func unsortedTeams() -> [TeamRegistration] { func unsortedTeams() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id } Store.main.filter { $0.tournament == self.id }
} }
func unsortedTeamsWithoutWO() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
}
func walkoutTeams() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id && $0.walkOut == true }
}
func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] { func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] {
var duplicates = [PlayerRegistration]() var duplicates = [PlayerRegistration]()
Set(players.compactMap({ $0.licenceId })).forEach { licenceId in Set(players.compactMap({ $0.licenceId })).forEach { licenceId in
@ -847,12 +868,20 @@ class Tournament : ModelObject, Storable {
func importTeams(_ teams: [FileImportManager.TeamHolder]) { func importTeams(_ teams: [FileImportManager.TeamHolder]) {
var teamsToImport = [TeamRegistration]() var teamsToImport = [TeamRegistration]()
let players = players().filter { $0.licenceId != nil }
teams.forEach { team in teams.forEach { team in
if let previousTeam = team.previousTeam { if let previousTeam = team.previousTeam {
previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory) previousTeam.updatePlayers(team.players, inTournamentCategory: team.tournamentCategory)
teamsToImport.append(previousTeam) teamsToImport.append(previousTeam)
} else { } else {
let newTeam = addTeam(team.players, registrationDate: team.registrationDate) var registrationDate = team.registrationDate
if let previousPlayer = players.first(where: { player in
let ids = team.players.compactMap({ $0.licenceId })
return ids.contains(player.licenceId!)
}), let previousTeamRegistrationDate = previousPlayer.team()?.registrationDate {
registrationDate = previousTeamRegistrationDate
}
let newTeam = addTeam(team.players, registrationDate: registrationDate)
teamsToImport.append(newTeam) teamsToImport.append(newTeam)
} }
} }
@ -1017,7 +1046,7 @@ class Tournament : ModelObject, Storable {
} }
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ") [tournamentLevel.localizedLabel(.wide) + " " + tournamentCategory.localizedLabel(.wide), displayStyle == .wide ? name : nil].compactMap({ $0 }).joined(separator: " - ")
} }
func subtitle(_ displayStyle: DisplayStyle = .wide) -> String { func subtitle(_ displayStyle: DisplayStyle = .wide) -> String {
@ -1128,6 +1157,15 @@ class Tournament : ModelObject, Storable {
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
} }
func confirmedSummonStatus() -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { $0.confirmationDate != nil }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " confirmées"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func bracketStatus() -> String { func bracketStatus() -> String {
let availableSeeds = availableSeeds() let availableSeeds = availableSeeds()
if availableSeeds.isEmpty == false { if availableSeeds.isEmpty == false {
@ -1711,7 +1749,7 @@ extension Tournament: FederalTournamentHolder {
extension Tournament: TournamentBuildHolder { extension Tournament: TournamentBuildHolder {
func buildHolderTitle() -> String { func buildHolderTitle() -> String {
tournamentTitle() tournamentTitle(.short)
} }
var category: TournamentCategory { var category: TournamentCategory {

@ -174,3 +174,15 @@ extension StringProtocol {
extension LosslessStringConvertible { extension LosslessStringConvertible {
var string: String { .init(self) } var string: String { .init(self) }
} }
extension String {
func createTxtFile(_ withName: String = "temp") -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(withName)
.appendingPathExtension("txt")
let string = self
try? FileManager.default.removeItem(at: url)
try? string.write(to: url, atomically: true, encoding: .utf8)
return url
}
}

@ -22,6 +22,7 @@ struct PadelClubApp: App {
.environment(navigationViewModel) .environment(navigationViewModel)
.accentColor(.master) .accentColor(.master)
.onAppear { .onAppear {
networkMonitor.checkConnection()
self._onAppear() self._onAppear()
} }
.task { .task {

@ -31,7 +31,7 @@ enum ContactType: Identifiable {
extension ContactType { extension ContactType {
static let defaultCustomMessage = "Il est conseillé de vous présenter 10 minutes avant de jouer.\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire." static let defaultCustomMessage = "Il est conseillé de vous présenter 10 minutes avant de jouer.\nMerci de me confirmer votre présence avec votre nom et de prévenir votre partenaire."
static let defaultAvailablePaymentMethods = "Règlement possible par chèque ou espèce." static let defaultAvailablePaymentMethods = "Règlement possible par chèque ou espèces."
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
@ -41,7 +41,7 @@ extension ContactType {
let date = startDate ?? tournament?.startDate ?? Date() let date = startDate ?? tournament?.startDate ?? Date()
if let tournament { if let tournament {
text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle()) text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.short))
text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage) text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage)
} }
@ -56,7 +56,7 @@ extension ContactType {
return text return text
} }
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String { static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String {
let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage
@ -77,13 +77,15 @@ extension ContactType {
} }
var computedMessage: String { var computedMessage: String {
[entryFeeMessage, message].compacted().map { $0.trimmed }.joined(separator: "\n") [entryFeeMessage, message].compacted().map { $0.trimmed }.joined(separator: "\n\n")
} }
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament { if let tournament {
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"
} else { } else {
return "Bonjour,\n\nVous êtes \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)"
} }
} }
} }

@ -27,8 +27,8 @@ class FileImportManager {
lastName.replace(characters: replacementsCharacters) lastName.replace(characters: replacementsCharacters)
var firstName = federalPlayer.firstName var firstName = federalPlayer.firstName
firstName.replace(characters: replacementsCharacters) firstName.replace(characters: replacementsCharacters)
importedPlayer.lastName = lastName importedPlayer.lastName = lastName.trimmed.uppercased()
importedPlayer.firstName = firstName importedPlayer.firstName = firstName.trimmed.capitalized
} }
} }
playersLeft.removeAll(where: { $0.lastName.isEmpty == false }) playersLeft.removeAll(where: { $0.lastName.isEmpty == false })

@ -24,6 +24,8 @@ struct CallMessageCustomizationView: View {
@State private var customCallMessageSignature: String = "" @State private var customCallMessageSignature: String = ""
@State private var summonsAvailablePaymentMethods: String = "" @State private var summonsAvailablePaymentMethods: String = ""
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 3)
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_customCallMessageBody = State(wrappedValue: DataStore.shared.user.summonsMessageBody ?? (DataStore.shared.user.summonsUseFullCustomMessage ? "" : ContactType.defaultCustomMessage)) _customCallMessageBody = State(wrappedValue: DataStore.shared.user.summonsMessageBody ?? (DataStore.shared.user.summonsUseFullCustomMessage ? "" : ContactType.defaultCustomMessage))
@ -46,7 +48,7 @@ struct CallMessageCustomizationView: View {
var finalMessage: String? { var finalMessage: String? {
let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s"
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)"
} }
var body: some View { var body: some View {
@ -73,6 +75,16 @@ struct CallMessageCustomizationView: View {
} footer: { } footer: {
HStack { HStack {
Spacer() Spacer()
FooterButtonView("effacer") {
customCallMessageSignature = ""
_save()
}
Divider()
FooterButtonView("défaut") {
customCallMessageSignature = DataStore.shared.user.defaultSignature()
_save()
}
Divider()
FooterButtonView("éditer") { FooterButtonView("éditer") {
focusedField = .signature focusedField = .signature
} }
@ -91,9 +103,6 @@ struct CallMessageCustomizationView: View {
Spacer() Spacer()
Button { Button {
focusedField = nil focusedField = nil
user.summonsMessageBody = customCallMessageBody
user.summonsMessageSignature = customCallMessageSignature
user.summonsAvailablePaymentMethods = summonsAvailablePaymentMethods
_save() _save()
} label: { } label: {
Text("Valider") Text("Valider")
@ -117,6 +126,9 @@ struct CallMessageCustomizationView: View {
} }
private func _save() { private func _save() {
self.dataStore.user.summonsMessageBody = customCallMessageBody
self.dataStore.user.summonsMessageSignature = customCallMessageSignature
self.dataStore.user.summonsAvailablePaymentMethods = summonsAvailablePaymentMethods
self.dataStore.saveUser() self.dataStore.saveUser()
} }
@ -142,16 +154,22 @@ struct CallMessageCustomizationView: View {
if user.summonsUseFullCustomMessage == false { if user.summonsUseFullCustomMessage == false {
HStack { HStack {
Spacer() Spacer()
FooterButtonView("effacer") {
customCallMessageBody = ""
_save()
}
Divider()
FooterButtonView("défaut") {
customCallMessageBody = ContactType.defaultCustomMessage
_save()
}
Divider()
FooterButtonView("éditer") { FooterButtonView("éditer") {
focusedField = .body focusedField = .body
} }
} }
} } else {
} LazyVGrid(columns: columns, spacing: 0) {
if user.summonsUseFullCustomMessage {
Section {
LazyHStack {
FooterButtonView("#titre") { FooterButtonView("#titre") {
customCallMessageBody.append("#titre") customCallMessageBody.append("#titre")
focusedField = .body focusedField = .body
@ -181,8 +199,6 @@ struct CallMessageCustomizationView: View {
focusedField = .body focusedField = .body
} }
} }
} header: {
Text("Utilisez ces balises")
} }
} }
} }
@ -275,6 +291,16 @@ struct CallMessageCustomizationView: View {
} footer: { } footer: {
HStack { HStack {
Spacer() Spacer()
FooterButtonView("effacer") {
summonsAvailablePaymentMethods = ""
_save()
}
Divider()
FooterButtonView("défaut") {
summonsAvailablePaymentMethods = ContactType.defaultAvailablePaymentMethods
_save()
}
Divider()
FooterButtonView("éditer") { FooterButtonView("éditer") {
focusedField = .paymentMethods focusedField = .paymentMethods
} }

@ -6,11 +6,13 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct CallSettingsView: View { struct CallSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var showSendToAllView: Bool = false @State private var showSendToAllView: Bool = false
@State private var addLink: Bool = false
var body: some View { var body: some View {
List { List {
@ -29,6 +31,27 @@ struct CallSettingsView: View {
} }
} }
Section {
RowButtonView("Envoyer le lien du tournoi") {
addLink = true
}
.disabled(tournament.isPrivate)
} footer: {
if tournament.isPrivate {
Button {
tournament.isPrivate = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} label: {
Text(.init("Le tournoi n'est pas visible sur [Padel Club](\(URLs.main.rawValue)), ")).foregroundStyle(.logoRed) + Text("le rendre visible ?").underline().foregroundStyle(.master)
}
}
}
#if DEBUG
Section { Section {
RowButtonView("Annuler toutes les convocations", role: .destructive) { RowButtonView("Annuler toutes les convocations", role: .destructive) {
let teams = tournament.unsortedTeams() let teams = tournament.unsortedTeams()
@ -48,9 +71,14 @@ struct CallSettingsView: View {
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams) try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} }
} }
#endif
} }
.sheet(isPresented: $showSendToAllView) { .sheet(isPresented: $showSendToAllView) {
SendToAllView() SendToAllView(addLink: false)
.tint(.master)
}
.sheet(isPresented: $addLink) {
SendToAllView(addLink: true)
.tint(.master) .tint(.master)
} }
} }

@ -81,11 +81,15 @@ struct CallView: View {
} }
var finalMessage: String { var finalMessage: String {
ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat) ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon)
} }
var reSummon: Bool {
teams.allSatisfy({ $0.called() })
}
var body: some View { var body: some View {
let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer" let callWord = reSummon ? "Reconvoquer" : "Convoquer"
HStack { HStack {
if teams.count == 1 { if teams.count == 1 {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {

@ -16,7 +16,7 @@ struct SendToAllView: View {
@State private var contactMethod: Int = 1 @State private var contactMethod: Int = 1
@State private var contactRecipients: Set<String> = Set() @State private var contactRecipients: Set<String> = Set()
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
let addLink: Bool
@State var cannotPayForTournament: Bool = false @State var cannotPayForTournament: Bool = false
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
@ -72,16 +72,16 @@ struct SendToAllView: View {
Section { Section {
RowButtonView("Contacter \(_totalString())") { RowButtonView("Contacter \(_totalString())") {
if contactMethod == 0 { if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: tournament.tournamentTitle(), tournamentBuild: nil) contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: addLink ? tournament.shareURL()?.absoluteString : nil, tournamentBuild: nil)
} else { } else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: nil, subject: tournament.tournamentTitle(), tournamentBuild: nil) contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: addLink ? tournament.shareURL()?.absoluteString : nil, subject: tournament.tournamentTitle(), tournamentBuild: nil)
} }
} }
} }
} }
.environment(\.editMode, Binding.constant(EditMode.active)) .environment(\.editMode, Binding.constant(EditMode.active))
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Réglages") .navigationTitle("Préparation")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
@ -169,5 +169,5 @@ struct SendToAllView: View {
} }
#Preview { #Preview {
SendToAllView() SendToAllView(addLink: true)
} }

@ -152,7 +152,7 @@ struct CashierView: View {
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur")) .searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur"))
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: _sharedData()) ShareLink(item: _sharedData().createTxtFile("bilan"))
} }
} }
} }

@ -32,6 +32,11 @@ struct GroupStageSettingsView: View {
Section { Section {
RowButtonView("Valider les poules", role: .destructive) { RowButtonView("Valider les poules", role: .destructive) {
tournament.shouldVerifyGroupStage = false tournament.shouldVerifyGroupStage = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
} footer: { } footer: {
Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de vos poules et valider que tout est ok.") Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de vos poules et valider que tout est ok.")

@ -281,70 +281,37 @@ struct MatchDetailView: View {
} }
.tint(.master) .tint(.master)
} }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
if match.courtIndex != nil {
Button(role: .destructive) {
match.removeCourt()
save()
} label: {
Text("Supprimer le terrain")
}
}
Button(role: .destructive) {
match.startDate = nil
match.endDate = nil
save()
} label: {
Text("Supprimer l'horaire")
}
// .refreshable { Button(role: .destructive) {
// if match.isBroadcasted() { match.resetScores()
// match.refreshBroadcast() save()
// } } label: {
// } Text("Supprimer les scores")
// .toolbar { }
// ToolbarItem(placement: .topBarTrailing) {
// Menu { } label: {
// Button { LabelOptions()
// scoreType = .live }
// } label: { }
// Label("Saisie Live", systemImage: "airplayaudio.circle") }
// }
//
// Button {
// scoreType = .prepare
// } label: {
// Label("Préparer", systemImage: "calendar")
// }
//
// Divider()
// Menu {
// if match.fieldIndex > 0 {
// Button(role: .destructive) {
// match.currentTournament?.removeField(match.fieldIndex)
// match.fieldIndex = 0
// match.refreshBroadcast()
// save()
// } label: {
// Label("Supprimer le terrain", systemImage: "figure.run")
// }
// }
// Button(role: .destructive) {
// match.restartMatch()
// save()
// } label: {
// Label("Supprimer l'horaire", systemImage: "xmark.circle.fill")
// }
//
// Button(role: .destructive) {
// match.resetScore()
// save()
// } label: {
// Label("Supprimer les scores", systemImage: "xmark.circle.fill")
// }
//
// if match.isFederalTournament == false && match.isFriendlyMatch == false {
// Button(role: .destructive) {
// match.resetMatch()
// save()
// } label: {
// Label("Supprimer les équipes et les scores", systemImage: "xmark.circle.fill")
// }
// }
// } label: {
// Text("Éditer")
// }
//
// } label: {
// Label("Options", systemImage: "ellipsis.circle")
// }
// }
// }
.navigationTitle(match.matchTitle()) .navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
@ -365,8 +332,6 @@ struct MatchDetailView: View {
@ViewBuilder @ViewBuilder
var menuView: some View { var menuView: some View {
broadcastView
if match.hasStarted() { if match.hasStarted() {
Section { Section {
editionView editionView
@ -454,24 +419,11 @@ struct MatchDetailView: View {
Text("Tirage au sort visuel") Text("Tirage au sort visuel")
} }
} }
// if match.canBroadcast() == true {
// Picker(selection: $broadcasted) {
// Text("Oui").tag(true)
// Text("Non").tag(false)
// } label: {
// Text("Diffuser automatiquement")
// }
// }
RowButtonView("Valider") { RowButtonView("Valider") {
match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup)
if broadcasted { save()
broadcastAndSave()
} else {
save()
}
isEditing.toggle() isEditing.toggle()
@ -481,22 +433,6 @@ struct MatchDetailView: View {
} }
} }
@ViewBuilder
var broadcastView: some View {
Section {
// if match.isBroadcasted() {
// RowButtonView("Arrêter de diffuser") {
// match.stopBroadcast()
// save()
// }
// } else if match.canBroadcast() == true {
// RowButtonView("Diffuser", systemImage: "airplayvideo") {
// broadcastAndSave()
// }
// }
}
}
var shareView: some View { var shareView: some View {
NavigationLink { NavigationLink {
//EditSharingView(match: match) //EditSharingView(match: match)
@ -509,16 +445,6 @@ struct MatchDetailView: View {
private func save() { private func save() {
try? dataStore.matches.addOrUpdate(instance: match) try? dataStore.matches.addOrUpdate(instance: match)
} }
private func broadcastAndSave() {
Task {
//try? await match.broadcast()
await MainActor.run {
}
}
}
} }
#Preview { #Preview {

@ -29,12 +29,12 @@ struct MatchSummaryView: View {
if let groupStage = match.groupStageObject { if let groupStage = match.groupStageObject {
self.roundTitle = groupStage.groupStageTitle() self.roundTitle = groupStage.groupStageTitle()
} else if let round = match.roundObject { } else if let round = match.roundObject {
self.roundTitle = round.roundTitle() self.roundTitle = round.roundTitle(.short)
} else { } else {
self.roundTitle = nil self.roundTitle = nil
} }
self.matchTitle = match.matchTitle() self.matchTitle = match.matchTitle(.short)
if let court = match.courtName(), match.hasEnded() == false { if let court = match.courtName(), match.hasEnded() == false {
self.courtName = court self.courtName = court

@ -85,7 +85,7 @@ struct CalendarView: View {
ForEach(tournamentsByDay, id: \.holderId) { tournamentHolder in ForEach(tournamentsByDay, id: \.holderId) { tournamentHolder in
if let tournament = tournamentHolder as? Tournament { if let tournament = tournamentHolder as? Tournament {
Section { Section {
Button(tournament.tournamentTitle()) { Button(tournament.tournamentTitle(.short)) {
navigation.path.append(tournament) navigation.path.append(tournament)
} }
} header: { } header: {

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct OngoingView: View { struct OngoingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@ -16,20 +17,42 @@ struct OngoingView: View {
var matches: [Match] { var matches: [Match] {
let sorting = sortByField ? fieldSorting : defaultSorting let sorting = sortByField ? fieldSorting : defaultSorting
return dataStore.matches.filter({ $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending) let now = Date()
return dataStore.matches.filter({ $0.startDate != nil && $0.startDate! < now && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending)
} }
var body: some View { var body: some View {
@Bindable var navigation = navigation @Bindable var navigation = navigation
NavigationStack(path: $navigation.ongoingPath) { NavigationStack(path: $navigation.ongoingPath) {
let matches = matches let matches = matches.filter { $0.currentTournament()?.isDeleted == false }
List { List {
ForEach(matches) { match in ForEach(matches) { match in
Section { Section {
MatchRowView(match: match, matchViewStyle: .standardStyle) MatchRowView(match: match, matchViewStyle: .standardStyle)
} header: { } header: {
if let tournament = match.currentTournament() { if let tournament = match.currentTournament() {
Text(tournament.tournamentTitle()) HStack {
Text(tournament.tournamentTitle(.short))
Spacer()
if let club = tournament.club() {
Text("@" + club.clubTitle(.short))
}
}
} else {
Text("Pas de tournoi")
}
} footer: {
HStack {
if let tournament = match.currentTournament() {
Text(tournament.eventLabel())
}
#if DEBUG
Spacer()
FooterButtonView("copier l'id") {
let pasteboard = UIPasteboard.general
pasteboard.string = match.id
}
#endif
} }
} }
} }
@ -51,6 +74,16 @@ struct OngoingView: View {
} label: { } label: {
} }
#if DEBUG
Button("effacer les mauvais matchs") {
let bad = matches.filter({ $0.currentTournament() == nil })
do {
try dataStore.matches.delete(contentOfs: bad)
} catch {
Logger.error(error)
}
}
#endif
//todo //todo
//presentFilterView.toggle() //presentFilterView.toggle()
} label: { } label: {

@ -34,7 +34,7 @@ struct TournamentOrganizerView: View {
HStack { HStack {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack { HStack {
ForEach(dataStore.tournaments) { tournament in ForEach(dataStore.tournaments.filter({ $0.isDeleted == false })) { tournament in
TournamentButtonView(tournament: tournament) TournamentButtonView(tournament: tournament)
} }
} }

@ -6,14 +6,57 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct ToolboxView: View { struct ToolboxView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
var body: some View { var body: some View {
@Bindable var navigation = navigation @Bindable var navigation = navigation
NavigationStack(path: $navigation.toolboxPath) { NavigationStack(path: $navigation.toolboxPath) {
List { List {
#if DEBUG
Section {
RowButtonView("Fix Names") {
let playerRegistrations = dataStore.playerRegistrations
playerRegistrations.forEach { player in
player.firstName = player.firstName.trimmed.capitalized
player.lastName = player.lastName.trimmed.uppercased()
}
do {
try dataStore.playerRegistrations.addOrUpdate(contentOfs: playerRegistrations)
} catch {
Logger.error(error)
}
}
}
Section {
RowButtonView("Delete teams") {
let teamRegistrations = dataStore.teamRegistrations.filter({ $0.tournamentObject() == nil })
do {
try dataStore.teamRegistrations.delete(contentOfs: teamRegistrations)
} catch {
Logger.error(error)
}
}
}
Section {
RowButtonView("Delete players") {
let playersRegistrations = dataStore.playerRegistrations.filter({ $0.team() == nil })
do {
try dataStore.playerRegistrations.delete(contentOfs: playersRegistrations)
} catch {
Logger.error(error)
}
}
}
#endif
Section { Section {
NavigationLink { NavigationLink {
SelectablePlayerListView() SelectablePlayerListView()

@ -30,23 +30,34 @@ struct SchedulerView: View {
List { List {
switch destination { switch destination {
case .scheduleGroupStage: case .scheduleGroupStage:
MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) { Section {
Task { MatchFormatPickingView(matchFormat: $tournament.groupStageMatchFormat) {
tournament.matchScheduler()?.updateSchedule(tournament: tournament) Task {
tournament.matchScheduler()?.updateSchedule(tournament: tournament)
}
} }
} .onChange(of: tournament.groupStageMatchFormat) {
.onChange(of: tournament.groupStageMatchFormat) { let groupStages = tournament.groupStages()
let groupStages = tournament.groupStages() groupStages.forEach { groupStage in
groupStages.forEach { groupStage in groupStage.updateMatchFormat(tournament.groupStageMatchFormat)
groupStage.updateMatchFormat(tournament.groupStageMatchFormat) }
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} catch {
Logger.error(error)
}
} }
do { } footer: {
try dataStore.tournaments.addOrUpdate(instance: tournament) if tournament.groupStageMatchFormat.weight > tournament.groupStageSmartMatchFormat().weight {
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages) Button {
} catch { tournament.groupStageMatchFormat = tournament.groupStageSmartMatchFormat()
Logger.error(error) } label: {
Text("devrait être joué au moins en " + tournament.groupStageSmartMatchFormat().format + ", ") + Text("modifier ?").underline().foregroundStyle(.master)
}
.buttonStyle(.plain)
} }
} }
ForEach(tournament.groupStages()) { ForEach(tournament.groupStages()) {
@ -63,6 +74,19 @@ struct SchedulerView: View {
} }
.headerProminence(.increased) .headerProminence(.increased)
.monospacedDigit() .monospacedDigit()
.onChange(of: tournament.groupStageMatchFormat) {
let groupStages = tournament.groupStages()
tournament.groupStages().forEach { groupStage in
groupStage.updateMatchFormat(tournament.groupStageMatchFormat)
}
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} catch {
Logger.error(error)
}
}
} }
@ViewBuilder @ViewBuilder
@ -92,33 +116,67 @@ struct SchedulerView: View {
} }
} header: { } header: {
Text(round.titleLabel()) Text(round.titleLabel())
} footer: {
let federalFormat = tournament.roundSmartMatchFormat(round.index)
if round.matchFormat.weight > federalFormat.weight {
Button {
round.updateMatchFormatAndAllMatches(federalFormat)
do {
try dataStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
} label: {
Text("devrait être joué au moins en " + federalFormat.format + ", ") + Text("modifier ?").underline().foregroundStyle(.master)
}
.buttonStyle(.plain)
}
} }
Section { if round.index != 0 {
NavigationLink { Section {
LoserRoundScheduleEditorView(upperRound: round, tournament: tournament) NavigationLink {
.environment(tournament) LoserRoundScheduleEditorView(upperRound: round, tournament: tournament)
} label: { .environment(tournament)
LabeledContent {
let count = round.loserRounds().filter({ $0.isDisabled() == false }).count
Text(count.formatted() + " tour" + count.pluralSuffix)
} label: { } label: {
if let startDate = round.getLoserRoundStartDate() { LabeledContent {
HStack { let count = round.loserRounds().filter({ $0.isDisabled() == false }).count
Text(startDate.formattedAsHourMinute()).font(.title3) Text(count.formatted() + " tour" + count.pluralSuffix)
if let estimatedEndDate = round.estimatedLoserRoundEndDate(tournament.additionalEstimationDuration) { } label: {
Image(systemName: "arrowshape.forward.fill") if let startDate = round.getLoserRoundStartDate() {
Text(estimatedEndDate.formattedAsHourMinute()).font(.title3) HStack {
Text(startDate.formattedAsHourMinute()).font(.title3)
if let estimatedEndDate = round.estimatedLoserRoundEndDate(tournament.additionalEstimationDuration) {
Image(systemName: "arrowshape.forward.fill")
Text(estimatedEndDate.formattedAsHourMinute()).font(.title3)
}
} }
Text(startDate.formattedAsDate())
} else {
Text("Aucun horaire")
} }
Text(startDate.formattedAsDate()) }
} else { }
Text("Aucun horaire") } header: {
Text("Match de classement \(round.roundTitle(.short))")
} footer: {
if round.index == 1, let semi = round.loserRounds().first {
let federalFormat = tournament.loserBracketSmartMatchFormat(1)
if semi.matchFormat.weight > federalFormat.weight {
Button {
round.updateMatchFormatAndAllMatches(federalFormat)
do {
try dataStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
} label: {
Text("devrait être joué au moins en " + federalFormat.format + ", ") + Text("modifier ?").underline().foregroundStyle(.master)
}
.buttonStyle(.plain)
} }
} }
} }
} header: {
Text("Match de classement \(round.roundTitle(.short))")
} }
} }
} }

@ -32,7 +32,6 @@ struct PlayerPopoverView: View {
@State private var source: String? @State private var source: String?
init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) { init(source: String?, sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) {
let source = source
if let source { if let source {
let words = source.components(separatedBy: .whitespaces) let words = source.components(separatedBy: .whitespaces)
if words.isEmpty == false { if words.isEmpty == false {
@ -44,6 +43,8 @@ struct PlayerPopoverView: View {
} else { } else {
_firstName = State(wrappedValue: source) _firstName = State(wrappedValue: source)
} }
_source = State(wrappedValue: source)
} }
_sex = State(wrappedValue: sex) _sex = State(wrappedValue: sex)
@ -51,7 +52,6 @@ struct PlayerPopoverView: View {
self.requiredField = requiredField self.requiredField = requiredField
self.creationCompletionHandler = creationCompletionHandler self.creationCompletionHandler = creationCompletionHandler
_source = State(wrappedValue: source)
} }
var body: some View { var body: some View {
@ -94,6 +94,7 @@ struct PlayerPopoverView: View {
Spacer() Spacer()
TextField("Prénom", text: $firstName) TextField("Prénom", text: $firstName)
.submitLabel(.next) .submitLabel(.next)
.autocorrectionDisabled()
.keyboardType(.alphabet) .keyboardType(.alphabet)
.textInputAutocapitalization(.words) .textInputAutocapitalization(.words)
.focused($firstNameIsFocused) .focused($firstNameIsFocused)
@ -108,6 +109,7 @@ struct PlayerPopoverView: View {
Spacer() Spacer()
TextField("Nom", text: $lastName) TextField("Nom", text: $lastName)
.submitLabel(.next) .submitLabel(.next)
.autocorrectionDisabled()
.textInputAutocapitalization(.words) .textInputAutocapitalization(.words)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.focused($lastNameIsFocused) .focused($lastNameIsFocused)
@ -180,7 +182,6 @@ struct PlayerPopoverView: View {
.onAppear { .onAppear {
firstNameIsFocused = true firstNameIsFocused = true
} }
.autocorrectionDisabled()
.navigationTitle(sex == 1 ? "Nouveau joueur" : "Nouvelle joueuse") .navigationTitle(sex == 1 ? "Nouveau joueur" : "Nouvelle joueuse")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
@ -199,23 +200,27 @@ struct PlayerPopoverView: View {
} }
} }
if amountIsFocused || licenseIsFocused { if licenseIsFocused || amountIsFocused {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {
Button("Confirmer") { HStack {
if licenseIsFocused { Spacer()
license = license.trimmed Button("Confirmer") {
if requiredField.contains(.license) { if licenseIsFocused {
if license.isLicenseNumber { license = license.trimmed
amountIsFocused = true if requiredField.contains(.license) {
if license.isLicenseNumber {
amountIsFocused = true
} else {
displayWrongLicenceError = true
}
} else { } else {
displayWrongLicenceError = true amountIsFocused = true
} }
} else { } else {
amountIsFocused = true amountIsFocused = false
} }
} else {
amountIsFocused = false
} }
.buttonStyle(.bordered)
} }
} }
} }

@ -46,6 +46,9 @@ struct PlayerDetailView: View {
.focused($textFieldIsFocus) .focused($textFieldIsFocus)
} label: { } label: {
Text("Rang") Text("Rang")
if player.rank == nil {
Text("Classement calculé : " + player.computedRank.formatted())
}
} }
} header: { } header: {
Text("Classement actuel") Text("Classement actuel")

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct RoundSettingsView: View { struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -18,6 +19,11 @@ struct RoundSettingsView: View {
Section { Section {
RowButtonView("Valider le tableau", role: .destructive) { RowButtonView("Valider le tableau", role: .destructive) {
tournament.shouldVerifyBracket = false tournament.shouldVerifyBracket = false
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
} footer: { } footer: {
Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.")

@ -28,7 +28,7 @@ struct LearnMoreSheetView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
ShareLink(item: tournament.pasteDataForImporting()) { ShareLink(item: tournament.pasteDataForImporting().createTxtFile(tournament.tournamentTitle(.short))) {
HStack { HStack {
Spacer() Spacer()
Text("Exporter les inscriptions") Text("Exporter les inscriptions")

@ -9,12 +9,19 @@ import SwiftUI
import LeStorage import LeStorage
struct TournamentGeneralSettingsView: View { struct TournamentGeneralSettingsView: View {
@Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@FocusState private var textFieldIsFocus: Bool @State private var entryFee: Double? = nil
@FocusState private var focusedField: Tournament.CodingKeys?
init(tournament: Tournament) {
self.tournament = tournament
_tournamentName = State(wrappedValue: tournament.name ?? "")
_entryFee = State(wrappedValue: tournament.entryFee)
}
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
Form { Form {
@ -29,44 +36,41 @@ struct TournamentGeneralSettingsView: View {
Section { Section {
LabeledContent { LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.focused($textFieldIsFocus) .focused($focusedField, equals: ._entryFee)
} label: { } label: {
Text("Inscription") Text("Inscription")
} }
} }
Section { Section {
LabeledContent { TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
TextField("Nom", text: $tournamentName) .lineLimit(2)
.multilineTextAlignment(.trailing) .frame(maxWidth: .infinity)
.frame(maxWidth: .infinity) .keyboardType(.alphabet)
.keyboardType(.alphabet) .focused($focusedField, equals: ._name)
.autocorrectionDisabled()
.onSubmit {
if tournamentName.trimmed.isEmpty {
tournament.name = nil
} else {
tournament.name = tournamentName
}
}
} label: {
Text("Nom du tournoi")
}
} }
} }
.scrollDismissesKeyboard(.immediately)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
if textFieldIsFocus { if focusedField != nil {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {
HStack { HStack {
Spacer() Spacer()
Button("Valider") { Button("Valider") {
textFieldIsFocus = false if focusedField == ._name {
if tournamentName.trimmed.isEmpty {
tournament.name = nil
} else {
tournament.name = tournamentName
}
} else if focusedField == ._entryFee {
tournament.entryFee = entryFee
}
focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
@ -115,7 +119,3 @@ struct TournamentGeneralSettingsView: View {
} }
} }
} }
#Preview {
TournamentGeneralSettingsView()
}

@ -120,18 +120,22 @@ struct InscriptionManagerView: View {
} }
} }
.onAppear { .onAppear {
self.presentationCount += 1
if self.teamsHash == nil { if self.teamsHash == nil {
self.teamsHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) self.teamsHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
} }
} }
.onDisappear { .onDisappear {
self.presentationCount -= 1 let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
if self.presentationCount == 0 { if let teamsHash, newHash != teamsHash {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) self.teamsHash = newHash
if let teamsHash { if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false {
self.tournament.shouldVerifyBracket = newHash != teamsHash self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = newHash != teamsHash self.tournament.shouldVerifyGroupStage = true
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
} }
} }
@ -161,6 +165,7 @@ struct InscriptionManagerView: View {
} }
.sheet(isPresented: $presentPlayerCreation) { .sheet(isPresented: $presentPlayerCreation) {
PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in
p.setComputedRank(in: tournament)
createdPlayers.insert(p) createdPlayers.insert(p)
createdPlayerIds.insert(p.id) createdPlayerIds.insert(p.id)
} }
@ -249,7 +254,7 @@ struct InscriptionManagerView: View {
Label("Clôturer", systemImage: "lock") Label("Clôturer", systemImage: "lock")
} }
Divider() Divider()
ShareLink(item: tournament.pasteDataForImporting()) { ShareLink(item: tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))) {
Label("Exporter les paires", systemImage: "square.and.arrow.up") Label("Exporter les paires", systemImage: "square.and.arrow.up")
} }
Button { Button {
@ -531,6 +536,23 @@ struct InscriptionManagerView: View {
private func _informationView(count: Int) -> some View { private func _informationView(count: Int) -> some View {
Section { Section {
let unsortedTeams = tournament.unsortedTeams()
let walkoutTeams = tournament.walkoutTeams()
LabeledContent {
Text(unsortedTeams.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
} label: {
Text("Paire\(unsortedTeams.count.pluralSuffix) inscrite\(unsortedTeams.count.pluralSuffix)")
Text("dont \(walkoutTeams.count) forfait\(walkoutTeams.count.pluralSuffix)")
}
let unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
LabeledContent {
Text(max(0, unsortedTeamsWithoutWO.count - tournament.teamCount).formatted()).font(.largeTitle)
} label: {
Text("Liste d'attente")
}
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView()
.environment(tournament) .environment(tournament)
@ -544,8 +566,6 @@ struct InscriptionManagerView: View {
} }
} }
} }
} header: {
Text(count.formatted() + "/" + tournament.teamCount.formatted() + " paires inscrites")
} }
} }
@ -849,6 +869,11 @@ struct InscriptionManagerView: View {
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View { private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu { Menu {
Section { Section {
Button("Copier") {
let pasteboard = UIPasteboard.general
pasteboard.string = team.playersPasteData()
}
Divider()
Button("Changer les joueurs") { Button("Changer les joueurs") {
editedTeam = team editedTeam = team
team.unsortedPlayers().forEach { player in team.unsortedPlayers().forEach { player in
@ -906,6 +931,7 @@ struct InscriptionManagerView: View {
} label: { } label: {
LabelDelete() LabelDelete()
} }
// } header: { // } header: {
// Text(team.teamLabel(.short)) // Text(team.teamLabel(.short))
} }

@ -67,7 +67,7 @@ struct TournamentSettingsView: View {
case .matchFormats: case .matchFormats:
TournamentMatchFormatsSettingsView() TournamentMatchFormatsSettingsView()
case .general: case .general:
TournamentGeneralSettingsView() TournamentGeneralSettingsView(tournament: tournament)
case .club: case .club:
TournamentClubSettingsView() TournamentClubSettingsView()
} }

@ -71,7 +71,7 @@ struct TournamentCellView: View {
Spacer() Spacer()
if let tournament = tournament as? Tournament, displayStyle == .wide { if let tournament = tournament as? Tournament, displayStyle == .wide {
let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted() let hasStarted = tournament.inscriptionClosed() || tournament.hasStarted()
let count = hasStarted ? tournament.selectedSortedTeams().count : tournament.unsortedTeams().count let count = hasStarted ? tournament.selectedSortedTeams().count : tournament.unsortedTeamsWithoutWO().count
Text(count.formatted()) Text(count.formatted())
} else if let federalTournament = tournament as? FederalTournament { } else if let federalTournament = tournament as? FederalTournament {
Button { Button {

@ -17,7 +17,7 @@ struct TournamentInscriptionView: View {
Section { Section {
NavigationLink(value: Screen.inscription) { NavigationLink(value: Screen.inscription) {
LabeledContent { LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) Text(tournament.unsortedTeamsWithoutWO().count.formatted() + "/" + tournament.teamCount.formatted())
} label: { } label: {
Text("Gestion des inscriptions") Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate { if let closedRegistrationDate = tournament.closedRegistrationDate {

@ -121,7 +121,7 @@ struct TournamentView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
VStack(spacing: -4.0) { VStack(spacing: -4.0) {
Text(tournament.tournamentTitle()).font(.headline) Text(tournament.tournamentTitle(.short)).font(.headline)
Text(tournament.formattedDate()) Text(tournament.formattedDate())
.font(.subheadline).foregroundStyle(.secondary) .font(.subheadline).foregroundStyle(.secondary)
} }

@ -96,7 +96,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) 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)
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)
@ -130,7 +130,8 @@ final class ServerDataTests: XCTestCase {
assert(t.publishSummons == tournament.publishSummons) assert(t.publishSummons == tournament.publishSummons)
assert(t.publishGroupStages == tournament.publishGroupStages) assert(t.publishGroupStages == tournament.publishGroupStages)
assert(t.publishBrackets == tournament.publishBrackets) assert(t.publishBrackets == tournament.publishBrackets)
assert(t.shouldVerifyBracket == tournament.shouldVerifyBracket)
assert(t.shouldVerifyGroupStage == tournament.shouldVerifyGroupStage)
} }
func testGroupStage() async throws { func testGroupStage() async throws {

Loading…
Cancel
Save