diff --git a/PadelClub/Data/Gen/BaseTournament.swift b/PadelClub/Data/Gen/BaseTournament.swift index c397195..3843b7f 100644 --- a/PadelClub/Data/Gen/BaseTournament.swift +++ b/PadelClub/Data/Gen/BaseTournament.swift @@ -63,6 +63,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { var minimumPlayerPerTeam: Int = 2 var maximumPlayerPerTeam: Int = 2 var information: String? = nil + var umpireCustomMail: String? = nil + var disableRankingFederalRuling: Bool = false init( id: String = Store.randomId(), @@ -116,7 +118,9 @@ class BaseTournament: SyncedModelObject, SyncedStorable { licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, - information: String? = nil + information: String? = nil, + umpireCustomMail: String? = nil, + disableRankingFederalRuling: Bool = false ) { super.init() self.id = id @@ -171,6 +175,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.minimumPlayerPerTeam = minimumPlayerPerTeam self.maximumPlayerPerTeam = maximumPlayerPerTeam self.information = information + self.umpireCustomMail = umpireCustomMail + self.disableRankingFederalRuling = disableRankingFederalRuling } enum CodingKeys: String, CodingKey { @@ -226,6 +232,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { case _minimumPlayerPerTeam = "minimumPlayerPerTeam" case _maximumPlayerPerTeam = "maximumPlayerPerTeam" case _information = "information" + case _umpireCustomMail = "umpireCustomMail" + case _disableRankingFederalRuling = "disableRankingFederalRuling" } private static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { @@ -343,6 +351,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2 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.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false try super.init(from: decoder) } @@ -400,6 +410,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam) 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.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try super.encode(to: encoder) } @@ -462,6 +474,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.minimumPlayerPerTeam = tournament.minimumPlayerPerTeam self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam self.information = tournament.information + self.umpireCustomMail = tournament.umpireCustomMail + self.disableRankingFederalRuling = tournament.disableRankingFederalRuling } static func relationships() -> [Relationship] { @@ -470,4 +484,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 2c752ea..9a2928c 100644 --- a/PadelClub/Data/Gen/Tournament.json +++ b/PadelClub/Data/Gen/Tournament.json @@ -264,6 +264,17 @@ "name": "information", "type": "String", "optional": true + }, + { + "name": "umpireCustomMail", + "type": "String", + "optional": true + }, + { + "name": "disableRankingFederalRuling", + "type": "Bool", + "defaultValue": false, + "optional": false } ] } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 5149a60..07e7cea 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -484,6 +484,20 @@ defer { } } + func removeWalkOut() { + teamScores.forEach { teamScore in + teamScore.walkOut = nil + teamScore.score = nil + } + tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores) + endDate = nil + winningTeamId = nil + losingTeamId = nil + groupStageObject?.updateGroupStageState() + roundObject?.updateTournamentState() + updateFollowingMatchTeamScore() + } + func setWalkOut(_ teamPosition: TeamPosition) { let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition)) teamScoreWalkout.walkOut = 0 diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index cd458bf..09c1c9f 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -30,7 +30,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, + disableRankingFederalRuling: Bool = false + ) { super.init() self.event = event self.name = name @@ -95,6 +98,8 @@ final class Tournament: BaseTournament { self.minimumPlayerPerTeam = minimumPlayerPerTeam self.maximumPlayerPerTeam = maximumPlayerPerTeam self.information = information + self.umpireCustomMail = umpireCustomMail + self.disableRankingFederalRuling = disableRankingFederalRuling } required init(from decoder: Decoder) throws { @@ -1133,13 +1138,7 @@ defer { let groupStages = groupStages() var baseRank = teamCount - groupStageSpots() + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - //TODO: RAZ ajouté une option pour choisir entre la règle officiel et la règle 'maison' - /* - Request by Philippe Morin 24/03/2025 - */ - let defaultOption = false - - if defaultOption { + if disableRankingFederalRuling == false { baseRank += qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified - 1 } let alreadyPlaceTeams = Array(teams.values.flatMap({ $0 })) @@ -1443,7 +1442,7 @@ defer { } func umpireMail() -> [String]? { - return [DataStore.shared.user.email] + return [umpireCustomMail ?? DataStore.shared.user.email] } func earnings() -> Double { @@ -2583,12 +2582,15 @@ extension Tournament { } else if Guard.main.purchasedTransactions.isEmpty == false { shouldBePrivate = false } + + let disableRankingFederalRuling = tournaments.first?.disableRankingFederalRuling ?? false + let umpireCustomMail = tournaments.first?.umpireCustomMail 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) + 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) } static func fake() -> Tournament { diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index d770ab1..5222650 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -13,7 +13,6 @@ struct MenuWarningView: View { let teams: [TeamRegistration] var date: Date? var message: String? - var umpireMail: String? var subject: String? @Binding var contactType: ContactType? @@ -24,10 +23,7 @@ struct MenuWarningView: View { @State var savedContactType: ContactType? = nil private func _getUmpireMail() -> [String]? { - if let umpireMail { - return [umpireMail] - } - return nil + return tournament.umpireMail() } var body: some View { diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 5dd2485..0b49a01 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -98,7 +98,7 @@ struct MatchDetailView: View { } Spacer() if let tournament = match.currentTournament() { - MenuWarningView(tournament: tournament, teams: match.teams(), message: match.matchWarningMessage(), umpireMail: dataStore.user.email, subject: match.matchWarningSubject(), contactType: $contactType) + MenuWarningView(tournament: tournament, teams: match.teams(), message: match.matchWarningMessage(), subject: match.matchWarningSubject(), contactType: $contactType) .buttonStyle(.borderless) } } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index d04bf4a..8f417c6 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -134,6 +134,15 @@ struct EditScoreView: View { } label: { Text(matchDescriptor.teamLabelTwo) } + + Divider() + + Button { + self.matchDescriptor.match?.removeWalkOut() + save() + } label: { + Text("Annuler un forfait") + } } label: { Text("Forfait d'une équipe ?") .underline() @@ -190,6 +199,16 @@ struct EditScoreView: View { } footer: { Text("Met à jour le score, ne termine pas la rencontre") } + + Section { + RowButtonView("Terminer la rencontre") { + matchDescriptor.match?.setScore(fromMatchDescriptor: matchDescriptor) + save() + dismiss() + } + } footer: { + Text("Le match n'a pas pu aboutir.") + } } } .sheet(isPresented: $presentMatchFormatSelection) { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 635d458..a04c13a 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -18,15 +18,18 @@ struct TournamentGeneralSettingsView: View { @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 umpireCustomMailIsInvalid: 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 ?? "") } var body: some View { @@ -73,6 +76,8 @@ struct TournamentGeneralSettingsView: View { } } + _customUmpireView() + Section { TextField("Nom du tournoi", text: $tournamentName, axis: .vertical) .lineLimit(2) @@ -150,7 +155,7 @@ struct TournamentGeneralSettingsView: View { Button("Annuler", role: .cancel) { loserBracketMode = tournament.loserBracketMode } - + }) .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { @@ -186,6 +191,27 @@ struct TournamentGeneralSettingsView: View { .buttonStyle(.bordered) } + } else { + if focusedField == ._name, tournamentName.isEmpty == false { + 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 { + Button("Effacer") { + _deleteUmpireMail() + } + .buttonStyle(.borderless) + } } Spacer() Button("Valider") { @@ -205,6 +231,8 @@ struct TournamentGeneralSettingsView: View { } } else if focusedField == ._entryFee { tournament.entryFee = entryFee + } else if focusedField == ._umpireCustomMail { + _confirmUmpireMail() } focusedField = nil } @@ -232,6 +260,26 @@ struct TournamentGeneralSettingsView: View { } } + private func _confirmUmpireMail() { + umpireCustomMailIsInvalid = false + if umpireCustomMail.isEmpty { + tournament.umpireCustomMail = nil + _save() + } else if umpireCustomMail.isValidEmail() { + tournament.umpireCustomMail = umpireCustomMail + _save() + } else { + umpireCustomMailIsInvalid = true + } + } + + private func _deleteUmpireMail() { + umpireCustomMailIsInvalid = false + umpireCustomMail = "" + tournament.umpireCustomMail = nil + _save() + } + private func _save() { do { try dataStore.tournaments.addOrUpdate(instance: tournament) @@ -275,6 +323,25 @@ struct TournamentGeneralSettingsView: View { } } + private func _customUmpireView() -> some View { + Section { + 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() + } + } header: { + Text("Email du 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.") + } + } + private func _footerView() -> some View { Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) + diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index be0dff0..97b4d78 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -59,6 +59,7 @@ struct TournamentRankView: View { Logger.error(error) } } + //affiche l'onglet sur le site, car sur le broadcast c'est dispo automatiquement de toute façon Toggle(isOn: $tournament.publishRankings) { if calculating { @@ -68,6 +69,13 @@ struct TournamentRankView: View { } } .disabled(calculating) + + Toggle(isOn: $tournament.disableRankingFederalRuling) { + Text("Désactiver la règle fédéral") + Text("Dernier de poule ≠ derner du tournoi") + } + .disabled(calculating) + } footer: { if let url = tournament.shareURL(.rankings) { Link(destination: url) {