sync_v2
Raz 8 months ago
parent acb0be6ec3
commit 74d6f8ba3c
  1. 12
      PadelClub/Data/CustomUser.swift
  2. 28
      PadelClub/Data/Gen/BaseTournament.swift
  3. 22
      PadelClub/Data/Gen/Tournament.json
  4. 13
      PadelClub/Data/Match.swift
  5. 11
      PadelClub/Data/Tournament.swift
  6. 5
      PadelClub/Extensions/String+Extensions.swift
  7. 4
      PadelClub/Utils/ContactManager.swift
  8. 4
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  9. 2
      PadelClub/Views/Calling/CallView.swift
  10. 2
      PadelClub/Views/Calling/SendToAllView.swift
  11. 2
      PadelClub/Views/Score/EditScoreView.swift
  12. 113
      PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift
  13. 262
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  14. 6
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  15. 2
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  16. 9
      PadelClubTests/ServerDataTests.swift

@ -82,15 +82,13 @@ class CustomUser: BaseCustomUser, UserBase {
return try? federalContext.fetch(fetchRequest).first return try? federalContext.fetch(fetchRequest).first
} }
func defaultSignature() -> String { func defaultSignature(_ tournament: Tournament?) -> String {
return "Sportivement,\n\(firstName) \(lastName), votre JAP." let fullName = tournament?.umpireCustomContact ?? fullName()
return "Sportivement,\n\(fullName), votre JAP."
} }
func fullName() -> String? { func fullName() -> String {
guard firstName.isEmpty == false && lastName.isEmpty == false else { [firstName, lastName].joined(separator: " ")
return nil
}
return "\(firstName) \(lastName)"
} }
func hasTenupClubs() -> Bool { func hasTenupClubs() -> Bool {

@ -64,6 +64,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
var maximumPlayerPerTeam: Int = 2 var maximumPlayerPerTeam: Int = 2
var information: String? = nil var information: String? = nil
var umpireCustomMail: String? = nil var umpireCustomMail: String? = nil
var umpireCustomContact: String? = nil
var umpireCustomPhone: String? = nil
var hideUmpireMail: Bool = false
var hideUmpirePhone: Bool = true
var disableRankingFederalRuling: Bool = false var disableRankingFederalRuling: Bool = false
init( init(
@ -120,6 +124,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
maximumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2,
information: String? = nil, information: String? = nil,
umpireCustomMail: String? = nil, umpireCustomMail: String? = nil,
umpireCustomContact: String? = nil,
umpireCustomPhone: String? = nil,
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true,
disableRankingFederalRuling: Bool = false disableRankingFederalRuling: Bool = false
) { ) {
super.init() super.init()
@ -176,6 +184,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.maximumPlayerPerTeam = maximumPlayerPerTeam self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information self.information = information
self.umpireCustomMail = umpireCustomMail self.umpireCustomMail = umpireCustomMail
self.umpireCustomContact = umpireCustomContact
self.umpireCustomPhone = umpireCustomPhone
self.hideUmpireMail = hideUmpireMail
self.hideUmpirePhone = hideUmpirePhone
self.disableRankingFederalRuling = disableRankingFederalRuling self.disableRankingFederalRuling = disableRankingFederalRuling
} }
@ -233,6 +245,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
case _maximumPlayerPerTeam = "maximumPlayerPerTeam" case _maximumPlayerPerTeam = "maximumPlayerPerTeam"
case _information = "information" case _information = "information"
case _umpireCustomMail = "umpireCustomMail" case _umpireCustomMail = "umpireCustomMail"
case _umpireCustomContact = "umpireCustomContact"
case _umpireCustomPhone = "umpireCustomPhone"
case _hideUmpireMail = "hideUmpireMail"
case _hideUmpirePhone = "hideUmpirePhone"
case _disableRankingFederalRuling = "disableRankingFederalRuling" case _disableRankingFederalRuling = "disableRankingFederalRuling"
} }
@ -352,6 +368,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2 self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2
self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? nil self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? nil
self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil
self.umpireCustomContact = try container.decodeIfPresent(String.self, forKey: ._umpireCustomContact) ?? nil
self.umpireCustomPhone = try container.decodeIfPresent(String.self, forKey: ._umpireCustomPhone) ?? nil
self.hideUmpireMail = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpireMail) ?? false
self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true
self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -411,6 +431,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam) try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam)
try container.encode(self.information, forKey: ._information) try container.encode(self.information, forKey: ._information)
try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail) try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail)
try container.encode(self.umpireCustomContact, forKey: ._umpireCustomContact)
try container.encode(self.umpireCustomPhone, forKey: ._umpireCustomPhone)
try container.encode(self.hideUmpireMail, forKey: ._hideUmpireMail)
try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone)
try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -475,6 +499,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam
self.information = tournament.information self.information = tournament.information
self.umpireCustomMail = tournament.umpireCustomMail self.umpireCustomMail = tournament.umpireCustomMail
self.umpireCustomContact = tournament.umpireCustomContact
self.umpireCustomPhone = tournament.umpireCustomPhone
self.hideUmpireMail = tournament.hideUmpireMail
self.hideUmpirePhone = tournament.hideUmpirePhone
self.disableRankingFederalRuling = tournament.disableRankingFederalRuling self.disableRankingFederalRuling = tournament.disableRankingFederalRuling
} }

@ -270,10 +270,30 @@
"type": "String", "type": "String",
"optional": true "optional": true
}, },
{
"name": "umpireCustomContact",
"type": "String",
"optional": true
},
{
"name": "umpireCustomPhone",
"type": "String",
"optional": true
},
{
"name": "hideUmpireMail",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "hideUmpirePhone",
"type": "Bool",
"defaultValue": "true"
},
{ {
"name": "disableRankingFederalRuling", "name": "disableRankingFederalRuling",
"type": "Bool", "type": "Bool",
"defaultValue": false, "defaultValue": "false",
"optional": false "optional": false
} }
] ]

@ -522,6 +522,19 @@ defer {
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
func setUnfinishedScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
updateScore(fromMatchDescriptor: matchDescriptor)
if endDate == nil {
endDate = Date()
}
if startDate == nil {
startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
} else if let startDate, let endDate, startDate >= endDate {
self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
}
confirmed = true
}
func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
updateScore(fromMatchDescriptor: matchDescriptor) updateScore(fromMatchDescriptor: matchDescriptor)
if endDate == nil { if endDate == nil {

@ -32,6 +32,10 @@ final class Tournament: BaseTournament {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil, internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil,
umpireCustomMail: String? = nil, umpireCustomMail: String? = nil,
umpireCustomContact: String? = nil,
umpireCustomPhone: String? = nil,
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true,
disableRankingFederalRuling: Bool = false disableRankingFederalRuling: Bool = false
) { ) {
super.init() super.init()
@ -99,6 +103,7 @@ final class Tournament: BaseTournament {
self.maximumPlayerPerTeam = maximumPlayerPerTeam self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information self.information = information
self.umpireCustomMail = umpireCustomMail self.umpireCustomMail = umpireCustomMail
self.umpireCustomContact = umpireCustomContact
self.disableRankingFederalRuling = disableRankingFederalRuling self.disableRankingFederalRuling = disableRankingFederalRuling
} }
@ -2595,12 +2600,16 @@ extension Tournament {
let disableRankingFederalRuling = tournaments.first?.disableRankingFederalRuling ?? false let disableRankingFederalRuling = tournaments.first?.disableRankingFederalRuling ?? false
let umpireCustomMail = tournaments.first?.umpireCustomMail let umpireCustomMail = tournaments.first?.umpireCustomMail
let umpireCustomPhone = tournaments.first?.umpireCustomPhone
let umpireCustomContact = tournaments.first?.umpireCustomContact
let hideUmpireMail = tournaments.first?.hideUmpireMail ?? false
let hideUmpirePhone = tournaments.first?.hideUmpirePhone ?? true
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//creator: DataStore.shared.user?.id //creator: DataStore.shared.user?.id
return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode, umpireCustomMail: umpireCustomMail, disableRankingFederalRuling: disableRankingFederalRuling) return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode, umpireCustomMail: umpireCustomMail, umpireCustomContact: umpireCustomContact, umpireCustomPhone: umpireCustomPhone, hideUmpireMail: hideUmpireMail, hideUmpirePhone: hideUmpirePhone, disableRankingFederalRuling: disableRankingFederalRuling)
} }
static func fake() -> Tournament { static func fake() -> Tournament {

@ -171,12 +171,17 @@ extension String {
extension String { extension String {
enum RegexStatic { enum RegexStatic {
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/
} }
func isMobileNumber() -> Bool { func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil firstMatch(of: RegexStatic.mobileNumber) != nil
} }
func isPhoneNumber() -> Bool {
firstMatch(of: RegexStatic.phoneNumber) != nil
}
//april 04-2024 bug with accent characters / adobe / fft //april 04-2024 bug with accent characters / adobe / fft
mutating func replace(characters: [(Character, Character)]) { mutating func replace(characters: [(Character, Character)]) {
for (targetChar, replacementChar) in characters { for (targetChar, replacementChar) in characters {

@ -91,7 +91,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))")
text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))") text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))")
let signature = DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature() let signature = DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature(tournament)
text = text.replacingOccurrences(of: "#signature", with: signature) text = text.replacingOccurrences(of: "#signature", with: signature)
return text return text
@ -109,7 +109,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let clubName = tournament?.clubName ?? "" let clubName = tournament?.clubName ?? ""
let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature() let signature = DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature(tournament)
let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s"

@ -29,7 +29,7 @@ struct CallMessageCustomizationView: View {
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))
_customCallMessageSignature = State(wrappedValue: DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature()) _customCallMessageSignature = State(wrappedValue: DataStore.shared.user.summonsMessageSignature ?? DataStore.shared.user.defaultSignature(tournament))
_customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi") _customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi")
_summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods) _summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods)
} }
@ -89,7 +89,7 @@ struct CallMessageCustomizationView: View {
} }
Divider() Divider()
FooterButtonView("défaut") { FooterButtonView("défaut") {
customCallMessageSignature = DataStore.shared.user.defaultSignature() customCallMessageSignature = DataStore.shared.user.defaultSignature(tournament)
_save() _save()
} }
Divider() Divider()

@ -139,7 +139,7 @@ struct CallView: View {
func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String { func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String {
if simpleMode || forcedEmptyMessage { if simpleMode || forcedEmptyMessage {
let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature() let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature(tournament)
return "\n\n\n\n" + signature return "\n\n\n\n" + signature
} }

@ -261,7 +261,7 @@ struct SendToAllView: View {
message.append(tournament.shareURL(pageLink)?.absoluteString) message.append(tournament.shareURL(pageLink)?.absoluteString)
} }
let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature() let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature(tournament)
message.append(signature) message.append(signature)

@ -202,7 +202,7 @@ struct EditScoreView: View {
Section { Section {
RowButtonView("Terminer la rencontre") { RowButtonView("Terminer la rencontre") {
matchDescriptor.match?.setScore(fromMatchDescriptor: matchDescriptor) matchDescriptor.match?.setUnfinishedScore(fromMatchDescriptor: matchDescriptor)
save() save()
dismiss() dismiss()
} }

@ -10,13 +10,71 @@ import SwiftUI
import LeStorage import LeStorage
struct TournamentCategorySettingsView: View { struct TournamentCategorySettingsView: View {
@Environment(Tournament.self) private var tournament: Tournament @Bindable var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State private var confirmationRequired: Bool = false
@State private var presentConfirmation: Bool = false
@State private var loserBracketMode: LoserBracketMode
init(tournament: Tournament) {
self.tournament = tournament
_loserBracketMode = .init(wrappedValue: tournament.loserBracketMode)
}
var body: some View { var body: some View {
List { List {
TournamentLevelPickerView() TournamentLevelPickerView()
Section {
Picker(selection: $loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: loserBracketMode) {
if tournament.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false {
_refreshLoserBracketMode()
} else {
confirmationRequired = true
}
}
} header: {
Text("Matchs de classement")
} footer: {
if confirmationRequired == false {
if dataStore.user.loserBracketMode != tournament.loserBracketMode {
_footerView()
.onTapGesture(perform: {
self.dataStore.user.loserBracketMode = tournament.loserBracketMode
self.dataStore.saveUser()
})
} else {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
}
} else {
_footerViewConfirmationRequired()
.onTapGesture(perform: {
presentConfirmation = true
})
}
}
} }
.confirmationDialog("Attention", isPresented: $presentConfirmation, actions: {
Button("Confirmer", role: .destructive) {
_refreshLoserBracketMode()
confirmationRequired = false
}
Button("Annuler", role: .cancel) {
loserBracketMode = tournament.loserBracketMode
}
})
.toolbarBackground(.visible, for: .navigationBar)
.onChange(of: [ .onChange(of: [
tournament.federalCategory, tournament.federalCategory,
]) { ]) {
@ -32,8 +90,49 @@ struct TournamentCategorySettingsView: View {
]) { ]) {
_save() _save()
} }
.onChange(of: [
tournament.groupStageSortMode,
]) {
_save()
}
}
private func _refreshLoserBracketMode() {
tournament.loserBracketMode = loserBracketMode
_save()
let rounds = tournament.rounds()
rounds.forEach { round in
let matches = round.loserRoundsAndChildren().flatMap({ $0._matches() })
matches.forEach { match in
match.resetTeamScores(outsideOf: [])
match.resetMatch()
match.confirmed = false
}
round.loserBracketMode = tournament.loserBracketMode
if loserBracketMode == .automatic {
matches.forEach { match in
match.updateTeamScores()
}
}
do {
try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
}
do {
try self.tournament.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
}
} }
private func _save() { private func _save() {
do { do {
if tournament.onlineRegistrationCanBeEnabled() == false, tournament.enableOnlineRegistration { if tournament.onlineRegistrationCanBeEnabled() == false, tournament.enableOnlineRegistration {
@ -45,4 +144,16 @@ struct TournamentCategorySettingsView: View {
} }
} }
private func _footerView() -> some View {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
+
Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue)
}
private func _footerViewConfirmationRequired() -> some View {
Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.")
+
Text(" Modifier quand même ?").foregroundStyle(.red)
}
} }

@ -15,21 +15,23 @@ struct TournamentGeneralSettingsView: View {
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var tournamentInformation: String = "" @State private var tournamentInformation: String = ""
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@State private var confirmationRequired: Bool = false
@State private var presentConfirmation: Bool = false
@State private var loserBracketMode: LoserBracketMode
@State private var umpireCustomMail: String @State private var umpireCustomMail: String
@State private var umpireCustomPhone: String
@State private var umpireCustomContact: String
@State private var umpireCustomMailIsInvalid: Bool = false @State private var umpireCustomMailIsInvalid: Bool = false
@State private var umpireCustomPhoneIsInvalid: Bool = false
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0] let priceTags: [Double] = [15.0, 20.0, 25.0]
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_loserBracketMode = .init(wrappedValue: tournament.loserBracketMode)
_tournamentName = State(wrappedValue: tournament.name ?? "") _tournamentName = State(wrappedValue: tournament.name ?? "")
_tournamentInformation = State(wrappedValue: tournament.information ?? "") _tournamentInformation = State(wrappedValue: tournament.information ?? "")
_entryFee = State(wrappedValue: tournament.entryFee) _entryFee = State(wrappedValue: tournament.entryFee)
_umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "") _umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "")
_umpireCustomPhone = State(wrappedValue: tournament.umpireCustomPhone ?? "")
_umpireCustomContact = State(wrappedValue: tournament.umpireCustomContact ?? "")
} }
var body: some View { var body: some View {
@ -78,6 +80,22 @@ struct TournamentGeneralSettingsView: View {
_customUmpireView() _customUmpireView()
Section {
if tournament.hideUmpireMail, tournament.hideUmpirePhone, tournament.enableOnlineRegistration {
Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed)
}
Toggle(isOn: $tournament.hideUmpireMail) {
Text("Masquer l'email")
}
Toggle(isOn: $tournament.hideUmpirePhone) {
Text("Masquer le téléphone")
}
} footer: {
Text("Ces informations ne seront pas affichées sur la page d'information du tournoi sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
}
Section { Section {
TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
.lineLimit(2) .lineLimit(2)
@ -110,53 +128,7 @@ struct TournamentGeneralSettingsView: View {
tournamentInformation.append("\n" + tournament.entryFeeMessage) tournamentInformation.append("\n" + tournament.entryFeeMessage)
} }
} }
Section {
Picker(selection: $loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: loserBracketMode) {
if tournament.allLoserRoundMatches().anySatisfy({ $0.hasEnded() }) == false {
_refreshLoserBracketMode()
} else {
confirmationRequired = true
}
}
} header: {
Text("Matchs de classement")
} footer: {
if confirmationRequired == false {
if dataStore.user.loserBracketMode != tournament.loserBracketMode {
_footerView()
.onTapGesture(perform: {
self.dataStore.user.loserBracketMode = tournament.loserBracketMode
self.dataStore.saveUser()
})
} else {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
}
} else {
_footerViewConfirmationRequired()
.onTapGesture(perform: {
presentConfirmation = true
})
}
}
} }
.confirmationDialog("Attention", isPresented: $presentConfirmation, actions: {
Button("Confirmer", role: .destructive) {
_refreshLoserBracketMode()
confirmationRequired = false
}
Button("Annuler", role: .cancel) {
loserBracketMode = tournament.loserBracketMode
}
})
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
@ -196,14 +168,12 @@ struct TournamentGeneralSettingsView: View {
Button("Effacer") { Button("Effacer") {
tournament.name = nil tournament.name = nil
tournamentName = "" tournamentName = ""
_save()
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} else if focusedField == ._information, tournamentInformation.isEmpty == false { } else if focusedField == ._information, tournamentInformation.isEmpty == false {
Button("Effacer") { Button("Effacer") {
tournament.information = nil tournament.information = nil
tournamentInformation = "" tournamentInformation = ""
_save()
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { } else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false {
@ -211,29 +181,20 @@ struct TournamentGeneralSettingsView: View {
_deleteUmpireMail() _deleteUmpireMail()
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false {
Button("Effacer") {
_deleteUmpirePhone()
}
.buttonStyle(.borderless)
} else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false {
Button("Effacer") {
_deleteUmpireContact()
}
.buttonStyle(.borderless)
} }
} }
Spacer() Spacer()
Button("Valider") { Button("Valider") {
if focusedField == ._name {
let tournamentName = tournamentName.prefixMultilineTrimmed(200)
if tournamentName.isEmpty {
tournament.name = nil
} else {
tournament.name = tournamentName
}
} else if focusedField == ._information {
let tournamentInformation = tournamentInformation.prefixMultilineTrimmed(4000)
if tournamentInformation.isEmpty {
tournament.information = nil
} else {
tournament.information = tournamentInformation
}
} else if focusedField == ._entryFee {
tournament.entryFee = entryFee
} else if focusedField == ._umpireCustomMail {
_confirmUmpireMail()
}
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -247,15 +208,39 @@ struct TournamentGeneralSettingsView: View {
.onChange(of: tournament.entryFee) { .onChange(of: tournament.entryFee) {
_save() _save()
} }
.onChange(of: [tournament.name, tournament.information]) { .onChange(of: [tournament.name, tournament.information, tournament.umpireCustomMail, tournament.umpireCustomPhone, tournament.umpireCustomContact]) {
_save()
}
.onChange(of: [tournament.hideUmpireMail, tournament.hideUmpirePhone]) {
_save() _save()
} }
.onChange(of: tournament.dayDuration) { .onChange(of: tournament.dayDuration) {
_save() _save()
} }
.onChange(of: [ .onChange(of: focusedField) { old, new in
tournament.groupStageSortMode, if old == ._name {
]) { let tournamentName = tournamentName.prefixMultilineTrimmed(200)
if tournamentName.isEmpty {
tournament.name = nil
} else {
tournament.name = tournamentName
}
} else if old == ._information {
let tournamentInformation = tournamentInformation.prefixMultilineTrimmed(4000)
if tournamentInformation.isEmpty {
tournament.information = nil
} else {
tournament.information = tournamentInformation
}
} else if old == ._entryFee {
tournament.entryFee = entryFee
} else if old == ._umpireCustomMail {
_confirmUmpireMail()
} else if old == ._umpireCustomPhone {
_confirmUmpirePhone()
} else if old == ._umpireCustomContact {
_confirmUmpireContact()
}
_save() _save()
} }
} }
@ -264,10 +249,8 @@ struct TournamentGeneralSettingsView: View {
umpireCustomMailIsInvalid = false umpireCustomMailIsInvalid = false
if umpireCustomMail.isEmpty { if umpireCustomMail.isEmpty {
tournament.umpireCustomMail = nil tournament.umpireCustomMail = nil
_save()
} else if umpireCustomMail.isValidEmail() { } else if umpireCustomMail.isValidEmail() {
tournament.umpireCustomMail = umpireCustomMail tournament.umpireCustomMail = umpireCustomMail
_save()
} else { } else {
umpireCustomMailIsInvalid = true umpireCustomMailIsInvalid = true
} }
@ -277,47 +260,41 @@ struct TournamentGeneralSettingsView: View {
umpireCustomMailIsInvalid = false umpireCustomMailIsInvalid = false
umpireCustomMail = "" umpireCustomMail = ""
tournament.umpireCustomMail = nil tournament.umpireCustomMail = nil
_save()
} }
private func _save() { private func _confirmUmpirePhone() {
do { umpireCustomPhoneIsInvalid = false
try dataStore.tournaments.addOrUpdate(instance: tournament) if umpireCustomPhone.isEmpty {
} catch { tournament.umpireCustomPhone = nil
Logger.error(error) } else if umpireCustomPhone.isPhoneNumber() {
tournament.umpireCustomPhone = umpireCustomPhone.prefixMultilineTrimmed(15)
} else {
umpireCustomPhoneIsInvalid = true
} }
} }
private func _refreshLoserBracketMode() { private func _deleteUmpirePhone() {
tournament.loserBracketMode = loserBracketMode umpireCustomPhoneIsInvalid = false
_save() umpireCustomPhone = ""
tournament.umpireCustomPhone = nil
let rounds = tournament.rounds() }
rounds.forEach { round in
let matches = round.loserRoundsAndChildren().flatMap({ $0._matches() })
matches.forEach { match in
match.resetTeamScores(outsideOf: [])
match.resetMatch()
match.confirmed = false
}
round.loserBracketMode = tournament.loserBracketMode
if loserBracketMode == .automatic {
matches.forEach { match in
match.updateTeamScores()
}
}
do { private func _confirmUmpireContact() {
try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: matches) if umpireCustomContact.isEmpty {
} catch { tournament.umpireCustomContact = nil
Logger.error(error) } else {
} tournament.umpireCustomContact = umpireCustomContact.prefixMultilineTrimmed(200)
} }
}
private func _deleteUmpireContact() {
umpireCustomContact = ""
tournament.umpireCustomContact = nil
}
private func _save() {
do { do {
try self.tournament.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds) try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -325,32 +302,55 @@ struct TournamentGeneralSettingsView: View {
private func _customUmpireView() -> some View { private func _customUmpireView() -> some View {
Section { Section {
if umpireCustomMailIsInvalid { VStack(alignment: .leading) {
Text("Vous n'avez pas indiqué un email valide.").foregroundStyle(.logoRed) TextField(dataStore.user.email, text: $umpireCustomMail)
.frame(maxWidth: .infinity)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.focused($focusedField, equals: ._umpireCustomMail)
.onSubmit {
_confirmUmpireMail()
}
if umpireCustomMailIsInvalid {
Text("Vous n'avez pas indiqué un email valide.").foregroundStyle(.logoRed)
}
} }
TextField(dataStore.user.email, text: $umpireCustomMail)
.frame(maxWidth: .infinity) VStack(alignment: .leading) {
.keyboardType(.emailAddress) TextField(dataStore.user.phone ?? "Téléphone", text: $umpireCustomPhone)
.focused($focusedField, equals: ._umpireCustomMail) .frame(maxWidth: .infinity)
.onSubmit { .keyboardType(.phonePad)
_confirmUmpireMail() .focused($focusedField, equals: ._umpireCustomPhone)
.onSubmit {
_confirmUmpirePhone()
}
if umpireCustomPhoneIsInvalid {
Text("Vous n'avez pas indiqué un téléphone valide.").foregroundStyle(.logoRed)
} }
}
VStack(alignment: .leading) {
TextField(dataStore.user.fullName(), text: $umpireCustomContact)
.frame(maxWidth: .infinity)
.keyboardType(.default)
.focused($focusedField, equals: ._umpireCustomContact)
.onSubmit {
_confirmUmpireContact()
}
if dataStore.user.summonsMessageSignature != nil, umpireCustomContact != dataStore.user.fullName() {
Text("Attention vous avez une signature personnalisée contenant un contact différent.").foregroundStyle(.logoRed)
FooterButtonView("retirer la personnalisation ?") {
dataStore.user.summonsMessageSignature = nil
self.dataStore.saveUser()
}
} }
} header: { } header: {
Text("Email du juge-arbitre") Text("Juge-arbitre")
} footer: { } footer: {
Text("Cet email sera utilisé pour vous contacter. Vous pouvez le modifier si vous souhaitez utiliser un autre email que celui de votre compte Padel Club.") Text("Ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
} }
} }
private func _footerView() -> some View {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
+
Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue)
}
private func _footerViewConfirmationRequired() -> some View {
Text("Au moins un match de classement est terminé, en modifiant ce réglage, les résultats de ces matchs de classement seront perdus.")
+
Text(" Modifier quand même ?").foregroundStyle(.red)
}
} }

@ -74,6 +74,12 @@ struct TournamentRankView: View {
Text("Désactiver la règle fédéral") Text("Désactiver la règle fédéral")
Text("Dernier de poule ≠ derner du tournoi") Text("Dernier de poule ≠ derner du tournoi")
} }
.onChange(of: tournament.disableRankingFederalRuling) {
dataStore.tournaments.addOrUpdate(instance: tournament)
Task {
await _calculateRankings()
}
}
.disabled(calculating) .disabled(calculating)
} footer: { } footer: {

@ -70,7 +70,7 @@ struct TournamentSettingsView: View {
case .matchFormats: case .matchFormats:
TournamentMatchFormatsSettingsView() TournamentMatchFormatsSettingsView()
case .tournamentType: case .tournamentType:
TournamentCategorySettingsView() TournamentCategorySettingsView(tournament: tournament)
case .general: case .general:
TournamentGeneralSettingsView(tournament: tournament) TournamentGeneralSettingsView(tournament: tournament)
case .club: case .club:

@ -113,7 +113,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, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4, accountIsRequired: false, licenseIsRequired: false, minimumPlayerPerTeam: 3, maximumPlayerPerTeam: 5, information: "Super") 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, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4, accountIsRequired: false, licenseIsRequired: false, minimumPlayerPerTeam: 3, maximumPlayerPerTeam: 5, information: "Super", umpireCustomMail: "razmig@padelclub.app", umpireCustomContact: "Raz", umpireCustomPhone: "+33681598193", hideUmpireMail: true, hideUmpirePhone: true, disableRankingFederalRuling: true)
if let t = try await StoreCenter.main.service().post(tournament) { if let t = try await StoreCenter.main.service().post(tournament) {
@ -163,7 +163,12 @@ final class ServerDataTests: XCTestCase {
assert(t.minimumPlayerPerTeam == tournament.minimumPlayerPerTeam) assert(t.minimumPlayerPerTeam == tournament.minimumPlayerPerTeam)
assert(t.maximumPlayerPerTeam == tournament.maximumPlayerPerTeam) assert(t.maximumPlayerPerTeam == tournament.maximumPlayerPerTeam)
assert(t.information == tournament.information) assert(t.information == tournament.information)
assert(t.umpireCustomMail == tournament.umpireCustomMail)
assert(t.umpireCustomContact == tournament.umpireCustomContact)
assert(t.umpireCustomPhone == tournament.umpireCustomPhone)
assert(t.hideUmpireMail == tournament.hideUmpireMail)
assert(t.hideUmpirePhone == tournament.hideUmpirePhone)
assert(t.disableRankingFederalRuling == tournament.disableRankingFederalRuling)
} else { } else {
XCTFail("missing data") XCTFail("missing data")
} }

Loading…
Cancel
Save