add new feature : double group stages and loser bracket smart auto generation

sync2
Raz 1 year ago
parent 6ac26eb1e4
commit 13be596b26
  1. 14
      PadelClub/Data/GroupStage.swift
  2. 13
      PadelClub/Data/MatchScheduler.swift
  3. 3
      PadelClub/Data/Round.swift
  4. 21
      PadelClub/Data/Tournament.swift
  5. 8
      PadelClub/Extensions/FixedWidthInteger+Extensions.swift
  6. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  7. 13
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  8. 43
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  9. 2
      PadelClub/Views/Match/MatchSummaryView.swift
  10. 2
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  11. 2
      PadelClub/Views/Planning/PlanningSettingsView.swift
  12. 2
      PadelClub/Views/Planning/PlanningView.swift
  13. 4
      PadelClub/Views/Planning/SchedulerView.swift
  14. 2
      PadelClub/Views/Team/TeamRowView.swift
  15. 20
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  16. 8
      PadelClub/Views/Tournament/TournamentBuildView.swift

@ -71,14 +71,26 @@ final class GroupStage: ModelObject, Storable {
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let name { return name }
var stepLabel = ""
if step > 0 {
stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)"
}
switch displayStyle {
case .wide, .title:
case .title:
return "Poule \(index + 1)" + stepLabel
case .wide:
return "Poule \(index + 1)"
case .short:
return "#\(index + 1)"
}
}
var computedOrder: Int {
index + step * 100
}
func isRunning() -> Bool { // at least a match has started
_matches().anySatisfy({ $0.isRunning() })
}

@ -93,9 +93,9 @@ final class MatchScheduler : ModelObject, Storable {
}
@discardableResult
func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil) -> Date {
func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil, atStep step: Int = 0, startDate: Date? = nil) -> Date {
let computedGroupStageChunkCount = groupStageChunkCount ?? tournament.getGroupStageChunkValue()
var groupStages: [GroupStage] = tournament.groupStages()
var groupStages: [GroupStage] = tournament.groupStages(atStep: step)
if let specificGroupStage {
groupStages = [specificGroupStage]
}
@ -108,7 +108,7 @@ final class MatchScheduler : ModelObject, Storable {
$0.confirmed = false
})
var lastDate : Date = tournament.startDate
var lastDate : Date = startDate ?? tournament.startDate
let times = Set(groupStages.compactMap { $0.startDate }).sorted()
if let first = times.first {
@ -123,7 +123,9 @@ final class MatchScheduler : ModelObject, Storable {
}
times.forEach({ time in
lastDate = time
if lastDate.isEarlierThan(time) {
lastDate = time
}
let groups = groupStages.filter({ $0.startDate == time })
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
@ -743,6 +745,9 @@ final class MatchScheduler : ModelObject, Storable {
if tournament.groupStageCount > 0 {
lastDate = updateGroupStageSchedule(tournament: tournament)
}
if tournament.groupStages(atStep: 1).isEmpty == false {
lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate)
}
return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
}
}

@ -598,9 +598,6 @@ defer {
func deleteLoserBracket() {
do {
let loserRounds = loserRounds()
for loserRound in loserRounds {
try loserRound.deleteDependencies()
}
try self.tournamentStore.rounds.delete(contentOfs: loserRounds)
} catch {
Logger.error(error)

@ -385,6 +385,10 @@ final class Tournament : ModelObject, Storable {
return groupStages.sorted(by: \.index)
}
func allGroupStages() -> [GroupStage] {
return self.tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
}
func allRounds() -> [Round] {
return Array(self.tournamentStore.rounds)
}
@ -1757,7 +1761,7 @@ defer {
func deleteGroupStages() {
do {
try self.tournamentStore.groupStages.delete(contentOfs: groupStages())
try self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
} catch {
Logger.error(error)
}
@ -2088,6 +2092,8 @@ defer {
Logger.error(error)
}
}
groupStages(atStep: 1).forEach { $0.buildMatches() }
}
func lastStep() -> Int {
@ -2095,11 +2101,24 @@ defer {
}
func generateSmartLoserGroupStageBracket() {
guard let groupStageLoserBracket = groupStageLoserBracket() else { return }
for i in qualifiedPerGroupStage..<teamsPerGroupStage {
groupStages().chunked(into: 2).forEach { gss in
let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, matchFormat: groupStageLoserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place"
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
print("rang \(i)")
print(score1.teamLabel(.short), "vs", score2.teamLabel(.short))
match.setLuckyLoser(team: score1, teamPosition: .one)
match.setLuckyLoser(team: score2, teamPosition: .two)
}
}
}

@ -8,15 +8,15 @@
import Foundation
public extension FixedWidthInteger {
func ordinalFormattedSuffix() -> String {
func ordinalFormattedSuffix(feminine: Bool = false) -> String {
switch self {
case 1: return "er"
case 1: return feminine ? "ère" : "er"
default: return "ème"
}
}
func ordinalFormatted() -> String {
return self.formatted() + self.ordinalFormattedSuffix()
func ordinalFormatted(feminine: Bool = false) -> String {
return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine)
}
var pluralSuffix: String {

@ -70,7 +70,7 @@ struct GroupStageView: View {
_groupStageMenuView()
}
}
.navigationTitle(groupStage.groupStageTitle())
.navigationTitle(groupStage.groupStageTitle(.title))
}
private enum GroupStageSortingMode {

@ -10,7 +10,7 @@ import LeStorage
struct GroupStagesSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
@Environment(Tournament.self) var tournament: Tournament
@State private var generationDone: Bool = false
let step: Int
@ -89,7 +89,6 @@ struct GroupStagesSettingsView: View {
} else if let groupStageLoserBracket = tournament.groupStageLoserBracket() {
RowButtonView("Supprimer les matchs de classements", role: .destructive) {
do {
try groupStageLoserBracket.deleteDependencies()
try tournamentStore.rounds.delete(instance: groupStageLoserBracket)
} catch {
Logger.error(error)
@ -98,21 +97,25 @@ struct GroupStagesSettingsView: View {
}
}
if tournament.lastStep() == 0, step == 0, tournament.rounds().isEmpty {
if tournament.lastStep() == 0, step == 0 {
Section {
RowButtonView("Ajouter une phase de poule", role: .destructive) {
tournament.addNewGroupStageStep()
}
} footer: {
Text("Padel Club peut vous créer une 2ème phase de poule utilisant les résultats de la première phase : les premiers de chaque poule joueront ensemble et ainsi de suite.")
}
} else if step > 0 {
Section {
RowButtonView("Supprimer cette phase de poule", role: .destructive) {
let gs = tournament.groupStages(atStep: tournament.lastStep())
let groupStages = tournament.groupStages(atStep: tournament.lastStep())
do {
try tournament.tournamentStore.groupStages.delete(contentOfs: gs)
try tournament.tournamentStore.groupStages.delete(contentOfs: groupStages)
} catch {
Logger.error(error)
}
dismiss()
}
}

@ -33,9 +33,13 @@ struct LoserBracketFromGroupStageView: View {
List {
if isEditingLoserBracketGroupStage == true && displayableMatches.isEmpty == false {
Section {
RowButtonView("Ajouter un match", role: .destructive) {
_addNewMatch()
}
_addButton()
}
Section {
_smartGenerationButton()
} footer: {
Text("La génération intelligente ajoutera un match par rang entre 2 poules. Si vos poules sont terminées, Padel Club placera les équipes automatiquement.")
}
}
@ -75,17 +79,10 @@ struct LoserBracketFromGroupStageView: View {
ContentUnavailableView {
Label("Aucun match de classement", systemImage: "figure.tennis")
} description: {
Text("Vous n'avez créé aucun match de classement entre les perdants de poules.")
Text("Vous n'avez créé aucun match de classement entre les perdants de poules. La génération intelligente ajoutera un match par rang entre 2 poules")
} actions: {
RowButtonView("Ajouter un match") {
isEditingLoserBracketGroupStage = true
_addNewMatch()
}
RowButtonView("Génération intelligente", role: .destructive) {
isEditingLoserBracketGroupStage = true
tournament.generateSmartLoserGroupStageBracket()
}
_addButton()
_smartGenerationButton()
}
}
}
@ -122,15 +119,28 @@ struct LoserBracketFromGroupStageView: View {
let displayableMatches = loserBracket.playedMatches().sorted(by: \.index)
do {
for match in displayableMatches {
try match.deleteDependencies()
}
try tournamentStore.matches.delete(contentOfs: displayableMatches)
} catch {
Logger.error(error)
}
}
private func _smartGenerationButton() -> some View {
RowButtonView("Génération intelligente", role: .destructive, confirmationMessage: displayableMatches.isEmpty ? nil : "Les matchs de classement de poules déjà existants seront supprimés") {
isEditingLoserBracketGroupStage = true
_deleteAllMatches()
tournament.generateSmartLoserGroupStageBracket()
}
}
private func _addButton() -> some View {
RowButtonView("Ajouter un match") {
isEditingLoserBracketGroupStage = true
_addNewMatch()
}
}
}
struct GroupStageLoserBracketMatchFooterView: View {
@ -160,7 +170,6 @@ struct GroupStageLoserBracketMatchFooterView: View {
Spacer()
FooterButtonView("Effacer", role: .destructive) {
do {
try match.deleteDependencies()
try match.tournamentStore.matches.delete(instance: match)
} catch {
Logger.error(error)

@ -28,7 +28,7 @@ struct MatchSummaryView: View {
self.color = Color(white: 0.9)
if let groupStage = match.groupStageObject {
self.roundTitle = groupStage.groupStageTitle()
self.roundTitle = groupStage.groupStageTitle(.title)
} else if let round = match.roundObject {
self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short)
} else {

@ -27,7 +27,7 @@ struct GroupStageScheduleEditorView: View {
}
var body: some View {
GroupStageDatePickingView(title: groupStage.groupStageTitle(), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
groupStage.startDate = startDate
tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage)
_save()

@ -114,7 +114,7 @@ struct PlanningSettingsView: View {
}
let allMatches = tournament.allMatches()
let allGroupStages = tournament.groupStages()
let allGroupStages = tournament.allGroupStages()
let allRounds = tournament.allRounds()
let matchesWithDate = allMatches.filter({ $0.startDate != nil })
let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil })

@ -98,7 +98,7 @@ struct PlanningView: View {
}
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
Text(groupStage.groupStageTitle(.title))
} else if let round = match.roundObject {
Text(round.roundTitle())
}

@ -43,7 +43,7 @@ struct SchedulerView: View {
}
}
.onChange(of: tournament.groupStageMatchFormat) {
let groupStages = tournament.groupStages()
let groupStages = tournament.allGroupStages()
groupStages.forEach { groupStage in
groupStage.updateMatchFormat(tournament.groupStageMatchFormat)
}
@ -68,7 +68,7 @@ struct SchedulerView: View {
}
}
ForEach(tournament.groupStages()) {
ForEach(tournament.allGroupStages()) {
GroupStageScheduleEditorView(groupStage: $0, tournament: tournament)
.id(UUID())
}

@ -19,7 +19,7 @@ struct TeamRowView: View {
VStack(alignment: .leading) {
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle())
Text(groupStage.groupStageTitle(.title))
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
}

@ -174,13 +174,13 @@ struct TableStructureView: View {
Section {
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
if groupStageCount > 0 {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
if structurePreset == .manual {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
@ -203,6 +203,14 @@ struct TableStructureView: View {
} label: {
Text("Équipes en tableau final")
}
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
let mp2 = (groupStageCount * (groupStageCount - 1) / 2) * teamsPerGroupStage
Text((mp1 + mp2).formatted())
} label: {
Text("Total de matchs")
}
}
}

@ -30,7 +30,11 @@ struct TournamentBuildView: View {
ProgressView()
}
} label: {
Text("Poules")
if tournament.groupStages(atStep: 1).isEmpty == false {
Text("1ère phase de poules")
} else {
Text("Poules")
}
if tournament.groupStagesAreOver(), tournament.moreQualifiedToDraw() > 0 {
let moreQualifiedToDraw = tournament.moreQualifiedToDraw()
Text("Qualifié\(moreQualifiedToDraw.pluralSuffix) sortant\(moreQualifiedToDraw.pluralSuffix) manquant\(moreQualifiedToDraw.pluralSuffix)").foregroundStyle(.logoRed)
@ -56,7 +60,7 @@ struct TournamentBuildView: View {
}
if tournament.groupStages(atStep: 1).isEmpty == false {
NavigationLink("Step 1") {
NavigationLink("2ème phase de poules") {
GroupStagesView(tournament: tournament, step: 1)
}
}

Loading…
Cancel
Save