From 74d6f8ba3c73b837343f13f2bbb9bea083fb8055 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 26 Mar 2025 11:36:56 +0100 Subject: [PATCH] wip done --- PadelClub/Data/CustomUser.swift | 12 +- PadelClub/Data/Gen/BaseTournament.swift | 30 +- PadelClub/Data/Gen/Tournament.json | 22 +- PadelClub/Data/Match.swift | 13 + PadelClub/Data/Tournament.swift | 11 +- PadelClub/Extensions/String+Extensions.swift | 5 + PadelClub/Utils/ContactManager.swift | 4 +- .../CallMessageCustomizationView.swift | 4 +- PadelClub/Views/Calling/CallView.swift | 2 +- PadelClub/Views/Calling/SendToAllView.swift | 2 +- PadelClub/Views/Score/EditScoreView.swift | 2 +- .../TournamentCategorySettingsView.swift | 113 +++++++- .../TournamentGeneralSettingsView.swift | 266 +++++++++--------- .../Screen/TournamentRankView.swift | 6 + .../Screen/TournamentSettingsView.swift | 2 +- PadelClubTests/ServerDataTests.swift | 9 +- 16 files changed, 349 insertions(+), 154 deletions(-) diff --git a/PadelClub/Data/CustomUser.swift b/PadelClub/Data/CustomUser.swift index 0624736..22de699 100644 --- a/PadelClub/Data/CustomUser.swift +++ b/PadelClub/Data/CustomUser.swift @@ -82,15 +82,13 @@ class CustomUser: BaseCustomUser, UserBase { return try? federalContext.fetch(fetchRequest).first } - func defaultSignature() -> String { - return "Sportivement,\n\(firstName) \(lastName), votre JAP." + func defaultSignature(_ tournament: Tournament?) -> String { + let fullName = tournament?.umpireCustomContact ?? fullName() + return "Sportivement,\n\(fullName), votre JAP." } - func fullName() -> String? { - guard firstName.isEmpty == false && lastName.isEmpty == false else { - return nil - } - return "\(firstName) \(lastName)" + func fullName() -> String { + [firstName, lastName].joined(separator: " ") } func hasTenupClubs() -> Bool { diff --git a/PadelClub/Data/Gen/BaseTournament.swift b/PadelClub/Data/Gen/BaseTournament.swift index 3843b7f..d7778db 100644 --- a/PadelClub/Data/Gen/BaseTournament.swift +++ b/PadelClub/Data/Gen/BaseTournament.swift @@ -64,6 +64,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { var maximumPlayerPerTeam: Int = 2 var information: 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 init( @@ -120,6 +124,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { maximumPlayerPerTeam: Int = 2, information: String? = nil, umpireCustomMail: String? = nil, + umpireCustomContact: String? = nil, + umpireCustomPhone: String? = nil, + hideUmpireMail: Bool = false, + hideUmpirePhone: Bool = true, disableRankingFederalRuling: Bool = false ) { super.init() @@ -176,6 +184,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.maximumPlayerPerTeam = maximumPlayerPerTeam self.information = information self.umpireCustomMail = umpireCustomMail + self.umpireCustomContact = umpireCustomContact + self.umpireCustomPhone = umpireCustomPhone + self.hideUmpireMail = hideUmpireMail + self.hideUmpirePhone = hideUmpirePhone self.disableRankingFederalRuling = disableRankingFederalRuling } @@ -233,6 +245,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { case _maximumPlayerPerTeam = "maximumPlayerPerTeam" case _information = "information" case _umpireCustomMail = "umpireCustomMail" + case _umpireCustomContact = "umpireCustomContact" + case _umpireCustomPhone = "umpireCustomPhone" + case _hideUmpireMail = "hideUmpireMail" + case _hideUmpirePhone = "hideUmpirePhone" case _disableRankingFederalRuling = "disableRankingFederalRuling" } @@ -352,6 +368,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2 self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? 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 try super.init(from: decoder) } @@ -411,6 +431,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam) try container.encode(self.information, forKey: ._information) 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 super.encode(to: encoder) } @@ -475,6 +499,10 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam self.information = tournament.information self.umpireCustomMail = tournament.umpireCustomMail + self.umpireCustomContact = tournament.umpireCustomContact + self.umpireCustomPhone = tournament.umpireCustomPhone + self.hideUmpireMail = tournament.hideUmpireMail + self.hideUmpirePhone = tournament.hideUmpirePhone self.disableRankingFederalRuling = tournament.disableRankingFederalRuling } @@ -484,4 +512,4 @@ class BaseTournament: SyncedModelObject, SyncedStorable { ] } -} +} \ No newline at end of file diff --git a/PadelClub/Data/Gen/Tournament.json b/PadelClub/Data/Gen/Tournament.json index 9a2928c..a56d265 100644 --- a/PadelClub/Data/Gen/Tournament.json +++ b/PadelClub/Data/Gen/Tournament.json @@ -270,10 +270,30 @@ "type": "String", "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", "type": "Bool", - "defaultValue": false, + "defaultValue": "false", "optional": false } ] diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 07e7cea..55864d6 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -522,6 +522,19 @@ defer { 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) { updateScore(fromMatchDescriptor: matchDescriptor) if endDate == nil { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index dc54551..e5c3914 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -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, umpireCustomMail: String? = nil, + umpireCustomContact: String? = nil, + umpireCustomPhone: String? = nil, + hideUmpireMail: Bool = false, + hideUmpirePhone: Bool = true, disableRankingFederalRuling: Bool = false ) { super.init() @@ -99,6 +103,7 @@ final class Tournament: BaseTournament { self.maximumPlayerPerTeam = maximumPlayerPerTeam self.information = information self.umpireCustomMail = umpireCustomMail + self.umpireCustomContact = umpireCustomContact self.disableRankingFederalRuling = disableRankingFederalRuling } @@ -2595,12 +2600,16 @@ extension Tournament { let disableRankingFederalRuling = tournaments.first?.disableRankingFederalRuling ?? false 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 tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) //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 { diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index d1e895b..abee514 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -171,12 +171,17 @@ extension String { extension String { enum RegexStatic { 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 { firstMatch(of: RegexStatic.mobileNumber) != nil } + func isPhoneNumber() -> Bool { + firstMatch(of: RegexStatic.phoneNumber) != nil + } + //april 04-2024 bug with accent characters / adobe / fft mutating func replace(characters: [(Character, Character)]) { for (targetChar, replacementChar) in characters { diff --git a/PadelClub/Utils/ContactManager.swift b/PadelClub/Utils/ContactManager.swift index a72204a..23e0bac 100644 --- a/PadelClub/Utils/ContactManager.swift +++ b/PadelClub/Utils/ContactManager.swift @@ -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: "#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) 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 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" diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 2914565..b7ce6d3 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -29,7 +29,7 @@ struct CallMessageCustomizationView: View { init(tournament: Tournament) { self.tournament = tournament _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") _summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods) } @@ -89,7 +89,7 @@ struct CallMessageCustomizationView: View { } Divider() FooterButtonView("défaut") { - customCallMessageSignature = DataStore.shared.user.defaultSignature() + customCallMessageSignature = DataStore.shared.user.defaultSignature(tournament) _save() } Divider() diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 0baae88..cc8e152 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -139,7 +139,7 @@ struct CallView: View { func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String { 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 } diff --git a/PadelClub/Views/Calling/SendToAllView.swift b/PadelClub/Views/Calling/SendToAllView.swift index a0515e0..91509d6 100644 --- a/PadelClub/Views/Calling/SendToAllView.swift +++ b/PadelClub/Views/Calling/SendToAllView.swift @@ -261,7 +261,7 @@ struct SendToAllView: View { 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) diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 8f417c6..3ffb742 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -202,7 +202,7 @@ struct EditScoreView: View { Section { RowButtonView("Terminer la rencontre") { - matchDescriptor.match?.setScore(fromMatchDescriptor: matchDescriptor) + matchDescriptor.match?.setUnfinishedScore(fromMatchDescriptor: matchDescriptor) save() dismiss() } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift index 4ae0340..c0bfcff 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentCategorySettingsView.swift @@ -10,13 +10,71 @@ import SwiftUI import LeStorage struct TournamentCategorySettingsView: View { - @Environment(Tournament.self) private var tournament: Tournament + @Bindable var tournament: Tournament @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 { List { 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: [ tournament.federalCategory, ]) { @@ -32,7 +90,48 @@ struct TournamentCategorySettingsView: View { ]) { _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() { do { @@ -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) + } + } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index a04c13a..ddd76fa 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -15,21 +15,23 @@ struct TournamentGeneralSettingsView: View { @State private var tournamentName: String = "" @State private var tournamentInformation: String = "" @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 umpireCustomPhone: String + @State private var umpireCustomContact: String @State private var umpireCustomMailIsInvalid: Bool = false + @State private var umpireCustomPhoneIsInvalid: Bool = false + @FocusState private var focusedField: Tournament.CodingKeys? let priceTags: [Double] = [15.0, 20.0, 25.0] init(tournament: Tournament) { self.tournament = tournament - _loserBracketMode = .init(wrappedValue: tournament.loserBracketMode) _tournamentName = State(wrappedValue: tournament.name ?? "") _tournamentInformation = State(wrappedValue: tournament.information ?? "") _entryFee = State(wrappedValue: tournament.entryFee) _umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "") + _umpireCustomPhone = State(wrappedValue: tournament.umpireCustomPhone ?? "") + _umpireCustomContact = State(wrappedValue: tournament.umpireCustomContact ?? "") } var body: some View { @@ -78,6 +80,22 @@ struct TournamentGeneralSettingsView: View { _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 { TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) .lineLimit(2) @@ -110,53 +128,7 @@ struct TournamentGeneralSettingsView: View { 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) .toolbar(content: { if focusedField != nil { @@ -196,14 +168,12 @@ struct TournamentGeneralSettingsView: View { Button("Effacer") { tournament.name = nil tournamentName = "" - _save() } .buttonStyle(.borderless) } else if focusedField == ._information, tournamentInformation.isEmpty == false { Button("Effacer") { tournament.information = nil tournamentInformation = "" - _save() } .buttonStyle(.borderless) } else if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { @@ -211,29 +181,20 @@ struct TournamentGeneralSettingsView: View { _deleteUmpireMail() } .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() 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 } .buttonStyle(.bordered) @@ -247,15 +208,39 @@ struct TournamentGeneralSettingsView: View { .onChange(of: tournament.entryFee) { _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() } .onChange(of: tournament.dayDuration) { _save() } - .onChange(of: [ - tournament.groupStageSortMode, - ]) { + .onChange(of: focusedField) { old, new in + 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() } } @@ -264,10 +249,8 @@ struct TournamentGeneralSettingsView: View { umpireCustomMailIsInvalid = false if umpireCustomMail.isEmpty { tournament.umpireCustomMail = nil - _save() } else if umpireCustomMail.isValidEmail() { tournament.umpireCustomMail = umpireCustomMail - _save() } else { umpireCustomMailIsInvalid = true } @@ -277,47 +260,41 @@ struct TournamentGeneralSettingsView: View { umpireCustomMailIsInvalid = false umpireCustomMail = "" tournament.umpireCustomMail = nil - _save() } - - private func _save() { - do { - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) + + private func _confirmUmpirePhone() { + umpireCustomPhoneIsInvalid = false + if umpireCustomPhone.isEmpty { + tournament.umpireCustomPhone = nil + } else if umpireCustomPhone.isPhoneNumber() { + tournament.umpireCustomPhone = umpireCustomPhone.prefixMultilineTrimmed(15) + } else { + umpireCustomPhoneIsInvalid = true } } - 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) - } + private func _deleteUmpirePhone() { + umpireCustomPhoneIsInvalid = false + umpireCustomPhone = "" + tournament.umpireCustomPhone = nil + } + + private func _confirmUmpireContact() { + if umpireCustomContact.isEmpty { + tournament.umpireCustomContact = nil + } else { + tournament.umpireCustomContact = umpireCustomContact.prefixMultilineTrimmed(200) } + } + private func _deleteUmpireContact() { + umpireCustomContact = "" + tournament.umpireCustomContact = nil + } + + private func _save() { do { - try self.tournament.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds) + try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } @@ -325,32 +302,55 @@ struct TournamentGeneralSettingsView: View { private func _customUmpireView() -> some View { Section { - if umpireCustomMailIsInvalid { - Text("Vous n'avez pas indiqué un email valide.").foregroundStyle(.logoRed) + VStack(alignment: .leading) { + 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) - .keyboardType(.emailAddress) - .focused($focusedField, equals: ._umpireCustomMail) - .onSubmit { - _confirmUmpireMail() + + 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) + + FooterButtonView("retirer la personnalisation ?") { + dataStore.user.summonsMessageSignature = nil + self.dataStore.saveUser() + } + } } + } header: { - Text("Email du juge-arbitre") + Text("Juge-arbitre") } 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) - } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 97b4d78..04d6827 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -74,6 +74,12 @@ struct TournamentRankView: View { Text("Désactiver la règle fédéral") Text("Dernier de poule ≠ derner du tournoi") } + .onChange(of: tournament.disableRankingFederalRuling) { + dataStore.tournaments.addOrUpdate(instance: tournament) + Task { + await _calculateRankings() + } + } .disabled(calculating) } footer: { diff --git a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift index 2b14cc7..c0d7b8b 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift @@ -70,7 +70,7 @@ struct TournamentSettingsView: View { case .matchFormats: TournamentMatchFormatsSettingsView() case .tournamentType: - TournamentCategorySettingsView() + TournamentCategorySettingsView(tournament: tournament) case .general: TournamentGeneralSettingsView(tournament: tournament) case .club: diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 0ef72d8..703fe65 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -113,7 +113,7 @@ final class ServerDataTests: XCTestCase { 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) { @@ -163,7 +163,12 @@ final class ServerDataTests: XCTestCase { assert(t.minimumPlayerPerTeam == tournament.minimumPlayerPerTeam) assert(t.maximumPlayerPerTeam == tournament.maximumPlayerPerTeam) 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 { XCTFail("missing data") }