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. 240
      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()
} }
Spacer() .buttonStyle(.borderless)
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()
} }
Spacer()
Button("Valider") {
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 private func _confirmUmpireContact() {
if umpireCustomContact.isEmpty {
if loserBracketMode == .automatic { tournament.umpireCustomContact = nil
matches.forEach { match in } else {
match.updateTeamScores() tournament.umpireCustomContact = umpireCustomContact.prefixMultilineTrimmed(200)
} }
} }
do { private func _deleteUmpireContact() {
try self.tournament.tournamentStore?.matches.addOrUpdate(contentOfs: matches) umpireCustomContact = ""
} catch { tournament.umpireCustomContact = nil
Logger.error(error)
}
} }
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) TextField(dataStore.user.email, text: $umpireCustomMail)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.keyboardType(.emailAddress) .keyboardType(.emailAddress)
.autocapitalization(.none)
.focused($focusedField, equals: ._umpireCustomMail) .focused($focusedField, equals: ._umpireCustomMail)
.onSubmit { .onSubmit {
_confirmUmpireMail() _confirmUmpireMail()
} }
} header: { if umpireCustomMailIsInvalid {
Text("Email du juge-arbitre") Text("Vous n'avez pas indiqué un email valide.").foregroundStyle(.logoRed)
} 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.")
} }
VStack(alignment: .leading) {
TextField(dataStore.user.phone ?? "Téléphone", text: $umpireCustomPhone)
.frame(maxWidth: .infinity)
.keyboardType(.phonePad)
.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)
private func _footerView() -> some View { FooterButtonView("retirer la personnalisation ?") {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) dataStore.user.summonsMessageSignature = nil
+ self.dataStore.saveUser()
Text(" Modifier le réglage par défaut pour tous vos tournois").foregroundStyle(.blue)
} }
} }
private func _footerViewConfirmationRequired() -> some View { } header: {
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("Juge-arbitre")
+ } footer: {
Text(" Modifier quand même ?").foregroundStyle(.red) 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.")
}
} }
} }

@ -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