diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index aa4f940..e3cf09b 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -42,7 +42,7 @@ final class TeamRegistration: ModelObject, Storable { init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) { self.tournament = tournament self.groupStage = groupStage - self.registrationDate = registrationDate + self.registrationDate = registrationDate ?? Date() self.callDate = callDate self.bracketPosition = bracketPosition self.groupStagePosition = groupStagePosition @@ -523,6 +523,15 @@ final class TeamRegistration: ModelObject, Storable { return nil } + func wildcardLabel() -> String? { + if isWildCard() { + let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ") + return wildcardLabel + } else { + return nil + } + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 31f6bbb..b0c08dc 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1661,16 +1661,61 @@ defer { deleteGroupStages() switch preset { - case .manual: - buildGroupStages() - buildBracket() case .doubleGroupStage: buildGroupStages() addNewGroupStageStep() - qualifiedPerGroupStage = 0 groupStageAdditionalQualified = 0 + default: + buildGroupStages() + buildBracket() + } + } + + func addWildCardIfNeeded(_ count: Int, _ type: MatchType) { + let currentCount = selectedSortedTeams().filter({ + if type == .bracket { + return $0.wildCardBracket + } else { + return $0.wildCardGroupStage + } + }).count + + if currentCount < count { + let _diff = count - currentCount + addWildCard(_diff, type) + } + } + + func addWildCard(_ count: Int, _ type: MatchType) { + let wcs = (0.., registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { - let date: Date = registrationDate ?? Date() - let team = TeamRegistration(tournament: id, registrationDate: date, name: name) + let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) players.forEach { player in player.teamRegistration = team.id diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 7db94a5..c5c0d1a 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1680,6 +1680,113 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { case manual case doubleGroupStage + case federalStructure_8 + case federalStructure_12 + case federalStructure_16 + case federalStructure_20 + case federalStructure_24 + case federalStructure_32 + case federalStructure_48 + case federalStructure_64 + + // Maximum qualified pairs based on the structure preset + func tableDimension() -> Int { + switch self { + case .federalStructure_8: + return 8 + case .federalStructure_12: + return 12 + case .federalStructure_16: + return 16 + case .federalStructure_20: + return 20 + case .federalStructure_24: + return 24 + case .federalStructure_32: + return 32 + case .federalStructure_48: + return 48 + case .federalStructure_64: + return 64 + case .manual: + return 24 + case .doubleGroupStage: + return 9 + } + } + + // Wildcards allowed in the Qualifiers + func wildcardBrackets() -> Int { + switch self { + case .federalStructure_8: + return 0 + case .federalStructure_12: + return 1 + case .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32: + return 2 + case .federalStructure_48, .federalStructure_64: + return 4 + case .manual, .doubleGroupStage: + return 0 + } + } + // Wildcards allowed in the Qualifiers + func wildcardQualifiers() -> Int { + switch self { + case .federalStructure_8: + return 0 + case .federalStructure_12, .federalStructure_16: + return 1 + case .federalStructure_20, .federalStructure_24: + return 2 + case .federalStructure_32: + return 4 + case .federalStructure_48: + return 6 + case .federalStructure_64: + return 8 + case .manual, .doubleGroupStage: + return 0 + } + } + + // Number of teams admitted to the Qualifiers + func teamsInQualifiers() -> Int { + switch self { + case .federalStructure_8: + return 8 + case .federalStructure_12: + return 12 + case .federalStructure_16: + return 16 + case .federalStructure_20: + return 20 + case .federalStructure_24: + return 24 + case .federalStructure_32: + return 32 + case .federalStructure_48: + return 48 + case .federalStructure_64: + return 64 + case .manual, .doubleGroupStage: + return 0 + } + } + + // Maximum teams that can qualify from the Qualifiers to the Final Table + func maxTeamsFromQualifiers() -> Int { + switch self { + case .federalStructure_8, .federalStructure_12: + return 2 + case .federalStructure_16, .federalStructure_20, .federalStructure_24: + return 4 + case .federalStructure_32, .federalStructure_48, .federalStructure_64: + return 8 + case .manual, .doubleGroupStage: + return 0 + } + } func localizedStructurePresetTitle() -> String { switch self { @@ -1687,6 +1794,22 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { return "Défaut" case .doubleGroupStage: return "2 phases de poules" + case .federalStructure_8: + return "Structure fédérale 8" + case .federalStructure_12: + return "Structure fédérale 12" + case .federalStructure_16: + return "Structure fédérale 16" + case .federalStructure_20: + return "Structure fédérale 20" + case .federalStructure_24: + return "Structure fédérale 24" + case .federalStructure_32: + return "Structure fédérale 32" + case .federalStructure_48: + return "Structure fédérale 48" + case .federalStructure_64: + return "Structure fédérale 64" } } @@ -1695,9 +1818,75 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { case .manual: return "24 équipes, 4 poules de 4, 1 qualifié par poule" case .doubleGroupStage: - return "Poules qui enchaîne sur une autre phase de poule : les premiers de chaque se retrouve ensemble, puis les 2èmes, etc." + return "Poules qui enchaînent sur une autre phase de poules: les premiers de chaque se retrouvent ensemble, puis les deuxièmes, etc." + case .federalStructure_8: + return "Tableau final à 8 paires, dont 2 qualifiées sortant de qualifications à 8 paires maximum. Aucune wildcard." + case .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: + return "Tableau final à \(tableDimension()) paires, dont \(maxTeamsFromQualifiers()) qualifiées sortant de qualifications à \(teamsInQualifiers()) paires maximum. \(wildcardBrackets()) wildcard\(wildcardBrackets().pluralSuffix) en tableau et \(wildcardQualifiers()) wildcard\(wildcardQualifiers().pluralSuffix) en qualifications." } } + + func groupStageCount() -> Int { + switch self { + case .manual: + 4 + case .doubleGroupStage: + 3 + case .federalStructure_8: + 2 + case .federalStructure_12: + 2 + case .federalStructure_16: + 4 + case .federalStructure_20: + 4 + case .federalStructure_24: + 4 + case .federalStructure_32: + 8 + case .federalStructure_48: + 8 + case .federalStructure_64: + 8 + } + } + + func teamsPerGroupStage() -> Int { + switch self { + case .manual: + 4 + case .doubleGroupStage: + 3 + case .federalStructure_8: + 4 + case .federalStructure_12: + 6 + case .federalStructure_16: + 4 + case .federalStructure_20: + 5 + case .federalStructure_24: + 6 + case .federalStructure_32: + 4 + case .federalStructure_48: + 6 + case .federalStructure_64: + 8 + } + } + + func qualifiedPerGroupStage() -> Int { + switch self { + case .doubleGroupStage: + 0 + default: + 1 + } + } + func hasWildcards() -> Bool { + wildcardBrackets() > 0 || wildcardQualifiers() > 0 + } } enum TournamentDeadlineType: String, CaseIterable { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index c095d97..173d693 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -88,8 +88,10 @@ struct EditingTeamView: View { Text("Cette équipe n'a pas été convoquée") } - Toggle(isOn: hasArrived) { - Text("Équipe sur place") + if team.unsortedPlayers().isEmpty == false { + Toggle(isOn: hasArrived) { + Text("Équipe sur place") + } } Toggle(isOn: .init(get: { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 6cbf087..bd92533 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -30,8 +30,8 @@ struct TeamRowView: View { Text(round.roundTitle(.wide)) } - if team.isWildCard() { - Text("wildcard").italic().foregroundStyle(.red).font(.caption) + if let wildcardLabel = team.wildcardLabel() { + Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) } } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 063f382..939be8a 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -26,7 +26,7 @@ struct InscriptionManagerView: View { @Environment(\.dismiss) var dismiss - var tournament: Tournament + @Bindable var tournament: Tournament var cancelShouldDismiss: Bool = false @State private var searchField: String = "" @@ -326,6 +326,27 @@ struct InscriptionManagerView: View { } label: { Label("Clôturer", systemImage: "lock") } + + Divider() + + Section { + Button("+1 en tableau") { + tournament.addWildCard(1, .bracket) + } + + if tournament.groupStageCount > 0 { + Button("+1 en poules") { + tournament.addWildCard(1, .groupStage) + } + } + } header: { + Text("Ajout de wildcards") + } + + Button("Bloquer une place") { + tournament.addEmptyTeamRegistration(1) + } + Divider() _sharingTeamsMenuView() Button { @@ -350,6 +371,14 @@ struct InscriptionManagerView: View { } } } else { + Button("Bloquer une place") { + tournament.addEmptyTeamRegistration(1) + } + + Toggle(isOn: $tournament.hideTeamsWeight) { + Text("Masquer les poids des équipes") + } + rankingDateSourcePickerView(showDateInLabel: true) Divider() @@ -368,7 +397,7 @@ struct InscriptionManagerView: View { if tournament.inscriptionClosed() == false { LabelOptions() } else { - Label("Clôturer", systemImage: "lock") + Label("Clôturé", systemImage: "lock") } } } @@ -376,6 +405,9 @@ struct InscriptionManagerView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Inscriptions") .navigationBarTitleDisplayMode(.inline) + .onChange(of: tournament.hideTeamsWeight) { + _save() + } } private func _sharingTeamsMenuView() -> some View { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index ad7b39b..76cd345 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -20,6 +20,7 @@ struct TableStructureView: View { @State private var groupStageAdditionalQualified: Int = 0 @State private var updatedElements: Set = Set() @State private var structurePreset: PadelTournamentStructurePreset = .manual + @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool var qualifiedFromGroupStage: Int { @@ -71,20 +72,11 @@ struct TableStructureView: View { Text(structurePreset.localizedDescriptionStructurePresetTitle()) } .onChange(of: structurePreset) { - switch structurePreset { - case .manual: - teamCount = 24 - groupStageCount = 4 - teamsPerGroupStage = 4 - qualifiedPerGroupStage = 1 - groupStageAdditionalQualified = 0 - case .doubleGroupStage: - teamCount = 9 - groupStageCount = 3 - teamsPerGroupStage = 3 - qualifiedPerGroupStage = 0 - groupStageAdditionalQualified = 0 - } + teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() + groupStageCount = structurePreset.groupStageCount() + teamsPerGroupStage = structurePreset.teamsPerGroupStage() + qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() + groupStageAdditionalQualified = 0 } } @@ -112,7 +104,7 @@ struct TableStructureView: View { Text("Équipes par poule") } - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) } label: { @@ -136,7 +128,7 @@ struct TableStructureView: View { } if groupStageCount > 0 && teamsPerGroupStage > 0 { - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 Text(mp.formatted()) @@ -179,7 +171,7 @@ struct TableStructureView: View { Section { let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) if groupStageCount > 0 { - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { Text(teamsFromGroupStages.formatted()) } label: { @@ -195,7 +187,7 @@ struct TableStructureView: View { } } - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { Text(tsPure.formatted()) } label: { @@ -220,17 +212,25 @@ struct TableStructureView: View { } } } footer: { - if tsPure > 0 && structurePreset == .manual { + if tsPure > 0 && structurePreset != .doubleGroupStage { if tsPure > teamCount / 2 { - Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif").foregroundStyle(.red) + Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red) } else if tsPure < teamCount / 8 { - Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif").foregroundStyle(.red) + Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red) } else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { - Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes").foregroundStyle(.red) + Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) } } } + if structurePreset.hasWildcards() { + Section { + Toggle("Avec wildcards", isOn: $buildWildcards) + } footer: { + Text("Padel Club réservera des places pour eux dans votre liste d'inscription.") + } + } + if tournament.state() != .initial { Section { @@ -387,6 +387,10 @@ struct TableStructureView: View { if rebuildEverything { tournament.deleteAndBuildEverything(preset: structurePreset) + if structurePreset.hasWildcards(), buildWildcards { + tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket) + tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage) + } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages()