gs step wip

sync2
Raz 1 year ago
parent 0352fffc4d
commit 4c4e472c09
  1. 30
      PadelClub/Data/GroupStage.swift
  2. 10
      PadelClub/Data/TeamRegistration.swift
  3. 58
      PadelClub/Data/Tournament.swift
  4. 24
      PadelClub/Utils/PadelRule.swift
  5. 2
      PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift
  6. 4
      PadelClub/Views/GroupStage/GroupStageView.swift
  7. 21
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  8. 13
      PadelClub/Views/GroupStage/GroupStagesView.swift
  9. 78
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  10. 6
      PadelClub/Views/Tournament/TournamentBuildView.swift

@ -24,6 +24,7 @@ final class GroupStage: ModelObject, Storable {
private var format: MatchFormat?
var startDate: Date?
var name: String?
var step: Int = 0
var matchFormat: MatchFormat {
get {
@ -34,13 +35,14 @@ final class GroupStage: ModelObject, Storable {
}
}
internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil) {
internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) {
self.tournament = tournament
self.index = index
self.size = size
self.format = matchFormat
self.startDate = startDate
self.name = name
self.step = step
}
var tournamentStore: TournamentStore {
@ -61,7 +63,10 @@ final class GroupStage: ModelObject, Storable {
// MARK: -
func teamAt(groupStagePosition: Int) -> TeamRegistration? {
teams().first(where: { $0.groupStagePosition == groupStagePosition })
if step > 0 {
return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition })
}
return teams().first(where: { $0.groupStagePosition == groupStagePosition })
}
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
@ -190,7 +195,7 @@ final class GroupStage: ModelObject, Storable {
}
func initialStartDate(forTeam team: TeamRegistration) -> Date? {
guard let groupStagePosition = team.groupStagePosition else { return nil }
guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil }
return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate
}
@ -326,6 +331,9 @@ final class GroupStage: ModelObject, Storable {
}
func unsortedTeams() -> [TeamRegistration] {
if step > 0 {
return self.tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] })
}
return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
}
@ -397,6 +405,19 @@ final class GroupStage: ModelObject, Storable {
self.tournamentStore.matches.deleteDependencies(matches)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: ._id)
tournament = try container.decode(String.self, forKey: ._tournament)
index = try container.decode(Int.self, forKey: ._index)
size = try container.decode(Int.self, forKey: ._size)
format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
name = try container.decodeIfPresent(String.self, forKey: ._name)
step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
@ -422,6 +443,8 @@ final class GroupStage: ModelObject, Storable {
} else {
try container.encodeNil(forKey: ._name)
}
try container.encode(step, forKey: ._step)
}
func insertOnServer() {
@ -442,6 +465,7 @@ extension GroupStage {
case _format = "format"
case _startDate = "startDate"
case _name = "name"
case _step = "step"
}
}

@ -511,6 +511,16 @@ final class TeamRegistration: ModelObject, Storable {
return Store.main.findById(tournament)
}
func groupStagePositionAtStep(_ step: Int) -> Int? {
guard let groupStagePosition else { return nil }
if step == 0 {
return groupStagePosition
} else if let groupStageObject = groupStageObject(), groupStageObject.hasEnded() {
return groupStageObject.index
}
return nil
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"

@ -380,15 +380,11 @@ final class Tournament : ModelObject, Storable {
return Array(self.tournamentStore.teamRegistrations)
}
func groupStages() -> [GroupStage] {
let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id }
func groupStages(atStep step: Int = 0) -> [GroupStage] {
let groupStages: [GroupStage] = self.tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
return groupStages.sorted(by: \.index)
}
func allGroupStages() -> [GroupStage] {
return Array(self.tournamentStore.groupStages)
}
func allRounds() -> [Round] {
return Array(self.tournamentStore.rounds)
}
@ -767,8 +763,8 @@ defer {
closedRegistrationDate != nil
}
func getActiveGroupStage() -> GroupStage? {
let groupStages = groupStages()
func getActiveGroupStage(atStep step: Int = 0) -> GroupStage? {
let groupStages = groupStages(atStep: step)
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
@ -1118,8 +1114,8 @@ defer {
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
}
func groupStagesMatches() -> [Match] {
return self.tournamentStore.matches.filter { $0.groupStage != nil }
func groupStagesMatches(atStep step: Int = 0) -> [Match] {
return groupStages(atStep: step).flatMap({ $0._matches() })
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
}
@ -1191,6 +1187,19 @@ defer {
var teams: [Int: [String]] = [:]
var ids: Set<String> = Set<String>()
let rounds = rounds()
let lastStep = lastStep()
if rounds.isEmpty, lastStep > 0 {
let groupStages = groupStages(atStep: lastStep)
for groupStage in groupStages {
for (teamIndex, team) in groupStage.teams(true).enumerated() {
teams[groupStage.index + 1 + teamIndex] = [team.id]
}
}
return teams
}
let final = rounds.last?.playedMatches().last
if let winner = final?.winningTeamId {
teams[1] = [winner]
@ -1600,12 +1609,23 @@ defer {
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
}
func deleteAndBuildEverything() {
func deleteAndBuildEverything(preset: PadelTournamentStructurePreset = .manual) {
resetBracketPosition()
deleteStructure()
deleteGroupStages()
switch preset {
case .manual:
buildGroupStages()
buildBracket()
case .doubleGroupStage:
buildGroupStages()
addNewGroupStageStep()
qualifiedPerGroupStage = 0
groupStageAdditionalQualified = 0
}
}
func buildGroupStages() {
@ -2021,6 +2041,22 @@ defer {
return teamCount - teamsPerGroupStage * groupStageCount + qualifiedPerGroupStage * groupStageCount + groupStageAdditionalQualified + 1
}
func addNewGroupStageStep() {
let lastStep = lastStep() + 1
for i in 0..<teamsPerGroupStage {
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep)
do {
try tournamentStore.groupStages.addOrUpdate(instance: gs)
} catch {
Logger.error(error)
}
}
}
func lastStep() -> Int {
self.tournamentStore.groupStages.sorted(by: \.step).last?.step ?? 0
}
// MARK: -
func insertOnServer() throws {

@ -1637,3 +1637,27 @@ enum AnimationType: Int, CaseIterable, Hashable, Identifiable {
}
}
enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case manual
case doubleGroupStage
func localizedStructurePresetTitle() -> String {
switch self {
case .manual:
return "Défaut"
case .doubleGroupStage:
return "2 phases de poules"
}
}
func localizedDescriptionStructurePresetTitle() -> String {
switch self {
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."
}
}
}

@ -40,7 +40,7 @@ struct TournamentConfigurationView: View {
}
Picker(selection: $tournament.federalCategory, label: Text("Catégorie")) {
ForEach(TournamentCategory.allCases) { type in
Text(type.localizedLabel(.wide)).tag(type)
Text(type.localizedLabel(.title)).tag(type)
}
}
Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) {

@ -105,7 +105,7 @@ struct GroupStageView: View {
var body: some View {
ForEach(0..<(groupStage.size), id: \.self) { index in
if let team = _teamAt(atIndex: index), let groupStagePosition = team.groupStagePosition {
if let team = _teamAt(atIndex: index), let groupStagePosition = team.groupStagePositionAtStep(groupStage.step) {
NavigationLink {
GroupStageTeamView(groupStage: groupStage, team: team)
.environment(self.tournament)
@ -137,7 +137,7 @@ struct GroupStageView: View {
}
}
Spacer()
if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePosition == groupStagePosition })) {
if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePositionAtStep(groupStage.step) == groupStagePosition })) {
VStack(alignment: .trailing) {
HStack(spacing: 0.0) {
Text(score.wins)

@ -13,6 +13,7 @@ struct GroupStagesSettingsView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var generationDone: Bool = false
let step: Int
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
@ -97,6 +98,26 @@ struct GroupStagesSettingsView: View {
}
}
if tournament.lastStep() == 0, step == 0, tournament.rounds().isEmpty {
Section {
RowButtonView("Ajouter une phase de poule", role: .destructive) {
tournament.addNewGroupStageStep()
}
}
} else if step > 0 {
Section {
RowButtonView("Supprimer cette phase de poule", role: .destructive) {
let gs = tournament.groupStages(atStep: tournament.lastStep())
do {
try tournament.tournamentStore.groupStages.delete(contentOfs: gs)
} catch {
Logger.error(error)
}
}
}
}
#if DEBUG
Section {
RowButtonView("delete all group stages") {

@ -12,6 +12,7 @@ struct GroupStagesView: View {
@State var tournament: Tournament
@State private var selectedDestination: GroupStageDestination?
@EnvironmentObject var dataStore: DataStore
let step: Int
enum GroupStageDestination: Selectable, Identifiable, Equatable {
static func == (lhs: GroupStagesView.GroupStageDestination, rhs: GroupStagesView.GroupStageDestination) -> Bool {
@ -77,17 +78,18 @@ struct GroupStagesView: View {
}
var allMatches: [Match] {
tournament.groupStagesMatches()
tournament.groupStagesMatches(atStep: step)
}
init(tournament: Tournament) {
init(tournament: Tournament, step: Int = 0) {
self.tournament = tournament
self.step = step
if tournament.shouldVerifyGroupStage {
_selectedDestination = State(wrappedValue: nil)
} else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty {
_selectedDestination = State(wrappedValue: nil)
} else {
let gs = tournament.getActiveGroupStage()
let gs = tournament.getActiveGroupStage(atStep: step)
if let gs {
_selectedDestination = State(wrappedValue: .groupStage(gs))
}
@ -96,7 +98,7 @@ struct GroupStagesView: View {
func allDestinations() -> [GroupStageDestination] {
var allDestinations : [GroupStageDestination] = [.all(tournament)]
let groupStageDestinations : [GroupStageDestination] = tournament.groupStages().map { GroupStageDestination.groupStage($0) }
let groupStageDestinations : [GroupStageDestination] = tournament.groupStages(atStep: step).map { GroupStageDestination.groupStage($0) }
if let loserBracket = tournament.groupStageLoserBracket() {
allDestinations.insert(.loserBracket(loserBracket), at: 0)
}
@ -158,10 +160,11 @@ struct GroupStagesView: View {
case .loserBracket(let loserBracket):
LoserBracketFromGroupStageView(loserBracket: loserBracket).id(loserBracket.id)
case nil:
GroupStagesSettingsView()
GroupStagesSettingsView(step: step)
.navigationTitle("Réglages")
}
}
.environment(tournament)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}

@ -19,6 +19,7 @@ struct TableStructureView: View {
@State private var qualifiedPerGroupStage: Int = 0
@State private var groupStageAdditionalQualified: Int = 0
@State private var updatedElements: Set<StructureElement> = Set()
@State private var structurePreset: PadelTournamentStructurePreset = .manual
@FocusState private var stepperFieldIsFocused: Bool
var qualifiedFromGroupStage: Int {
@ -51,6 +52,37 @@ struct TableStructureView: View {
@ViewBuilder
var body: some View {
List {
if tournament.state() != .build {
Section {
Picker(selection: $structurePreset) {
ForEach(PadelTournamentStructurePreset.allCases) { preset in
Text(preset.localizedStructurePresetTitle()).tag(preset)
}
} label: {
Text("Préréglage")
}
} footer: {
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
}
}
}
Section {
LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128)
@ -73,6 +105,7 @@ struct TableStructureView: View {
Text("Équipes par poule")
}
if structurePreset == .manual {
LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1))
} label: {
@ -93,14 +126,42 @@ struct TableStructureView: View {
}
}
}
}
if groupStageCount > 0 && teamsPerGroupStage > 0 {
if structurePreset == .manual {
LabeledContent {
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
}
} else {
LabeledContent {
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
Text("Première phase")
}
LabeledContent {
let mp = (groupStageCount * (groupStageCount - 1) / 2)
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
Text("Deuxième phase")
}
LabeledContent {
let mp = groupStageCount - 1 + teamsPerGroupStage - 1
Text(mp.formatted())
} label: {
Text("Matchs à jouer par équipe")
Text("Total")
}
}
}
}
} else {
@ -116,12 +177,19 @@ struct TableStructureView: View {
} label: {
Text("Équipes en poule")
}
if structurePreset == .manual {
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Équipes qualifiées de poule")
}
}
}
if structurePreset == .manual {
LabeledContent {
let tsPure = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
Text(tsPure.formatted())
@ -134,6 +202,7 @@ struct TableStructureView: View {
Text("Équipes en tableau final")
}
}
}
if tournament.state() != .initial {
@ -154,6 +223,13 @@ struct TableStructureView: View {
_save(rebuildEverything: true)
}
}
Section {
RowButtonView("Remise-à-zéro", role: .destructive) {
tournament.deleteGroupStages()
tournament.deleteStructure()
}
}
}
}
.focused($stepperFieldIsFocused)
@ -283,7 +359,7 @@ struct TableStructureView: View {
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if rebuildEverything {
tournament.deleteAndBuildEverything()
tournament.deleteAndBuildEverything(preset: structurePreset)
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages()
tournament.buildGroupStages()

@ -55,6 +55,12 @@ struct TournamentBuildView: View {
}
}
if tournament.groupStages(atStep: 1).isEmpty == false {
NavigationLink("Step 1") {
GroupStagesView(tournament: tournament, step: 1)
}
}
if tournament.rounds().isEmpty == false {
NavigationLink(value: Screen.round) {
LabeledContent {

Loading…
Cancel
Save