add final ranking

fix stuff on teams update
multistore
Razmig Sarkissian 1 year ago
parent e7df5cef10
commit b4e8a09603
  1. 16
      PadelClub/Data/TeamRegistration.swift
  2. 24
      PadelClub/Data/Tournament.swift
  3. 10
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  4. 4
      PadelClub/Views/GroupStage/GroupStagesView.swift
  5. 15
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  6. 10
      PadelClub/Views/Round/RoundSettingsView.swift
  7. 4
      PadelClub/Views/Round/RoundsView.swift
  8. 27
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  9. 31
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  10. 1
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  11. 50
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  12. 66
      PadelClub/Views/Tournament/TournamentBuildView.swift
  13. 2
      PadelClub/Views/Tournament/TournamentInitView.swift
  14. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -33,6 +33,8 @@ class TeamRegistration: ModelObject, Storable {
var lockedWeight: Int? var lockedWeight: Int?
var confirmationDate: Date? var confirmationDate: Date?
var qualified: Bool = false var qualified: Bool = false
var finalRanking: Int?
var pointsEarned: Int?
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) { 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.tournament = tournament
@ -353,6 +355,8 @@ class TeamRegistration: ModelObject, Storable {
case _lockedWeight = "lockedWeight" case _lockedWeight = "lockedWeight"
case _confirmationDate = "confirmationDate" case _confirmationDate = "confirmationDate"
case _qualified = "qualified" case _qualified = "qualified"
case _finalRanking = "finalRanking"
case _pointsEarned = "pointsEarned"
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -439,6 +443,18 @@ class TeamRegistration: ModelObject, Storable {
} }
try container.encode(qualified, forKey: ._qualified) try container.encode(qualified, forKey: ._qualified)
if let finalRanking {
try container.encode(finalRanking, forKey: ._finalRanking)
} else {
try container.encodeNil(forKey: ._finalRanking)
}
if let pointsEarned {
try container.encode(pointsEarned, forKey: ._pointsEarned)
} else {
try container.encodeNil(forKey: ._pointsEarned)
}
} }
} }

@ -49,6 +49,10 @@ class Tournament : ModelObject, Storable {
var publishGroupStages: Bool = false var publishGroupStages: Bool = false
var publishBrackets: Bool = false var publishBrackets: Bool = false
//local
var shouldVerifyGroupStage: Bool = false
var shouldVerifyBracket: Bool = false
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -460,7 +464,7 @@ class Tournament : ModelObject, Storable {
} }
func groupStageTeams() -> [TeamRegistration] { func groupStageTeams() -> [TeamRegistration] {
selectedSortedTeams().filter({ $0.bracketPosition == nil && $0.groupStagePosition != nil }) selectedSortedTeams().filter({ $0.groupStagePosition != nil })
} }
func seeds() -> [TeamRegistration] { func seeds() -> [TeamRegistration] {
@ -939,7 +943,7 @@ class Tournament : ModelObject, Storable {
let groupStageTeams = groupStage.teams(true) let groupStageTeams = groupStage.teams(true)
for (index, team) in groupStageTeams.enumerated() { for (index, team) in groupStageTeams.enumerated() {
if team.qualified == false { if team.qualified == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? teamsPerGroupStage - groupStageAdditionalQualified : teamsPerGroupStage) * (index - qualifiedPerGroupStage), 0) let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1 let _index = baseRank + groupStageWidth + 1
if let existingTeams = teams[_index] { if let existingTeams = teams[_index] {
@ -1125,6 +1129,14 @@ class Tournament : ModelObject, Storable {
} }
func bracketStatus() -> String { func bracketStatus() -> String {
let availableSeeds = availableSeeds()
if availableSeeds.isEmpty == false {
return "placer \(availableSeeds.count) tête\(availableSeeds.count.pluralSuffix) de série"
}
let availableQualifiedTeams = availableQualifiedTeams()
if availableQualifiedTeams.isEmpty == false {
return "placer \(availableQualifiedTeams.count) qualifié" + availableQualifiedTeams.count.pluralSuffix
}
if let round = getActiveRound() { if let round = getActiveRound() {
return [round.roundTitle(), round.roundStatus()].joined(separator: " ") return [round.roundTitle(), round.roundStatus()].joined(separator: " ")
} else { } else {
@ -1133,6 +1145,10 @@ class Tournament : ModelObject, Storable {
} }
func groupStageStatus() -> String { func groupStageStatus() -> String {
let groupStageTeamsCount = groupStageTeams().count
if groupStageTeamsCount == 0 || groupStageTeamsCount != teamsPerGroupStage * groupStageCount {
return "à faire"
}
let runningGroupStages = groupStages().filter({ $0.isRunning() }) let runningGroupStages = groupStages().filter({ $0.isRunning() })
if groupStagesAreOver() { return "terminées" } if groupStagesAreOver() { return "terminées" }
if runningGroupStages.isEmpty { if runningGroupStages.isEmpty {
@ -1152,9 +1168,13 @@ class Tournament : ModelObject, Storable {
} }
func structureDescriptionLocalizedLabel() -> String { func structureDescriptionLocalizedLabel() -> String {
if state() == .initial {
return "à valider"
} else {
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
} }
}
func deleteAndBuildEverything() { func deleteAndBuildEverything() {
deleteStructure() deleteStructure()

@ -28,6 +28,16 @@ struct GroupStageSettingsView: View {
var body: some View { var body: some View {
List { List {
if tournament.shouldVerifyGroupStage {
Section {
RowButtonView("Valider les poules", role: .destructive) {
tournament.shouldVerifyGroupStage = false
}
} footer: {
Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de vos poules et valider que tout est ok.")
}
}
#if DEBUG #if DEBUG
Section { Section {
RowButtonView("delete all group stages") { RowButtonView("delete all group stages") {

@ -53,7 +53,9 @@ struct GroupStagesView: View {
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty { if tournament.shouldVerifyGroupStage {
_selectedDestination = State(wrappedValue: nil)
} else if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty {
_selectedDestination = State(wrappedValue: nil) _selectedDestination = State(wrappedValue: nil)
} else { } else {
let gs = tournament.getActiveGroupStage() let gs = tournament.getActiveGroupStage()

@ -26,21 +26,8 @@ struct OngoingView: View {
List { List {
ForEach(matches) { match in ForEach(matches) { match in
Section { Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) MatchRowView(match: match, matchViewStyle: .standardStyle)
} header: { } header: {
HStack {
if let roundTitle = match.roundTitle() {
Text(roundTitle)
}
Text(match.matchTitle(.short))
Spacer()
if let courtName = match.courtName() {
Spacer()
Text(courtName)
}
}
} footer: {
if let tournament = match.currentTournament() { if let tournament = match.currentTournament() {
Text(tournament.tournamentTitle()) Text(tournament.tournamentTitle())
} }

@ -14,6 +14,16 @@ struct RoundSettingsView: View {
var body: some View { var body: some View {
List { List {
if tournament.shouldVerifyBracket {
Section {
RowButtonView("Valider le tableau", role: .destructive) {
tournament.shouldVerifyBracket = false
}
} footer: {
Text("Suite à changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.")
}
}
// Section { // Section {
// RowButtonView("Enabled", role: .destructive) { // RowButtonView("Enabled", role: .destructive) {
// let allMatches = tournament._allMatchesIncludingDisabled() // let allMatches = tournament._allMatchesIncludingDisabled()

@ -14,7 +14,11 @@ struct RoundsView: View {
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
if tournament.shouldVerifyBracket {
_selectedRound = State(wrappedValue: nil)
} else {
_selectedRound = State(wrappedValue: tournament.getActiveRound()) _selectedRound = State(wrappedValue: tournament.getActiveRound())
}
if tournament.availableSeeds().isEmpty == false || tournament.availableQualifiedTeams().isEmpty == false { if tournament.availableSeeds().isEmpty == false || tournament.availableQualifiedTeams().isEmpty == false {
_isEditingTournamentSeed = State(wrappedValue: true) _isEditingTournamentSeed = State(wrappedValue: true)
} }

@ -109,6 +109,7 @@ struct SelectablePlayerListView: View {
} }
.scrollDismissesKeyboard(.immediately) .scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection) .navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
.toolbarBackground(.visible, for: .bottomBar)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor) // .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false) .interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet)) .navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
@ -158,6 +159,8 @@ struct SelectablePlayerListView: View {
if searchViewModel.selectedPlayers.isEmpty && searchViewModel.filterSelectionEnabled { if searchViewModel.selectedPlayers.isEmpty && searchViewModel.filterSelectionEnabled {
searchViewModel.filterSelectionEnabled = false searchViewModel.filterSelectionEnabled = false
} else {
searchViewModel.filterSelectionEnabled = true
} }
} }
.onChange(of: searchViewModel.dataSet) { .onChange(of: searchViewModel.dataSet) {
@ -178,25 +181,21 @@ struct SelectablePlayerListView: View {
} }
if searchViewModel.selectedPlayers.isEmpty == false { if searchViewModel.selectedPlayers.isEmpty == false {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .topBarTrailing) {
Button { ButtonValidateView {
searchViewModel.filterSelectionEnabled.toggle() if let playerSelectionAction {
} label: { playerSelectionAction(searchViewModel.selectedPlayers)
if searchViewModel.filterSelectionEnabled {
Image(systemName: "line.3.horizontal.decrease.circle.fill")
} else {
Image(systemName: "line.3.horizontal.decrease.circle")
} }
dismiss()
} }
} }
ToolbarItem(placement: .status) { ToolbarItem(placement: .status) {
Button { let count = searchViewModel.selectedPlayers.count
if let playerSelectionAction { VStack(spacing: 0) {
playerSelectionAction(searchViewModel.selectedPlayers) Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary)
FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") {
searchViewModel.filterSelectionEnabled.toggle()
} }
dismiss()
} label: {
Text("Ajouter le" + searchViewModel.selectedPlayers.count.pluralSuffix + " \(searchViewModel.selectedPlayers.count) joueur" + searchViewModel.selectedPlayers.count.pluralSuffix)
} }
} }
} }

@ -32,6 +32,8 @@ struct InscriptionManagerView: View {
@State private var confirmUpdateRank = false @State private var confirmUpdateRank = false
@State private var selectionSearchField: String? @State private var selectionSearchField: String?
@State private var autoSelect: Bool = false @State private var autoSelect: Bool = false
@State private var teamsHash: Int?
@State private var presentationCount: Int = 0
let slideToDeleteTip = SlideToDeleteTip() let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
@ -60,6 +62,19 @@ struct InscriptionManagerView: View {
} }
} }
// Function to create a simple hash from a list of IDs
private func _simpleHash(ids: [String]) -> Int {
// Combine the hash values of each string
return ids.reduce(0) { $0 ^ $1.hashValue }
}
// Function to check if two lists of IDs produce different hashes
private func _areDifferent(ids1: [String], ids2: [String]) -> Bool {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
_managementView() _managementView()
@ -71,6 +86,22 @@ struct InscriptionManagerView: View {
_teamRegisteredView() _teamRegisteredView()
} }
} }
.onAppear {
self.presentationCount += 1
if self.teamsHash == nil {
self.teamsHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
}
}
.onDisappear {
self.presentationCount -= 1
if self.presentationCount == 0 {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
if let teamsHash {
self.tournament.shouldVerifyBracket = newHash != teamsHash
self.tournament.shouldVerifyGroupStage = newHash != teamsHash
}
}
}
.sheet(isPresented: $isLearningMore) { .sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament) LearnMoreSheetView(tournament: tournament)
.tint(.master) .tint(.master)

@ -203,7 +203,6 @@ struct TableStructureView: View {
presentRefreshStructureWarning = true presentRefreshStructureWarning = true
} }
} }
.disabled(updatedElements.isEmpty)
.confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: { .confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: {
if requirements.allSatisfy({ $0 == .groupStage }) { if requirements.allSatisfy({ $0 == .groupStage }) {

@ -16,6 +16,47 @@ struct TournamentRankView: View {
var body: some View { var body: some View {
List { List {
Section {
let matchs = tournament.runningMatches(tournament.allMatches())
let rankingPublished = tournament.selectedSortedTeams().allSatisfy({ $0.finalRanking != nil })
LabeledContent {
Text(matchs.count.formatted())
} label: {
Text("Matchs en cours")
}
LabeledContent {
if rankingPublished {
Image(systemName: "checkmark")
} else {
Image(systemName: "xmark")
}
} label: {
Text("Classement publié")
}
RowButtonView("Publier le classement", role: .destructive) {
rankings.keys.sorted().forEach { rank in
if let rankedTeams = rankings[rank] {
rankedTeams.forEach { team in
team.finalRanking = rank
team.pointsEarned = tournament.tournamentLevel.points(for: rank - 1, count: tournament.teamCount)
}
}
}
_save()
}
} footer: {
FooterButtonView("masquer le classement") {
tournament.unsortedTeams().forEach { team in
team.finalRanking = nil
team.pointsEarned = nil
}
_save()
}
}
let keys = rankings.keys.sorted() let keys = rankings.keys.sorted()
ForEach(keys, id: \.self) { key in ForEach(keys, id: \.self) { key in
if let rankedTeams = rankings[key] { if let rankedTeams = rankings[key] {
@ -86,7 +127,6 @@ struct TournamentRankView: View {
} }
} }
} }
.listStyle(.grouped)
.onAppear { .onAppear {
let finalRanks = tournament.finalRanking() let finalRanks = tournament.finalRanking()
finalRanks.keys.sorted().forEach { rank in finalRanks.keys.sorted().forEach { rank in
@ -99,6 +139,14 @@ struct TournamentRankView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
private func _save() {
do {
try dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
} catch {
Logger.error(error)
}
}
} }
#Preview { #Preview {

@ -12,6 +12,42 @@ struct TournamentBuildView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if tournament.hasEnded() {
Section {
NavigationLink(value: Screen.rankings) {
Text("Classement")
}
}
}
Section {
if tournament.groupStageCount > 0 {
NavigationLink(value: Screen.groupStage) {
LabeledContent {
Text(tournament.groupStageStatus())
} label: {
Text("Poules")
if tournament.shouldVerifyGroupStage {
Text("Veuillez vérifier les poules").foregroundStyle(.logoRed)
}
}
}
}
if tournament.rounds().isEmpty == false {
NavigationLink(value: Screen.round) {
LabeledContent {
Text(tournament.bracketStatus())
} label: {
Text("Tableau")
if tournament.shouldVerifyBracket {
Text("Veuillez vérifier la tableau").foregroundStyle(.logoRed)
}
}
}
}
}
Section { Section {
if tournament.state() != .finished { if tournament.state() != .finished {
NavigationLink(value: Screen.schedule) { NavigationLink(value: Screen.schedule) {
@ -47,36 +83,6 @@ struct TournamentBuildView: View {
} }
} }
} }
if tournament.hasEnded() {
Section {
NavigationLink(value: Screen.rankings) {
Text("Classement")
}
}
}
Section {
if tournament.groupStageCount > 0 {
NavigationLink(value: Screen.groupStage) {
LabeledContent {
Text(tournament.groupStageStatus())
} label: {
Text("Poules")
}
}
}
if tournament.rounds().isEmpty == false {
NavigationLink(value: Screen.round) {
LabeledContent {
Text(tournament.bracketStatus())
} label: {
Text("Tableau")
}
}
}
}
} }
} }

@ -28,7 +28,6 @@ struct TournamentInitView: View {
NavigationLink(value: Screen.structure) { NavigationLink(value: Screen.structure) {
LabeledContent { LabeledContent {
Text(tournament.structureDescriptionLocalizedLabel()) Text(tournament.structureDescriptionLocalizedLabel())
.tint(.master)
} label: { } label: {
LabelStructure() LabelStructure()
} }
@ -37,7 +36,6 @@ struct TournamentInitView: View {
NavigationLink(value: Screen.settings) { NavigationLink(value: Screen.settings) {
LabeledContent { LabeledContent {
Text(tournament.settingsDescriptionLocalizedLabel()) Text(tournament.settingsDescriptionLocalizedLabel())
.tint(.master)
} label: { } label: {
LabelSettings() LabelSettings()
} }

@ -143,7 +143,7 @@ struct TournamentView: View {
} }
Divider() Divider()
if tournament.state() == .running { if tournament.state() == .running || tournament.state() == .finished {
NavigationLink(value: Screen.event) { NavigationLink(value: Screen.event) {
Text("Gestion de l'événement") Text("Gestion de l'événement")
} }

Loading…
Cancel
Save