From c21d4688e3944804d9db31443c262f11a3e26547 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 8 Oct 2024 14:45:55 +0200 Subject: [PATCH 001/106] fix seed removing not deleting team score add a refresh team button in groupestage setting view set tournament date start date when finishing a match before the set startdate --- PadelClub/Data/Match.swift | 12 +++++++++++- .../Components/GroupStageSettingsView.swift | 10 ++++++++++ PadelClub/Views/Match/MatchSetupView.swift | 10 ++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7fee09c..548055c 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -546,7 +546,17 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() - currentTournament()?.updateTournamentState() + if let tournament = currentTournament(), let endDate, let startDate { + if endDate.isEarlierThan(tournament.startDate) { + tournament.startDate = startDate + } + do { + try DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + tournament.updateTournamentState() + } updateFollowingMatchTeamScore() } diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 64abb0f..84b529e 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -149,6 +149,16 @@ struct GroupStageSettingsView: View { Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") } + Section { + RowButtonView("Rafraichir", role: .destructive) { + let playedMatches = groupStage.playedMatches() + playedMatches.forEach { match in + match.updateTeamScores() + } + } + } footer: { + Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.") + } } .onChange(of: size) { if size != groupStage.size { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 094dc80..ac777a6 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -190,15 +190,21 @@ struct MatchSetupView: View { func _removeTeam(team: TeamRegistration, teamPosition: TeamPosition) -> some View { Button(role: .cancel) { - //todo if match.isSeededBy(team: team, inTeamPosition: teamPosition) { + if let score = match.teamScore(ofTeam: team) { + do { + try tournamentStore.teamScores.delete(instance: score) + } catch { + Logger.error(error) + } + } + team.bracketPosition = nil do { try tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } - //match.updateTeamScores() match.previousMatches().forEach { previousMatch in if previousMatch.disabled { previousMatch.enableMatch() From bfb5d78b04239bff994d25a78921eb93424461a9 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 9 Oct 2024 10:46:16 +0200 Subject: [PATCH 002/106] add federal table structure selection --- PadelClub/Data/TeamRegistration.swift | 11 +- PadelClub/Data/Tournament.swift | 56 ++++- PadelClub/Utils/PadelRule.swift | 191 +++++++++++++++++- PadelClub/Views/Team/EditingTeamView.swift | 6 +- PadelClub/Views/Team/TeamRowView.swift | 4 +- .../Screen/InscriptionManagerView.swift | 36 +++- .../Screen/TableStructureView.swift | 48 +++-- 7 files changed, 316 insertions(+), 36 deletions(-) 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() From 412df66c1e2fc5f3b22956fec48c0a68e97b3afc Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 9 Oct 2024 10:46:54 +0200 Subject: [PATCH 003/106] v1.0.20 b1 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b8a1649..5b4ff6d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3158,7 +3158,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.19; + MARKETING_VERSION = 1.0.20; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3202,7 +3202,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.19; + MARKETING_VERSION = 1.0.20; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 176df45214019bc2785833227d74ffb8b9b86614 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 9 Oct 2024 11:46:41 +0200 Subject: [PATCH 004/106] fix preset management when no group stage --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Utils/PadelRule.swift | 12 ++++++++++++ .../Views/Tournament/Screen/TableStructureView.swift | 8 ++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5b4ff6d..d860ba4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3134,7 +3134,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3179,7 +3179,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index c5c0d1a..8caae56 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1884,9 +1884,21 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { 1 } } + func hasWildcards() -> Bool { wildcardBrackets() > 0 || wildcardQualifiers() > 0 } + + func isFederalPreset() -> Bool { + switch self { + case .manual: + return false + case .doubleGroupStage: + return false + case .federalStructure_8, .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: + return true + } + } } enum TournamentDeadlineType: String, CaseIterable { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 76cd345..10e660a 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -193,7 +193,7 @@ struct TableStructureView: View { } label: { Text("Nombre de têtes de série") - if tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { + if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { Text("Attention !").foregroundStyle(.red) } } @@ -212,7 +212,7 @@ struct TableStructureView: View { } } } footer: { - if tsPure > 0 && structurePreset != .doubleGroupStage { + if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 { 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) } else if tsPure < teamCount / 8 { @@ -288,6 +288,10 @@ struct TableStructureView: View { } else { updatedElements.remove(.groupStageCount) } + + if structurePreset.isFederalPreset(), groupStageCount == 0 { + teamCount = structurePreset.tableDimension() + } } .onChange(of: teamsPerGroupStage) { if teamsPerGroupStage != tournament.teamsPerGroupStage { From 8cf59a31c8da1620627c86e9cd4acbcd62314711 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 9 Oct 2024 16:00:59 +0200 Subject: [PATCH 005/106] fix textfield stuff in player edition add better way to copy paste --- .../Components/CopyPasteButtonView.swift | 28 +++++++--- PadelClub/Views/Player/PlayerDetailView.swift | 54 +++++++++++++------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/PadelClub/Views/Components/CopyPasteButtonView.swift b/PadelClub/Views/Components/CopyPasteButtonView.swift index aac21d4..27c6ad2 100644 --- a/PadelClub/Views/Components/CopyPasteButtonView.swift +++ b/PadelClub/Views/Components/CopyPasteButtonView.swift @@ -11,13 +11,29 @@ struct CopyPasteButtonView: View { let pasteValue: String? @State private var copied: Bool = false + @ViewBuilder var body: some View { - Button { - let pasteboard = UIPasteboard.general - pasteboard.string = pasteValue - copied = true - } label: { - Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) + if let pasteValue { + Button { + let pasteboard = UIPasteboard.general + pasteboard.string = pasteValue + copied = true + } label: { + Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) + } + } + } +} + +struct PasteButtonView: View { + @Binding var text: String + + @ViewBuilder + var body: some View { + PasteButton(payloadType: String.self) { strings in + if let pasteboard = strings.first { + text = pasteboard + } } } } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 982f11e..c30b70d 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -113,7 +113,9 @@ struct PlayerDetailView: View { Section { LabeledContent { TextField("Licence", text: $licenceId) + .focused($focusedField, equals: ._licenceId) .keyboardType(.alphabet) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) @@ -122,16 +124,23 @@ struct PlayerDetailView: View { _save() } } label: { - Text("Licence") + Menu { + CopyPasteButtonView(pasteValue: player.licenceId) + PasteButtonView(text: $licenceId) + .onChange(of: licenceId) { + player.licenceId = licenceId + _save() + } + } label: { + Text("Licence") + } } - } footer: { - CopyPasteButtonView(pasteValue: player.licenceId?.strippedLicense) - } - - Section { + LabeledContent { TextField("Téléphone", text: $phoneNumber) + .focused($focusedField, equals: ._phoneNumber) .keyboardType(.namePhonePad) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) @@ -140,16 +149,23 @@ struct PlayerDetailView: View { _save() } } label: { - Text("Téléphone") + Menu { + CopyPasteButtonView(pasteValue: player.phoneNumber) + PasteButtonView(text: $phoneNumber) + .onChange(of: phoneNumber) { + player.phoneNumber = phoneNumber + _save() + } + } label: { + Text("Téléphone") + } } - } footer: { - CopyPasteButtonView(pasteValue: player.phoneNumber) - } - - Section { + LabeledContent { TextField("Email", text: $email) + .focused($focusedField, equals: ._email) .keyboardType(.emailAddress) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) @@ -158,12 +174,18 @@ struct PlayerDetailView: View { _save() } } label: { - Text("Email") + Menu { + CopyPasteButtonView(pasteValue: player.email) + PasteButtonView(text: $email) + .onChange(of: email) { + player.email = email + _save() + } + } label: { + Text("Email") + } } - } footer: { - CopyPasteButtonView(pasteValue: player.email) } - } .toolbar { ToolbarItem(placement: .topBarTrailing) { From a68ffa20c59ffb5c6c38357adee222e2870e3b27 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 09:17:18 +0200 Subject: [PATCH 006/106] fix scheduler when rotation start date is too early no matter what, so delay the startdate to a new computed startdate --- PadelClub/Data/MatchScheduler.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 2ef29f9..b1518af 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -493,13 +493,23 @@ final class MatchScheduler : ModelObject, Storable { let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) var difference = differenceWithBreak - if differenceWithBreak <= 0 { + if differenceWithBreak <= 0, accountUpperBracketBreakTime == false { difference = differenceWithoutBreak } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) } - if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { + print("Final difference to evaluate: \(difference)") + + if (difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate) || difference < 0 { + print(""" + Adjusting rotation start: + - Initial rotationStartDate: \(rotationStartDate) + - Adjusted by difference: \(difference) + - Adjusted rotationStartDate: \(rotationStartDate.addingTimeInterval(-difference)) + - PreviousEndDate: \(previousEndDate) + """) + courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) freeCourtPerRotation[rotationIndex] = courts courts = freeCourtPreviousRotation From b3144be82d88baabaaf5387a13b4ca1e515d46a5 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 09:21:53 +0200 Subject: [PATCH 007/106] phrasing --- .../Views/Planning/PlanningSettingsView.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index ed03bc6..0ee5485 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -109,7 +109,7 @@ struct PlanningSettingsView: View { NavigationLink { _optionsView() } label: { - Text("Voir les options avancées") + Text("Voir plus d'options intelligentes") } } @@ -271,14 +271,14 @@ struct PlanningSettingsView: View { } } - Section { - Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { - Text("Remplir au maximum les terrains d'une rotation") - } - } footer: { - Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") - } - +// Section { +// Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { +// Text("Remplir au maximum les terrains d'une rotation") +// } +// } footer: { +// Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") +// } +// Section { Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { Text("Équilibrer les matchs d'une manche") From c025559b5e42ca7044fb8a7029119a3a274da40e Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 11:11:33 +0200 Subject: [PATCH 008/106] fix scheduler and add court pickup --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/MatchScheduler.swift | 107 +++++++++++++----- PadelClub/Data/Tournament.swift | 4 + .../Components/MultiCourtPickerView.swift | 38 +++++++ .../Views/Planning/PlanningSettingsView.swift | 56 ++++++--- 5 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 PadelClub/Views/Planning/Components/MultiCourtPickerView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index d860ba4..9f7a7bb 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -81,6 +81,9 @@ FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; + FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -977,6 +980,7 @@ FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; + FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1514,6 +1518,7 @@ isa = PBXGroup; children = ( FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, + FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */, ); path = Components; sourceTree = ""; @@ -2388,6 +2393,7 @@ FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, + FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, @@ -2657,6 +2663,7 @@ FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */, FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */, FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */, + FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */, FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */, FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */, @@ -2905,6 +2912,7 @@ FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */, FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */, FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */, + FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */, FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */, FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */, diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index b1518af..869df0a 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -31,6 +31,7 @@ final class MatchScheduler : ModelObject, Storable { var groupStageChunkCount: Int? var overrideCourtsUnavailability: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = false + var courtsAvailable: Set = Set() init(tournament: String, timeDifferenceLimit: Int = 5, @@ -42,7 +43,7 @@ final class MatchScheduler : ModelObject, Storable { rotationDifferenceIsImportant: Bool = false, shouldHandleUpperRoundSlice: Bool = true, shouldEndRoundBeforeStartingNext: Bool = true, - groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { + groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false, courtsAvailable: Set = Set()) { self.tournament = tournament self.timeDifferenceLimit = timeDifferenceLimit self.loserBracketRotationDifference = loserBracketRotationDifference @@ -56,6 +57,7 @@ final class MatchScheduler : ModelObject, Storable { self.groupStageChunkCount = groupStageChunkCount self.overrideCourtsUnavailability = overrideCourtsUnavailability self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable + self.courtsAvailable = courtsAvailable } enum CodingKeys: String, CodingKey { @@ -73,6 +75,7 @@ final class MatchScheduler : ModelObject, Storable { case _groupStageChunkCount = "groupStageChunkCount" case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" + case _courtsAvailable = "courtsAvailable" } var courtsUnavailability: [DateInterval]? { @@ -99,7 +102,6 @@ final class MatchScheduler : ModelObject, Storable { if let specificGroupStage { groupStages = [specificGroupStage] } - let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount let matches = groupStages.flatMap { $0._matches() } matches.forEach({ @@ -127,7 +129,7 @@ final class MatchScheduler : ModelObject, Storable { lastDate = time } let groups = groupStages.filter({ $0.startDate == time }) - let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) + let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { @@ -151,7 +153,7 @@ final class MatchScheduler : ModelObject, Storable { Logger.error(error) } - let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) + let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { @@ -174,7 +176,7 @@ final class MatchScheduler : ModelObject, Storable { return lastDate } - func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { + func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { let _groupStages = groupStages @@ -214,7 +216,7 @@ final class MatchScheduler : ModelObject, Storable { print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)") } return teamsAvailable - }).prefix(numberOfCourtsAvailablePerRotation)) + }).prefix(courtsAvailable.count)) if rotationIndex > 0 { rotationMatches = rotationMatches.sorted(by: { @@ -226,7 +228,7 @@ final class MatchScheduler : ModelObject, Storable { }) } - (0.. MatchDispatcher { + func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { var slots = [TimeMatch]() var _startDate: Date? var rotationIndex = 0 @@ -436,7 +438,7 @@ final class MatchScheduler : ModelObject, Storable { var issueFound: Bool = false // Log start of the function - print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") + print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available") flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in if _startDate == nil { @@ -455,8 +457,7 @@ final class MatchScheduler : ModelObject, Storable { } var freeCourtPerRotation = [Int: [Int]]() - let availableCourt = numberOfCourtsAvailablePerRotation - var courts = initialCourts ?? (0.. 0 while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { @@ -468,7 +469,7 @@ final class MatchScheduler : ModelObject, Storable { rotationStartDate = dispatcherStartDate shouldStartAtDispatcherDate = false } else { - courts = rotationIndex == 0 ? courts : (0.. 0 && indexInRound == 0, let nextMatch = match.next() { - if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { - print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") - return true - } else { - print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") - return false + + if shouldTryToFillUpCourtsAvailable == false { + if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") + return true + } else { + print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") + return false + } } } @@ -626,7 +636,7 @@ final class MatchScheduler : ModelObject, Storable { } - if freeCourtPerRotation[rotationIndex]?.count == availableCourts { + if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { print("All courts in rotation \(rotationIndex) are free") } } @@ -713,7 +723,7 @@ final class MatchScheduler : ModelObject, Storable { print("initial available courts at beginning: \(courts ?? [])") - let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) + let roundDispatch = self.roundDispatcher(flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) roundDispatch.timedMatches.forEach { matchSchedule in if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { @@ -750,7 +760,50 @@ final class MatchScheduler : ModelObject, Storable { }) } + func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) { + var earliestEndDate: Date? + var availableCourtsAtEarliest: [Int] = [] + + // Iterate through each court and find the earliest time it becomes free + for courtIndex in courts { + let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } + var isAvailable = true + + for interval in unavailabilityForCourt { + if interval.startDate <= startDate && interval.endDate > startDate { + isAvailable = false + if let currentEarliest = earliestEndDate { + earliestEndDate = min(currentEarliest, interval.endDate) + } else { + earliestEndDate = interval.endDate + } + } + } + + // If the court is available at the start date, add it to the list of available courts + if isAvailable { + availableCourtsAtEarliest.append(courtIndex) + } + } + + // If there are no unavailable courts, return the original start date and all courts + if let earliestEndDate = earliestEndDate { + // Find which courts will be available at the earliest free date + let courtsAvailableAtEarliest = courts.filter { courtIndex in + let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } + return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate } + } + return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest) + } else { + // If no courts were unavailable, all courts are available at the start date + return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts) + } + } + func updateSchedule(tournament: Tournament) -> Bool { + if tournament.courtCount < courtsAvailable.count { + courtsAvailable = Set(tournament.courtsAvailable()) + } var lastDate = tournament.startDate if tournament.groupStageCount > 0 { lastDate = updateGroupStageSchedule(tournament: tournament) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index b0c08dc..a493e39 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2045,6 +2045,10 @@ defer { return self._matchSchedulers().first } + func courtsAvailable() -> [Int] { + (0.. MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) diff --git a/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift b/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift new file mode 100644 index 0000000..55c2b26 --- /dev/null +++ b/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift @@ -0,0 +1,38 @@ +// +// MultiCourtPickerView.swift +// PadelClub +// +// Created by razmig on 11/10/2024. +// + +import SwiftUI + +struct MultiCourtPickerView: View { + @Bindable var matchScheduler: MatchScheduler + @Environment(Tournament.self) var tournament: Tournament + + var body: some View { + List { + ForEach(tournament.courtsAvailable(), id: \.self) { courtIndex in + LabeledContent { + Button { + if matchScheduler.courtsAvailable.contains(courtIndex) { + matchScheduler.courtsAvailable.remove(courtIndex) + } else { + matchScheduler.courtsAvailable.insert(courtIndex) + } + } label: { + if matchScheduler.courtsAvailable.contains(courtIndex) { + Image(systemName: "checkmark.circle.fill") + } + } + } label: { + Text(tournament.courtName(atIndex: courtIndex)) + } + } + } + .navigationTitle("Terrains disponibles") + .toolbarBackground(.visible, for: .navigationBar) + .environment(\.editMode, Binding.constant(EditMode.active)) + } +} diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 0ee5485..1246d65 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -39,7 +39,7 @@ struct PlanningSettingsView: View { _groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) } } else { - self.matchScheduler = MatchScheduler(tournament: tournament.id) + self.matchScheduler = MatchScheduler(tournament: tournament.id, courtsAvailable: Set(tournament.courtsAvailable())) self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) } } @@ -68,7 +68,26 @@ struct PlanningSettingsView: View { CourtAvailabilitySettingsView(event: event) .environment(tournament) } label: { - Text("Indisponibilités des terrains") + LabeledContent { + Text(event.courtsUnavailability.count.formatted()) + } label: { + Text("Créneaux d'indisponibilités") + } + } + } + + NavigationLink { + MultiCourtPickerView(matchScheduler: matchScheduler) + .environment(tournament) + } label: { + LabeledContent { + Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted()) + } label: { + Text("Sélection des terrains") + if matchScheduler.courtsAvailable.count > tournament.courtCount { + Text("Attention !") + .tint(.red) + } } } } footer: { @@ -105,12 +124,23 @@ struct PlanningSettingsView: View { .foregroundStyle(.logoRed) } + let event = tournament.eventObject() Section { NavigationLink { _optionsView() } label: { Text("Voir plus d'options intelligentes") } + + if let event, event.tournaments.count > 1 { + Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { + Text("Ne pas tenir compte des autres tournois") + } + } + } footer: { + if let event, event.tournaments.count > 1 { + Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.") + } } let allMatches = tournament.allMatches() @@ -271,30 +301,22 @@ struct PlanningSettingsView: View { } } -// Section { -// Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { -// Text("Remplir au maximum les terrains d'une rotation") -// } -// } footer: { -// Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") -// } -// Section { - Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { - Text("Équilibrer les matchs d'une manche") + Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { + Text("Remplir au maximum les terrains d'une rotation") } } footer: { - Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") + Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") } Section { - Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { - Text("Ne pas tenir compte des autres tournois") + Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) { + Text("Équilibrer les matchs d'une manche") } } footer: { - Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi est toujours considéré comme libre.") + Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") } - + Section { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { Text("Finir une manche, classement inclus avant de continuer") From ac60d64accb78c80e3bf2fc122d5bf963408f979 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 17:18:31 +0200 Subject: [PATCH 009/106] add match followup picker --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/GroupStage.swift | 4 +- PadelClub/Data/Match.swift | 23 ++- PadelClub/Data/TeamRegistration.swift | 4 + PadelClub/Data/Tournament.swift | 4 +- PadelClub/Extensions/Date+Extensions.swift | 7 + PadelClub/Utils/DisplayContext.swift | 18 +++ .../Views/GroupStage/GroupStageView.swift | 8 +- .../Match/Components/MatchDateView.swift | 22 ++- .../Match/Components/PlayerBlockView.swift | 16 ++- PadelClub/Views/Match/MatchDetailView.swift | 28 ++-- PadelClub/Views/Match/MatchRowView.swift | 5 +- PadelClub/Views/Match/MatchSummaryView.swift | 10 +- PadelClub/Views/Score/FollowUpMatchView.swift | 133 ++++++++++++++++++ 14 files changed, 261 insertions(+), 29 deletions(-) create mode 100644 PadelClub/Views/Score/FollowUpMatchView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 9f7a7bb..dad8d2c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -84,6 +84,9 @@ FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -981,6 +984,7 @@ FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; + FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1860,6 +1864,7 @@ isa = PBXGroup; children = ( FFCFC0012BBC39A600B82851 /* EditScoreView.swift */, + FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */, FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */, FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */, FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */, @@ -2263,6 +2268,7 @@ FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */, FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, @@ -2533,6 +2539,7 @@ FF4CBF672C996C0600151637 /* EventClubSettingsView.swift in Sources */, FF4CBF682C996C0600151637 /* AccountView.swift in Sources */, FF4CBF692C996C0600151637 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FF4CBF6A2C996C0600151637 /* TeamsCallingView.swift in Sources */, FF4CBF6B2C996C0600151637 /* Calendar+Extensions.swift in Sources */, FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */, @@ -2782,6 +2789,7 @@ FF70FAE62C90584900129CC2 /* EventClubSettingsView.swift in Sources */, FF70FAE72C90584900129CC2 /* AccountView.swift in Sources */, FF70FAE82C90584900129CC2 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FF70FAE92C90584900129CC2 /* TeamsCallingView.swift in Sources */, FF70FAEA2C90584900129CC2 /* Calendar+Extensions.swift in Sources */, FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */, diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index f51fe5e..8f81297 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -238,7 +238,7 @@ final class GroupStage: ModelObject, Storable { return _matches().first(where: { matchIndexes.contains($0.index) }) } - func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] { + func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -246,7 +246,7 @@ final class GroupStage: ModelObject, Storable { print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) + return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }) } func runningMatches(playedMatches: [Match]) -> [Match] { diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 548055c..acce813 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -9,7 +9,11 @@ import Foundation import LeStorage @Observable -final class Match: ModelObject, Storable { +final class Match: ModelObject, Storable, Equatable { + static func == (lhs: Match, rhs: Match) -> Bool { + lhs.id == rhs.id && lhs.startDate == rhs.startDate + } + static func resourceName() -> String { "matches" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -684,12 +688,15 @@ defer { self.courtIndex = courtIndex } - func canBeStarted(inMatches matches: [Match]) -> Bool { + func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool { let teams = teamScores - guard teams.count == 2 else { return false } - guard hasEnded() == false else { return false } - guard hasStarted() == false else { return false } - return teams.compactMap({ $0.team }).allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false }) + guard teams.count == 2 else { + print("teams.count != 2") + return false + } + return teams.compactMap({ $0.team }).allSatisfy({ + ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false + }) } func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { @@ -891,6 +898,10 @@ defer { } } + var restingTimeForSorting: TimeInterval { + (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow + } + enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index e3cf09b..b85fceb 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -532,6 +532,10 @@ final class TeamRegistration: ModelObject, Storable { } } + func restingTime() -> Date? { + matches().sorted(by: \.computedEndDateForSorting).last?.endDate + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a493e39..0105ee0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1109,7 +1109,7 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { + func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1117,7 +1117,7 @@ defer { print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) } func runningMatches(_ allMatches: [Match]) -> [Match] { diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 2758f48..6e99c01 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -231,4 +231,11 @@ extension Date { func localizedWeekDay() -> String { self.formatted(.dateTime.weekday(.wide)) } + + static var hourMinuteFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute] // Customize units + formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short + return formatter + }() } diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index a5aaebe..71786af 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -27,6 +27,24 @@ enum MatchViewStyle { case feedStyle // vue programmation case plainStyle // vue detail case tournamentResultStyle //vue resultat tournoi + case followUpStyle // vue normal + + func displayRestingTime() -> Bool { + switch self { + case .standardStyle: + return false + case .sectionedStandardStyle: + return false + case .feedStyle: + return false + case .plainStyle: + return false + case .tournamentResultStyle: + return false + case .followUpStyle: + return true + } + } } struct DeviceHelper { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 8a2e134..292347b 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -28,6 +28,8 @@ struct GroupStageView: View { var body: some View { List { + let playedMatches = groupStage.playedMatches() + Section { GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score) } header: { @@ -49,8 +51,12 @@ struct GroupStageView: View { } } .headerProminence(.increased) + .onChange(of: playedMatches) { + if groupStage.hasEnded() { + sortingMode = .score + } + } - let playedMatches = groupStage.playedMatches() let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches) diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 8034631..7c37d38 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -17,13 +17,15 @@ struct MatchDateView: View { private var isReady: Bool private var hasWalkoutTeam: Bool private var hasEnded: Bool + private let updatedField: Int? - init(match: Match, showPrefix: Bool) { + init(match: Match, showPrefix: Bool, updatedField: Int? = nil) { self.match = match self.showPrefix = showPrefix self.isReady = match.isReady() self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasEnded = match.hasEnded() + self.updatedField = updatedField } var body: some View { @@ -34,21 +36,33 @@ struct MatchDateView: View { let estimatedDuration = match.getDuration() if match.startDate == nil && isReady { Button("Démarrer") { + if let updatedField { + match.setCourt(updatedField) + } match.startDate = Date() match.confirmed = true _save() } Button("Démarrer dans 5 minutes") { + if let updatedField { + match.setCourt(updatedField) + } match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) match.confirmed = true _save() } Button("Démarrer dans 15 minutes") { + if let updatedField { + match.setCourt(updatedField) + } match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) match.confirmed = true _save() } Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { + if let updatedField { + match.setCourt(updatedField) + } match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) match.confirmed = true _save() @@ -56,6 +70,9 @@ struct MatchDateView: View { } else { if isReady { Button("Démarrer maintenant") { + if let updatedField { + match.setCourt(updatedField) + } match.startDate = Date() match.endDate = nil match.confirmed = true @@ -63,6 +80,9 @@ struct MatchDateView: View { } } else { Button("Décaler de \(estimatedDuration) minutes") { + if let updatedField { + match.setCourt(updatedField) + } match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) } } diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 5f5b278..864a734 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -15,8 +15,9 @@ struct PlayerBlockView: View { let width: CGFloat let teamScore: TeamScore? let isWalkOut: Bool + let displayRestingTime: Bool - init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) { + init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat, displayRestingTime: Bool) { self.match = match self.teamPosition = teamPosition let theTeam = match.team(teamPosition) @@ -26,6 +27,7 @@ struct PlayerBlockView: View { let theTeamScore = match.teamScore(ofTeam: theTeam) self.teamScore = theTeamScore self.isWalkOut = theTeamScore?.isWalkOut() == true + self.displayRestingTime = displayRestingTime } var names: [String]? { @@ -77,6 +79,18 @@ struct PlayerBlockView: View { Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) } } + + if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { + if restingTime > -300 { + Text("vient de finir") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("en repos depuis " + value) + .font(.footnote) + .foregroundStyle(.secondary) + } + } } .bold(hasWon) Spacer() diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 300a5fa..a4a63bb 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -33,6 +33,8 @@ struct MatchDetailView: View { @State var showSubscriptionView: Bool = false @State var showUserCreationView: Bool = false + @State private var presentFollowUpMatch: Bool = false + @State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false var tournamentStore: TournamentStore { return match.tournamentStore @@ -50,7 +52,7 @@ struct MatchDetailView: View { var match: Match - init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) { + init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle, updatedField: Int? = nil) { self.match = match self.matchViewStyle = matchViewStyle @@ -69,7 +71,7 @@ struct MatchDetailView: View { _endDate = State(wrappedValue: endDate) } - if let courtIndex = match.courtIndex { + if let courtIndex = updatedField ?? match.courtIndex { _fieldSetup = State(wrappedValue: .field(courtIndex)) } } @@ -153,9 +155,17 @@ struct MatchDetailView: View { } } }) + .sheet(isPresented: $presentFollowUpMatch, onDismiss: { + if dismissWhenPresentFollowUpMatchIsDismissed { + dismiss() + } + }) { + FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) + .tint(.master) + } .sheet(item: $scoreType, onDismiss: { if match.hasEnded() { - dismiss() + presentFollowUpMatch = true } }) { scoreType in let matchDescriptor = MatchDescriptor(match: match) @@ -402,13 +412,11 @@ struct MatchDetailView: View { Text("Partage sur les réseaux sociaux") } -// if let followUpMatch = match.followUpMatch { -// Section { -// MatchRowView(match: followUpMatch) -// } header: { -// Text("à suivre terrain \(match.fieldIndex)") -// } -// } + Section { + RowButtonView("Match à suivre") { + presentFollowUpMatch = true + } + } } var editionView: some View { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 954da7f..1cde456 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -13,6 +13,7 @@ struct MatchRowView: View { @State var match: Match let matchViewStyle: MatchViewStyle var title: String? = nil + var updatedField: Int? = nil @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @@ -58,9 +59,9 @@ struct MatchRowView: View { // }) NavigationLink { - MatchDetailView(match: match, matchViewStyle: matchViewStyle) + MatchDetailView(match: match, matchViewStyle: matchViewStyle, updatedField: updatedField) } label: { - MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title) + MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title, updatedField: updatedField) .contextMenu { NavigationLink { EditSharingView(match: match) diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5b890f0..fc03111 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -18,14 +18,16 @@ struct MatchSummaryView: View { let padding: CGFloat let color: Color let width: CGFloat + let updatedField: Int? - init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil) { + init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil, updatedField: Int? = nil) { self.match = match self.matchViewStyle = matchViewStyle self.padding = matchViewStyle == .plainStyle ? 0 : 8 self.spacing = matchViewStyle == .plainStyle ? 8 : 0 self.width = matchViewStyle == .plainStyle ? 1 : 2 self.color = Color(white: 0.9) + self.updatedField = updatedField if let groupStage = match.groupStageObject { self.roundTitle = groupStage.groupStageTitle(.title) @@ -69,14 +71,14 @@ struct MatchSummaryView: View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: spacing) { - PlayerBlockView(match: match, teamPosition: .one, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .one, color: color, width: width, displayRestingTime: matchViewStyle.displayRestingTime()) .padding(padding) if width == 1 { Divider() } else { Divider().frame(height: width).overlay(color) } - PlayerBlockView(match: match, teamPosition: .two, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .two, color: color, width: width, displayRestingTime: matchViewStyle.displayRestingTime()) .padding(padding) } } @@ -90,7 +92,7 @@ struct MatchSummaryView: View { if matchViewStyle != .plainStyle { HStack { Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle) + MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField) } } } diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift new file mode 100644 index 0000000..b65c800 --- /dev/null +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -0,0 +1,133 @@ +// +// FollowUpMatchView.swift +// PadelClub +// +// Created by razmig on 11/10/2024. +// + +import SwiftUI + +struct FollowUpMatchView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss + let match: Match + let readyMatches: [Match] + let isFree: Bool + + @State private var sortingMode: SortingMode = .index + @State private var selectedCourt: Int? + @State private var checkCanPlay: Bool = false + @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool + + enum SortingMode: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case index + case restingTime + case court + + func localizedSortingModeLabel() -> String { + switch self { + case .index: + return "Ordre" + case .court: + return "Terrain" + case .restingTime: + return "Temps de repos" + } + } + } + + init(match: Match, dismissWhenPresentFollowUpMatchIsDismissed: Binding) { + _dismissWhenPresentFollowUpMatchIsDismissed = dismissWhenPresentFollowUpMatchIsDismissed + self.match = match + _selectedCourt = .init(wrappedValue: match.courtIndex) + let currentTournament = match.currentTournament() + let allMatches = currentTournament?.allMatches() ?? [] + let runningMatches = currentTournament?.runningMatches(allMatches) ?? [] + let readyMatches = currentTournament?.readyMatches(allMatches) ?? [] + self.readyMatches = currentTournament?.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) ?? [] + self.isFree = currentTournament?.isFree() ?? true + } + + var sortedMatches: [Match] { + switch sortingMode { + case .index: + return readyMatches + case .restingTime: + return readyMatches.sorted(by: \.restingTimeForSorting) + case .court: + return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) + } + } + + var body: some View { + NavigationStack { + List { + Section { + Picker(selection: $selectedCourt) { + Text("Aucun").tag(nil as Int?) + if let tournament = match.currentTournament() { + ForEach(0.. Date: Fri, 11 Oct 2024 19:39:53 +0200 Subject: [PATCH 010/106] =?UTF-8?q?Indiquer=20le=20prochain=20match=20de?= =?UTF-8?q?=20chaque=20paire=20=C3=A0=20la=20saisie=20du=20r=C3=A9sultat?= =?UTF-8?q?=20(=C3=A0=20chaque=20fin=20de=20match,=20le=20joueur=20me=20de?= =?UTF-8?q?mande=20=C3=A0=20quelle=20heure=20et=20sur=20quel=20court=20le?= =?UTF-8?q?=20prochain)=20Dans=20gestionnaire/horaire,=20faire=20appara?= =?UTF-8?q?=C3=AEtre=20les=20matchs=20=C3=A0=20venir=20plut=C3=B4t=20que?= =?UTF-8?q?=20les=20matchs=20en=20cours=20/=20termin=C3=A9=20=C3=80=20la?= =?UTF-8?q?=20saisie=20du=20dernier=20match,=20inform=C3=A9=20du=20classem?= =?UTF-8?q?ent=20final=20imm=C3=A9diatement=20pour=20r=C3=A9pondre=20?= =?UTF-8?q?=C3=A0=20la=20paire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PadelClub/Data/Match.swift | 6 + PadelClub/Data/Tournament.swift | 16 ++- PadelClub/ViewModel/SetDescriptor.swift | 4 + .../Components/CopyPasteButtonView.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 24 +++- .../Components/EditablePlayerView.swift | 2 +- PadelClub/Views/Score/EditScoreView.swift | 124 +++++++++++++++++- PadelClub/Views/Score/FollowUpMatchView.swift | 67 +++++++++- PadelClub/Views/Score/SetInputView.swift | 2 +- .../Tournament/TournamentRunningView.swift | 3 +- .../ViewModifiers/ListRowViewModifier.swift | 7 +- 11 files changed, 241 insertions(+), 16 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index acce813..0c25194 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -186,6 +186,12 @@ defer { return self.tournamentStore.teamRegistrations.findById(winningTeamId) } + func loser() -> TeamRegistration? { + guard let losingTeamId else { return nil } + return self.tournamentStore.teamRegistrations.findById(losingTeamId) + } + + func localizedStartDate() -> String { if let startDate { return startDate.formatted(date: .abbreviated, time: .shortened) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 0105ee0..e2e0a6f 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1088,6 +1088,8 @@ defer { let duplicates : [PlayerRegistration] = duplicates(in: players) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) + let homonyms = homonyms(in: players) + let ageInadequatePlayers = ageInadequatePlayers(in: players) let isImported = players.anySatisfy({ $0.isImported() }) let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported) let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) @@ -1095,7 +1097,7 @@ defer { let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) - return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count } func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { @@ -1141,6 +1143,18 @@ defer { #endif return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) } + + func matchesLeft(_ allMatches: [Match]) -> [Match] { + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + } + func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 2811e4e..d5b8e5f 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -30,4 +30,8 @@ struct SetDescriptor: Identifiable, Equatable { return nil } } + + var shouldTieBreak: Bool { + setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) + } } diff --git a/PadelClub/Views/Components/CopyPasteButtonView.swift b/PadelClub/Views/Components/CopyPasteButtonView.swift index 27c6ad2..0b5c976 100644 --- a/PadelClub/Views/Components/CopyPasteButtonView.swift +++ b/PadelClub/Views/Components/CopyPasteButtonView.swift @@ -19,7 +19,7 @@ struct CopyPasteButtonView: View { pasteboard.string = pasteValue copied = true } label: { - Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) + Label(copied ? "Copié" : "Copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index a4a63bb..b1c1fe3 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -35,6 +35,7 @@ struct MatchDetailView: View { @State var showUserCreationView: Bool = false @State private var presentFollowUpMatch: Bool = false @State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false + @State private var presentRanking: Bool = false var tournamentStore: TournamentStore { return match.tournamentStore @@ -163,9 +164,30 @@ struct MatchDetailView: View { FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) .tint(.master) } + .sheet(isPresented: $presentRanking, content: { + if let currentTournament = match.currentTournament() { + NavigationStack { + TournamentRankView() + .environment(currentTournament) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + presentRanking = false + dismiss() + } + } + } + } + .tint(.master) + } + }) .sheet(item: $scoreType, onDismiss: { if match.hasEnded() { - presentFollowUpMatch = true + if match.index == 0, match.roundObject?.parent == nil { + presentRanking = true + } else { + presentFollowUpMatch = true + } } }) { scoreType in let matchDescriptor = MatchDescriptor(match: match) diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index b2955fb..cc4e393 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -137,7 +137,7 @@ struct EditablePlayerView: View { Button { player.validateLicenceId(licenseYearValidity) } label: { - Text("Valider la licence \(licenseYearValidity)") + Text("Valider la licence \(String(licenseYearValidity))") } } } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index c1de469..06eb65c 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -8,12 +8,46 @@ import SwiftUI import LeStorage +struct ArrowView: View { + var direction: ArrowDirection // Enum for left or right direction + var isActive: Bool // Whether the arrow is visible + var color: Color + @State private var isAnimating = false + + enum ArrowDirection { + case left, right + } + + var body: some View { + Image(systemName: direction == .left ? "arrow.left" : "arrow.right") + .font(.title) + .foregroundColor(isActive ? color : .clear) // Arrow visible only when active + .opacity(isAnimating ? 1.0 : 0.4) // Animate opacity between 1.0 and 0.4 + .offset(x: isAnimating ? (direction == .left ? -5 : 5) : 0) // Slight left/right movement + .animation( + Animation.easeInOut(duration: 0.7).repeatForever(autoreverses: true), + value: isAnimating + ) + .onAppear { + isAnimating = true + } + .onDisappear { + isAnimating = false + } + .padding() + } +} + + struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore @ObservedObject var matchDescriptor: MatchDescriptor @Environment(\.dismiss) private var dismiss + + let colorTeamOne: Color = .mint + let colorTeamTwo: Color = .cyan func walkout(_ team: TeamPosition) { self.matchDescriptor.match?.setWalkOut(team) @@ -21,14 +55,44 @@ struct EditScoreView: View { dismiss() } + func pointRange(winner: Bool) -> Int? { + guard let match = matchDescriptor.match else { return nil } + guard let tournament = match.currentTournament() else { + return nil + } + + let teamsCount = tournament.teamCount + guard let round = match.roundObject else { return nil } + + guard let seedInterval = round.seedInterval(), match.index == 0 else { + return nil + } + + return winner ? tournament.tournamentLevel.points(for: seedInterval.first - 1, count: teamsCount) : tournament.tournamentLevel.points(for: seedInterval.last - 1, count: teamsCount) + } + var body: some View { Form { Section { - Text(matchDescriptor.teamLabelOne) + VStack(alignment: .leading) { + Text(matchDescriptor.teamLabelOne) + if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .one) { + Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") + .bold() + } + } + .listRowView(isActive: true, color: colorTeamOne, hideColorVariation: true) HStack { Spacer() - Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + VStack(alignment: .trailing) { + Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .two) { + Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") + .bold() + } + } } + .listRowView(isActive: true, color: colorTeamTwo, hideColorVariation: true, alignment: .trailing) } footer: { HStack { Menu { @@ -67,6 +131,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } + .tint(getColor()) } if matchDescriptor.hasEnded { @@ -112,4 +177,59 @@ struct EditScoreView: View { } } } + + + var teamOneSetupIsActive: Bool { + guard let setDescriptor = matchDescriptor.setDescriptors.last else { + return false + } + if setDescriptor.valueTeamOne == nil { + return true + } else if setDescriptor.valueTeamTwo == nil { + return false + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return true + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return false + } + + return false + } + + var teamTwoSetupIsActive: Bool { + guard let setDescriptor = matchDescriptor.setDescriptors.last else { + return false + } + + if setDescriptor.valueTeamOne == nil { + return false + } else if setDescriptor.valueTeamTwo == nil { + return true + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return false + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return true + } + + return true + } + + func getColor() -> Color { + guard let setDescriptor = matchDescriptor.setDescriptors.last else { + return .master + } + + if setDescriptor.valueTeamOne == nil { + return colorTeamOne + } else if setDescriptor.valueTeamTwo == nil { + return colorTeamTwo + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return colorTeamOne + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return colorTeamTwo + } + + return colorTeamTwo + } + } diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index b65c800..994d723 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -12,6 +12,7 @@ struct FollowUpMatchView: View { @Environment(\.dismiss) private var dismiss let match: Match let readyMatches: [Match] + let matchesLeft: [Match] let isFree: Bool @State private var sortingMode: SortingMode = .index @@ -21,6 +22,8 @@ struct FollowUpMatchView: View { enum SortingMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } + case winner + case loser case index case restingTime case court @@ -32,7 +35,11 @@ struct FollowUpMatchView: View { case .court: return "Terrain" case .restingTime: - return "Temps de repos" + return "Repos" + case .winner: + return "Gagnant" + case .loser: + return "Perdant" } } } @@ -43,12 +50,44 @@ struct FollowUpMatchView: View { _selectedCourt = .init(wrappedValue: match.courtIndex) let currentTournament = match.currentTournament() let allMatches = currentTournament?.allMatches() ?? [] + self.matchesLeft = currentTournament?.matchesLeft(allMatches) ?? [] let runningMatches = currentTournament?.runningMatches(allMatches) ?? [] let readyMatches = currentTournament?.readyMatches(allMatches) ?? [] self.readyMatches = currentTournament?.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) ?? [] self.isFree = currentTournament?.isFree() ?? true } + var winningTeam: TeamRegistration? { + match.winner() + } + + var losingTeam: TeamRegistration? { + match.loser() + } + + func contentUnavailableDescriptionLabel() -> String { + switch sortingMode { + case .winner: + if let winningTeam { + return "Aucun match à suivre pour \(winningTeam.teamLabel())" + } else { + return "La paire gagnante n'a pas été décidé" + } + case .loser: + if let losingTeam { + return "Aucun match à suivre pour \(losingTeam.teamLabel())" + } else { + return "La paire perdante n'a pas été décidé" + } + case .index: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .restingTime: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .court: + return "Ce tournoi n'a aucun match prêt à démarrer" + } + } + var sortedMatches: [Match] { switch sortingMode { case .index: @@ -57,6 +96,18 @@ struct FollowUpMatchView: View { return readyMatches.sorted(by: \.restingTimeForSorting) case .court: return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) + case .winner: + if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) { + return [followUpMatch] + } else { + return [] + } + case .loser: + if let losingTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(losingTeam.id) }) { + return [followUpMatch] + } else { + return [] + } } } @@ -93,10 +144,14 @@ struct FollowUpMatchView: View { // Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé") // } } - + Section { - ForEach(sortedMatches) { match in - MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) + if sortedMatches.isEmpty == false { + ForEach(sortedMatches) { match in + MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) + } + } else { + ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) } } header: { Picker(selection: $sortingMode) { @@ -112,7 +167,6 @@ struct FollowUpMatchView: View { } .headerProminence(.increased) .textCase(nil) - } .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Match à suivre") @@ -120,6 +174,9 @@ struct FollowUpMatchView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Retour", role: .cancel) { + if readyMatches.isEmpty && matchesLeft.isEmpty { + dismissWhenPresentFollowUpMatchIsDismissed = true + } dismiss() } } diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index 3e4c787..504710e 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -15,7 +15,7 @@ struct SetInputView: View { var setFormat: SetFormat { setDescriptor.setFormat } private var showTieBreakView: Bool { - setFormat.shouldTiebreak(scoreTeamOne: setDescriptor.valueTeamOne ?? 0, scoreTeamTwo: setDescriptor.valueTeamTwo ?? 0) + setDescriptor.shouldTieBreak } private var isMainViewTieBreakView: Bool { diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 626a82e..6c61e24 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -18,8 +18,9 @@ struct TournamentRunningView: View { @ViewBuilder var body: some View { + MatchListView(section: "à venir", matches: tournament.readyMatches(allMatches), hideWhenEmpty: true, isExpanded: false) + MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches), hideWhenEmpty: tournament.hasEnded()) -// MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false) // MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) let finishedMatches = tournament.finishedMatches(allMatches, limit: tournament.courtCount) MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded()) diff --git a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift index bcd0f36..6cfc20a 100644 --- a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift @@ -11,6 +11,7 @@ struct ListRowViewModifier: ViewModifier { let isActive: Bool let color: Color var hideColorVariation: Bool = false + let alignment: Alignment func colorVariation() -> Color { hideColorVariation ? Color(uiColor: .systemBackground) : color.variation() @@ -21,7 +22,7 @@ struct ListRowViewModifier: ViewModifier { content .listRowBackground( colorVariation() - .overlay(alignment: .leading, content: { + .overlay(alignment: alignment, content: { color.frame(width: 8) }) ) @@ -32,7 +33,7 @@ struct ListRowViewModifier: ViewModifier { } extension View { - func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false) -> some View { - modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation)) + func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false, alignment: Alignment = .leading) -> some View { + modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation, alignment: alignment)) } } From 9b0bb5efe59a8913fc9c9adf93a22d2cb3e20f2b Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 19:40:52 +0200 Subject: [PATCH 011/106] update version --- PadelClub.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index dad8d2c..b2dcd1a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3150,7 +3150,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3174,7 +3174,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.20; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3195,7 +3195,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3218,7 +3218,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.20; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3311,7 +3311,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3334,7 +3334,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3355,7 +3355,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3377,7 +3377,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3421,7 +3421,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3462,7 +3462,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.21; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 131ee06fd4cb63789b6658935195f4e27c6a787b Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 11 Oct 2024 21:25:08 +0200 Subject: [PATCH 012/106] fix registration issues lag swap default settings for scheduler --- PadelClub/Data/MatchScheduler.swift | 9 ++++++--- PadelClub/Data/Tournament.swift | 6 +++--- .../Screen/InscriptionManagerView.swift | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 869df0a..2f306c3 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -30,7 +30,7 @@ final class MatchScheduler : ModelObject, Storable { var shouldEndRoundBeforeStartingNext: Bool var groupStageChunkCount: Int? var overrideCourtsUnavailability: Bool = false - var shouldTryToFillUpCourtsAvailable: Bool = false + var shouldTryToFillUpCourtsAvailable: Bool = true var courtsAvailable: Set = Set() init(tournament: String, @@ -41,9 +41,12 @@ final class MatchScheduler : ModelObject, Storable { accountLoserBracketBreakTime: Bool = false, randomizeCourts: Bool = true, rotationDifferenceIsImportant: Bool = false, - shouldHandleUpperRoundSlice: Bool = true, + shouldHandleUpperRoundSlice: Bool = false, shouldEndRoundBeforeStartingNext: Bool = true, - groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false, courtsAvailable: Set = Set()) { + groupStageChunkCount: Int? = nil, + overrideCourtsUnavailability: Bool = false, + shouldTryToFillUpCourtsAvailable: Bool = true, + courtsAvailable: Set = Set()) { self.tournament = tournament self.timeDifferenceLimit = timeDifferenceLimit self.loserBracketRotationDifference = loserBracketRotationDifference diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index e2e0a6f..a1feebc 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -800,13 +800,13 @@ defer { func selectedSortedTeams() -> [TeamRegistration] { - #if _DEBUG_TIME //DEBUGING TIME +// #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } - #endif +// #endif var _sortedTeams : [TeamRegistration] = [] var _teams = unsortedTeams().filter({ $0.walkOut == false }) @@ -1081,7 +1081,7 @@ defer { } } - func registrationIssues() -> Int { + func registrationIssues() async -> Int { let players : [PlayerRegistration] = unsortedPlayers() let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 939be8a..1b724d7 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -48,6 +48,7 @@ struct InscriptionManagerView: View { @State private var presentAddTeamView: Bool = false @State private var compactMode: Bool = true @State private var pasteString: String? + @State private var registrationIssues: Int? = nil var tournamentStore: TournamentStore { return self.tournament.tournamentStore @@ -164,6 +165,9 @@ struct InscriptionManagerView: View { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } + Task { + self.registrationIssues = await tournament.registrationIssues() + } } private func _handleHashDiff() { @@ -700,16 +704,19 @@ struct InscriptionManagerView: View { .fixedSize(horizontal: false, vertical: false) .listRowSeparator(.hidden) - let registrationIssues = tournament.registrationIssues() - if tournament.isAnimation() == false, registrationIssues > 0 { + if tournament.isAnimation() == false { NavigationLink { InscriptionInfoView() .environment(tournament) } label: { LabeledContent { - Text(tournament.registrationIssues().formatted()) - .foregroundStyle(.logoRed) - .fontWeight(.bold) + if let registrationIssues { + Text(registrationIssues.formatted()) + .foregroundStyle(.logoRed) + .fontWeight(.bold) + } else { + ProgressView() + } } label: { Text("Problèmes détectés") } From ad5b2f06c45255a10fdd6112539a50535adab164 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 12 Oct 2024 07:53:58 +0200 Subject: [PATCH 013/106] fix round title issues --- PadelClub/Data/Round.swift | 29 ++++++++----- PadelClub/Data/Tournament.swift | 4 +- PadelClub/Views/Round/LoserRoundView.swift | 25 +++++++---- PadelClub/Views/Round/RoundView.swift | 50 ++++++++++++---------- 4 files changed, 65 insertions(+), 43 deletions(-) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index c618811..d7cf727 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -503,17 +503,26 @@ defer { if isUpperBracket() { if index == 0 { return SeedInterval(first: 1, last: 2) } let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) - let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { - $0.bracketPosition != nil - && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex - } - let playedMatches = playedMatches().count - let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2 - //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) - let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) - //print(seedInterval.localizedLabel()) - return seedInterval + if initialMode { + let playedMatches = RoundRule.numberOfMatches(forRoundIndex: index) + let seedInterval = SeedInterval(first: playedMatches + 1, last: playedMatches * 2) + //print(seedInterval.localizedLabel()) + return seedInterval + } else { + let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { + $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + } + + let playedMatches = playedMatches().count + let minimumMatches = playedMatches * 2 + //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) + //print(seedInterval.localizedLabel()) + return seedInterval + + } } if let previousRound = previousRound() { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a1feebc..3c70565 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -800,13 +800,13 @@ defer { func selectedSortedTeams() -> [TeamRegistration] { -// #if _DEBUG_TIME //DEBUGING TIME + #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } -// #endif + #endif var _sortedTeams : [TeamRegistration] = [] var _teams = unsortedTeams().filter({ $0.walkOut == false }) diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index eb7b2fa..1707e75 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -70,13 +70,13 @@ struct LoserRoundView: View { if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { - Text("no previous round") + Text("seedInterval is missing (previous round)") } } else if let parentRound = loserRound.parentRound { if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { - Text("no parent round") + Text("seedInterval is missing (parent round)") } } } @@ -103,13 +103,7 @@ struct LoserRoundView: View { isEditingTournamentSeed.wrappedValue.toggle() if isEditingTournamentSeed.wrappedValue == false { - let allRoundMatches = loserBracket.allMatches - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) - do { - try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) - } catch { - Logger.error(error) - } + _refreshNames() } } } @@ -131,4 +125,17 @@ struct LoserRoundView: View { } } } + + private func _refreshNames() { + DispatchQueue.global(qos: .background).async { + + let allRoundMatches = loserBracket.allMatches + allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } + } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 540b94a..3b31959 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -338,31 +338,37 @@ struct RoundView: View { Logger.error(error) } - //todo should be done server side - let rounds = tournament.rounds() - rounds.forEach { round in - let matches = round.playedMatches() - matches.forEach { match in - match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) - } - } - - let loserMatches = self.upperRound.loserMatches() - loserMatches.forEach { match in - match.name = match.roundTitle() - } - - let allRoundMatches = tournament.allRoundMatches() - - do { - try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) - } catch { - Logger.error(error) - } - if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } + + _refreshNames() + } + + private func _refreshNames() { + DispatchQueue.global(qos: .background).async { + //todo should be done server side + let rounds = tournament.rounds() + rounds.forEach { round in + let matches = round.playedMatches() + matches.forEach { match in + match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) + } + } + + let loserMatches = self.upperRound.loserMatches() + loserMatches.forEach { match in + match.name = match.roundTitle() + } + + let allRoundMatches = tournament.allRoundMatches() + + do { + try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } } } From 996220fe6fe408d2302161771d31809abf3bcb22 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 12 Oct 2024 18:43:53 +0200 Subject: [PATCH 014/106] fix issues with match readiness add match per day warning data update the match format picker --- PadelClub/Data/Match.swift | 2 + PadelClub/Utils/PadelRule.swift | 6 +- .../Views/Planning/PlanningSettingsView.swift | 136 ++++++++++++++++++ .../Views/Shared/MatchFormatPickerView.swift | 8 +- 4 files changed, 148 insertions(+), 4 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 0c25194..accc976 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -700,6 +700,8 @@ defer { print("teams.count != 2") return false } + guard hasEnded() == false else { return false } + guard hasStarted() == false else { return false } return teams.compactMap({ $0.team }).allSatisfy({ ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false }) diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 8caae56..bc7aee6 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1262,9 +1262,9 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { case .nineGamesDecisivePoint: return 40 case .megaTie: - return 30 + return 20 case .superTie: - return 25 + return 15 } } @@ -1349,7 +1349,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { case .superTie: return "E" case .megaTie: - return "F" + return "F (non officiel)" case .twoSetsDecisivePoint: return "A2" case .twoSetsDecisivePointSuperTie: diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 1246d65..c545c81 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -147,6 +147,13 @@ struct PlanningSettingsView: View { let allGroupStages = tournament.allGroupStages() let allRounds = tournament.allRounds() let matchesWithDate = allMatches.filter({ $0.startDate != nil }) + + let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) + + let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) + + _formatPerDayView(matchCountPerDay: countedSet) + let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) let roundsWithDate = allRounds.filter({ $0.startDate != nil }) if matchesWithDate.isEmpty == false || groupStagesWithDate.isEmpty == false || roundsWithDate.isEmpty == false { @@ -391,6 +398,135 @@ struct PlanningSettingsView: View { Logger.error(error) } } + + private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { + var matchesByDay = [Date: [Match]]() + let calendar = Calendar.current + + for match in matches { + // Extract day/month/year and create a date with only these components + let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting) + let strippedDate = calendar.date(from: components)! + + // Group matches by the strippedDate (only day/month/year) + if matchesByDay[strippedDate] == nil { + matchesByDay[strippedDate] = [] + } + + let shouldIncludeMatch: Bool + switch match.matchType { + case .groupStage: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!) + case .bracket: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!) + case .loserBracket: + shouldIncludeMatch = true + } + + if shouldIncludeMatch { + matchesByDay[strippedDate]!.append(match) + } + } + + return matchesByDay + } + + private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] { + let days = matchesByDay.keys + var matchCountPerDay = [Date: NSCountedSet]() + + for day in days { + if let matches = matchesByDay[day] { + var groupStageCount = 0 + let countedSet = NSCountedSet() + + for match in matches { + switch match.matchType { + case .groupStage: + if let groupStage = match.groupStageObject { + if groupStageCount < groupStage.size - 1 { + groupStageCount = groupStage.size - 1 + } + } + case .bracket: + countedSet.add(match.matchFormat) + case .loserBracket: + break + } + } + + if groupStageCount > 0 { + for _ in 0.. some View { + ForEach(Array(matchCountPerDay.keys).sorted(), id: \.self) { date in + if let countedSet = matchCountPerDay[date] { + Section { + let totalMatches = countedSet.totalCount() + ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in + + let count = countedSet.count(for: matchFormat) + let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches) + let error = count > totalForThisFormat + // Presenting LabeledContent for each match format and its count + LabeledContent { + Image(systemName: error ? "exclamationmark.triangle" : "checkmark") + .font(.title3) + .foregroundStyle(error ? .red : .green) + } label: { + let label = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)" + let subtitle = "pas plus de " + totalForThisFormat.formatted() + " match\(totalForThisFormat.pluralSuffix) pour ce jour" + Text(label) + Text(subtitle) + } + } + } header: { + Text(date.formatted(.dateTime.weekday(.abbreviated).day(.twoDigits).month(.abbreviated))) + } + } + } + } + + // Helper function to format date to string (you can customize the format) + private func _formattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium // Set the date style (e.g., Oct 12, 2024) + return formatter.string(from: date) + } + + +} + +// Extension to compute the total count in an NSCountedSet +extension NSCountedSet { + func totalCount() -> Int { + var total = 0 + for element in self { + total += self.count(for: element) + } + return total + } } //#Preview { diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift index 8004386..c50c3a7 100644 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ b/PadelClub/Views/Shared/MatchFormatPickerView.swift @@ -17,7 +17,13 @@ struct MatchFormatPickerView: View { DisclosureGroup(isExpanded: $isExpanded) { Picker(selection: $matchFormat) { ForEach(MatchFormat.allCases, id: \.rawValue) { format in - Text(format.computedShortLabel).tag(format) + LabeledContent { + Text("~" + format.formattedEstimatedDuration(tournament.additionalEstimationDuration)) + } label: { + Text(format.format + " " + format.suffix) + Text(format.longLabel) + } + .tag(format) } } label: { } From 34ad82f1771d589431ffc632571ec720502d9c61 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 13 Oct 2024 09:28:52 +0200 Subject: [PATCH 015/106] fix stuff scheduler fix min more qualified team fix min qualified --- PadelClub/Data/MatchScheduler.swift | 20 ++++------ PadelClub/Data/PlayerRegistration.swift | 2 +- PadelClub/Data/Tournament.swift | 16 +++++++- .../Views/Calling/TeamsCallingView.swift | 2 + .../Views/Planning/PlanningSettingsView.swift | 3 +- PadelClub/Views/Planning/PlanningView.swift | 34 +++++++++++++++-- .../Components/EditablePlayerView.swift | 7 ++++ PadelClub/Views/Player/PlayerDetailView.swift | 21 ++++++++++ .../Components/InscriptionInfoView.swift | 38 +++++++++---------- .../Screen/InscriptionManagerView.swift | 7 ++++ .../Screen/TableStructureView.swift | 4 +- 11 files changed, 114 insertions(+), 40 deletions(-) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 2f306c3..1f9d2ad 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -386,15 +386,9 @@ final class MatchScheduler : ModelObject, Storable { return true } } else { - if targetedStartDate == minimumTargetedEndDate { - print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") - minimumTargetedEndDate = minimumPossibleEndDate - } else { - print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") - minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) - } - print("Targeted start date is before the minimum possible end date, returning false.") - return false + print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") + minimumTargetedEndDate = minimumPossibleEndDate + return true } } @@ -463,7 +457,7 @@ final class MatchScheduler : ModelObject, Storable { var courts = initialCourts ?? Array(courtsAvailable) var shouldStartAtDispatcherDate = rotationIndex > 0 - while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate @@ -604,7 +598,9 @@ final class MatchScheduler : ModelObject, Storable { if shouldTryToFillUpCourtsAvailable == false { if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { - if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + + var nextMinimumTargetedEndDate = minimumTargetedEndDate + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) { print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") return true } else { @@ -625,7 +621,7 @@ final class MatchScheduler : ModelObject, Storable { matchID: firstMatch.id, rotationIndex: rotationIndex, courtIndex: courtIndex, - startDate: rotationStartDate, + startDate: minimumTargetedEndDate, durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime ) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 92d29cf..4ac2e11 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -300,7 +300,7 @@ final class PlayerRegistration: ModelObject, Storable { if let currentLicenceId = licenceId { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") - } else if let computedLicense = currentLicenceId.strippedLicense { + } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense { self.licenceId = computedLicense + " (\(year))" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3c70565..1bf494d 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1021,8 +1021,20 @@ defer { func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { let licenseYearValidity = self.licenseYearValidity() - return players.filter({ - ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true) || ($0.isImported() == false && isImported)) + return players.filter({ player in + if player.isImported() { + // Player is marked as imported: check if the license is valid + return !player.isValidLicenseNumber(year: licenseYearValidity) + } else { + // Player is not imported: validate license and handle `isImported` flag for non-imported players + let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true + let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false + + // If global `isImported` is true, check license number as well + let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity) + + return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag + } }) } diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 986322d..832daae 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -14,6 +14,8 @@ struct TeamsCallingView: View { var body: some View { List { + PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) + Section { ForEach(teams) { team in Menu { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index c545c81..5eeef89 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -107,7 +107,7 @@ struct PlanningSettingsView: View { } } label: { if isCreatedByUser { - Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") + Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club. ") + Text("Mettre à jour le club ?").underline().foregroundStyle(.master) } else { Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) @@ -257,6 +257,7 @@ struct PlanningSettingsView: View { _save() } .onChange(of: tournament.courtCount) { + matchScheduler.courtsAvailable = Set(tournament.courtsAvailable()) _save() } .onChange(of: tournament.dayDuration) { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f2b0348..a20841b 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -10,7 +10,8 @@ import SwiftUI struct PlanningView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament - + @State private var selectedDay: Date? + let matches: [Match] @Binding var selectedScheduleDestination: ScheduleDestination? @@ -39,7 +40,7 @@ struct PlanningView: View { case .byCourt: return "Par terrain" case .byDefault: - return "Par défaut" + return "Par ordre des matchs" } } } @@ -49,11 +50,38 @@ struct PlanningView: View { _selectedScheduleDestination = selectedScheduleDestination } + private func _computedTitle() -> String { + if let selectedDay { + return selectedDay.formatted(.dateTime.day().weekday().month()) + } else { + if days.count > 1 { + return "Tous les jours" + } else { + return "Horaires" + } + } + } + var body: some View { List { _bySlotView() } + .navigationTitle(Text(_computedTitle())) .toolbar(content: { + if days.count > 1 { + ToolbarTitleMenu { + Picker(selection: $selectedDay) { + Text("Tous les jours").tag(nil as Date?) + ForEach(days, id: \.self) { day in + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } + } label: { + Text("Jour") + } + .pickerStyle(.automatic) + } + } + ToolbarItem(placement: .topBarTrailing) { Menu { Picker(selection: $filterOption) { @@ -89,7 +117,7 @@ struct PlanningView: View { @ViewBuilder func _bySlotView() -> some View { if matches.allSatisfy({ $0.startDate == nil }) == false { - ForEach(days, id: \.self) { day in + ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in Section { ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in if let _matches = timeSlots[key] { diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index cc4e393..6569682 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -78,6 +78,13 @@ struct EditablePlayerView: View { Logger.error(error) } } + .onChange(of: player.licenceId) { + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } .onChange(of: player.hasArrived) { do { try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index c30b70d..046cdc1 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -186,6 +186,27 @@ struct PlayerDetailView: View { } } } + + Section { + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } + } + if let url = URL(string: "sms:\(number)") { + Link(destination: url) { + Label("Message", systemImage: "message") + } + } + } + + if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { + Link(destination: mailURL) { + Label("Mail", systemImage: "mail") + } + } + } } .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index c670fbd..fe03ff1 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -126,9 +126,7 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } - - Section { + DisclosureGroup { ForEach(homonyms) { player in ImportedPlayerView(player: player) @@ -141,8 +139,20 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } + DisclosureGroup { + ForEach(playersMissing) { + TeamDetailView(team: $0) + } + } label: { + LabeledContent { + Text(playersMissing.count.formatted()) + } label: { + Text("Paires incomplètes") + } + } + .listRowView(color: .pink) + } Section { DisclosureGroup { @@ -200,6 +210,11 @@ struct InscriptionInfoView: View { ForEach(playersWithoutValidLicense) { EditablePlayerView(player: $0, editingOptions: [.licenceId]) .environmentObject(tournament.tournamentStore) + .onChange(of: $0.licenceId) { + players = tournament.unsortedPlayers() + let isImported = players.anySatisfy({ $0.isImported() }) + playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players, isImported: isImported) + } } } label: { LabeledContent { @@ -212,21 +227,6 @@ struct InscriptionInfoView: View { } footer: { Text("importé du fichier beach-padel sans licence valide ou créé sans licence") } - - Section { - DisclosureGroup { - ForEach(playersMissing) { - TeamDetailView(team: $0) - } - } label: { - LabeledContent { - Text(playersMissing.count.formatted()) - } label: { - Text("Paires incomplètes") - } - } - .listRowView(color: .pink) - } } .task { await _getIssues() diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 1b724d7..81a6368 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -165,6 +165,7 @@ struct InscriptionManagerView: View { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } + self.registrationIssues = nil Task { self.registrationIssues = await tournament.registrationIssues() } @@ -708,6 +709,12 @@ struct InscriptionManagerView: View { NavigationLink { InscriptionInfoView() .environment(tournament) + .onDisappear { + self.registrationIssues = nil + Task { + self.registrationIssues = await tournament.registrationIssues() + } + } } label: { LabeledContent { if let registrationIssues { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 10e660a..317bf6a 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -106,14 +106,14 @@ struct TableStructureView: View { if structurePreset != .doubleGroupStage { LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) + StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) } label: { Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") } if qualifiedPerGroupStage < teamsPerGroupStage - 1 { LabeledContent { - StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified) + StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) } label: { Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text(moreQualifiedLabel) From e1d9b150727a028ddf65526eedde39a93e67bda6 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 13 Oct 2024 09:42:56 +0200 Subject: [PATCH 016/106] 1.0.21 b2 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b2dcd1a..40bb823 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3399,7 +3399,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3441,7 +3441,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From a7af5c6b1c3b7a079029689a9f150952ed13a569 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 13 Oct 2024 17:38:48 +0200 Subject: [PATCH 017/106] fix validate match saving --- PadelClub/Data/Match.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index accc976..f0584c6 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -651,7 +651,9 @@ defer { endDate = toEndDate } - confirmed = true + if hasStarted() { + confirmed = true + } } func courtName() -> String? { From 5d2a52dce751b103968485e6c4b99f9e2d8f5f11 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 13 Oct 2024 19:56:35 +0200 Subject: [PATCH 018/106] fix stuff --- PadelClub.xcodeproj/project.pbxproj | 4 +-- PadelClub/Data/Match.swift | 2 +- .../Views/Planning/PlanningByCourtView.swift | 2 +- PadelClub/Views/Planning/PlanningView.swift | 31 +++++++++++++------ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 40bb823..b5f19d5 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3399,7 +3399,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3441,7 +3441,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index f0584c6..9a92f61 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -864,7 +864,7 @@ defer { func hasStarted() -> Bool { // meaning at least one match is over if let startDate { - return startDate.timeIntervalSinceNow < 0 + return startDate.timeIntervalSinceNow < 0 && confirmed } if hasEnded() { return true diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 9b0744a..2ed79c0 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -38,7 +38,7 @@ struct PlanningByCourtView: View { } init(matches: [Match], selectedScheduleDestination: Binding, startDate: Date) { - self.matches = matches + self.matches = matches.filter({ $0.endDate == nil }) _selectedScheduleDestination = selectedScheduleDestination _selectedDay = State(wrappedValue: startDate) } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index a20841b..ab6131d 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -11,10 +11,21 @@ struct PlanningView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament @State private var selectedDay: Date? - - let matches: [Match] @Binding var selectedScheduleDestination: ScheduleDestination? + @State private var filterOption: PlanningFilterOption = .byDefault + @State private var showFinishedMatches: Bool = false + + let allMatches: [Match] + + init(matches: [Match], selectedScheduleDestination: Binding) { + self.allMatches = matches + _selectedScheduleDestination = selectedScheduleDestination + } + var matches: [Match] { + allMatches.filter({ showFinishedMatches || $0.endDate == nil }) + } + var timeSlots: [Date:[Match]] { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } } @@ -27,8 +38,6 @@ struct PlanningView: View { timeSlots.keys.sorted() } - @State private var filterOption: PlanningFilterOption = .byDefault - enum PlanningFilterOption: Int, CaseIterable, Identifiable { var id: Int { self.rawValue } @@ -45,11 +54,6 @@ struct PlanningView: View { } } - init(matches: [Match], selectedScheduleDestination: Binding) { - self.matches = matches - _selectedScheduleDestination = selectedScheduleDestination - } - private func _computedTitle() -> String { if let selectedDay { return selectedDay.formatted(.dateTime.day().weekday().month()) @@ -93,6 +97,15 @@ struct PlanningView: View { } .labelsHidden() .pickerStyle(.inline) + + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) } label: { Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") .symbolVariant(filterOption == .byCourt ? .fill : .none) From 91a3ec170003577c151f2f689318422f441f6b84 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 11:35:42 +0200 Subject: [PATCH 019/106] fix some stuff with match validation and planning match limit view --- PadelClub/Data/Match.swift | 10 ++- .../Match/Components/MatchDateView.swift | 80 +++++++++---------- PadelClub/Views/Match/MatchDetailView.swift | 4 +- .../Views/Planning/PlanningSettingsView.swift | 9 ++- PadelClub/Views/Score/EditScoreView.swift | 19 +++-- PadelClub/Views/Score/SetInputView.swift | 4 +- 6 files changed, 72 insertions(+), 54 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 9a92f61..f3570f6 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -213,7 +213,7 @@ defer { func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { startDate = targetStartDate - confirmed = targetStartDate == nil ? false : true + confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil) @@ -629,7 +629,7 @@ defer { } } - func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { + func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) { if hasEnded() == false { startDate = fromStartDate @@ -650,9 +650,11 @@ defer { startDate = fromStartDate endDate = toEndDate } - - if hasStarted() { + + if let startDate, startDate.timeIntervalSinceNow <= 300 { confirmed = true + } else { + confirmed = false } } diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 7c37d38..f64f8bb 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -34,41 +34,8 @@ struct MatchDateView: View { } else { Menu { let estimatedDuration = match.getDuration() - if match.startDate == nil && isReady { - Button("Démarrer") { - if let updatedField { - match.setCourt(updatedField) - } - match.startDate = Date() - match.confirmed = true - _save() - } - Button("Démarrer dans 5 minutes") { - if let updatedField { - match.setCourt(updatedField) - } - match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) - match.confirmed = true - _save() - } - Button("Démarrer dans 15 minutes") { - if let updatedField { - match.setCourt(updatedField) - } - match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) - match.confirmed = true - _save() - } - Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { - if let updatedField { - match.setCourt(updatedField) - } - match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) - match.confirmed = true - _save() - } - } else { - if isReady { + if isReady && match.hasStarted() == false { + Section { Button("Démarrer maintenant") { if let updatedField { match.setCourt(updatedField) @@ -78,18 +45,51 @@ struct MatchDateView: View { match.confirmed = true _save() } - } else { - Button("Décaler de \(estimatedDuration) minutes") { + Button("Démarrer dans 5 minutes") { if let updatedField { match.setCourt(updatedField) } - match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) + match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) + match.endDate = nil + match.confirmed = true + _save() } + Button("Démarrer dans 15 minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) + match.endDate = nil + match.confirmed = true + _save() + } + Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) + match.endDate = nil + match.confirmed = true + _save() + } + } header: { + Text("Le match apparaîtra dans les en cours") } - Button("Retirer l'horaire") { - match.cleanScheduleAndSave() + } else { + Button("Décaler de \(estimatedDuration) minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) } } + Button("Indiquer un score") { + + } + Divider() + Button("Retirer l'horaire") { + match.cleanScheduleAndSave() + } } label: { label } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index b1c1fe3..ba73ef8 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -183,7 +183,9 @@ struct MatchDetailView: View { }) .sheet(item: $scoreType, onDismiss: { if match.hasEnded() { - if match.index == 0, match.roundObject?.parent == nil { + if match.index == 0, match.isGroupStage() == false, match.roundObject?.parent == nil { + presentRanking = true + } else if match.isGroupStage(), match.currentTournament()?.hasEnded() == true { presentRanking = true } else { presentFollowUpMatch = true diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 5eeef89..53feb6d 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -496,14 +496,19 @@ struct PlanningSettingsView: View { .font(.title3) .foregroundStyle(error ? .red : .green) } label: { - let label = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)" - let subtitle = "pas plus de " + totalForThisFormat.formatted() + " match\(totalForThisFormat.pluralSuffix) pour ce jour" + let label : String = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)" + let optionA : String = "aucun match possible à ce format" + let optionB : String = "max " + totalForThisFormat.formatted() + "/\(totalMatches) match\(totalForThisFormat.pluralSuffix) à ce format" + let subtitle : String = (totalForThisFormat == 0) ? optionA : optionB Text(label) Text(subtitle) } } } header: { Text(date.formatted(.dateTime.weekday(.abbreviated).day(.twoDigits).month(.abbreviated))) + } footer: { + let totalMatches = countedSet.totalCount() + Text("Une équipe jouera potentiellement jusqu'à \(totalMatches) match\(totalMatches.pluralSuffix) ce jour.") } } } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 06eb65c..a080a03 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -45,7 +45,9 @@ struct EditScoreView: View { @ObservedObject var matchDescriptor: MatchDescriptor @Environment(\.dismiss) private var dismiss - + @State private var showSetInputView: Bool = true + @State private var showTieBreakInputView: Bool = false + let colorTeamOne: Color = .mint let colorTeamTwo: Color = .cyan @@ -81,7 +83,7 @@ struct EditScoreView: View { .bold() } } - .listRowView(isActive: true, color: colorTeamOne, hideColorVariation: true) + .listRowView(isActive: teamOneSetupIsActive, color: .master, hideColorVariation: false) HStack { Spacer() VStack(alignment: .trailing) { @@ -92,7 +94,7 @@ struct EditScoreView: View { } } } - .listRowView(isActive: true, color: colorTeamTwo, hideColorVariation: true, alignment: .trailing) + .listRowView(isActive: teamTwoSetupIsActive, color: .master, hideColorVariation: false, alignment: .trailing) } footer: { HStack { Menu { @@ -120,7 +122,7 @@ struct EditScoreView: View { } } ForEach($matchDescriptor.setDescriptors) { $setDescriptor in - SetInputView(setDescriptor: $setDescriptor) + SetInputView(setDescriptor: $setDescriptor, showSetInputView: $showSetInputView, showTieBreakInputView: $showTieBreakInputView) .onChange(of: setDescriptor.hasEnded) { if setDescriptor.hasEnded { if matchDescriptor.hasEnded == false { @@ -131,7 +133,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } - .tint(getColor()) + .tint(.master) } if matchDescriptor.hasEnded { @@ -180,6 +182,10 @@ struct EditScoreView: View { var teamOneSetupIsActive: Bool { + if matchDescriptor.hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } + guard let setDescriptor = matchDescriptor.setDescriptors.last else { return false } @@ -197,6 +203,9 @@ struct EditScoreView: View { } var teamTwoSetupIsActive: Bool { + if matchDescriptor.hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } guard let setDescriptor = matchDescriptor.setDescriptors.last else { return false } diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index 504710e..4fbbb64 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -9,8 +9,8 @@ import SwiftUI struct SetInputView: View { @Binding var setDescriptor: SetDescriptor - @State private var showSetInputView: Bool = true - @State private var showTieBreakInputView: Bool = false + @Binding var showSetInputView: Bool + @Binding var showTieBreakInputView: Bool var setFormat: SetFormat { setDescriptor.setFormat } From 673e405c2d810cb2899b0f2f648e528b0bb0b0fa Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 12:56:18 +0200 Subject: [PATCH 020/106] fix add team incomplete players --- PadelClub/Views/Tournament/Screen/AddTeamView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index a86f18a..4850645 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -374,13 +374,17 @@ struct AddTeamView: View { pasteString = nil editableTextField = "" + createdPlayers.removeAll() + createdPlayerIds.removeAll() if team.players().count > 1 { - createdPlayers.removeAll() - createdPlayerIds.removeAll() dismiss() } else { editedTeam = team + team.unsortedPlayers().forEach { player in + createdPlayers.insert(player) + createdPlayerIds.insert(player.id) + } } } From df0a0b5e56ba43f51239ea9b90ae31cd45f8277c Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 12:56:49 +0200 Subject: [PATCH 021/106] b2 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b5f19d5..7b1f59c 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3150,7 +3150,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3195,7 +3195,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From fe5ba820e433dec4e581489a7b9e6a531e04f244 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 18:13:44 +0200 Subject: [PATCH 022/106] fix stuff --- PadelClub/Data/Match.swift | 4 ++- PadelClub/Data/MatchScheduler.swift | 14 +++++--- PadelClub/Data/TournamentStore.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 10 ++++-- .../Views/Planning/PlanningSettingsView.swift | 2 +- PadelClub/Views/Planning/PlanningView.swift | 33 ++++++++++++------- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index f3570f6..95e08fb 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -491,7 +491,7 @@ defer { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } - return roundObject.isLoserBracket() ? (roundObject.index + 1) * 1000 + indexInRound() : (roundObject.index + 1) * 10000 + indexInRound() + return roundObject.isLoserBracket() ? (roundObject.index + 1) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound() } func previousMatches() -> [Match] { @@ -966,6 +966,8 @@ enum MatchDateSetup: Hashable, Identifiable { case inMinutes(Int) case now case customDate + case previousRotation + case nextRotation var id: Int { hashValue } } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 1f9d2ad..5121c1f 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -386,9 +386,15 @@ final class MatchScheduler : ModelObject, Storable { return true } } else { - print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") - minimumTargetedEndDate = minimumPossibleEndDate - return true + if targetedStartDate == minimumTargetedEndDate { + print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") + minimumTargetedEndDate = minimumPossibleEndDate + } else { + print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") + minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) + } + print("Targeted start date is before the minimum possible end date, returning false.") + return false } } @@ -621,7 +627,7 @@ final class MatchScheduler : ModelObject, Storable { matchID: firstMatch.id, rotationIndex: rotationIndex, courtIndex: courtIndex, - startDate: minimumTargetedEndDate, + startDate: rotationStartDate, durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime ) diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index 6cb2fc7..a01e6a0 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -38,7 +38,7 @@ class TournamentStore: Store, ObservableObject { var synchronized: Bool = true let indexed: Bool = true - #if _DEBUG_OPTIONS + #if DEBUG if let sync = PListReader.readBool(plist: "local", key: "synchronized") { synchronized = sync } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index ba73ef8..471f914 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -461,8 +461,8 @@ struct MatchDetailView: View { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) } - Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) + Text("Précédente rotation").tag(MatchDateSetup.previousRotation) + Text("Prochaine rotation").tag(MatchDateSetup.nextRotation) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") @@ -473,6 +473,12 @@ struct MatchDetailView: View { break case .now: startDate = Date() + case .nextRotation: + let baseDate = match.startDate ?? Date() + startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60) + case .previousRotation: + let baseDate = match.startDate ?? Date() + startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60) case .inMinutes(let minutes): startDate = Date().addingTimeInterval(Double(minutes) * 60) } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 53feb6d..70957f6 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -498,7 +498,7 @@ struct PlanningSettingsView: View { } label: { let label : String = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)" let optionA : String = "aucun match possible à ce format" - let optionB : String = "max " + totalForThisFormat.formatted() + "/\(totalMatches) match\(totalForThisFormat.pluralSuffix) à ce format" + let optionB : String = "pas plus de " + totalForThisFormat.formatted() + " match\(totalForThisFormat.pluralSuffix) à ce format" let subtitle : String = (totalForThisFormat == 0) ? optionA : optionB Text(label) Text(subtitle) diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index ab6131d..4a06f82 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -86,30 +86,35 @@ struct PlanningView: View { } } - ToolbarItem(placement: .topBarTrailing) { + ToolbarItemGroup(placement: .topBarTrailing) { Menu { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) - } + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) } label: { Text("Option de filtrage") } .labelsHidden() .pickerStyle(.inline) - - Picker(selection: $showFinishedMatches) { - Text("Afficher tous les matchs").tag(true) - Text("Masquer les matchs terminés").tag(false) + } label: { + Label("Filtrer", systemImage: "clock.badge.checkmark") + .symbolVariant(showFinishedMatches ? .fill : .none) + } + Menu { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } } label: { - Text("Option de filtrage") + Text("Option de triage") } .labelsHidden() .pickerStyle(.inline) } label: { - Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") .symbolVariant(filterOption == .byCourt ? .fill : .none) } + } }) .overlay { @@ -163,7 +168,11 @@ struct PlanningView: View { Text(day.formatted(.dateTime.day().weekday().month())) Spacer() let count = _matchesCount(inDayInt: day.dayInt) - Text(self._formattedMatchCount(count)) + if showFinishedMatches { + Text(self._formattedMatchCount(count)) + } else { + Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") + } } } .headerProminence(.increased) From 50a0f8a6aeb935c392281605ab32092f4bd53b96 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 18:37:35 +0200 Subject: [PATCH 023/106] fix single group stage final ranking stuff --- PadelClub.xcodeproj/project.pbxproj | 4 +-- PadelClub/Data/Tournament.swift | 4 +++ PadelClub/Utils/PadelRule.swift | 3 +- .../Components/GroupStageTeamView.swift | 34 +++++++++++-------- .../Screen/TableStructureView.swift | 6 ++-- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 7b1f59c..ce71154 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3150,7 +3150,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3195,7 +3195,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 5; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 1bf494d..a7d71c3 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1778,6 +1778,10 @@ defer { return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode) } + if rounds.isEmpty { + return + } + do { try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) } catch { diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index bc7aee6..72abedc 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1592,7 +1592,8 @@ enum RoundRule { } static func numberOfRounds(forTeams teams: Int) -> Int { - Int(log2(Double(teamsInFirstRound(forTeams: teams)))) + if teams == 0 { return 0 } + return Int(log2(Double(teamsInFirstRound(forTeams: teams)))) } static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index b8794db..1d4b467 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -66,24 +66,28 @@ struct GroupStageTeamView: View { } } } - - Section { - if team.qualified == false { - RowButtonView("Qualifier l'équipe", role: .destructive) { - team.qualified = true - //team.bracketPosition = nil - _save() - } - } else { - RowButtonView("Annuler la qualification", role: .destructive) { - team.qualified = false - groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil - _save() - } + } + + + Section { + if team.qualified == false { + RowButtonView("Qualifier l'équipe", role: .destructive) { + team.qualified = true + //team.bracketPosition = nil + _save() + } + } else { + RowButtonView("Annuler la qualification", role: .destructive) { + team.qualified = false + groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition) + team.bracketPosition = nil + _save() } } + } + + if groupStage.tournamentObject()?.hasEnded() == false { if team.qualified == false { Section { RowButtonView("Retirer de la poule", role: .destructive) { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 317bf6a..ff126af 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -106,7 +106,7 @@ struct TableStructureView: View { if structurePreset != .doubleGroupStage { LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) + StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) } label: { Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") } @@ -445,8 +445,8 @@ struct TableStructureView: View { teamsPerGroupStage = 2 } - if qualifiedPerGroupStage < 1 { - qualifiedPerGroupStage = 1 + if qualifiedPerGroupStage < 0 { + qualifiedPerGroupStage = 0 } if groupStageAdditionalQualified < 0 { From 1d70070a688a2d44a7d50c157de4baf747e52daf Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 14 Oct 2024 22:35:51 +0200 Subject: [PATCH 024/106] fix final ranking in groupstages when groupStageAdditionalQualified is used --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Data/Tournament.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ce71154..534edbf 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3150,7 +3150,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3195,7 +3195,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a7d71c3..8b78f8a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1309,7 +1309,7 @@ defer { if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) - let _index = baseRank + groupStageWidth + 1 + let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0) if let existingTeams = teams[_index] { teams[_index] = existingTeams + [team.id] } else { From 47198e9b880f139227ce6c6e1384b969700729c6 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 15 Oct 2024 11:06:56 +0200 Subject: [PATCH 025/106] add a bracket calling view fix lag when opening planning view with a lot of matches --- PadelClub.xcodeproj/project.pbxproj | 8 + .../Views/Calling/BracketCallingView.swift | 152 +++++++++++ PadelClub/Views/Planning/PlanningView.swift | 239 ++++++++++-------- .../Screen/TournamentCallView.swift | 22 +- 4 files changed, 309 insertions(+), 112 deletions(-) create mode 100644 PadelClub/Views/Calling/BracketCallingView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 534edbf..0635e0e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -87,6 +87,9 @@ FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -985,6 +988,7 @@ FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = ""; }; + FF17CA522CBE4788003C7323 /* BracketCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCallingView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1772,6 +1776,7 @@ FF9268082BCEDC2C0080F940 /* CallView.swift */, FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */, FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */, + FF17CA522CBE4788003C7323 /* BracketCallingView.swift */, FFEF7F4C2BDE68F80033D0F0 /* Components */, ); path = Calling; @@ -2426,6 +2431,7 @@ FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, + FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, @@ -2697,6 +2703,7 @@ FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, + FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */, FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */, @@ -2947,6 +2954,7 @@ FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, + FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */, FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */, diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift new file mode 100644 index 0000000..2650d8f --- /dev/null +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -0,0 +1,152 @@ +// +// BracketCallingView.swift +// PadelClub +// +// Created by razmig on 15/10/2024. +// + +import SwiftUI + +struct BracketCallingView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var displayByMatch: Bool = true + @State private var initialSeedRound: Int = 0 + @State private var initialSeedCount: Int = 0 + let tournamentRounds: [Round] + let teams: [TeamRegistration] + + init(tournament: Tournament) { + let rounds = tournament.rounds() + self.tournamentRounds = rounds + self.teams = tournament.availableSeeds() + let index = rounds.count - 1 + _initialSeedRound = .init(wrappedValue: index) + _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + } + + var initialRound: Round { + tournamentRounds.first(where: { $0.index == initialSeedRound })! + } + + func filteredRounds() -> [Round] { + tournamentRounds.filter({ $0.index >= initialSeedRound }).reversed() + } + + func seedCount(forRoundIndex roundIndex: Int) -> Int { + if roundIndex < initialSeedRound { return 0 } + if roundIndex == initialSeedRound { + return initialSeedCount + } + + let seedCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) + let previousSeedCount = self.seedCount(forRoundIndex: roundIndex - 1) + + let total = seedCount - previousSeedCount + if total < 0 { return 0 } + return total + } + + func seeds(forRoundIndex roundIndex: Int) -> [TeamRegistration] { + let previousSeeds: Int = (initialSeedRound.. some View { + List { + NavigationLink("Équipes non contactées") { + TeamsCallingView(teams: seeds.filter({ $0.callDate == nil })) + } + Section { + ForEach(seeds) { team in + CallView.TeamView(team: team) + } + } header: { + Text(round.roundTitle()) + } + } + .overlay { + if seeds.isEmpty { + ContentUnavailableView { + Label("Aucune équipe dans ce tour", systemImage: "clock.badge.questionmark") + } description: { + Text("Padel Club n'a pas réussi à déterminer quelles équipes jouent ce tour.") + } actions: { +// RowButtonView("Horaire intelligent") { +// selectedScheduleDestination = nil +// } + } + } + } + .headerProminence(.increased) + .navigationTitle(round.roundTitle()) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +//#Preview { +// SeedsCallingView() +//} diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 4a06f82..f677510 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -30,11 +30,11 @@ struct PlanningView: View { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } } - var days: [Date] { + func days(timeSlots: [Date:[Match]]) -> [Date] { Set(timeSlots.keys.map { $0.startOfDay }).sorted() } - var keys: [Date] { + func keys(timeSlots: [Date:[Match]]) -> [Date] { timeSlots.keys.sorted() } @@ -54,7 +54,7 @@ struct PlanningView: View { } } - private func _computedTitle() -> String { + private func _computedTitle(days: [Date]) -> String { if let selectedDay { return selectedDay.formatted(.dateTime.day().weekday().month()) } else { @@ -67,143 +67,160 @@ struct PlanningView: View { } var body: some View { - List { - _bySlotView() - } - .navigationTitle(Text(_computedTitle())) - .toolbar(content: { - if days.count > 1 { - ToolbarTitleMenu { - Picker(selection: $selectedDay) { - Text("Tous les jours").tag(nil as Date?) - ForEach(days, id: \.self) { day in - Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + let timeSlots = self.timeSlots + let keys = self.keys(timeSlots: timeSlots) + let days = self.days(timeSlots: timeSlots) + let matches = matches + BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches) + .navigationTitle(Text(_computedTitle(days: days))) + .toolbar(content: { + if days.count > 1 { + ToolbarTitleMenu { + Picker(selection: $selectedDay) { + Text("Tous les jours").tag(nil as Date?) + ForEach(days, id: \.self) { day in + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } + } label: { + Text("Jour") } - } label: { - Text("Jour") + .pickerStyle(.automatic) } - .pickerStyle(.automatic) } - } - ToolbarItemGroup(placement: .topBarTrailing) { - Menu { - Picker(selection: $showFinishedMatches) { - Text("Afficher tous les matchs").tag(true) - Text("Masquer les matchs terminés").tag(false) + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) } label: { - Text("Option de filtrage") + Label("Filtrer", systemImage: "clock.badge.checkmark") + .symbolVariant(showFinishedMatches ? .fill : .none) } - .labelsHidden() - .pickerStyle(.inline) - } label: { - Label("Filtrer", systemImage: "clock.badge.checkmark") - .symbolVariant(showFinishedMatches ? .fill : .none) - } - Menu { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) + Menu { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de triage") } + .labelsHidden() + .pickerStyle(.inline) } label: { - Text("Option de triage") + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant(filterOption == .byCourt ? .fill : .none) } - .labelsHidden() - .pickerStyle(.inline) - } label: { - Label("Trier", systemImage: "line.3.horizontal.decrease.circle") - .symbolVariant(filterOption == .byCourt ? .fill : .none) - } - } - }) - .overlay { - if matches.allSatisfy({ $0.startDate == nil }) { - ContentUnavailableView { - Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") - } description: { - Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi") - } actions: { - RowButtonView("Horaire intelligent") { - selectedScheduleDestination = nil + } + }) + .overlay { + if matches.allSatisfy({ $0.startDate == nil }) { + ContentUnavailableView { + Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") + } description: { + Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi") + } actions: { + RowButtonView("Horaire intelligent") { + selectedScheduleDestination = nil + } } } } - } } - @ViewBuilder - func _bySlotView() -> some View { - if matches.allSatisfy({ $0.startDate == nil }) == false { - ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in - Section { - ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in - if let _matches = timeSlots[key] { - DisclosureGroup { - ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in - NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) - } label: { - LabeledContent { - if let courtName = match.courtName() { - Text(courtName) - } - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle(.title)) - } else if let round = match.roundObject { - Text(round.roundTitle()) + struct BySlotView: View { + @Environment(Tournament.self) var tournament: Tournament + let days: [Date] + let keys: [Date] + let timeSlots: [Date:[Match]] + let matches: [Match] + let selectedDay: Date? + let filterOption: PlanningFilterOption + let showFinishedMatches: Bool + + var body: some View { + List { + if matches.allSatisfy({ $0.startDate == nil }) == false { + ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in + Section { + ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in + if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { + DisclosureGroup { + ForEach(_matches) { match in + NavigationLink { + MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) + } label: { + LabeledContent { + if let courtName = match.courtName() { + Text(courtName) + } + } label: { + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle(.title)) + } else if let round = match.roundObject { + Text(round.roundTitle()) + } + Text(match.matchTitle()) + } } - Text(match.matchTitle()) } + } label: { + _timeSlotView(key: key, matches: _matches) } } - } label: { - _timeSlotView(key: key, matches: _matches) + } + } header: { + HStack { + Text(day.formatted(.dateTime.day().weekday().month())) + Spacer() + let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) + if showFinishedMatches { + Text(self._formattedMatchCount(count)) + } else { + Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") + } } } - } - } header: { - HStack { - Text(day.formatted(.dateTime.day().weekday().month())) - Spacer() - let count = _matchesCount(inDayInt: day.dayInt) - if showFinishedMatches { - Text(self._formattedMatchCount(count)) - } else { - Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") - } + .headerProminence(.increased) } } - .headerProminence(.increased) } } - } - - private func _matchesCount(inDayInt dayInt: Int) -> Int { - timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count - } - private func _timeSlotView(key: Date, matches: [Match]) -> some View { - LabeledContent { - Text(self._formattedMatchCount(matches.count)) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - let names = matches.sorted(by: \.computedOrder) - .compactMap({ $0.roundTitle() }) - .reduce(into: [String]()) { uniqueNames, name in - if !uniqueNames.contains(name) { - uniqueNames.append(name) - } + private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { + timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count + } + + private func _timeSlotView(key: Date, matches: [Match]) -> some View { + LabeledContent { + Text(self._formattedMatchCount(matches.count)) + } label: { + Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + if matches.count <= tournament.courtCount { + let names = matches.sorted(by: \.computedOrder) + .compactMap({ $0.roundTitle() }) + .reduce(into: [String]()) { uniqueNames, name in + if !uniqueNames.contains(name) { + uniqueNames.append(name) + } + } + Text(names.joined(separator: ", ")) + } else { + Text(matches.count.formatted().appending(" matchs")) } - Text(names.joined(separator: ", ")) + } + } + + fileprivate func _formattedMatchCount(_ count: Int) -> String { + return "\(count.formatted()) match\(count.pluralSuffix)" } } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" - } - } //#Preview { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 8b93bda..422d16e 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -16,6 +16,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { case teams(Tournament) case seeds(Tournament) case groupStages(Tournament) + case brackets(Tournament) var id: String { switch self { @@ -25,6 +26,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { return "seed" case .groupStages: return "groupStage" + case .brackets: + return "bracket" } } @@ -36,6 +39,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { return "Têtes de série" case .groupStages: return "Poules" + case .brackets: + return "Tableau" } } @@ -47,6 +52,9 @@ enum CallDestination: Identifiable, Selectable, Equatable { case .seeds(let tournament): let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count + case .brackets(let tournament): + let availableSeeds = tournament.availableSeeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + return availableSeeds.count case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count @@ -65,6 +73,9 @@ enum CallDestination: Identifiable, Selectable, Equatable { case .seeds(let tournament): let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil + case .brackets(let tournament): + let availableSeeds = tournament.availableSeeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + return availableSeeds ? .checkmark : nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil @@ -83,16 +94,23 @@ struct TournamentCallView: View { self.tournament = tournament var destinations = [CallDestination]() let groupStageTeams = tournament.groupStageTeams() + let seededTeams = tournament.seededTeams() if groupStageTeams.isEmpty == false { destinations.append(.groupStages(tournament)) self._selectedDestination = State(wrappedValue: .groupStages(tournament)) } - if tournament.seededTeams().isEmpty == false { + if seededTeams.isEmpty == false { destinations.append(.seeds(tournament)) if groupStageTeams.isEmpty { self._selectedDestination = State(wrappedValue: .seeds(tournament)) } } + if tournament.availableSeeds().isEmpty == false { + destinations.append(.brackets(tournament)) + if seededTeams.isEmpty { + self._selectedDestination = State(wrappedValue: .brackets(tournament)) + } + } destinations.append(.teams(tournament)) self.allDestinations = destinations } @@ -109,6 +127,8 @@ struct TournamentCallView: View { TeamsCallingView(teams: tournament.selectedSortedTeams()) case .groupStages: GroupStageCallingView() + case .brackets: + BracketCallingView(tournament: tournament) case .seeds: SeedsCallingView() } From 93036ecdd5c2eb69440e077e3c6f9f7797a6ba50 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 15 Oct 2024 11:47:54 +0200 Subject: [PATCH 026/106] fix scheduler --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Data/MatchScheduler.swift | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 0635e0e..74d334d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3158,7 +3158,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3182,7 +3182,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3203,7 +3203,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3226,7 +3226,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 5121c1f..21f0936 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -393,7 +393,7 @@ final class MatchScheduler : ModelObject, Storable { print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) } - print("Targeted start date is before the minimum possible end date, returning false.") + print("Targeted start date \(targetedStartDate) is before the minimum possible end date, returning false. \(minimumTargetedEndDate)") return false } } @@ -462,11 +462,20 @@ final class MatchScheduler : ModelObject, Storable { var freeCourtPerRotation = [Int: [Int]]() var courts = initialCourts ?? Array(courtsAvailable) var shouldStartAtDispatcherDate = rotationIndex > 0 - + var suitableDate: Date? + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) - var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + + var rotationStartDate: Date + if previousRotationSlots.isEmpty && rotationIndex > 0 { + let computedSuitableDate = slots.sorted(by: \.computedEndDateForSorting).last?.computedEndDateForSorting + print("Previous rotation was empty, find a suitable rotationStartDate \(suitableDate)") + rotationStartDate = suitableDate ?? computedSuitableDate ?? dispatcherStartDate + } else { + rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + } if shouldStartAtDispatcherDate { rotationStartDate = dispatcherStartDate @@ -539,7 +548,7 @@ final class MatchScheduler : ModelObject, Storable { } // Dispatch courts and schedule matches - dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) + suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) rotationIndex += 1 } @@ -561,7 +570,7 @@ final class MatchScheduler : ModelObject, Storable { return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) } - func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) { + func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { var matchPerRound = [String: Int]() var minimumTargetedEndDate = rotationStartDate @@ -644,6 +653,8 @@ final class MatchScheduler : ModelObject, Storable { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { print("All courts in rotation \(rotationIndex) are free") } + + return minimumTargetedEndDate } @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { @@ -839,6 +850,10 @@ struct TimeMatch { let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) return startDate.addingTimeInterval(minutesToAdd * 60.0) } + + var computedEndDateForSorting: Date { + estimatedEndDate(includeBreakTime: false) + } } struct GroupStageMatchDispatcher { From 138551c32a1262f5ddea9402c7c34dd587acc976 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 15 Oct 2024 22:01:37 +0200 Subject: [PATCH 027/106] fix bracket calling view update --- PadelClub/Views/Calling/BracketCallingView.swift | 7 ++++--- PadelClub/Views/Tournament/Screen/TournamentCallView.swift | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index 2650d8f..bede32f 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -90,19 +90,20 @@ struct BracketCallingView: View { ForEach(filteredRounds()) { round in let seeds = seeds(forRoundIndex: round.index) - let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + let startDate = round.startDate ?? round.playedMatches().first?.startDate + let callSeeds = seeds.filter({ $0.callDate == startDate }) if seeds.isEmpty == false { Section { NavigationLink { _roundView(round: round, seeds: seeds) .environment(tournament) } label: { - CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: round.playedMatches().first?.startDate) + CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: startDate) } } header: { Text(round.roundTitle()) } footer: { - if let startDate = round.startDate ?? round.playedMatches().first?.startDate { + if let startDate { CallView(teams: seeds, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle()) } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 422d16e..57785fe 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -53,7 +53,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count case .brackets(let tournament): - let availableSeeds = tournament.availableSeeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) + let availableSeeds = tournament.availableSeeds().filter({ $0.callDate == nil }) return availableSeeds.count case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) @@ -74,7 +74,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil case .brackets(let tournament): - let availableSeeds = tournament.availableSeeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) + let availableSeeds = tournament.availableSeeds().allSatisfy({ $0.called() }) return availableSeeds ? .checkmark : nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) @@ -95,7 +95,7 @@ struct TournamentCallView: View { var destinations = [CallDestination]() let groupStageTeams = tournament.groupStageTeams() let seededTeams = tournament.seededTeams() - if groupStageTeams.isEmpty == false { + if groupStageTeams.isEmpty == false && tournament.groupStageCount > 0 { destinations.append(.groupStages(tournament)) self._selectedDestination = State(wrappedValue: .groupStages(tournament)) } From 17f371e450e378916d30eba6b3d02b6f3d53af12 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 16 Oct 2024 14:18:43 +0200 Subject: [PATCH 028/106] fix calling stuff and round lag efficiency --- PadelClub/Data/GroupStage.swift | 25 ++++++++----- PadelClub/Data/Match.swift | 21 ++++++++++- PadelClub/Data/Round.swift | 37 ++++++++++++++++--- PadelClub/Data/Tournament.swift | 23 +++++++----- PadelClub/Utils/ContactManager.swift | 10 ++++- .../Views/Calling/BracketCallingView.swift | 36 ++++++++++++++++-- .../CallMessageCustomizationView.swift | 12 +++++- PadelClub/Views/Calling/CallView.swift | 3 +- .../Views/Calling/TeamsCallingView.swift | 12 ++++++ .../GroupStage/GroupStagesSettingsView.swift | 2 +- .../LoserBracketFromGroupStageView.swift | 4 +- PadelClub/Views/Match/MatchSetupView.swift | 2 +- PadelClub/Views/Planning/PlanningView.swift | 2 +- .../Views/Round/LoserRoundSettingsView.swift | 4 +- PadelClub/Views/Round/LoserRoundView.swift | 4 +- PadelClub/Views/Round/RoundSettingsView.swift | 10 +++-- PadelClub/Views/Round/RoundView.swift | 28 +++++++------- .../Screen/TableStructureView.swift | 13 +++++++ .../Screen/TournamentCallView.swift | 6 +-- 19 files changed, 189 insertions(+), 65 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 8f81297..4f07484 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -115,17 +115,24 @@ final class GroupStage: ModelObject, Storable { return match } - func buildMatches() { - _removeMatches() - - var matches = [Match]() + func buildMatches(keepExistingMatches: Bool = false) { var teamScores = [TeamScore]() + var matches = [Match]() - for i in 0..<_numberOfMatchesToBuild() { - let newMatch = self._createMatch(index: i) -// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) - teamScores.append(contentsOf: newMatch.createTeamScores()) - matches.append(newMatch) + if keepExistingMatches == false { + _removeMatches() + + for i in 0..<_numberOfMatchesToBuild() { + let newMatch = self._createMatch(index: i) + // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) + teamScores.append(contentsOf: newMatch.createTeamScores()) + matches.append(newMatch) + } + } else { + for match in _matches() { + match.resetTeamScores(outsideOf: []) + teamScores.append(contentsOf: match.createTeamScores()) + } } do { diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 95e08fb..7c4eb85 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -63,6 +63,20 @@ final class Match: ModelObject, Storable, Equatable { // self.order = order } + func setMatchName(_ serverName: String?) { + self.name = serverName + } + + func isFromLastRound() -> Bool { + guard let roundObject, roundObject.parent == nil else { return false } + guard let currentTournament = currentTournament() else { return false } + if currentTournament.rounds().count - 1 == roundObject.index { + return true + } else { + return false + } + } + var tournamentStore: TournamentStore { if let store = self.store as? TournamentStore { return store @@ -380,16 +394,17 @@ defer { func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } + let currentState = disabled disabled = state - if disabled { + if disabled != currentState { do { try self.tournamentStore.teamScores.delete(contentOfs: teamScores) } catch { Logger.error(error) } } - if state == true { + if state == true, state != currentState { let teams = teams() for team in teams { if isSeededBy(team: team) { @@ -403,6 +418,8 @@ defer { } } //byeState = false + roundObject?._cachedSeedInterval = nil + name = nil do { try self.tournamentStore.matches.addOrUpdate(instance: self) } catch { diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index d7cf727..0604917 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -24,7 +24,8 @@ final class Round: ModelObject, Storable { var startDate: Date? var groupStageLoserBracket: Bool = false var loserBracketMode: LoserBracketMode = .automatic - + var _cachedSeedInterval: SeedInterval? + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { self.tournament = tournament self.index = index @@ -451,6 +452,8 @@ defer { func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { + if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) } + #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -469,8 +472,16 @@ defer { // && $0.bracketPosition != nil // && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex // }) + + var seedsCount = seedsAfterThisRound.count + if seedsAfterThisRound.isEmpty { + let nextRoundsDisableMatches = nextRoundsDisableMatches() + seedsCount = disabledMatches().count - nextRoundsDisableMatches + } let playedMatches = playedMatches() - let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount) + + _cachedSeedInterval = seedInterval return seedInterval.localizedLabel(displayStyle) } @@ -492,6 +503,8 @@ defer { } func seedInterval(initialMode: Bool = false) -> SeedInterval? { + if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval } + #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -515,11 +528,17 @@ defer { && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex } - let playedMatches = playedMatches().count - let minimumMatches = playedMatches * 2 + var seedsCount = seedsAfterThisRound.count + if seedsAfterThisRound.isEmpty { + let nextRoundsDisableMatches = nextRoundsDisableMatches() + seedsCount = disabledMatches().count - nextRoundsDisableMatches + } + + let playedMatches = playedMatches() //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) - let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount) //print(seedInterval.localizedLabel()) + _cachedSeedInterval = seedInterval return seedInterval } @@ -659,6 +678,14 @@ defer { guard let parent = parent else { return nil } return self.tournamentStore.rounds.findById(parent) } + + func nextRoundsDisableMatches() -> Int { + if parent == nil, index > 0 { + return tournamentObject()?.rounds().suffix(index).flatMap { $0.disabledMatches() }.count ?? 0 + } else { + return 0 + } + } func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) { if updatedMatchFormat.weight < self.matchFormat.weight { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 8b78f8a..285a7b9 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1112,9 +1112,10 @@ defer { return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count } - func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool { guard let summonDate = team.callDate else { return true } - guard let expectedSummonDate = team.expectedSummonDate() else { return true } + let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate + guard let expectedSummonDate else { return true } return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } @@ -1609,7 +1610,9 @@ defer { func callStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } - let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) convoquées au bon horaire" + let justCalled = selectedSortedTeams.filter { $0.called() } + + let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)" let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) @@ -1852,7 +1855,7 @@ defer { } } - func refreshGroupStages() { + func refreshGroupStages(keepExistingMatches: Bool = false) { unsortedTeams().forEach { team in team.groupStage = nil team.groupStagePosition = nil @@ -1861,16 +1864,16 @@ defer { if groupStageCount > 0 { switch groupStageOrderingMode { case .random: - setGroupStage(randomize: true) + setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches) case .snake: - setGroupStage(randomize: false) + setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches) case .swiss: - setGroupStage(randomize: true) + setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches) } } } - func setGroupStage(randomize: Bool) { + func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) { let groupStages = groupStages() let numberOfBracketsAsInt = groupStages.count // let teamsPerBracket = teamsPerBracket @@ -1879,7 +1882,7 @@ defer { buildGroupStages() } else { setGroupStageTeams(randomize: randomize) - groupStages.forEach { $0.buildMatches() } + groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) } } } @@ -2195,7 +2198,7 @@ defer { 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" + match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place") do { try tournamentStore.matches.addOrUpdate(instance: match) } catch { diff --git a/PadelClub/Utils/ContactManager.swift b/PadelClub/Utils/ContactManager.swift index 5c9d793..8c620a7 100644 --- a/PadelClub/Utils/ContactManager.swift +++ b/PadelClub/Utils/ContactManager.swift @@ -117,8 +117,16 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c (DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil } + var linkMessage: String? { + if let tournament, tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString { + return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink) + } else { + return nil + } + } + var computedMessage: String { - [entryFeeMessage, message].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") + [entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") } let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index bede32f..0ba3512 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -86,19 +86,21 @@ struct BracketCallingView: View { } label: { Text("Têtes de série") } + } footer: { + Text("Permet de convoquer par tour du tableau sans avoir tirer au sort les tétes de série. Vous pourrez ensuite confirmer leur horaire plus précis si le tour se joue sur plusieurs rotations. Les équipes ne peuvent pas être considéré comme convoqué au bon horaire en dehors de cet écran tant qu'elles n'ont pas été placé dans le tableau.") } ForEach(filteredRounds()) { round in let seeds = seeds(forRoundIndex: round.index) let startDate = round.startDate ?? round.playedMatches().first?.startDate - let callSeeds = seeds.filter({ $0.callDate == startDate }) + let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false }) if seeds.isEmpty == false { Section { NavigationLink { _roundView(round: round, seeds: seeds) .environment(tournament) } label: { - CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: startDate) + CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: startDate, title: "convoquées") } } header: { Text(round.roundTitle()) @@ -111,7 +113,7 @@ struct BracketCallingView: View { } } .headerProminence(.increased) - .navigationTitle("Prévision") + .navigationTitle("Pré-convocation") } @ViewBuilder @@ -120,12 +122,38 @@ struct BracketCallingView: View { NavigationLink("Équipes non contactées") { TeamsCallingView(teams: seeds.filter({ $0.callDate == nil })) } + + let startDate = round.startDate ?? round.playedMatches().first?.startDate + let badCalled = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) }) + + if badCalled.isEmpty == false { + Section { + ForEach(badCalled) { team in + CallView.TeamView(team: team) + } + } header: { + HStack { + Text("Mauvais horaire") + Spacer() + Text(badCalled.count.formatted() + " équipe\(badCalled.count.pluralSuffix)") + } + } footer: { + if let startDate { + CallView(teams: badCalled, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle()) + } + } + } + Section { ForEach(seeds) { team in CallView.TeamView(team: team) } } header: { - Text(round.roundTitle()) + HStack { + Text(round.roundTitle()) + Spacer() + Text(seeds.count.formatted() + " équipe\(seeds.count.pluralSuffix)") + } } } .overlay { diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 569f420..0ed9a54 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -43,7 +43,15 @@ struct CallMessageCustomizationView: View { } var computedMessage: String { - [entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmedMultiline }.joined(separator: "\n") + var linkMessage: String? { + if tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString { + return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink) + } else { + return nil + } + } + + return [entryFeeMessage, customCallMessageBody, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n") } var finalMessage: String? { @@ -259,7 +267,7 @@ struct CallMessageCustomizationView: View { } }.italic().foregroundStyle(.gray) } header: { - Text("Rendu généré automatiquement") + Text("Exemple généré automatiquement") } } diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 58f953c..0367f2e 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -14,6 +14,7 @@ struct CallView: View { let count: Int let total: Int let startDate: Date? + var title: String = "convoquées au bon horaire" var body: some View { VStack(spacing: 0) { @@ -32,7 +33,7 @@ struct CallView: View { Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) } Spacer() - Text("convoquées au bon horaire") + Text(title) } .font(.caption) .foregroundColor(.secondary) diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 832daae..9b7b06a 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -16,6 +16,13 @@ struct TeamsCallingView: View { List { PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) + let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } + let justCalled = teams.filter { $0.called() } + + let label = "\(justCalled.count.formatted()) / \(teams.count.formatted()) convoquées (dont \(called.count.formatted()) au bon horaire)" + + Text(label) + Section { ForEach(teams) { team in Menu { @@ -34,6 +41,11 @@ struct TeamsCallingView: View { .buttonStyle(.plain) .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) } + } footer: { + HStack { + Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur ") + Image(systemName: "ellipsis.circle").font(.footnote) + } } } .headerProminence(.increased) diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index ab40d64..8f31945 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -248,7 +248,7 @@ struct GroupStagesSettingsView: View { func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View { RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) { tournament.groupStageOrderingMode = mode - tournament.refreshGroupStages() + tournament.refreshGroupStages(keepExistingMatches: true) generationDone = true tournament.shouldVerifyGroupStage = false _save() diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 15f41b1..aeb532a 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -107,7 +107,7 @@ struct LoserBracketFromGroupStageView: View { let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace() let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2) let match = Match(round: loserBracket.id, index: placeCount, matchFormat: loserBracket.matchFormat) - match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" + match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix()) place") do { try tournamentStore.matches.addOrUpdate(instance: match) } catch { @@ -202,7 +202,7 @@ struct GroupStageLoserBracketMatchFooterView: View { match.index = newIndexValidated - match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place" + match.setMatchName("\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place") do { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index ac777a6..b804de0 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -166,7 +166,7 @@ struct MatchSetupView: View { Text("Libérer") .underline() } - } else { + } else if match.isFromLastRound() == false { ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) do { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f677510..03ae980 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -210,7 +210,7 @@ struct PlanningView: View { uniqueNames.append(name) } } - Text(names.joined(separator: ", ")) + Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) } else { Text(matches.count.formatted().appending(" matchs")) } diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 3b4706a..eb0c827 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -62,7 +62,7 @@ struct LoserRoundSettingsView: View { RowButtonView("Synchroniser les noms des matchs") { let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches }) - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) do { try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { @@ -82,7 +82,7 @@ struct LoserRoundSettingsView: View { RowButtonView("Créer les matchs de classements", role: .destructive) { upperBracketRound.round.buildLoserBracket() upperBracketRound.round.disabledMatches().forEach { match in - match.disableMatch() + match._toggleLoserMatchDisableState(true) } do { try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index 1707e75..843765d 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -129,8 +129,8 @@ struct LoserRoundView: View { private func _refreshNames() { DispatchQueue.global(qos: .background).async { - let allRoundMatches = loserBracket.allMatches - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + let allRoundMatches = loserBracket.allMatches.filter({ $0.name == nil }) + allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) do { try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 6efa24b..53a16ab 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -111,11 +111,11 @@ struct RoundSettingsView: View { //index du match courant pair = position basse du prochain match match.disabled = true } else { - match.name = Match.setServerTitle(upperRound: round, matchIndex: currentIndex) + match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex)) currentIndex += 1 } } else { - match.name = Match.setServerTitle(upperRound: round, matchIndex: currentIndex) + match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex)) currentIndex += 1 } @@ -157,14 +157,16 @@ struct RoundSettingsView: View { Section { RowButtonView("Synchroniser les noms des matchs") { - let allRoundMatches = tournament.allRoundMatches() - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + let allRoundMatches = tournament.allLoserRoundMatches() + allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) do { try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { Logger.error(error) } } + } header: { + Text("Matchs de classement") } } .toolbar { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 3b31959..5ed57c3 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -80,7 +80,7 @@ struct RoundView: View { LabeledContent { Text(leftToPlay.formatted()) } label: { - Text("Match\(leftToPlay.pluralSuffix) à jouer \(upperRound.title)") + Text("Match\(leftToPlay.pluralSuffix) à jouer en \(upperRound.title)") } } footer: { Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement") @@ -332,12 +332,12 @@ struct RoundView: View { } private func _save() { - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - } catch { - Logger.error(error) - } - +// do { +// try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) +// } catch { +// Logger.error(error) +// } +// if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } @@ -349,22 +349,22 @@ struct RoundView: View { DispatchQueue.global(qos: .background).async { //todo should be done server side let rounds = tournament.rounds() + var matchesToUpdate: [Match] = [Match]() rounds.forEach { round in - let matches = round.playedMatches() + let matches = round.playedMatches().filter({ $0.name == nil }) matches.forEach { match in - match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) + match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches))) } + matchesToUpdate.append(contentsOf: matches) } - let loserMatches = self.upperRound.loserMatches() + let loserMatches = self.upperRound.loserMatches().filter({ $0.name == nil }) loserMatches.forEach { match in - match.name = match.roundTitle() + match.setMatchName(match.roundTitle()) } - let allRoundMatches = tournament.allRoundMatches() - do { - try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + try tournament.tournamentStore.matches.addOrUpdate(contentOfs: matchesToUpdate + loserMatches) } catch { Logger.error(error) } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index ff126af..9571c44 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -369,6 +369,19 @@ struct TableStructureView: View { groupStage.updateGroupStageState() } } + + if groupStageCount == 0 { + let teams = tournament.unsortedTeams().filter({ $0.inGroupStage() }) + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil + } + do { + try tournament.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + } catch { + Logger.error(error) + } + } do { try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 57785fe..fd0db9d 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -53,8 +53,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count case .brackets(let tournament): - let availableSeeds = tournament.availableSeeds().filter({ $0.callDate == nil }) - return availableSeeds.count + return nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count @@ -74,8 +73,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil case .brackets(let tournament): - let availableSeeds = tournament.availableSeeds().allSatisfy({ $0.called() }) - return availableSeeds ? .checkmark : nil + return nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil From 7c3725ce5bcd9b284cfca8c4cd1a1b5223704ba7 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 16 Oct 2024 19:20:11 +0200 Subject: [PATCH 029/106] fix stuff --- PadelClub/Data/Match.swift | 18 ++- PadelClub/Data/TeamRegistration.swift | 8 ++ .../Calling/Components/MenuWarningView.swift | 2 +- PadelClub/Views/Cashier/CashierView.swift | 8 +- .../Components/GroupStageTeamView.swift | 2 +- .../Views/GroupStage/GroupStageView.swift | 19 ++- .../GroupStageTeamReplacementView.swift | 2 +- .../Match/Components/PlayerBlockView.swift | 20 ++-- .../Views/Round/LoserRoundSettingsView.swift | 21 ++-- PadelClub/Views/Round/RoundSettingsView.swift | 112 ++++++++++-------- .../Team/Components/TeamHeaderView.swift | 4 +- PadelClub/Views/Team/TeamRowView.swift | 8 +- .../Views/Tournament/FileImportView.swift | 7 +- .../Screen/TournamentRankView.swift | 4 +- 14 files changed, 133 insertions(+), 102 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7c4eb85..2caee20 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -347,15 +347,21 @@ defer { guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } guard let next = _otherMatch() else { return } if next.disabled && byeState == false && next.byeState == false { - forwardMatch.byeState = false - forwardMatch._toggleMatchDisableState(state, forward: true) + if forwardMatch.disabled != state || forwardMatch.byeState { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(state, forward: true) + } } else if byeState && next.byeState { print("don't disable forward match") - forwardMatch.byeState = false - forwardMatch._toggleMatchDisableState(false, forward: true) + if forwardMatch.byeState || forwardMatch.disabled { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(false, forward: true) + } } else { - forwardMatch.byeState = true - forwardMatch._toggleMatchDisableState(state, forward: true) + if forwardMatch.byeState == false || forwardMatch.disabled != state { + forwardMatch.byeState = true + forwardMatch._toggleMatchDisableState(state, forward: true) + } } // if next.disabled == false { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index b85fceb..1b46a68 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -536,6 +536,14 @@ final class TeamRegistration: ModelObject, Storable { matches().sorted(by: \.computedEndDateForSorting).last?.endDate } + func teamNameLabel() -> String { + if let name, name.isEmpty == false { + return name + } else { + return "Toute l'équipe" + } + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index ef094a0..d770ab1 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -124,7 +124,7 @@ struct MenuWarningView: View { @ViewBuilder func _teamActionView(_ team: TeamRegistration) -> some View { - Menu(team.name ?? "Toute l'équipe") { + Menu(team.teamNameLabel()) { let players = team.players() _actionView(players: players) } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index b022259..34cf916 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -377,8 +377,8 @@ struct CashierView: View { } } header: { HStack { - if let name = team.name { - Text(name) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName) } if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { Spacer() @@ -418,8 +418,8 @@ struct CashierView: View { EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { - if let name = team.name { - Text(name) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName) } if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index 1d4b467..d839870 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -48,7 +48,7 @@ struct GroupStageTeamView: View { var body: some View { List { Section { - if let name = team.name { + if let name = team.name, name.isEmpty == false { Text(name).foregroundStyle(.secondary) } ForEach(team.players()) { player in diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 292347b..63abc49 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -142,17 +142,16 @@ struct GroupStageView: View { .font(.footnote) HStack { VStack(alignment: .leading) { - if let teamName = team.name { - Text(teamName).font(.title3) - } else { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1) - .overlay { - if player.hasArrived && team.isHere() == false { - Color.green.opacity(0.6) - } + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .overlay { + if player.hasArrived && team.isHere() == false { + Color.green.opacity(0.6) } - } + } } } Spacer() diff --git a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift index cf613a4..dbc5673 100644 --- a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift +++ b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift @@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View { Section { Picker(selection: $selectedPlayer) { HStack { - Text(team.name ?? "Toute l'équipe") + Text(team.teamNameLabel()) Spacer() Text(team.weight.formatted()).bold() } diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 864a734..11c7ee8 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -58,22 +58,20 @@ struct PlayerBlockView: View { Text("Repêchée").italic().font(.caption) } - if let name = team?.name { - Text(name).font(.title3) - } else { - ForEach(names, id: \.self) { name in - Text(name).lineLimit(1) - } + if let teamName = team?.name { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } + ForEach(names, id: \.self) { name in + Text(name).lineLimit(1) } } else { ZStack(alignment: .leading) { VStack { - if let name = team?.name { - Text(name).font(.title3) - } else { - Text("longLabelPlayerOne").lineLimit(1) - Text("longLabelPlayerTwo").lineLimit(1) + if let teamName = team?.name { + Text(teamName).foregroundStyle(.secondary).font(.footnote) } + Text("longLabelPlayerOne").lineLimit(1) + Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index eb0c827..f39455f 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -80,15 +80,7 @@ struct LoserRoundSettingsView: View { Section { RowButtonView("Créer les matchs de classements", role: .destructive) { - upperBracketRound.round.buildLoserBracket() - upperBracketRound.round.disabledMatches().forEach { match in - match._toggleLoserMatchDisableState(true) - } - do { - try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) - } catch { - Logger.error(error) - } + await _addLoserBracketMatches() } } .disabled(upperBracketRound.round.loserRounds().isEmpty == false) @@ -147,6 +139,17 @@ struct LoserRoundSettingsView: View { Text(" Modifier quand même ?").foregroundStyle(.red) } + private func _addLoserBracketMatches() async { + upperBracketRound.round.buildLoserBracket() + upperBracketRound.round.disabledMatches().forEach { match in + match._toggleLoserMatchDisableState(true) + } + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) + } catch { + Logger.error(error) + } + } } //#Preview { diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 53a16ab..3dcd33e 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -93,64 +93,14 @@ struct RoundSettingsView: View { Section { let roundIndex = tournament.rounds().count RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex), role: .destructive) { - let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. = Set() @State private var chunkByParameter: Bool = true + enum ChunkMode { + case byParameter + case byCoupleOfLines + } + init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) { _fileProvider = .init(wrappedValue: defaultFileProvider) } @@ -558,7 +563,7 @@ struct FileImportView: View { Section { HStack { VStack(alignment: .leading) { - if let teamName = team.name { + if let teamName = team.name, teamName.isEmpty == false { Text(teamName).foregroundStyle(.secondary) } ForEach(team.players.sorted(by: \.computedRank)) { diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index ba33832..f53425f 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -233,8 +233,8 @@ struct TournamentRankView: View { Divider() VStack(alignment: .leading) { - if let name = team.name { - Text(name).foregroundStyle(.secondary) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary) } ForEach(team.players()) { player in From f174ccfe57394d0f043d6c40517af2694632bf8c Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 16 Oct 2024 21:02:08 +0200 Subject: [PATCH 030/106] fix calling views --- PadelClub.xcodeproj/project.pbxproj | 8 +++ .../Views/Calling/BracketCallingView.swift | 24 +++++--- .../Views/Calling/GroupStageCallingView.swift | 13 +++-- .../Views/Calling/SeedsCallingView.swift | 23 +++++--- .../Views/Calling/TeamsCallingView.swift | 45 ++++++++------- .../Components/MatchTeamDetailView.swift | 6 ++ PadelClub/Views/Team/CoachListView.swift | 56 +++++++++++++++++++ PadelClub/Views/Team/EditingTeamView.swift | 4 ++ .../Views/Tournament/FileImportView.swift | 38 +++++++++---- 9 files changed, 167 insertions(+), 50 deletions(-) create mode 100644 PadelClub/Views/Team/CoachListView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 74d334d..10b7a3f 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -90,6 +90,9 @@ FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; + FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; + FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -989,6 +992,7 @@ FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = ""; }; FF17CA522CBE4788003C7323 /* BracketCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCallingView.swift; sourceTree = ""; }; + FF17CA562CC02FEA003C7323 /* CoachListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachListView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1815,6 +1819,7 @@ FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */, + FF17CA562CC02FEA003C7323 /* CoachListView.swift */, FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; @@ -2412,6 +2417,7 @@ C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */, FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */, @@ -2684,6 +2690,7 @@ FF4CBFF02C996C0600151637 /* URLs.swift in Sources */, FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */, FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */, FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */, @@ -2935,6 +2942,7 @@ FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */, FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */, FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */, FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */, diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index 0ba3512..9aa7ec9 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -60,11 +60,14 @@ struct BracketCallingView: View { var body: some View { List { - NavigationLink { - TeamsCallingView(teams: teams.filter({ $0.callDate == nil })) - .environment(tournament) - } label: { - LabeledContent("Équipes non contactées", value: teams.filter({ $0.callDate == nil }).count.formatted()) + let uncalledTeams = teams.filter({ $0.callDate == nil }) + if uncalledTeams.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledTeams) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted()) + } } PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) @@ -119,8 +122,15 @@ struct BracketCallingView: View { @ViewBuilder private func _roundView(round: Round, seeds: [TeamRegistration]) -> some View { List { - NavigationLink("Équipes non contactées") { - TeamsCallingView(teams: seeds.filter({ $0.callDate == nil })) + + let uncalledTeams = seeds.filter({ $0.callDate == nil }) + if uncalledTeams.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledTeams) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted()) + } } let startDate = round.startDate ?? round.playedMatches().first?.startDate diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index 357df0f..6a419e1 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -15,11 +15,14 @@ struct GroupStageCallingView: View { let groupStages = tournament.groupStages() List { - NavigationLink { - TeamsCallingView(teams: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })) - .environment(tournament) - } label: { - LabeledContent("Équipes non contactées", value: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }).count.formatted()) + let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }) + if uncalled.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalled) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalled.count.pluralSuffix) non contactée\(uncalled.count.pluralSuffix)", value: uncalled.count.formatted()) + } } PlayersWithoutContactView(players: groupStages.flatMap({ $0.unsortedTeams() }).flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index abfcec7..452bd9d 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -14,12 +14,15 @@ struct SeedsCallingView: View { var body: some View { List { let tournamentRounds = tournament.rounds() + let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil }) - NavigationLink { - TeamsCallingView(teams: tournament.seededTeams().filter({ $0.callDate == nil })) - .environment(tournament) - } label: { - LabeledContent("Équipes non contactées", value: tournament.seededTeams().filter({ $0.callDate == nil }).count.formatted()) + if uncalledSeeds.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledSeeds) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledSeeds.count.pluralSuffix) non contactée\(uncalledSeeds.count.pluralSuffix)", value: uncalledSeeds.count.formatted()) + } } PlayersWithoutContactView(players: tournament.seededTeams().flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) @@ -63,8 +66,14 @@ struct SeedsCallingView: View { } } - NavigationLink("Équipes non contactées") { - TeamsCallingView(teams: round.teams().filter({ $0.callDate == nil })) + let uncalledTeams = round.teams().filter({ $0.callDate == nil }) + if uncalledTeams.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledTeams) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted()) + } } diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 9b7b06a..2c9117a 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -21,31 +21,34 @@ struct TeamsCallingView: View { let label = "\(justCalled.count.formatted()) / \(teams.count.formatted()) convoquées (dont \(called.count.formatted()) au bon horaire)" - Text(label) - - Section { - ForEach(teams) { team in - Menu { - _menuOptions(team: team) - } label: { - HStack { - TeamRowView(team: team, displayCallDate: true) - Spacer() - Menu { - _menuOptions(team: team) - } label: { - LabelOptions().labelStyle(.iconOnly) + if teams.isEmpty == false { + Section { + Text(label) + } footer: { + Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur 􀍡") + } + + Section { + ForEach(teams) { team in + Menu { + _menuOptions(team: team) + } label: { + HStack { + TeamRowView(team: team, displayCallDate: true) + Spacer() + Menu { + _menuOptions(team: team) + } label: { + LabelOptions().labelStyle(.iconOnly) + } } } + .buttonStyle(.plain) + .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) } - .buttonStyle(.plain) - .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) - } - } footer: { - HStack { - Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur ") - Image(systemName: "ellipsis.circle").font(.footnote) } + } else { + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash") } } .headerProminence(.increased) diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index cf952d0..f12eb91 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -31,9 +31,15 @@ struct MatchTeamDetailView: View { @ViewBuilder private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } ForEach(team.players()) { player in EditablePlayerView(player: player, editingOptions: _editingOptions()) } + if let coachList = team.comment, coachList.isEmpty == false { + Text("Coachs : " + coachList).foregroundStyle(.secondary).font(.footnote) + } } header: { TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) } diff --git a/PadelClub/Views/Team/CoachListView.swift b/PadelClub/Views/Team/CoachListView.swift new file mode 100644 index 0000000..0a4ca87 --- /dev/null +++ b/PadelClub/Views/Team/CoachListView.swift @@ -0,0 +1,56 @@ +// +// CoachListView.swift +// PadelClub +// +// Created by razmig on 16/10/2024. +// + +import SwiftUI +import LeStorage + +struct CoachListView: View { + @Environment(Tournament.self) var tournament + @State private var coachNames: String + var team: TeamRegistration + + init(team: TeamRegistration) { + self.team = team + _coachNames = .init(wrappedValue: team.comment ?? "") + } + + var body: some View { + Section { + TextField("Coach", text: $coachNames) + .autocorrectionDisabled() + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = coachNames.trimmed + if trimmed.isEmpty { + team.comment = nil + } else { + team.comment = trimmed + } + _save() + } + } header: { + Text("Coachs") + } footer: { + FooterButtonView("effacer") { + coachNames = "" + team.comment = nil + _save() + } + } + } + + private func _save() { + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + } + +} diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 173d693..9949e67 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -164,6 +164,10 @@ struct EditingTeamView: View { Text("Nom de l'équipe") } + if tournament.tournamentLevel.coachingIsAuthorized { + CoachListView(team: team) + } + Section { RowButtonView("Retirer des poules", role: .destructive) { team.resetGroupeStagePosition() diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 7e236d0..a25219b 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -84,11 +84,21 @@ struct FileImportView: View { @State private var multiImport: Bool = false @State private var presentFormatHelperView: Bool = false @State private var validatedTournamentIds: Set = Set() - @State private var chunkByParameter: Bool = true + @State private var chunkMode: ChunkMode = .byParameter - enum ChunkMode { + enum ChunkMode: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } case byParameter case byCoupleOfLines + + func localizedChunkModeLabel() -> String { + switch self { + case .byParameter: + return "Nom d'équipe" + case .byCoupleOfLines: + return "Groupe de 2 lignes" + } + } } init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) { @@ -99,6 +109,10 @@ struct FileImportView: View { return self.tournament.tournamentStore } + var chunkByParameter: Bool { + return chunkMode == .byParameter + } + private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] { if tournament.isAnimation() { return teams.sorted(by: \.weight) @@ -146,14 +160,7 @@ struct FileImportView: View { } if fileProvider == .custom || fileProvider == .customAutoSearch { - Toggle(isOn: $chunkByParameter) { - Text("Détection des équipes") - if chunkByParameter { - Text("via le nom de l'équipe") - } else { - Text("couple de deux lignes") - } - } + _chunkModePickerView() } RowButtonView("Démarrer l'importation") { @@ -595,6 +602,17 @@ struct FileImportView: View { Logger.error(error) } } + + private func _chunkModePickerView() -> some View { + Picker(selection: $chunkMode) { + ForEach(ChunkMode.allCases) { mode in + Text(mode.localizedChunkModeLabel()).tag(mode) + } + } label: { + Text("Détection des équipes") + } + } + } //#Preview { From 7811806925e8400ab8c6a32f195839d2070b17cc Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 17 Oct 2024 10:10:07 +0200 Subject: [PATCH 031/106] Make server purchase active --- PadelClub/Data/DataStore.swift | 4 ++++ PadelClub/Views/Tournament/Subscription/Guard.swift | 11 ++++++++++- PadelClub/Views/User/AccountView.swift | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 2a2052e..4ebdaad 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -101,6 +101,10 @@ class DataStore: ObservableObject { } + deinit { + NotificationCenter.default.removeObserver(self) + } + func saveUser() { do { if user.username.count > 0 { diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index 64cbdac..e2a03ec 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -19,7 +19,6 @@ import LeStorage var currentBestPurchase: Purchase? = nil var updateListenerTask: Task? = nil - override init() { @@ -34,10 +33,20 @@ import LeStorage Logger.error(error) } } + + NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) + } deinit { self.updateListenerTask?.cancel() + NotificationCenter.default.removeObserver(self) + } + + @objc func collectionDidLoad(notification: Notification) { + if let _ = notification.object as? StoredCollection { + self._updateBestPlan() + } } func productIds() async -> [String] { diff --git a/PadelClub/Views/User/AccountView.swift b/PadelClub/Views/User/AccountView.swift index a45da3e..340e162 100644 --- a/PadelClub/Views/User/AccountView.swift +++ b/PadelClub/Views/User/AccountView.swift @@ -15,6 +15,11 @@ struct AccountView: View { var body: some View { Form { + #if DEBUG + if let purchase = Guard.main.currentBestPurchase, let item = StoreItem(rawValue: purchase.productId) { + PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item)) + } + #endif Section { NavigationLink("Changer de mot de passe") { ChangePasswordView() From 87b46d7ccb9321705d3347513269b4215d521676 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 17 Oct 2024 11:05:15 +0200 Subject: [PATCH 032/106] fix issue with team selection enhance visibility in score editing add the seed 3/4 specific management in 1/8e add new fields limit all text input to suit the server side rules --- PadelClub/Data/PlayerRegistration.swift | 28 ++++---- PadelClub/Data/Tournament.swift | 65 +++++++++++++----- .../Views/Calling/BracketCallingView.swift | 31 +++++++-- .../Views/Calling/TeamsCallingView.swift | 2 +- .../Cashier/Event/EventSettingsView.swift | 2 +- PadelClub/Views/Club/CourtView.swift | 6 +- .../Components/GroupStageSettingsView.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 6 +- PadelClub/Views/Player/PlayerDetailView.swift | 16 ++--- PadelClub/Views/Round/RoundView.swift | 67 +++++++++++++------ PadelClub/Views/Score/EditScoreView.swift | 12 ++-- PadelClub/Views/Team/CoachListView.swift | 2 +- PadelClub/Views/Team/EditingTeamView.swift | 2 +- .../TournamentGeneralSettingsView.swift | 3 +- .../Screen/TournamentCallView.swift | 2 +- PadelClubTests/ServerDataTests.swift | 4 +- 16 files changed, 170 insertions(+), 80 deletions(-) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 4ac2e11..f894a4f 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -62,29 +62,29 @@ final class PlayerRegistration: ModelObject, Storable { internal init(importedPlayer: ImportedPlayer) { self.teamRegistration = "" - self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized - self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased() - self.licenceId = importedPlayer.license ?? nil + self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized + self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased() + self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil self.rank = Int(importedPlayer.rank) self.sex = importedPlayer.male ? .male : .female self.tournamentPlayed = importedPlayer.tournamentPlayed self.points = importedPlayer.getPoints() - self.clubName = importedPlayer.clubName - self.ligueName = importedPlayer.ligueName - self.assimilation = importedPlayer.assimilation + self.clubName = importedPlayer.clubName?.prefixTrimmed(200) + self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) + self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) self.source = .frenchFederation - self.birthdate = importedPlayer.birthYear + self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50) } internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { let _lastName = federalData[0].trimmed.uppercased() let _firstName = federalData[1].trimmed.capitalized if _lastName.isEmpty && _firstName.isEmpty { return nil } - lastName = _lastName - firstName = _firstName - birthdate = federalData[2].formattedAsBirthdate() - licenceId = federalData[3] - clubName = federalData[4] + lastName = _lastName.prefixTrimmed(50) + firstName = _firstName.prefixTrimmed(50) + birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50) + licenceId = federalData[3].prefixTrimmed(50) + clubName = federalData[4].prefixTrimmed(200) let stringRank = federalData[5] if stringRank.isEmpty { rank = nil @@ -93,11 +93,11 @@ final class PlayerRegistration: ModelObject, Storable { } let _email = federalData[6] if _email.isEmpty == false { - self.email = _email + self.email = _email.prefixTrimmed(50) } let _phoneNumber = federalData[7] if _phoneNumber.isEmpty == false { - self.phoneNumber = _phoneNumber + self.phoneNumber = _phoneNumber.prefixTrimmed(50) } source = .beachPadel diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 285a7b9..7d66f8a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -58,6 +58,8 @@ final class Tournament : ModelObject, Storable { var hidePointsEarned: Bool = false var publishRankings: Bool = false var loserBracketMode: LoserBracketMode = .automatic + var initialSeedRound: Int = 0 + var initialSeedCount: Int = 0 @ObservationIgnored var navigationPath: [Screen] = [] @@ -107,9 +109,12 @@ final class Tournament : ModelObject, Storable { case _hidePointsEarned = "hidePointsEarned" case _publishRankings = "publishRankings" case _loserBracketMode = "loserBracketMode" + case _initialSeedRound = "initialSeedRound" + case _initialSeedCount = "initialSeedCount" + } - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, 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) { + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, 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) { self.event = event self.name = name self.startDate = startDate @@ -148,6 +153,9 @@ final class Tournament : ModelObject, Storable { self.hidePointsEarned = hidePointsEarned self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode + self.initialSeedRound = initialSeedRound + self.initialSeedCount = initialSeedCount + } required init(from decoder: Decoder) throws { @@ -193,6 +201,8 @@ final class Tournament : ModelObject, Storable { hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0 + initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0 } fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() @@ -279,6 +289,8 @@ final class Tournament : ModelObject, Storable { try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(publishRankings, forKey: ._publishRankings) try container.encode(loserBracketMode, forKey: ._loserBracketMode) + try container.encode(initialSeedRound, forKey: ._initialSeedRound) + try container.encode(initialSeedCount, forKey: ._initialSeedCount) } fileprivate func _encodePayment(container: inout KeyedEncodingContainer) throws { @@ -673,11 +685,15 @@ defer { if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() { if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup } - let availableSeeds = seeds(inSeedGroup: availableSeedGroup) let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) + if availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { + print("availableSeedGroup == SeedInterval(first: 3, last: 4)") + return availableSeedGroup + } + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup } else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count { @@ -711,22 +727,35 @@ defer { let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeeds = seeds(inSeedGroup: seedGroup) - if availableSeeds.count <= availableSeedSpot.count { - let spots = availableSeedSpot.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) - } - } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { - - let spots = availableSeedOpponentSpot.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) + if seedGroup == SeedInterval(first: 3, last: 4) { + print("availableSeedGroup == SeedInterval(first: 3, last: 4)") + if availableSeedSpot.count == 6 { + var spots = [Match]() + spots.append(availableSeedSpot[1]) + spots.append(availableSeedSpot[4]) + spots = spots.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) + } } - } else if let chunks = seedGroup.chunks() { - if let chunk = chunks.first(where: { seedInterval in - seedInterval.first >= self.seededTeams().count - }) { - setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } else { + if availableSeeds.count <= availableSeedSpot.count { + let spots = availableSeedSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) + } + } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { + + let spots = availableSeedOpponentSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) + } + } else if let chunks = seedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } } } } @@ -829,7 +858,7 @@ defer { let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let groupStageSpots: Int = self.groupStageSpots() - var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count + var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index 9aa7ec9..2c20386 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -6,10 +6,11 @@ // import SwiftUI +import LeStorage struct BracketCallingView: View { + @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament - @State private var displayByMatch: Bool = true @State private var initialSeedRound: Int = 0 @State private var initialSeedCount: Int = 0 let tournamentRounds: [Round] @@ -19,9 +20,18 @@ struct BracketCallingView: View { let rounds = tournament.rounds() self.tournamentRounds = rounds self.teams = tournament.availableSeeds() - let index = rounds.count - 1 - _initialSeedRound = .init(wrappedValue: index) - _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + if tournament.initialSeedRound == 0, rounds.count > 0 { + let index = rounds.count - 1 + _initialSeedRound = .init(wrappedValue: index) + _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + } else if tournament.initialSeedRound < rounds.count { + _initialSeedRound = .init(wrappedValue: tournament.initialSeedRound) + _initialSeedCount = .init(wrappedValue: tournament.initialSeedCount) + } else if rounds.count > 0 { + let index = rounds.count - 1 + _initialSeedRound = .init(wrappedValue: index) + _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + } } var initialRound: Round { @@ -115,10 +125,23 @@ struct BracketCallingView: View { } } } + .onDisappear(perform: { + tournament.initialSeedCount = initialSeedCount + tournament.initialSeedRound = initialSeedRound + _save() + }) .headerProminence(.increased) .navigationTitle("Pré-convocation") } + private func _save() { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + @ViewBuilder private func _roundView(round: Round, seeds: [TeamRegistration]) -> some View { List { diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 2c9117a..1760e99 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -25,7 +25,7 @@ struct TeamsCallingView: View { Section { Text(label) } footer: { - Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur 􀍡") + Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur ") + Text(Image(systemName: "ellipsis.circle")) } Section { diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index cad0f0a..4186a8b 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -105,7 +105,7 @@ struct EventSettingsView: View { Button("Valider") { textFieldIsFocus = false if eventName.trimmed.isEmpty == false { - event.name = eventName.trimmed + event.name = eventName.prefixTrimmed(200) } else { event.name = nil } diff --git a/PadelClub/Views/Club/CourtView.swift b/PadelClub/Views/Club/CourtView.swift index c4e89f8..6b37fc8 100644 --- a/PadelClub/Views/Club/CourtView.swift +++ b/PadelClub/Views/Club/CourtView.swift @@ -30,9 +30,11 @@ struct CourtView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit { - court.name = name - if name.isEmpty { + let courtName = name.prefixTrimmed(50) + if courtName.isEmpty { court.name = nil + } else { + court.name = courtName } do { try dataStore.courts.addOrUpdate(instance: court) diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 84b529e..ad0d943 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -42,7 +42,7 @@ struct GroupStageSettingsView: View { .submitLabel(.done) .frame(maxWidth: .infinity) .onSubmit { - groupStageName = groupStageName.trimmed + groupStageName = groupStageName.prefixTrimmed(200) if groupStageName.isEmpty == false { groupStage.name = groupStageName _save() diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 471f914..24f19ae 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -36,6 +36,7 @@ struct MatchDetailView: View { @State private var presentFollowUpMatch: Bool = false @State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false @State private var presentRanking: Bool = false + @State private var confirmScoreEdition: Bool = false var tournamentStore: TournamentStore { return match.tournamentStore @@ -182,7 +183,8 @@ struct MatchDetailView: View { } }) .sheet(item: $scoreType, onDismiss: { - if match.hasEnded() { + if match.hasEnded(), confirmScoreEdition { + confirmScoreEdition = false if match.index == 0, match.isGroupStage() == false, match.roundObject?.parent == nil { presentRanking = true } else if match.isGroupStage(), match.currentTournament()?.hasEnded() == true { @@ -193,7 +195,7 @@ struct MatchDetailView: View { } }) { scoreType in let matchDescriptor = MatchDescriptor(match: match) - EditScoreView(matchDescriptor: matchDescriptor) + EditScoreView(matchDescriptor: matchDescriptor, confirmScoreEdition: $confirmScoreEdition) .tint(.master) // switch scoreType { diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 046cdc1..45cb3e7 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -41,7 +41,7 @@ struct PlayerDetailView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.lastName = player.lastName.trimmedMultiline + player.lastName = player.lastName.prefixTrimmed(50) _save() } } label: { @@ -54,7 +54,7 @@ struct PlayerDetailView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.firstName = player.firstName.trimmedMultiline + player.firstName = player.firstName.prefixTrimmed(50) _save() } } label: { @@ -120,7 +120,7 @@ struct PlayerDetailView: View { .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.licenceId = licenceId + player.licenceId = licenceId.prefixTrimmed(50) _save() } } label: { @@ -128,7 +128,7 @@ struct PlayerDetailView: View { CopyPasteButtonView(pasteValue: player.licenceId) PasteButtonView(text: $licenceId) .onChange(of: licenceId) { - player.licenceId = licenceId + player.licenceId = licenceId.prefixTrimmed(50) _save() } } label: { @@ -145,7 +145,7 @@ struct PlayerDetailView: View { .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.phoneNumber = phoneNumber + player.phoneNumber = phoneNumber.prefixTrimmed(50) _save() } } label: { @@ -153,7 +153,7 @@ struct PlayerDetailView: View { CopyPasteButtonView(pasteValue: player.phoneNumber) PasteButtonView(text: $phoneNumber) .onChange(of: phoneNumber) { - player.phoneNumber = phoneNumber + player.phoneNumber = phoneNumber.prefixTrimmed(50) _save() } } label: { @@ -170,7 +170,7 @@ struct PlayerDetailView: View { .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.email = email + player.email = email.prefixTrimmed(50) _save() } } label: { @@ -178,7 +178,7 @@ struct PlayerDetailView: View { CopyPasteButtonView(pasteValue: player.email) PasteButtonView(text: $email) .onChange(of: email) { - player.email = email + player.email = email.prefixTrimmed(50) _save() } } label: { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 5ed57c3..7f93335 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -73,10 +73,11 @@ struct RoundView: View { if disabledMatchesCount > 0 { let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) TipView(bracketTip).tipStyle(tint: .green, asSection: true) - - if upperRound.round.hasStarted() == false { + + let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) + + if upperRound.round.hasStarted() == false, leftToPlay >= 0 { Section { - let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) LabeledContent { Text(leftToPlay.formatted()) } label: { @@ -128,7 +129,7 @@ struct RoundView: View { RowButtonView("Placer \(availableSeedGroup.localizedInterval())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { Task { tournament.setSeeds(inRoundIndex: upperRound.round.index, inSeedGroup: availableSeedGroup) - _save() + _save(seeds: availableSeeds) } } } footer: { @@ -166,7 +167,8 @@ struct RoundView: View { team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) } } - _save() + + _save(seeds: [team]) } } } label: { @@ -193,7 +195,8 @@ struct RoundView: View { results.forEach { drawResult in team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) } - _save() + + _save(seeds: [team]) } } } label: { @@ -216,7 +219,7 @@ struct RoundView: View { results.forEach { drawResult in team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) } - _save() + _save(seeds: [team]) } } } label: { @@ -300,17 +303,15 @@ struct RoundView: View { } .fullScreenCover(isPresented: showVisualDrawView) { if let availableSeedGroup = selectedSeedGroup { - let seeds = tournament.seeds(inSeedGroup: availableSeedGroup) - let opposingSeeding = seedSpaceLeft.isEmpty ? true : false - let availableSeedSpot = opposingSeeding ? spaceLeft : seedSpaceLeft + let seeds = _seeds(availableSeedGroup: availableSeedGroup) + let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup) NavigationStack { SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in Task { draws.forEach { drawResult in seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) } - - _save() + _save(seeds: seeds) } } } @@ -331,17 +332,45 @@ struct RoundView: View { } } + private func _seeds(availableSeedGroup: SeedInterval) -> [TeamRegistration] { + tournament.seeds(inSeedGroup: availableSeedGroup) + } + + var opposingSeeding: Bool { + seedSpaceLeft.isEmpty ? true : false + } + + private func _availableSeedSpot(availableSeedGroup: SeedInterval) -> [Match] { + let spots = opposingSeeding ? spaceLeft : seedSpaceLeft + if availableSeedGroup == SeedInterval(first: 3, last: 4), spots.count == 6 { + var array = [Match]() + array.append(spots[1]) + array.append(spots[4]) + return array + } else { + return spots + } + } + + + private func _save(seeds: [TeamRegistration]) { + do { + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds) + } catch { + Logger.error(error) + } + + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { + self.isEditingTournamentSeed.wrappedValue = false + } + + _refreshNames() + } + private func _save() { -// do { -// try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) -// } catch { -// Logger.error(error) -// } -// if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } - _refreshNames() } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index a080a03..d30f20e 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -44,12 +44,13 @@ struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore @ObservedObject var matchDescriptor: MatchDescriptor + @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss @State private var showSetInputView: Bool = true @State private var showTieBreakInputView: Bool = false - let colorTeamOne: Color = .mint - let colorTeamTwo: Color = .cyan + let colorTeamOne: Color = .teal + let colorTeamTwo: Color = .indigo func walkout(_ team: TeamPosition) { self.matchDescriptor.match?.setWalkOut(team) @@ -83,7 +84,7 @@ struct EditScoreView: View { .bold() } } - .listRowView(isActive: teamOneSetupIsActive, color: .master, hideColorVariation: false) + .listRowView(isActive: teamOneSetupIsActive, color: colorTeamOne, hideColorVariation: false) HStack { Spacer() VStack(alignment: .trailing) { @@ -94,7 +95,7 @@ struct EditScoreView: View { } } } - .listRowView(isActive: teamTwoSetupIsActive, color: .master, hideColorVariation: false, alignment: .trailing) + .listRowView(isActive: teamTwoSetupIsActive, color: colorTeamTwo, hideColorVariation: false, alignment: .trailing) } footer: { HStack { Menu { @@ -133,7 +134,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } - .tint(.master) + .tint(getColor()) } if matchDescriptor.hasEnded { @@ -171,6 +172,7 @@ struct EditScoreView: View { } func save() { + self.confirmScoreEdition = true if let match = matchDescriptor.match { do { try match.tournamentStore.matches.addOrUpdate(instance: match) diff --git a/PadelClub/Views/Team/CoachListView.swift b/PadelClub/Views/Team/CoachListView.swift index 0a4ca87..bce29b4 100644 --- a/PadelClub/Views/Team/CoachListView.swift +++ b/PadelClub/Views/Team/CoachListView.swift @@ -26,7 +26,7 @@ struct CoachListView: View { .frame(maxWidth: .infinity) .submitLabel(.done) .onSubmit(of: .text) { - let trimmed = coachNames.trimmed + let trimmed = coachNames.prefixTrimmed(200) if trimmed.isEmpty { team.comment = nil } else { diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 9949e67..133ff49 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -151,7 +151,7 @@ struct EditingTeamView: View { .frame(maxWidth: .infinity) .submitLabel(.done) .onSubmit(of: .text) { - let trimmed = name.trimmedMultiline + let trimmed = name.prefixTrimmed(200) if trimmed.isEmpty { team.name = nil } else { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index f7096d1..1d8efa3 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -145,7 +145,8 @@ struct TournamentGeneralSettingsView: View { Spacer() Button("Valider") { if focusedField == ._name { - if tournamentName.trimmed.isEmpty { + let tournamentName = tournamentName.prefixTrimmed(200) + if tournamentName.isEmpty { tournament.name = nil } else { tournament.name = tournamentName diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index fd0db9d..16a9af1 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -103,7 +103,7 @@ struct TournamentCallView: View { self._selectedDestination = State(wrappedValue: .seeds(tournament)) } } - if tournament.availableSeeds().isEmpty == false { + if tournament.availableSeeds().isEmpty == false, tournament.rounds().count > 0 { destinations.append(.brackets(tournament)) if seededTeams.isEmpty { self._selectedDestination = State(wrappedValue: .brackets(tournament)) diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 3947937..f1a988e 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -99,7 +99,7 @@ final class ServerDataTests: XCTestCase { return } - let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual) + let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4) let t = try await StoreCenter.main.service().post(tournament) assert(t.event == tournament.event) @@ -140,6 +140,8 @@ final class ServerDataTests: XCTestCase { assert(t.hidePointsEarned == tournament.hidePointsEarned) assert(t.publishRankings == tournament.publishRankings) assert(t.loserBracketMode == tournament.loserBracketMode) + assert(t.initialSeedCount == tournament.initialSeedCount) + assert(t.initialSeedRound == tournament.initialSeedRound) } func testGroupStage() async throws { From 6791aed10aedc2173a94fcabe91cc2e206586965 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 17 Oct 2024 13:18:00 +0200 Subject: [PATCH 033/106] fix stuff --- PadelClub.xcodeproj/project.pbxproj | 4 +- .../Views/Calling/TeamsCallingView.swift | 47 ++++++++++++++++--- PadelClub/Views/Club/ClubDetailView.swift | 9 ++-- .../Components/GroupStageTeamView.swift | 8 ++++ .../Match/Components/MatchDateView.swift | 9 +++- .../Components/MatchTeamDetailView.swift | 13 ++++- PadelClub/Views/Match/MatchDetailView.swift | 3 +- PadelClub/Views/Score/EditScoreView.swift | 8 +++- PadelClub/Views/Team/CoachListView.swift | 37 ++++++++------- PadelClub/Views/Team/EditingTeamView.swift | 35 +++++++++----- 10 files changed, 125 insertions(+), 48 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 10b7a3f..4d0522e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3166,7 +3166,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3211,7 +3211,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 1760e99..f1cf5c3 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -11,25 +11,46 @@ import LeStorage struct TeamsCallingView: View { @Environment(Tournament.self) var tournament: Tournament let teams : [TeamRegistration] + + var filteredTeams: [TeamRegistration] { + teams.filter({ searchText.isEmpty || $0.contains(searchText) }) + } + + @State private var searchText: String = "" var body: some View { List { PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } + let confirmed = teams.filter { $0.confirmed() } let justCalled = teams.filter { $0.called() } - let label = "\(justCalled.count.formatted()) / \(teams.count.formatted()) convoquées (dont \(called.count.formatted()) au bon horaire)" - - if teams.isEmpty == false { + let label = "\(justCalled.count.formatted()) / \(teams.count.formatted())" + let subtitle = "dont \(called.count.formatted()) au bon horaire" + let confirmedLabel = "\(confirmed.count.formatted()) / \(teams.count.formatted())" + + if teams.isEmpty == false, searchText.isEmpty { Section { - Text(label) + LabeledContent { + Text(label).font(.title3) + } label: { + Text("Paire\(justCalled.count.pluralSuffix) convoquée\(justCalled.count.pluralSuffix)") + Text(subtitle) + } + LabeledContent { + Text(confirmedLabel).font(.title3) + } label: { + Text("Paire\(confirmed.count.pluralSuffix) confirmée\(confirmed.count.pluralSuffix)") + } } footer: { Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur ") + Text(Image(systemName: "ellipsis.circle")) } - + } + + if teams.isEmpty == false { Section { - ForEach(teams) { team in + ForEach(filteredTeams) { team in Menu { _menuOptions(team: team) } label: { @@ -51,6 +72,7 @@ struct TeamsCallingView: View { ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash") } } + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) .headerProminence(.increased) .navigationTitle("Statut des équipes") .navigationBarTitleDisplayMode(.inline) @@ -66,6 +88,7 @@ struct TeamsCallingView: View { } catch { Logger.error(error) } + searchText = "" } label: { if team.confirmed() { Label("Confirmation reçue", systemImage: "checkmark.circle.fill").foregroundStyle(.green) @@ -73,6 +96,16 @@ struct TeamsCallingView: View { Label("Confirmation reçue", systemImage: "circle").foregroundStyle(.logoRed) } } + + Divider() + + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("Détails de l'équipe") + } + Divider() Button(role: .destructive) { @@ -82,6 +115,7 @@ struct TeamsCallingView: View { } catch { Logger.error(error) } + searchText = "" } label: { Text("Effacer la date de convocation") } @@ -96,6 +130,7 @@ struct TeamsCallingView: View { } catch { Logger.error(error) } + searchText = "" } label: { Text("Indiquer comme convoquée") } diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index b04b4f0..b16e079 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -216,9 +216,12 @@ struct ClubDetailView: View { .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { - ToolbarItem(placement: .topBarLeading) { - Button("Annuler", role: .cancel) { - focusedField = nil + ToolbarItem(placement: .keyboard) { + HStack { + Button("Fermer", role: .cancel) { + focusedField = nil + } + Spacer() } } } diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index d839870..18a3786 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -54,6 +54,14 @@ struct GroupStageTeamView: View { ForEach(team.players()) { player in EditablePlayerView(player: player, editingOptions: _editingOptions()) } + } footer: { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("détails de l'équipe") + .underline() + } } if groupStage.tournamentObject()?.hasEnded() == false { diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index f64f8bb..ab962ed 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -11,7 +11,8 @@ import LeStorage struct MatchDateView: View { @EnvironmentObject var dataStore: DataStore - + @State private var showScoreEditView: Bool = false + @State private var confirmScoreEdition: Bool = false var match: Match var showPrefix: Bool = false private var isReady: Bool @@ -84,7 +85,7 @@ struct MatchDateView: View { } } Button("Indiquer un score") { - + showScoreEditView = true } Divider() Button("Retirer l'horaire") { @@ -165,6 +166,10 @@ struct MatchDateView: View { } } } + .sheet(isPresented: $showScoreEditView) { + EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition) + .tint(.master) + } } private func _save() { diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index f12eb91..3ee208b 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -25,7 +25,6 @@ struct MatchTeamDetailView: View { .headerProminence(.increased) .tint(.master) } - .presentationDetents([.fraction(0.66)]) } @ViewBuilder @@ -41,7 +40,17 @@ struct MatchTeamDetailView: View { Text("Coachs : " + coachList).foregroundStyle(.secondary).font(.footnote) } } header: { - TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) + TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: tournament) + } footer: { + if let tournament { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("détails de l'équipe") + .underline() + } + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 24f19ae..0071351 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -194,8 +194,7 @@ struct MatchDetailView: View { } } }) { scoreType in - let matchDescriptor = MatchDescriptor(match: match) - EditScoreView(matchDescriptor: matchDescriptor, confirmScoreEdition: $confirmScoreEdition) + EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition) .tint(.master) // switch scoreType { diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index d30f20e..6728c0d 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -43,7 +43,7 @@ struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore - @ObservedObject var matchDescriptor: MatchDescriptor + @StateObject var matchDescriptor: MatchDescriptor @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss @State private var showSetInputView: Bool = true @@ -51,6 +51,12 @@ struct EditScoreView: View { let colorTeamOne: Color = .teal let colorTeamTwo: Color = .indigo + + init(match: Match, confirmScoreEdition: Binding) { + let matchDescriptor = MatchDescriptor(match: match) + _matchDescriptor = .init(wrappedValue: matchDescriptor) + _confirmScoreEdition = confirmScoreEdition + } func walkout(_ team: TeamPosition) { self.matchDescriptor.match?.setWalkOut(team) diff --git a/PadelClub/Views/Team/CoachListView.swift b/PadelClub/Views/Team/CoachListView.swift index bce29b4..7a980d6 100644 --- a/PadelClub/Views/Team/CoachListView.swift +++ b/PadelClub/Views/Team/CoachListView.swift @@ -20,28 +20,31 @@ struct CoachListView: View { var body: some View { Section { - TextField("Coach", text: $coachNames) - .autocorrectionDisabled() - .keyboardType(.alphabet) - .frame(maxWidth: .infinity) - .submitLabel(.done) - .onSubmit(of: .text) { - let trimmed = coachNames.prefixTrimmed(200) - if trimmed.isEmpty { + HStack { + TextField("Coach", text: $coachNames) + .autocorrectionDisabled() + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = coachNames.prefixTrimmed(200) + if trimmed.isEmpty { + team.comment = nil + } else { + team.comment = trimmed + } + _save() + } + if coachNames.isEmpty == false { + FooterButtonView("effacer", role: .destructive) { + coachNames = "" team.comment = nil - } else { - team.comment = trimmed + _save() } - _save() } + } } header: { Text("Coachs") - } footer: { - FooterButtonView("effacer") { - coachNames = "" - team.comment = nil - _save() - } } } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index 133ff49..f8cc5d5 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -144,22 +144,31 @@ struct EditingTeamView: View { } Section { - TextField("Nom de l'équipe", text: $name) - .autocorrectionDisabled() - .focused($focusedField, equals: ._name) - .keyboardType(.alphabet) - .frame(maxWidth: .infinity) - .submitLabel(.done) - .onSubmit(of: .text) { - let trimmed = name.prefixTrimmed(200) - if trimmed.isEmpty { + HStack { + TextField("Nom de l'équipe", text: $name) + .autocorrectionDisabled() + .focused($focusedField, equals: ._name) + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = name.prefixTrimmed(200) + if trimmed.isEmpty { + team.name = nil + } else { + team.name = trimmed + } + + _save() + } + if name.isEmpty == false { + FooterButtonView("effacer", role: .destructive) { + name = "" team.name = nil - } else { - team.name = trimmed + _save() } - - _save() } + } } header: { Text("Nom de l'équipe") } From af1c53f1b8e619fb3bae2329b505b3455604c1de Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 17 Oct 2024 18:22:19 +0200 Subject: [PATCH 034/106] fix table structure check when no group stage add a way to simply contact list of teams depending of confirmed / called status --- PadelClub/Views/Calling/CallView.swift | 53 ++++++++++++++++--- .../Views/Calling/TeamsCallingView.swift | 45 ++++++++++++++-- .../Screen/TableStructureView.swift | 19 ++++--- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 0367f2e..514da7f 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -68,6 +68,24 @@ struct CallView: View { @State var summonParamByMessage: Bool = false @State var summonParamReSummon: Bool = false + let simpleMode : Bool + + init(teams: [TeamRegistration], callDate: Date, matchFormat: MatchFormat, roundLabel: String) { + self.teams = teams + self.callDate = callDate + self.matchFormat = matchFormat + self.roundLabel = roundLabel + self.simpleMode = false + } + + init(teams: [TeamRegistration]) { + self.teams = teams + self.callDate = Date() + self.matchFormat = MatchFormat.nineGames + self.roundLabel = "" + self.simpleMode = true + } + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -83,6 +101,9 @@ struct CallView: View { } private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) { + if simpleMode { + return + } if success { calledTeams.forEach { team in team.callDate = callDate @@ -96,21 +117,41 @@ struct CallView: View { } func finalMessage(reSummon: Bool) -> String { - ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon) + if simpleMode { + let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature() + return "\n\n\n\n" + signature + } + + return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon) } var reSummon: Bool { + if simpleMode { + return false + } return self.teams.allSatisfy({ $0.called() }) } + var mainWord: String { + if simpleMode { + return "Contacter" + } else { + return "Convoquer" + } + } + var body: some View { - let callWord : String = (reSummon ? "Reconvoquer" : "Convoquer") + let callWord : String = (reSummon ? "Reconvoquer" : mainWord) HStack { if self.teams.count == 1 { - if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { - Text("Reconvoquer \(self.callDate.localizedDate()) par") - } else { + if simpleMode { Text("\(callWord) cette paire par") + } else { + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer \(self.callDate.localizedDate()) par") + } else { + Text("\(callWord) cette paire par") + } } } else { Text("\(callWord) ces \(self.teams.count) paires par") @@ -222,7 +263,7 @@ struct CallView: View { private func _summonMenu(byMessage: Bool) -> some View { if self.reSummon { Menu { - Button("Convoquer") { + Button(mainWord) { self._summon(byMessage: byMessage, reSummon: false) } diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index f1cf5c3..844459d 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -12,12 +12,23 @@ struct TeamsCallingView: View { @Environment(Tournament.self) var tournament: Tournament let teams : [TeamRegistration] + @State private var hideConfirmed: Bool = false + @State private var hideGoodSummoned: Bool = false + @State private var hideSummoned: Bool = false + @State private var searchText: String = "" + var filteredTeams: [TeamRegistration] { - teams.filter({ searchText.isEmpty || $0.contains(searchText) }) + teams + .filter({ hideConfirmed == false || $0.confirmed() == false }) + .filter({ hideSummoned == false || $0.called() == false }) + .filter({ hideGoodSummoned == false || tournament.isStartDateIsDifferentThanCallDate($0) == true }) + .filter({ searchText.isEmpty || $0.contains(searchText) }) + } + + var anyFilterEnabled: Bool { + hideConfirmed || hideGoodSummoned || hideSummoned } - @State private var searchText: String = "" - var body: some View { List { PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) @@ -67,14 +78,40 @@ struct TeamsCallingView: View { .buttonStyle(.plain) .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) } + } header: { + HStack { + Text("Paire\(filteredTeams.count.pluralSuffix)") + Spacer() + Text(filteredTeams.count.formatted()) + } + } footer: { + CallView(teams: filteredTeams) } } else { ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash") } } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Toggle(isOn: $hideConfirmed) { + Text("Masquer les confirmées") + } + Toggle(isOn: $hideSummoned) { + Text("Masquer les convoquées") + } + Toggle(isOn: $hideGoodSummoned) { + Text("Masquer les convoquées à la bonne heure") + } + } label: { + LabelFilter() + .symbolVariant(anyFilterEnabled ? .fill : .none) + } + } + }) .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) .headerProminence(.increased) - .navigationTitle("Statut des équipes") + .navigationTitle("Statut des convocations") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 9571c44..b89c2b5 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -370,6 +370,17 @@ struct TableStructureView: View { } } + _checkGroupStagesTeams() + + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + dismiss() + } + + private func _checkGroupStagesTeams() { if groupStageCount == 0 { let teams = tournament.unsortedTeams().filter({ $0.inGroupStage() }) teams.forEach { team in @@ -382,12 +393,6 @@ struct TableStructureView: View { Logger.error(error) } } - do { - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } - dismiss() } private func _save(rebuildEverything: Bool = false) { @@ -412,6 +417,8 @@ struct TableStructureView: View { tournament.deleteGroupStages() tournament.buildGroupStages() } + + _checkGroupStagesTeams() try dataStore.tournaments.addOrUpdate(instance: tournament) From 20eeea99b8da872ea1adcc0490536da0c5888f07 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 17 Oct 2024 18:22:41 +0200 Subject: [PATCH 035/106] v1.0.23 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4d0522e..3fc341a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3166,7 +3166,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3190,7 +3190,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3211,7 +3211,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3234,7 +3234,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From a51adc6bbe0b6a9a01aefd9f1c21499430d1b4cb Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 18 Oct 2024 10:38:46 +0200 Subject: [PATCH 036/106] add QoL features on summoning screen --- PadelClub/Utils/DisplayContext.swift | 5 + .../Views/Calling/BracketCallingView.swift | 4 +- PadelClub/Views/Calling/CallView.swift | 128 +++++++++++++---- .../Views/Calling/GroupStageCallingView.swift | 2 +- .../Views/Calling/SeedsCallingView.swift | 2 +- .../Views/Calling/TeamsCallingView.swift | 130 +++++++++--------- .../Components/InscriptionInfoView.swift | 9 +- 7 files changed, 177 insertions(+), 103 deletions(-) diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index 71786af..7eafd7d 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -21,6 +21,11 @@ enum DisplayStyle { case short } +enum SummoningDisplayContext { + case footer + case menu +} + enum MatchViewStyle { case standardStyle // vue normal case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift index 2c20386..810919e 100644 --- a/PadelClub/Views/Calling/BracketCallingView.swift +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -162,7 +162,7 @@ struct BracketCallingView: View { if badCalled.isEmpty == false { Section { ForEach(badCalled) { team in - CallView.TeamView(team: team) + TeamCallView(team: team) } } header: { HStack { @@ -179,7 +179,7 @@ struct BracketCallingView: View { Section { ForEach(seeds) { team in - CallView.TeamView(team: team) + TeamCallView(team: team) } } header: { HStack { diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 514da7f..85fb5cb 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -41,14 +41,6 @@ struct CallView: View { } } - struct TeamView: View { - let team: TeamRegistration - - var body: some View { - TeamRowView(team: team, displayCallDate: true) - } - } - @EnvironmentObject var dataStore: DataStore @EnvironmentObject var networkMonitor: NetworkMonitor @@ -58,6 +50,7 @@ struct CallView: View { let callDate: Date let matchFormat: MatchFormat let roundLabel: String + let displayContext: SummoningDisplayContext @State private var contactType: ContactType? = nil @State private var sentError: ContactManagerError? = nil @@ -76,6 +69,7 @@ struct CallView: View { self.matchFormat = matchFormat self.roundLabel = roundLabel self.simpleMode = false + self.displayContext = .footer } init(teams: [TeamRegistration]) { @@ -84,6 +78,30 @@ struct CallView: View { self.matchFormat = MatchFormat.nineGames self.roundLabel = "" self.simpleMode = true + self.displayContext = .footer + } + + init(team: TeamRegistration, displayContext: SummoningDisplayContext) { + self.teams = [team] + let expectedSummonDate = team.expectedSummonDate() + self.displayContext = displayContext + + if let expectedSummonDate, let initialMatch = team.initialMatch() { + self.callDate = expectedSummonDate + self.matchFormat = initialMatch.matchFormat + self.roundLabel = initialMatch.roundTitle() ?? "tableau" + self.simpleMode = false + } else if let expectedSummonDate, let initialGroupStage = team.groupStageObject() { + self.callDate = expectedSummonDate + self.matchFormat = initialGroupStage.matchFormat + self.roundLabel = "poule" + self.simpleMode = false + } else { + self.callDate = Date() + self.matchFormat = MatchFormat.nineGames + self.roundLabel = "" + self.simpleMode = true + } } var tournamentStore: TournamentStore { @@ -107,6 +125,9 @@ struct CallView: View { if success { calledTeams.forEach { team in team.callDate = callDate + if reSummon { + team.confirmationDate = nil + } } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams) @@ -141,28 +162,14 @@ struct CallView: View { } var body: some View { - let callWord : String = (reSummon ? "Reconvoquer" : mainWord) - HStack { - if self.teams.count == 1 { - if simpleMode { - Text("\(callWord) cette paire par") - } else { - if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { - Text("Reconvoquer \(self.callDate.localizedDate()) par") - } else { - Text("\(callWord) cette paire par") - } - } - } else { - Text("\(callWord) ces \(self.teams.count) paires par") + Group { + switch displayContext { + case .footer: + _footerStyleView() + case .menu: + _menuStyleView() } - - self._summonMenu(byMessage: true) - Text("ou") - self._summonMenu(byMessage: false) } - .font(.subheadline) - .buttonStyle(.borderless) .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } @@ -258,6 +265,54 @@ struct CallView: View { } }) } + + private func _footerStyleView() -> some View { + HStack { + let callWord : String = (reSummon ? "Reconvoquer" : mainWord) + if self.teams.count == 1 { + if simpleMode { + Text("\(callWord) cette paire par") + } else { + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer \(self.callDate.localizedDate()) par") + } else { + Text("\(callWord) cette paire par") + } + } + } else { + Text("\(callWord) ces \(self.teams.count) paires par") + } + + self._summonMenu(byMessage: true) + Text("ou") + self._summonMenu(byMessage: false) + } + .font(.subheadline) + .buttonStyle(.borderless) + } + + private func _menuStyleView() -> some View { + Menu { + self._summonMenu(byMessage: true) + self._summonMenu(byMessage: false) + } label: { + let callWord : String = (reSummon ? "Reconvoquer" : mainWord) + if self.teams.count == 1 { + if simpleMode { + Text("\(callWord) cette paire") + } else { + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer \(self.callDate.localizedDate())") + } else { + Text("\(callWord) cette paire") + } + } + } else { + Text("\(callWord) ces \(self.teams.count) paires") + } + } + } + @ViewBuilder private func _summonMenu(byMessage: Bool) -> some View { @@ -334,3 +389,20 @@ struct CallView: View { } } + +struct TeamCallView: View { + @Environment(Tournament.self) var tournament: Tournament + let team: TeamRegistration + var action: (() -> Void)? + + var body: some View { + NavigationLink { + CallMenuOptionsView(team: team, action: action) + .environment(tournament) + } label: { + TeamRowView(team: team, displayCallDate: true) + } + .buttonStyle(.plain) + .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) + } +} diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index 6a419e1..cb12cd4 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -86,7 +86,7 @@ struct GroupStageCallingView: View { ForEach(teams) { team in if let startDate = groupStage.initialStartDate(forTeam: team) { Section { - CallView.TeamView(team: team) + TeamCallView(team: team) } header: { Text(startDate.localizedDate()) } footer: { diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index 452bd9d..e7c015d 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -101,7 +101,7 @@ struct SeedsCallingView: View { let teams = round.seeds(inMatchIndex: match.index) Section { ForEach(teams) { team in - CallView.TeamView(team: team) + TeamCallView(team: team) } } header: { HStack { diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 844459d..8a5d1e9 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -55,28 +55,16 @@ struct TeamsCallingView: View { Text("Paire\(confirmed.count.pluralSuffix) confirmée\(confirmed.count.pluralSuffix)") } } footer: { - Text("Vous pouvez indiquer si une équipe vous a confirmé sa convocation en appuyant sur ") + Text(Image(systemName: "ellipsis.circle")) + Text("Vous pouvez filtrer cette liste en appuyant sur ") + Text(Image(systemName: "line.3.horizontal.decrease.circle")) } } - if teams.isEmpty == false { + if filteredTeams.isEmpty == false { Section { ForEach(filteredTeams) { team in - Menu { - _menuOptions(team: team) - } label: { - HStack { - TeamRowView(team: team, displayCallDate: true) - Spacer() - Menu { - _menuOptions(team: team) - } label: { - LabelOptions().labelStyle(.iconOnly) - } - } + TeamCallView(team: team) { + searchText = "" } - .buttonStyle(.plain) - .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) } } header: { HStack { @@ -115,63 +103,79 @@ struct TeamsCallingView: View { .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } +} - @ViewBuilder - func _menuOptions(team: TeamRegistration) -> some View { - Button { +struct CallMenuOptionsView: View { + @Environment(\.dismiss) private var dismiss + @Environment(Tournament.self) var tournament: Tournament + let team: TeamRegistration + let action: (() -> Void)? + + var confirmed: Binding { + Binding { + team.confirmed() + } set: { _ in team.toggleSummonConfirmation() do { try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } - searchText = "" - } label: { - if team.confirmed() { - Label("Confirmation reçue", systemImage: "checkmark.circle.fill").foregroundStyle(.green) - } else { - Label("Confirmation reçue", systemImage: "circle").foregroundStyle(.logoRed) - } + action?() } - - Divider() - - NavigationLink { - EditingTeamView(team: team) - .environment(tournament) - } label: { - Text("Détails de l'équipe") - } - - Divider() - - Button(role: .destructive) { - team.callDate = nil - do { - try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + } + + var body: some View { + List { + Section { + TeamRowView(team: team, displayCallDate: true) + Toggle(isOn: confirmed) { + Text("Confirmation reçue") + } + if team.expectedSummonDate() != nil { + CallView(team: team, displayContext: .menu) + } + } footer: { + CallView(teams: [team]) } - searchText = "" - } label: { - Text("Effacer la date de convocation") - } - - - Divider() - - Button(role: .destructive) { - team.callDate = team.initialMatch()?.startDate ?? tournament.startDate - do { - try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + + Section { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("Détails de l'équipe") + } + } + + Section { + RowButtonView("Effacer la date de convocation", role: .destructive) { + team.callDate = nil + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + action?() + dismiss() + } + } + + Section { + RowButtonView("Indiquer comme convoquée", role: .destructive) { + team.callDate = team.initialMatch()?.startDate ?? tournament.startDate + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + action?() + dismiss() + } } - searchText = "" - } label: { - Text("Indiquer comme convoquée") } - - + .navigationTitle("Options de convocation") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index fe03ff1..a52b8c5 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -57,14 +57,7 @@ struct InscriptionInfoView: View { Section { DisclosureGroup { ForEach(callDateIssue) { team in - CallView.TeamView(team: team) - if let groupStage = team.groupStageObject(), let callDate = groupStage.startDate { - CallView(teams: [team], callDate: callDate, matchFormat: groupStage.matchFormat, roundLabel: "poule") - } else if let initialRound = team.initialRound(), - let initialMatch = team.initialMatch(), - let callDate = initialMatch.startDate { - CallView(teams: [team], callDate: callDate, matchFormat: initialMatch.matchFormat, roundLabel: initialRound.roundTitle()) - } + TeamCallView(team: team) } } label: { LabeledContent { From e72bc97bd7f71015d81897ce763888f004b4d53f Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 18 Oct 2024 11:33:02 +0200 Subject: [PATCH 037/106] force always compact size class --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/PadelClubApp.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 3fc341a..fac1aea 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3166,7 +3166,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3211,7 +3211,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index ccf8660..4cdba91 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -17,7 +17,8 @@ struct PadelClubApp: App { @StateObject var dataStore = DataStore.shared @State private var registrationError: RegistrationError? = nil @State private var importObserverViewModel = ImportObserver() - + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var presentError: Binding { @@ -62,6 +63,7 @@ struct PadelClubApp: App { var body: some Scene { WindowGroup { MainView() + .environment(\.horizontalSizeClass, .compact) .alert(isPresented: presentError, error: registrationError) { Button("Contactez-nous") { _openMail() From 86f95c319a0bf41e34ddfd56d5f47a4cc0e3218b Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 18 Oct 2024 11:33:02 +0200 Subject: [PATCH 038/106] force always compact size class --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/PadelClubApp.swift | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 3fc341a..8d78bc8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3166,7 +3166,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3190,7 +3190,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3211,7 +3211,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3234,7 +3234,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.22; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index ccf8660..4cdba91 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -17,7 +17,8 @@ struct PadelClubApp: App { @StateObject var dataStore = DataStore.shared @State private var registrationError: RegistrationError? = nil @State private var importObserverViewModel = ImportObserver() - + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var presentError: Binding { @@ -62,6 +63,7 @@ struct PadelClubApp: App { var body: some Scene { WindowGroup { MainView() + .environment(\.horizontalSizeClass, .compact) .alert(isPresented: presentError, error: registrationError) { Button("Contactez-nous") { _openMail() From 7b2989d29bb880f313d53f34f8fb7f25a7039ee8 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 20 Oct 2024 20:07:45 +0200 Subject: [PATCH 039/106] wip --- PadelClub.xcodeproj/project.pbxproj | 8 ++ PadelClub/Data/MatchScheduler.swift | 4 +- PadelClub/Data/PlayerRegistration.swift | 6 +- PadelClub/Data/Round.swift | 13 +- .../Views/Calling/SeedsCallingView.swift | 2 +- PadelClub/Views/Cashier/CashierView.swift | 17 ++- .../Match/Components/MatchDateView.swift | 2 +- PadelClub/Views/Match/MatchDetailView.swift | 8 +- .../Navigation/Ongoing/OngoingView.swift | 4 +- PadelClub/Views/Score/EditScoreView.swift | 4 + PadelClub/Views/Score/FollowUpMatchView.swift | 16 ++- PadelClub/Views/Team/TeamRestingView.swift | 123 ++++++++++++++++++ .../Screen/TournamentCashierView.swift | 31 ++--- .../Tournament/TournamentRunningView.swift | 21 ++- 14 files changed, 217 insertions(+), 42 deletions(-) create mode 100644 PadelClub/Views/Team/TeamRestingView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 8d78bc8..bec9fd4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -700,6 +700,9 @@ FF70FBC82C90584900129CC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */; }; FF70FBC92C90584900129CC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; FF70FBCB2C90584900129CC2 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; + FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; + FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; }; FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; }; @@ -1078,6 +1081,7 @@ FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = ""; }; FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = ""; }; FF70FBCF2C90584900129CC2 /* PadelClub TestFlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PadelClub TestFlight.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = ""; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; @@ -1820,6 +1824,7 @@ FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */, FF17CA562CC02FEA003C7323 /* CoachListView.swift */, + FF7DCD382CC330260041110C /* TeamRestingView.swift */, FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; @@ -2304,6 +2309,7 @@ FF5D30562BD95B1100F2B93D /* OngoingView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, + FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, @@ -2577,6 +2583,7 @@ FF4CBF802C996C0600151637 /* OngoingView.swift in Sources */, FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, + FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, @@ -2829,6 +2836,7 @@ FF70FAFF2C90584900129CC2 /* OngoingView.swift in Sources */, FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, + FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 21f0936..38e8f87 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -660,7 +660,7 @@ final class MatchScheduler : ModelObject, Storable { @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { let upperRounds: [Round] = tournament.rounds() - let allMatches: [Match] = tournament.allMatches() + let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false }) var rounds = [Round]() @@ -681,7 +681,7 @@ final class MatchScheduler : ModelObject, Storable { } let flattenedMatches = rounds.flatMap { round in - round._matches().filter({ $0.disabled == false }).sorted(by: \.index) + round._matches().filter({ $0.disabled == false && $0.hasEnded() == false }).sorted(by: \.index) } flattenedMatches.forEach({ diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index f894a4f..0047976 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -155,12 +155,12 @@ final class PlayerRegistration: ModelObject, Storable { } func contains(_ searchField: String) -> Bool { - firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField) + firstName.canonicalVersion.localizedCaseInsensitiveContains(searchField.canonicalVersion) || lastName.canonicalVersion.localizedCaseInsensitiveContains(searchField.canonicalVersion) } func isSameAs(_ player: PlayerRegistration) -> Bool { - firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame && - lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame + firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame && + lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame } func tournament() -> Tournament? { diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 0604917..e29e884 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -145,12 +145,21 @@ final class Round: ModelObject, Storable { let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) return self.tournamentStore.teamRegistrations.filter { - $0.tournament == tournament - && $0.bracketPosition != nil + $0.bracketPosition != nil && ($0.bracketPosition! / 2) >= initialMatchIndex && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches } } + + func teamsOrSeeds() -> [TeamRegistration] { + let seeds = seeds() + if seeds.isEmpty { + return playedMatches().flatMap({ $0.teams() }) + } else { + return seeds + } + } + func losers() -> [TeamRegistration] { let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index e7c015d..c1b899e 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -28,7 +28,7 @@ struct SeedsCallingView: View { PlayersWithoutContactView(players: tournament.seededTeams().flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) ForEach(tournamentRounds) { round in - let seeds = round.seeds() + let seeds = round.teamsOrSeeds() let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) if seeds.isEmpty == false { Section { diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index 34cf916..9189735 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -56,7 +56,7 @@ extension Array { class CashierViewModel: ObservableObject { let id: UUID = UUID() @Published var sortOption: SortOption = .callDate - @Published var filterOption: FilterOption = .all + @Published var filterOption: FilterOption = .didNotPay @Published var presenceFilterOption: PresenceFilterOption = .all @Published var sortOrder: SortOrder = .ascending @Published var searchText: String = "" @@ -306,8 +306,7 @@ struct CashierView: View { case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .callDate: - let _teams = teams.filter({ $0.callDate != nil }) - TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) + TeamCallDateView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) } } .onAppear { @@ -354,7 +353,7 @@ struct CashierView: View { } } footer: { if let teamCallDate = player.team()?.callDate { - Text("équipe convoqué") + Text(teamCallDate.localizedDate()) + Text("équipe convoquée ") + Text(teamCallDate.localizedDate()) } } } @@ -369,8 +368,8 @@ struct CashierView: View { var body: some View { ForEach(teams) { team in - let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) - if players.isEmpty == false { + let players = team.players() + if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) { Section { ForEach(players) { player in EditablePlayerView(player: player, editingOptions: editingOptions) @@ -404,15 +403,15 @@ struct CashierView: View { var body: some View { let groupedTeams = Dictionary(grouping: teams) { team in - team.callDate + team.callDate ?? .distantPast } let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() ForEach(keys, id: \.self) { key in if let _teams = groupedTeams[key] { ForEach(_teams) { team in - let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) - if players.isEmpty == false { + let players = team.players() + if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) { Section { ForEach(players) { player in EditablePlayerView(player: player, editingOptions: editingOptions) diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index ab962ed..d71d326 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -35,7 +35,7 @@ struct MatchDateView: View { } else { Menu { let estimatedDuration = match.getDuration() - if isReady && match.hasStarted() == false { + if isReady { Section { Button("Démarrer maintenant") { if let updatedField { diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 0071351..0548811 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -437,9 +437,11 @@ struct MatchDetailView: View { Text("Partage sur les réseaux sociaux") } - Section { - RowButtonView("Match à suivre") { - presentFollowUpMatch = true + if match.currentTournament()?.hasEnded() == false { + Section { + RowButtonView("Match à suivre") { + presentFollowUpMatch = true + } } } } diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 3c41cbb..226b052 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -70,8 +70,8 @@ struct OngoingView: View { .toolbar { ToolbarItem(placement: .status) { Picker(selection: $sortByField) { - Text("tri par date").tag(true) - Text("tri par terrain").tag(false) + Text("tri par date").tag(false) + Text("tri par terrain").tag(true) } label: { } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 6728c0d..3110e4f 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -102,6 +102,10 @@ struct EditScoreView: View { } } .listRowView(isActive: teamTwoSetupIsActive, color: colorTeamTwo, hideColorVariation: false, alignment: .trailing) + } header: { + if let roundTitle = matchDescriptor.match?.roundAndMatchTitle() { + Text(roundTitle) + } } footer: { HStack { Menu { diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 994d723..5eea9a2 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -65,6 +65,20 @@ struct FollowUpMatchView: View { match.loser() } + var sortingModeCases: [SortingMode] { + var sortingModes = [SortingMode]() + if let winningTeam { + sortingModes.append(.winner) + } + if let losingTeam { + sortingModes.append(.loser) + } + sortingModes.append(.index) + sortingModes.append(.restingTime) + sortingModes.append(.court) + return sortingModes + } + func contentUnavailableDescriptionLabel() -> String { switch sortingMode { case .winner: @@ -155,7 +169,7 @@ struct FollowUpMatchView: View { } } header: { Picker(selection: $sortingMode) { - ForEach(SortingMode.allCases) { sortingMode in + ForEach(sortingModeCases) { sortingMode in Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) } } label: { diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift new file mode 100644 index 0000000..d576132 --- /dev/null +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -0,0 +1,123 @@ +// +// TeamRestingView.swift +// PadelClub +// +// Created by razmig on 19/10/2024. +// + +import SwiftUI + +struct TeamRestingView: View { + @Environment(Tournament.self) var tournament + + let readyMatches: [Match] + let matchesLeft: [Match] + + @State private var sortingMode: SortingMode = .restingTime + @State private var selectedCourt: Int? + + enum SortingMode: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case index + case restingTime + case court + + func localizedSortingModeLabel() -> String { + switch self { + case .index: + return "Ordre" + case .court: + return "Terrain" + case .restingTime: + return "Repos" + } + } + } + + var sortingModeCases: [SortingMode] { + var sortingModes = [SortingMode]() + sortingModes.append(.index) + sortingModes.append(.restingTime) + sortingModes.append(.court) + return sortingModes + } + + func contentUnavailableDescriptionLabel() -> String { + switch sortingMode { + case .index: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .restingTime: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .court: + return "Ce tournoi n'a aucun match prêt à démarrer" + } + } + + var sortedMatches: [Match] { + switch sortingMode { + case .index: + return readyMatches + case .restingTime: + return readyMatches.sorted(by: \.restingTimeForSorting) + case .court: + return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) + } + } + + var body: some View { + NavigationStack { + List { + Section { + Picker(selection: $selectedCourt) { + Text("Aucun").tag(nil as Int?) + ForEach(0.. Date: Sun, 20 Oct 2024 21:40:11 +0200 Subject: [PATCH 040/106] fix issues --- PadelClub/Data/GroupStage.swift | 6 +- PadelClub/Data/Match.swift | 2 +- PadelClub/Data/Tournament.swift | 3 +- PadelClub/ViewModel/MatchDescriptor.swift | 74 +++++++++++- PadelClub/ViewModel/SetDescriptor.swift | 4 +- .../Toolbox/MatchFormatStorageView.swift | 5 +- PadelClub/Views/Score/EditScoreView.swift | 106 +----------------- PadelClub/Views/Score/SetInputView.swift | 10 +- 8 files changed, 98 insertions(+), 112 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 4f07484..15eee50 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -163,11 +163,13 @@ final class GroupStage: ModelObject, Storable { for (index, team) in teams.enumerated() { team.qualified = index < tournament.qualifiedPerGroupStage if team.bracketPosition != nil && team.qualified == false { - tournamentObject()?.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil + tournamentObject()?.shouldVerifyBracket = true } } try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + if let tournamentObject = tournamentObject() { + try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) + } } catch { Logger.error(error) } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 2caee20..b7fa039 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -128,7 +128,7 @@ defer { } func matchWarningMessage() -> String { - [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") + [roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n") } func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 7d66f8a..a32d6a4 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2189,7 +2189,8 @@ defer { let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let selectedIds = newGroup.map { $0.id } - let groupIds = currentGroup.map { $0.id } + let groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() }) + let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id } let shouldBeInIt = Set(selectedIds).subtracting(groupIds) let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index 8072cb3..bbb444a 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -1,11 +1,12 @@ // -// MatchDescriptor.swift +// swift // PadelClub // // Created by Razmig Sarkissian on 02/04/2024. // import Foundation +import SwiftUI class MatchDescriptor: ObservableObject { @Published var matchFormat: MatchFormat @@ -16,6 +17,77 @@ class MatchDescriptor: ObservableObject { var teamLabelTwo: String = "" var startDate: Date = Date() var match: Match? + let colorTeamOne: Color = .teal + let colorTeamTwo: Color = .indigo + + var teamOneSetupIsActive: Bool { + if hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } + + guard let setDescriptor = setDescriptors.last else { + return false + } + if setDescriptor.valueTeamOne == nil { + return true + } else if setDescriptor.valueTeamTwo == nil { + return false + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return true + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return false + } + + return false + } + + var teamTwoSetupIsActive: Bool { + if hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } + guard let setDescriptor = setDescriptors.last else { + return false + } + + if setDescriptor.valueTeamOne == nil { + return false + } else if setDescriptor.valueTeamTwo == nil { + return true + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return false + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return true + } + + return true + } + + func getColor() -> Color { + guard let setDescriptor = setDescriptors.last else { + return .master + } + + if setDescriptor.valueTeamOne == nil { + return colorTeamOne + } else if setDescriptor.valueTeamTwo == nil { + return colorTeamTwo + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return colorTeamOne + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return colorTeamTwo + } + + return colorTeamTwo + } + + + var showSetInputView: Bool { + return setDescriptors.anySatisfy({ $0.showSetInputView }) + } + + var showTieBreakInputView: Bool { + return setDescriptors.anySatisfy({ $0.showTieBreakInputView }) + } init(match: Match? = nil) { self.match = match diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index d5b8e5f..fef0db5 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -14,7 +14,9 @@ struct SetDescriptor: Identifiable, Equatable { var tieBreakValueTeamOne: Int? var tieBreakValueTeamTwo: Int? var setFormat: SetFormat - + var showSetInputView: Bool = true + var showTieBreakInputView: Bool = false + var hasEnded: Bool { if let valueTeamTwo, let valueTeamOne { return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo) diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift index 0e03920..b389892 100644 --- a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -24,8 +24,9 @@ struct MatchFormatStorageView: View { LabeledContent { StepperView(title: "minutes", count: $estimatedDuration, step: 5) } label: { - Text("Durée \(matchFormat.format)") - Text(matchFormat.computedShortLabelWithoutPrefix) + Text("Durée \(matchFormat.format)" + matchFormat.suffix) + Text(matchFormat.longLabel) + Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) } } footer: { if estimatedDuration != matchFormat.defaultEstimatedDuration { diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 3110e4f..639d2bf 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -8,37 +8,6 @@ import SwiftUI import LeStorage -struct ArrowView: View { - var direction: ArrowDirection // Enum for left or right direction - var isActive: Bool // Whether the arrow is visible - var color: Color - @State private var isAnimating = false - - enum ArrowDirection { - case left, right - } - - var body: some View { - Image(systemName: direction == .left ? "arrow.left" : "arrow.right") - .font(.title) - .foregroundColor(isActive ? color : .clear) // Arrow visible only when active - .opacity(isAnimating ? 1.0 : 0.4) // Animate opacity between 1.0 and 0.4 - .offset(x: isAnimating ? (direction == .left ? -5 : 5) : 0) // Slight left/right movement - .animation( - Animation.easeInOut(duration: 0.7).repeatForever(autoreverses: true), - value: isAnimating - ) - .onAppear { - isAnimating = true - } - .onDisappear { - isAnimating = false - } - .padding() - } -} - - struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore @@ -46,11 +15,6 @@ struct EditScoreView: View { @StateObject var matchDescriptor: MatchDescriptor @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss - @State private var showSetInputView: Bool = true - @State private var showTieBreakInputView: Bool = false - - let colorTeamOne: Color = .teal - let colorTeamTwo: Color = .indigo init(match: Match, confirmScoreEdition: Binding) { let matchDescriptor = MatchDescriptor(match: match) @@ -90,7 +54,7 @@ struct EditScoreView: View { .bold() } } - .listRowView(isActive: teamOneSetupIsActive, color: colorTeamOne, hideColorVariation: false) + .listRowView(isActive: matchDescriptor.teamOneSetupIsActive, color: matchDescriptor.colorTeamOne, hideColorVariation: false) HStack { Spacer() VStack(alignment: .trailing) { @@ -101,7 +65,7 @@ struct EditScoreView: View { } } } - .listRowView(isActive: teamTwoSetupIsActive, color: colorTeamTwo, hideColorVariation: false, alignment: .trailing) + .listRowView(isActive: matchDescriptor.teamTwoSetupIsActive, color: matchDescriptor.colorTeamTwo, hideColorVariation: false, alignment: .trailing) } header: { if let roundTitle = matchDescriptor.match?.roundAndMatchTitle() { Text(roundTitle) @@ -133,7 +97,7 @@ struct EditScoreView: View { } } ForEach($matchDescriptor.setDescriptors) { $setDescriptor in - SetInputView(setDescriptor: $setDescriptor, showSetInputView: $showSetInputView, showTieBreakInputView: $showTieBreakInputView) + SetInputView(setDescriptor: $setDescriptor) .onChange(of: setDescriptor.hasEnded) { if setDescriptor.hasEnded { if matchDescriptor.hasEnded == false { @@ -144,7 +108,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } - .tint(getColor()) + .tint(matchDescriptor.getColor()) } if matchDescriptor.hasEnded { @@ -191,66 +155,4 @@ struct EditScoreView: View { } } } - - - var teamOneSetupIsActive: Bool { - if matchDescriptor.hasEnded && showSetInputView == false && showTieBreakInputView == false { - return false - } - - guard let setDescriptor = matchDescriptor.setDescriptors.last else { - return false - } - if setDescriptor.valueTeamOne == nil { - return true - } else if setDescriptor.valueTeamTwo == nil { - return false - } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { - return true - } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { - return false - } - - return false - } - - var teamTwoSetupIsActive: Bool { - if matchDescriptor.hasEnded && showSetInputView == false && showTieBreakInputView == false { - return false - } - guard let setDescriptor = matchDescriptor.setDescriptors.last else { - return false - } - - if setDescriptor.valueTeamOne == nil { - return false - } else if setDescriptor.valueTeamTwo == nil { - return true - } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { - return false - } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { - return true - } - - return true - } - - func getColor() -> Color { - guard let setDescriptor = matchDescriptor.setDescriptors.last else { - return .master - } - - if setDescriptor.valueTeamOne == nil { - return colorTeamOne - } else if setDescriptor.valueTeamTwo == nil { - return colorTeamTwo - } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { - return colorTeamOne - } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { - return colorTeamTwo - } - - return colorTeamTwo - } - } diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index 4fbbb64..231e168 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -9,8 +9,8 @@ import SwiftUI struct SetInputView: View { @Binding var setDescriptor: SetDescriptor - @Binding var showSetInputView: Bool - @Binding var showTieBreakInputView: Bool + @State private var showSetInputView: Bool = true + @State private var showTieBreakInputView: Bool = false var setFormat: SetFormat { setDescriptor.setFormat } @@ -137,6 +137,12 @@ struct SetInputView: View { } } } + .onChange(of: showSetInputView) { + setDescriptor.showSetInputView = showSetInputView + } + .onChange(of: showTieBreakInputView) { + setDescriptor.showTieBreakInputView = showTieBreakInputView + } .onChange(of: setDescriptor.valueTeamOne) { if let newValue = setDescriptor.valueTeamOne { if newValue == setFormat.scoreToWin - 1 && setFormat.tieBreak == 8 { From d4fedd862b68f87559bfd44a5ef9456be1b52641 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 20 Oct 2024 22:48:41 +0200 Subject: [PATCH 041/106] add resting time --- PadelClub.xcodeproj/project.pbxproj | 4 +- PadelClub/ViewModel/Screen.swift | 1 + PadelClub/Views/Team/TeamRestingView.swift | 81 ++++++++++--------- .../Tournament/TournamentBuildView.swift | 4 + .../Tournament/TournamentRunningView.swift | 11 --- .../Views/Tournament/TournamentView.swift | 2 + 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index bec9fd4..f524ebf 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3174,7 +3174,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3219,7 +3219,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/ViewModel/Screen.swift b/PadelClub/ViewModel/Screen.swift index cb4d0d6..45534b1 100644 --- a/PadelClub/ViewModel/Screen.swift +++ b/PadelClub/ViewModel/Screen.swift @@ -20,4 +20,5 @@ enum Screen: String, Codable { case broadcast case event case print + case restingTime } diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift index d576132..c76297a 100644 --- a/PadelClub/Views/Team/TeamRestingView.swift +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -8,13 +8,11 @@ import SwiftUI struct TeamRestingView: View { - @Environment(Tournament.self) var tournament - - let readyMatches: [Match] - let matchesLeft: [Match] - + @Environment(Tournament.self) var tournament: Tournament @State private var sortingMode: SortingMode = .restingTime @State private var selectedCourt: Int? + @State private var readyMatches: [Match] = [] + @State private var matchesLeft: [Match] = [] enum SortingMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } @@ -65,17 +63,16 @@ struct TeamRestingView: View { } var body: some View { - NavigationStack { - List { - Section { - Picker(selection: $selectedCourt) { - Text("Aucun").tag(nil as Int?) - ForEach(0.. Date: Mon, 21 Oct 2024 08:12:04 +0200 Subject: [PATCH 042/106] fix player search in cashier --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Data/PlayerRegistration.swift | 17 ++++++++++++++++- PadelClub/Views/Cashier/CashierView.swift | 5 +---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f524ebf..542e56d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3174,7 +3174,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3219,7 +3219,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 0047976..c0874bf 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -155,7 +155,22 @@ final class PlayerRegistration: ModelObject, Storable { } func contains(_ searchField: String) -> Bool { - firstName.canonicalVersion.localizedCaseInsensitiveContains(searchField.canonicalVersion) || lastName.canonicalVersion.localizedCaseInsensitiveContains(searchField.canonicalVersion) + let nameComponents = searchField.canonicalVersion.split(separator: " ") + + if nameComponents.count > 1 { + let pairs = nameComponents.pairs() + return pairs.contains(where: { + (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($0)) && + lastName.canonicalVersion.localizedCaseInsensitiveContains(String($1))) || + (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($1)) && + lastName.canonicalVersion.localizedCaseInsensitiveContains(String($0))) + }) + } else { + return nameComponents.contains { component in + firstName.canonicalVersion.localizedCaseInsensitiveContains(component) || + lastName.canonicalVersion.localizedCaseInsensitiveContains(component) + } + } } func isSameAs(_ player: PlayerRegistration) -> Bool { diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index 9189735..ef95e65 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -70,10 +70,7 @@ class CashierViewModel: ObservableObject { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { if searchText.isEmpty == false { - sortOption.shouldDisplayPlayer(player) - && filterOption.shouldDisplayPlayer(player) - && presenceFilterOption.shouldDisplayPlayer(player) - && player.contains(searchText) + player.contains(searchText) } else { sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) From 585cb9cf9fc8cffbb16830f119fbcca3848c6ac7 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 21 Oct 2024 08:28:22 +0200 Subject: [PATCH 043/106] fix follow up matches view picker size --- PadelClub/Views/Score/FollowUpMatchView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 5eea9a2..3ae74cc 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -177,7 +177,6 @@ struct FollowUpMatchView: View { } .labelsHidden() .pickerStyle(.segmented) - .fixedSize() } .headerProminence(.increased) .textCase(nil) From 8eadd30759571ec638af1f41c7e526629aaa1774 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 21 Oct 2024 09:37:14 +0200 Subject: [PATCH 044/106] add maintenance tenup message --- PadelClub.xcodeproj/project.pbxproj | 4 +- .../ViewModel/FederalDataViewModel.swift | 3 +- .../Navigation/Agenda/ActivityView.swift | 39 ++++++++++++++----- .../Agenda/TournamentLookUpView.swift | 6 +++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 542e56d..0e37f4d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3174,7 +3174,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3219,7 +3219,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 71c0cf1..a5579b7 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -22,7 +22,8 @@ class FederalDataViewModel { var searchAttemptCount: Int = 0 var dayDuration: Int? var dayPeriod: DayPeriod = .all - + var lastError: NetworkManagerError? + func filterStatus() -> String { var labels: [String] = [] labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 58eb1fb..48bdf11 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -134,8 +134,12 @@ struct ActivityView: View { ContentUnavailableView { Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") } description: { - Text(error.localizedDescription) + Text("Tenup est peut-être en maintenance. " + error.localizedDescription) } actions: { + Link(destination: URLs.tenup.url) { + Text("Voir si tenup est en maintenance") + } + RowButtonView("D'accord.") { self.error = nil } @@ -510,15 +514,32 @@ struct ActivityView: View { .padding() } } else { - ContentUnavailableView { - Label("Aucun tournoi", systemImage: "shield.slash") - } description: { - Text("Aucun tournoi ne correspond aux critères sélectionnés.") - } actions: { - FooterButtonView("modifier vos critères de recherche") { - displaySearchView = true + if federalDataViewModel.lastError == nil { + ContentUnavailableView { + Label("Aucun tournoi", systemImage: "shield.slash") + } description: { + Text("Aucun tournoi ne correspond aux critères sélectionnés.") + } actions: { + FooterButtonView("modifier vos critères de recherche") { + displaySearchView = true + } + .padding() + } + } else { + ContentUnavailableView { + Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") + } description: { + Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.") + } actions: { + Link(destination: URLs.tenup.url) { + Text("Voir si tenup est en maintenance") + } + + FooterButtonView("modifier vos critères de recherche") { + displaySearchView = true + } + .padding() } - .padding() } } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 396fb0d..93e9aa6 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -181,6 +181,7 @@ struct TournamentLookUpView: View { federalDataViewModel.levels = Set(levels) federalDataViewModel.categories = Set(categories) federalDataViewModel.ageCategories = Set(ages) + federalDataViewModel.lastError = nil Task { await getNewPage() @@ -223,7 +224,12 @@ struct TournamentLookUpView: View { await getNewBuildForm() } else { let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup) + if commands.anySatisfy({ $0.command == "alert" }) { + federalDataViewModel.lastError = .maintenance + } + let resultCommand = commands.first(where: { $0.results != nil }) + if let newTournaments = resultCommand?.results?.items { newTournaments.forEach { ft in // let isValid = ft.tournaments.anySatisfy({ build in From 9291d63d054ed8b9fe26ade5a78b4c8cdf272f11 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 22 Oct 2024 09:39:26 +0200 Subject: [PATCH 045/106] drawlog wip --- PadelClub.xcodeproj/project.pbxproj | 16 ++++ PadelClub/Data/DrawLog.swift | 80 +++++++++++++++++++ PadelClub/Data/Match.swift | 16 +--- PadelClub/Data/TeamRegistration.swift | 28 ++++++- PadelClub/Data/Tournament.swift | 42 +++++++--- PadelClub/Data/TournamentStore.swift | 2 + PadelClub/Utils/PadelRule.swift | 9 +++ PadelClub/Views/Round/DrawLogsView.swift | 51 ++++++++++++ PadelClub/Views/Round/RoundSettingsView.swift | 23 ++++++ 9 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 PadelClub/Data/DrawLog.swift create mode 100644 PadelClub/Views/Round/DrawLogsView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 0e37f4d..9f57c36 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -424,6 +424,12 @@ FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; @@ -1066,6 +1072,8 @@ FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = ""; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = ""; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = ""; }; + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = ""; }; + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; @@ -1356,6 +1364,7 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9022B9479B900EA7F5A /* Federal */, ); @@ -1871,6 +1880,7 @@ FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */, ); path = Round; sourceTree = ""; @@ -2331,6 +2341,7 @@ C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, @@ -2391,6 +2402,7 @@ FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2605,6 +2617,7 @@ FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, @@ -2665,6 +2678,7 @@ FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2858,6 +2872,7 @@ FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, @@ -2918,6 +2933,7 @@ FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, diff --git a/PadelClub/Data/DrawLog.swift b/PadelClub/Data/DrawLog.swift new file mode 100644 index 0000000..3d9cb10 --- /dev/null +++ b/PadelClub/Data/DrawLog.swift @@ -0,0 +1,80 @@ +// +// DrawLog.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import Foundation +import SwiftUI +import LeStorage + +@Observable +final class DrawLog: ModelObject, Storable { + static func resourceName() -> String { return "draw-logs" } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + static func filterByStoreIdentifier() -> Bool { return false } + static var relationshipNames: [String] = [] + + var id: String = Store.randomId() + var tournament: String + var drawDate: Date = Date() + var drawSeed: Int? + var drawPosition: Int + var drawTeamPosition: TeamPosition + + internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int?, drawPosition: Int, drawTeamPosition: TeamPosition) { + self.id = id + self.tournament = tournament + self.drawDate = drawDate + self.drawSeed = drawSeed + self.drawPosition = drawPosition + self.drawTeamPosition = drawTeamPosition + } + + func exportedDrawLog() -> String { + [drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ") + } + + func localizedDrawSeedLabel() -> String { + if let drawSeed { + return "Tête de série #\(drawSeed + 1)" + } else { + return "Tête de série non trouvé" + } + } + + func localizedDrawLogLabel() -> String { + return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ") + } + + func localizedDrawBranch() -> String { + drawTeamPosition.localizedBranchLabel() + } + + func positionLabel() -> String { + let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawPosition) + return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawPosition })?.roundAndMatchTitle() ?? "" + } + + var tournamentStore: TournamentStore { + return TournamentStore.instance(tournamentId: self.tournament) + } + + override func deleteDependencies() throws { + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _drawDate = "drawDate" + case _drawSeed = "drawSeed" + case _drawPosition = "drawPosition" + case _drawTeamPosition = "drawTeamPosition" + } + + func insertOnServer() throws { + self.tournamentStore.drawLogs.writeChangeAndInsertOnServer(instance: self) + } + +} diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index b7fa039..214665f 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -164,22 +164,8 @@ defer { } @discardableResult - func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { + func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int { let matchIndex = index - var teamPosition : TeamPosition { - if let slot { - return slot - } else { - let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) - let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) - let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) - var teamPosition = slot ?? (isUpper ? .one : .two) - if opposingSeeding { - teamPosition = slot ?? (isUpper ? .two : .one) - } - return teamPosition - } - } previousMatch(teamPosition)?.disableMatch() return matchIndex * 2 + teamPosition.rawValue } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1b46a68..6993c4b 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -116,13 +116,37 @@ final class TeamRegistration: ModelObject, Storable { } func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { - let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) + var teamPosition : TeamPosition { + if let slot { + return slot + } else { + let matchIndex = match.index + let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) + let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) + var teamPosition = slot ?? (isUpper ? .one : .two) + if opposingSeeding { + teamPosition = slot ?? (isUpper ? .two : .one) + } + return teamPosition + } + } + + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) tournamentObject()?.resetTeamScores(in: bracketPosition) self.bracketPosition = seedPosition if groupStagePosition != nil && qualified == false { qualified = true } - tournamentObject()?.updateTeamScores(in: bracketPosition) + if let tournament = tournamentObject() { + let drawLog = DrawLog(tournament: tournament.id, drawSeed: index(in: tournament.selectedSortedTeams()), drawPosition: match.index, drawTeamPosition: teamPosition) + do { + try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) + } catch { + Logger.error(error) + } + tournament.updateTeamScores(in: bracketPosition) + } } func expectedSummonDate() -> Date? { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a32d6a4..188317a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -335,6 +335,12 @@ final class Tournament : ModelObject, Storable { override func deleteDependencies() throws { let store = self.tournamentStore + let drawLogs = self.tournamentStore.drawLogs + for drawLog in drawLogs { + try drawLog.deleteDependencies() + } + store.drawLogs.deleteDependencies(drawLogs) + let teams = self.tournamentStore.teamRegistrations for team in teams { try team.deleteDependencies() @@ -727,16 +733,13 @@ defer { let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeeds = seeds(inSeedGroup: seedGroup) - if seedGroup == SeedInterval(first: 3, last: 4) { - print("availableSeedGroup == SeedInterval(first: 3, last: 4)") - if availableSeedSpot.count == 6 { - var spots = [Match]() - spots.append(availableSeedSpot[1]) - spots.append(availableSeedSpot[4]) - spots = spots.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) - } + if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { + var spots = [Match]() + spots.append(availableSeedSpot[1]) + spots.append(availableSeedSpot[4]) + spots = spots.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } } else { if availableSeeds.count <= availableSeedSpot.count { @@ -2260,6 +2263,25 @@ defer { rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } } + func seedsCount() -> Int { + teamCount - groupStageSpots() + } + + func lastDrawnDate() -> Date? { + drawLogs().last?.drawDate + } + + func drawLogs() -> [DrawLog] { + self.tournamentStore.drawLogs.sorted(by: \.drawDate) + } + + func exportedDrawLogs() -> String { + var logs : [String] = ["Journal des tirages\n\n"] + logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) + return logs.joined() + } + + // MARK: - func insertOnServer() throws { diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index a01e6a0..d210849 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -26,6 +26,7 @@ class TournamentStore: Store, ObservableObject { fileprivate(set) var teamScores: StoredCollection = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection = StoredCollection.placeholder() + fileprivate(set) var drawLogs: StoredCollection = StoredCollection.placeholder() convenience init(tournament: Tournament) { self.init(identifier: tournament.id, parameter: "tournament") @@ -51,6 +52,7 @@ class TournamentStore: Store, ObservableObject { self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) + self.drawLogs = self.registerCollection(synchronized: false, indexed: indexed) self.loadCollectionsFromServerIfNoFile() diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 72abedc..d3b1d45 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { return shortName } } + + func localizedBranchLabel() -> String { + switch self { + case .one: + return "Branche du haut" + case .two: + return "Branche du bas" + } + } } enum SetFormat: Int, Hashable, Codable { diff --git a/PadelClub/Views/Round/DrawLogsView.swift b/PadelClub/Views/Round/DrawLogsView.swift new file mode 100644 index 0000000..83f0358 --- /dev/null +++ b/PadelClub/Views/Round/DrawLogsView.swift @@ -0,0 +1,51 @@ +// +// DrawLogsView.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import SwiftUI +import LeStorage + +struct DrawLogsView: View { + @Environment(Tournament.self) var tournament + + var drawLogs: [DrawLog] { + tournament.drawLogs().reversed() + } + + var body: some View { + List { + ForEach(drawLogs) { drawLog in + LabeledContent { + Text(drawLog.localizedDrawBranch()) + } label: { + Text(drawLog.localizedDrawSeedLabel()) + Text(drawLog.positionLabel()) + Text(drawLog.drawDate.localizedDate()) + } + } + } + .overlay(content: { + if drawLogs.isEmpty { + ContentUnavailableView("Aucun tirage", systemImage: "dice", description: Text("Aucun tirage au sort n'a été effectué.")) + } + }) + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + ShareLink(item: tournament.exportedDrawLogs()) { + Label("Partager les tirages", systemImage: "square.and.arrow.up") + .labelStyle(.titleAndIcon) + } + } label: { + LabelOptions() + } + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Journal des tirages") + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 3dcd33e..d58cd1a 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -73,7 +73,30 @@ struct RoundSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") } } + + Section { + NavigationLink { + DrawLogsView() + .environment(tournament) + } label: { + Text("Gestionnaire des tirages au sort") + } + + if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil { + + NavigationLink { + } label: { + Text("Aperçu du décalage") + } + RowButtonView("Décaler les têtes de série", role: .destructive) { + + } + } + } footer: { + Text("Vous avez une place libre dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + } + // Section { // RowButtonView("Enabled", role: .destructive) { // let allMatches = tournament._allMatchesIncludingDisabled() From 82e1f6a342c08962190597be015c133109cc062b Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 22 Oct 2024 09:42:50 +0200 Subject: [PATCH 046/106] fix regression seeding position 3/4 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Data/Tournament.swift | 17 +++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 0e37f4d..729362f 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3174,7 +3174,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3198,7 +3198,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3219,7 +3219,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3242,7 +3242,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.22; + MARKETING_VERSION = 1.0.23; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a32d6a4..de704a1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -727,16 +727,13 @@ defer { let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeeds = seeds(inSeedGroup: seedGroup) - if seedGroup == SeedInterval(first: 3, last: 4) { - print("availableSeedGroup == SeedInterval(first: 3, last: 4)") - if availableSeedSpot.count == 6 { - var spots = [Match]() - spots.append(availableSeedSpot[1]) - spots.append(availableSeedSpot[4]) - spots = spots.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) - } + if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { + var spots = [Match]() + spots.append(availableSeedSpot[1]) + spots.append(availableSeedSpot[4]) + spots = spots.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } } else { if availableSeeds.count <= availableSeedSpot.count { From 77b3f27685a26a1b6c3e4d833240edd572af3279 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 09:23:59 +0200 Subject: [PATCH 047/106] draw log final implementation fix scheduler + unavailability stuff add team resting view --- PadelClub.xcodeproj/project.pbxproj | 8 + PadelClub/Data/DrawLog.swift | 47 +++-- PadelClub/Data/Match.swift | 11 +- PadelClub/Data/MatchScheduler.swift | 19 +- PadelClub/Data/TeamRegistration.swift | 38 +++- PadelClub/Data/Tournament.swift | 107 ++++++++++- PadelClub/Data/TournamentStore.swift | 2 +- .../Match/Components/PlayerBlockView.swift | 12 +- PadelClub/Views/Round/DrawLogsView.swift | 30 +++- .../Round/PreviewBracketPositionView.swift | 107 +++++++++++ PadelClub/Views/Round/RoundSettingsView.swift | 76 +------- PadelClub/Views/Round/RoundView.swift | 167 ++++++++---------- PadelClub/Views/Team/TeamRestingView.swift | 103 ++++------- PadelClub/Views/Team/TeamRowView.swift | 140 ++++++++++----- PadelClubTests/ServerDataTests.swift | 18 ++ 15 files changed, 573 insertions(+), 312 deletions(-) create mode 100644 PadelClub/Views/Round/PreviewBracketPositionView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 9f57c36..fec25c6 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -430,6 +430,9 @@ FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; @@ -1074,6 +1077,7 @@ FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = ""; }; FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = ""; }; FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = ""; }; + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; @@ -1881,6 +1885,7 @@ FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */, + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */, ); path = Round; sourceTree = ""; @@ -2454,6 +2459,7 @@ FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, @@ -2730,6 +2736,7 @@ FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, @@ -2985,6 +2992,7 @@ FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, diff --git a/PadelClub/Data/DrawLog.swift b/PadelClub/Data/DrawLog.swift index 3d9cb10..29cf948 100644 --- a/PadelClub/Data/DrawLog.swift +++ b/PadelClub/Data/DrawLog.swift @@ -19,29 +19,40 @@ final class DrawLog: ModelObject, Storable { var id: String = Store.randomId() var tournament: String var drawDate: Date = Date() - var drawSeed: Int? - var drawPosition: Int + var drawSeed: Int + var drawMatchIndex: Int var drawTeamPosition: TeamPosition - internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int?, drawPosition: Int, drawTeamPosition: TeamPosition) { + internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) { self.id = id self.tournament = tournament self.drawDate = drawDate self.drawSeed = drawSeed - self.drawPosition = drawPosition + self.drawMatchIndex = drawMatchIndex self.drawTeamPosition = drawTeamPosition } + func tournamentObject() -> Tournament? { + Store.main.findById(self.tournament) + } + + func computedBracketPosition() -> Int { + drawMatchIndex * 2 + drawTeamPosition.rawValue + } + + func updateTeamBracketPosition(_ team: TeamRegistration) { + guard let match = drawMatch() else { return } + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition) + team.bracketPosition = seedPosition + tournamentObject()?.updateTeamScores(in: seedPosition) + } + func exportedDrawLog() -> String { [drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ") } func localizedDrawSeedLabel() -> String { - if let drawSeed { - return "Tête de série #\(drawSeed + 1)" - } else { - return "Tête de série non trouvé" - } + return "Tête de série #\(drawSeed + 1)" } func localizedDrawLogLabel() -> String { @@ -52,11 +63,23 @@ final class DrawLog: ModelObject, Storable { drawTeamPosition.localizedBranchLabel() } + func drawMatch() -> Match? { + let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) + return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) + } + func positionLabel() -> String { - let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawPosition) - return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawPosition })?.roundAndMatchTitle() ?? "" + return drawMatch()?.roundAndMatchTitle() ?? "" } + func roundLabel() -> String { + return drawMatch()?.roundTitle() ?? "" + } + + func matchLabel() -> String { + return drawMatch()?.matchTitle() ?? "" + } + var tournamentStore: TournamentStore { return TournamentStore.instance(tournamentId: self.tournament) } @@ -69,7 +92,7 @@ final class DrawLog: ModelObject, Storable { case _tournament = "tournament" case _drawDate = "drawDate" case _drawSeed = "drawSeed" - case _drawPosition = "drawPosition" + case _drawMatchIndex = "drawMatchIndex" case _drawTeamPosition = "drawTeamPosition" } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 214665f..4573827 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -233,6 +233,7 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() currentTournament()?.updateTournamentState() + teams().forEach({ $0.resetRestingTime() }) } func resetScores() { @@ -534,7 +535,7 @@ defer { if endDate == nil { endDate = Date() } - + teams().forEach({ $0.resetRestingTime() }) winningTeamId = teamScoreWinning.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() @@ -557,7 +558,9 @@ defer { teamOne?.hasArrived() teamTwo?.hasArrived() - + teamOne?.resetRestingTime() + teamTwo?.resetRestingTime() + winningTeamId = teamOne?.id losingTeamId = teamTwo?.id @@ -923,6 +926,10 @@ defer { (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow } + func isValidSpot() -> Bool { + previousMatches().allSatisfy({ $0.isSeeded() == false }) + } + enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 38e8f87..3eb7450 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -493,9 +493,17 @@ final class MatchScheduler : ModelObject, Storable { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { print("Handling break time conflicts or waiting for free courts") let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } - let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) - let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) + var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + if let courtsUnavailability, previousEndDate != nil { + previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + + if let courtsUnavailability, previousEndDateNoBreak != nil { + previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } if let previousEndDate, let previousEndDateNoBreak { @@ -651,9 +659,14 @@ final class MatchScheduler : ModelObject, Storable { } if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { - print("All courts in rotation \(rotationIndex) are free") + print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)") } + if let courtsUnavailability { + let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability) + return computedStartDateAndCourts.earliestFreeDate + } + return minimumTargetedEndDate } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 6993c4b..1d8d414 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -139,11 +139,13 @@ final class TeamRegistration: ModelObject, Storable { qualified = true } if let tournament = tournamentObject() { - let drawLog = DrawLog(tournament: tournament.id, drawSeed: index(in: tournament.selectedSortedTeams()), drawPosition: match.index, drawTeamPosition: teamPosition) - do { - try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) - } catch { - Logger.error(error) + if let index = index(in: tournament.selectedSortedTeams()) { + let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition) + do { + try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) + } catch { + Logger.error(error) + } } tournament.updateTeamScores(in: bracketPosition) } @@ -556,8 +558,21 @@ final class TeamRegistration: ModelObject, Storable { } } + var _cachedRestingTime: (Bool, Date?)? + func restingTime() -> Date? { - matches().sorted(by: \.computedEndDateForSorting).last?.endDate + if let _cachedRestingTime { return _cachedRestingTime.1 } + let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate + _cachedRestingTime = (true, restingTime) + return restingTime + } + + func resetRestingTime() { + _cachedRestingTime = nil + } + + var restingTimeForSorting: Date { + restingTime()! } func teamNameLabel() -> String { @@ -568,6 +583,17 @@ final class TeamRegistration: ModelObject, Storable { } } + func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { + if let bracketPosition, let drawMatchIndex { + return drawMatchIndex != bracketPosition + } else if let bracketPosition { + return true + } else if let drawMatchIndex { + return true + } + return false + } + enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 188317a..e1d6a4d 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1,5 +1,5 @@ // -// Tournament.swift +// swift // PadelClub // // Created by Laurent Morvillier on 02/02/2024. @@ -558,7 +558,7 @@ defer { return endDate != nil } - func state() -> Tournament.State { + func state() -> State { if self.isCanceled == true { return .canceled } @@ -2275,6 +2275,109 @@ defer { self.tournamentStore.drawLogs.sorted(by: \.drawDate) } + func seedSpotsLeft() -> Bool { + let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false }) + if alreadySeededRounds.isEmpty { return true } + let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() } + + return spotsLeft.isEmpty == false + } + + func isRoundValidForSeeding(roundIndex: Int) -> Bool { + if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) { + return roundIndex >= lastRoundWithSeeds.index + } else { + return true + } + } + + + func updateSeedsBracketPosition() async { + await removeAllSeeds() + let drawLogs = drawLogs().reversed() + let seeds = seeds() + for (index, seed) in seeds.enumerated() { + if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { + drawLog.updateTeamBracketPosition(seed) + } + } + + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds) + } catch { + Logger.error(error) + } + } + + func removeAllSeeds() async { + unsortedTeams().forEach({ team in + team.bracketPosition = nil + }) + let ts = allRoundMatches().flatMap { match in + match.teamScores + } + + do { + try tournamentStore.teamScores.delete(contentOfs: ts) + } catch { + Logger.error(error) + } + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + allRounds().forEach({ round in + round.enableRound() + }) + + } + + func addNewRound(_ roundIndex: Int) async { + let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) + let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) + let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) + let nextRound = round.nextRound() + var currentIndex = 0 + let matches = (0.. String { var logs : [String] = ["Journal des tirages\n\n"] logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index d210849..78e593b 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -52,7 +52,7 @@ class TournamentStore: Store, ObservableObject { self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) - self.drawLogs = self.registerCollection(synchronized: false, indexed: indexed) + self.drawLogs = self.registerCollection(synchronized: synchronized, indexed: indexed) self.loadCollectionsFromServerIfNoFile() diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 11c7ee8..5901d37 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -78,16 +78,8 @@ struct PlayerBlockView: View { } } - if displayRestingTime, let restingTime = team?.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { - if restingTime > -300 { - Text("vient de finir") - .font(.footnote) - .foregroundStyle(.secondary) - } else { - Text("en repos depuis " + value) - .font(.footnote) - .foregroundStyle(.secondary) - } + if displayRestingTime, let team { + TeamRowView.TeamRestingView(team: team) } } .bold(hasWon) diff --git a/PadelClub/Views/Round/DrawLogsView.swift b/PadelClub/Views/Round/DrawLogsView.swift index 83f0358..6459955 100644 --- a/PadelClub/Views/Round/DrawLogsView.swift +++ b/PadelClub/Views/Round/DrawLogsView.swift @@ -18,12 +18,20 @@ struct DrawLogsView: View { var body: some View { List { ForEach(drawLogs) { drawLog in - LabeledContent { - Text(drawLog.localizedDrawBranch()) - } label: { - Text(drawLog.localizedDrawSeedLabel()) - Text(drawLog.positionLabel()) - Text(drawLog.drawDate.localizedDate()) + HStack { + VStack(alignment: .leading) { + Text(drawLog.localizedDrawSeedLabel()) + Text(drawLog.drawDate.localizedDate()) + .font(.footnote) + .foregroundStyle(.secondary) + } + Spacer() + VStack(alignment: .trailing) { + Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + .font(.footnote) + .foregroundStyle(.secondary) + } } } } @@ -39,6 +47,16 @@ struct DrawLogsView: View { Label("Partager les tirages", systemImage: "square.and.arrow.up") .labelStyle(.titleAndIcon) } + + Divider() + + Button("Tout effacer", role: .destructive) { + do { + try tournament.tournamentStore.drawLogs.deleteAll() + } catch { + Logger.error(error) + } + } } label: { LabelOptions() } diff --git a/PadelClub/Views/Round/PreviewBracketPositionView.swift b/PadelClub/Views/Round/PreviewBracketPositionView.swift new file mode 100644 index 0000000..f906247 --- /dev/null +++ b/PadelClub/Views/Round/PreviewBracketPositionView.swift @@ -0,0 +1,107 @@ +// +// PreviewBracketPositionView.swift +// PadelClub +// +// Created by razmig on 23/10/2024. +// + +import SwiftUI + +struct PreviewBracketPositionView: View { + let seeds: [TeamRegistration] + let drawLogs: [DrawLog] + + @State private var filterOption: PreviewBracketPositionFilterOption = .difference + + enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case all + case difference + case summon + + func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool { + switch self { + case .all: + true + case .difference: + team.isDifferentPosition(drawLog?.computedBracketPosition()) == true + case .summon: + team.callDate != drawLog?.drawMatch()?.startDate + } + } + } + + var body: some View { + List { + Section { + ForEach(seeds.indices, id: \.self) { seedIndex in + let seed = seeds[seedIndex] + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + if filterOption.isDisplayable(seed, drawLog: drawLog) { + HStack { + VStack(alignment: .leading) { + Text("Tête de série #\(seedIndex + 1)").font(.caption) + TeamRowView.TeamView(team: seed) + TeamRowView.TeamCallDateView(team: seed) + } + Spacer() + if let drawLog { + VStack(alignment: .trailing) { + Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption) + Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + if let expectedDate = drawLog.drawMatch()?.startDate { + Text(expectedDate.localizedDate()) + .font(.caption) + } else { + Text("Aucun horaire") + .font(.caption) + } + } + } + } + .listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true) + } + } + } header: { + Picker(selection: $filterOption) { + Text("Tous").tag(PreviewBracketPositionFilterOption.all) + Text("Changements").tag(PreviewBracketPositionFilterOption.difference) + Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon) + } label: { + Text("Filter") + } + .labelsHidden() + .pickerStyle(.segmented) + .textCase(nil) + } + } + .overlay(content: { + if seeds.isEmpty { + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi.")) + } else if filterOption == .difference, noSeedHasDifferentPlace() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } else if filterOption == .summon, noSeedHasDifferentSummon() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Aperçu du tableau tiré") + + } + + func noSeedHasDifferentPlace() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false + }) + } + + func noSeedHasDifferentSummon() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.callDate == drawLog?.drawMatch()?.startDate + }) + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index d58cd1a..35311cb 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -82,19 +82,19 @@ struct RoundSettingsView: View { Text("Gestionnaire des tirages au sort") } - if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil { + if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil, tournament.seedSpotsLeft() { NavigationLink { - + PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs()) } label: { - Text("Aperçu du décalage") + Text("Aperçu du repositionnement") } - RowButtonView("Décaler les têtes de série", role: .destructive) { - + RowButtonView("Replacer toutes les têtes de série", role: .destructive) { + await tournament.updateSeedsBracketPosition() } } } footer: { - Text("Vous avez une place libre dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") } // Section { @@ -150,72 +150,12 @@ struct RoundSettingsView: View { } private func _removeAllSeeds() async { - tournament.unsortedTeams().forEach({ team in - team.bracketPosition = nil - }) - let ts = tournament.allRoundMatches().flatMap { match in - match.teamScores - } - - do { - try tournamentStore.teamScores.delete(contentOfs: ts) - } catch { - Logger.error(error) - } - do { - try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - } catch { - Logger.error(error) - } - tournament.allRounds().forEach({ round in - round.enableRound() - }) + await tournament.removeAllSeeds() self.isEditingTournamentSeed.wrappedValue = true } private func _addNewRound(_ roundIndex: Int) async { - let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. 0 { let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) TipView(bracketTip).tipStyle(tint: .green, asSection: true) - + let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) - + if upperRound.round.hasStarted() == false, leftToPlay >= 0 { Section { LabeledContent { @@ -96,7 +96,7 @@ struct RoundView: View { showPrintScreen = true } .tipStyle(tint: .master, asSection: true) - + if upperRound.round.index > 0 { let correspondingLoserRoundTitle = upperRound.round.correspondingLoserRoundTitle() Section { @@ -121,9 +121,10 @@ struct RoundView: View { } } } else { + let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index) let availableSeeds = tournament.availableSeeds() let availableQualifiedTeams = tournament.availableQualifiedTeams() - + if availableSeeds.isEmpty == false, let availableSeedGroup { Section { RowButtonView("Placer \(availableSeedGroup.localizedInterval())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { @@ -148,94 +149,53 @@ struct RoundView: View { } } } - + if availableQualifiedTeams.isEmpty == false { let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft - if availableSeedSpot.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableQualifiedTeams) { team in - NavigationLink { - - SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in - Task { - results.forEach { drawResult in - if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { - team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) - } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { - team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) - } + Section { + DisclosureGroup { + ForEach(availableQualifiedTeams) { team in + NavigationLink { + + SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in + Task { + results.forEach { drawResult in + if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { + team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) + } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { + team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) } - - _save(seeds: [team]) } + + _save(seeds: [team]) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + .disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false) } - } header: { - Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } label: { + Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + } + } header: { + Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } footer: { + if availableSeedSpot.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible !") + .foregroundStyle(.red) } } } if availableSeeds.isEmpty == false { - if seedSpaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) - } - - _save(seeds: [team]) - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } else if spaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: spaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) - } - _save(seeds: [team]) - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } + let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft + let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true + _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding) } } - + if isEditingTournamentSeed.wrappedValue == true { let slideToDelete = SlideToDeleteSeedTip() TipView(slideToDelete).tipStyle(tint: .logoRed, asSection: true) @@ -259,11 +219,11 @@ struct RoundView: View { } } - #if DEBUG +#if DEBUG Spacer() Text(match.index.formatted() + " " + match.teamScores.count.formatted()) - #endif +#endif } } footer: { if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true { @@ -351,7 +311,7 @@ struct RoundView: View { return spots } } - + private func _save(seeds: [TeamRegistration]) { do { @@ -363,10 +323,10 @@ struct RoundView: View { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } - + _refreshNames() } - + private func _save() { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false @@ -399,13 +359,40 @@ struct RoundView: View { } } } + + private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View { + Section { + DisclosureGroup { + ForEach(availableSeeds) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: spots) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) + } + + _save(seeds: [team]) + } + } + } label: { + TeamRowView(team: team, displayCallDate: false) + } + .disabled(spots.isEmpty || isRoundValidForSeeding == false) + } + } label: { + Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) + } + } header: { + Text("Tirage au sort visuel d'une tête de série").font(.subheadline) + } footer: { + if spots.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.") + .foregroundStyle(.red) + } + } + } } -//#Preview { -// RoundView(round: Round.mock()) -// .environment(Tournament.mock()) -//} - struct MatchSpot: SpinDrawable { let match: Match let teamPosition: TeamPosition diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift index c76297a..548e218 100644 --- a/PadelClub/Views/Team/TeamRestingView.swift +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -9,111 +9,81 @@ import SwiftUI struct TeamRestingView: View { @Environment(Tournament.self) var tournament: Tournament - @State private var sortingMode: SortingMode = .restingTime + @State private var displayMode: DisplayMode = .teams @State private var selectedCourt: Int? @State private var readyMatches: [Match] = [] @State private var matchesLeft: [Match] = [] + @State private var teams: [TeamRegistration] = [] - enum SortingMode: Int, Identifiable, CaseIterable { + enum DisplayMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } - case index + case teams case restingTime - case court func localizedSortingModeLabel() -> String { switch self { - case .index: - return "Ordre" - case .court: - return "Terrain" + case .teams: + return "Équipes" case .restingTime: - return "Repos" + return "Matchs" } } } - var sortingModeCases: [SortingMode] { - var sortingModes = [SortingMode]() - sortingModes.append(.index) - sortingModes.append(.restingTime) - sortingModes.append(.court) - return sortingModes - } - func contentUnavailableDescriptionLabel() -> String { - switch sortingMode { - case .index: - return "Ce tournoi n'a aucun match prêt à démarrer" + switch displayMode { case .restingTime: return "Ce tournoi n'a aucun match prêt à démarrer" - case .court: - return "Ce tournoi n'a aucun match prêt à démarrer" + case .teams: + return "Ce tournoi n'a aucune équipe ayant déjà terminé un match." } } var sortedMatches: [Match] { - switch sortingMode { - case .index: - return readyMatches - case .restingTime: - return readyMatches.sorted(by: \.restingTimeForSorting) - case .court: - return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) - } + return readyMatches.sorted(by: \.restingTimeForSorting) } - + + var sortedTeams: [TeamRegistration] { + return teams + } + var body: some View { List { Section { - Picker(selection: $selectedCourt) { - Text("Aucun").tag(nil as Int?) - ForEach(0.. -300 { + Text("vient de finir") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("en repos depuis " + value) + .font(.footnote) + .foregroundStyle(.secondary) } + } + } + } + + struct TeamView: View { + let team: TeamRegistration - if let name = team.name, name.isEmpty == false { - Text(name).foregroundStyle(.secondary).font(.footnote) - if team.players().isEmpty { - Text("Aucun joueur") - } else { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) - } - } + var body: some View { + if let name = team.name, name.isEmpty == false { + Text(name).foregroundStyle(.secondary).font(.footnote) + if team.players().isEmpty { + Text("Aucun joueur") + } else { + CompactTeamView(team: team) + } + } else { + if team.players().isEmpty == false { + CompactTeamView(team: team) } else { - if team.players().isEmpty == false { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + Text("Place réservée") + Text("Place réservée") + } + } + } + } + + struct TeamHeadlineView: View { + let team: TeamRegistration + + var body: some View { + HStack { + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle(.title)) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) } - } else { - Text("Place réservée") - Text("Place réservée") } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) } - } - if displayCallDate { - if let callDate = team.callDate { - Text("Déjà convoquée \(callDate.localizedDate())") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) - } else { - Text("Pas encore convoquée") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) + + if let wildcardLabel = team.wildcardLabel() { + Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) } } } } -} -//#Preview { -// TeamRowView(team: TeamRegistration.mock()) -//} + struct TeamCallDateView: View { + let team: TeamRegistration + + var body: some View { + if let callDate = team.callDate { + Text("Déjà convoquée \(callDate.localizedDate())") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } else { + Text("Pas encore convoquée") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } + } + } + + struct CompactTeamView: View { + let team: TeamRegistration + + var body: some View { + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + } + } + } +} diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index f1a988e..559e88e 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -369,4 +369,22 @@ final class ServerDataTests: XCTestCase { } + func testDrawLog() async throws { + + let tournament: [Tournament] = try await StoreCenter.main.service().get() + guard let tournamentId = tournament.first?.id else { + assertionFailure("missing tournament in database") + return + } + + let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two) + let d: DrawLog = try await StoreCenter.main.service().post(drawLog) + + assert(d.tournament == drawLog.tournament) + assert(d.drawDate.formatted() == drawLog.drawDate.formatted()) + assert(d.drawSeed == drawLog.drawSeed) + assert(d.drawTeamPosition == drawLog.drawTeamPosition) + assert(d.drawMatchIndex == drawLog.drawMatchIndex) + } + } From 5496bf2e964823df0411d1237b594a456a52c696 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 12:39:19 +0200 Subject: [PATCH 048/106] improve unavailibity view --- PadelClub/Data/Tournament.swift | 2 +- .../CourtAvailabilitySettingsView.swift | 210 ++++++++++++------ PadelClub/Views/Round/RoundSettingsView.swift | 9 +- 3 files changed, 143 insertions(+), 78 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index e1d6a4d..3dc56c1 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -2264,7 +2264,7 @@ defer { } func seedsCount() -> Int { - teamCount - groupStageSpots() + selectedSortedTeams().count - groupStageSpots() } func lastDrawnDate() -> Date? { diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 9ade115..93d52fa 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View { let event: Event @State private var showingPopover: Bool = false - @State private var courtIndex: Int = 0 - @State private var startDate: Date = Date() - @State private var endDate: Date = Date() @State private var editingSlot: DateInterval? var courtsUnavailability: [Int: [DateInterval]] { @@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View { } Button("éditer") { editingSlot = dateInterval - courtIndex = dateInterval.courtIndex - startDate = dateInterval.startDate - endDate = dateInterval.endDate - showingPopover = true } Button("effacer", role: .destructive) { do { @@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View { Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") } actions: { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -130,66 +119,10 @@ struct CourtAvailabilitySettingsView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Créneau indisponible") .sheet(isPresented: $showingPopover) { - NavigationStack { - Form { - Section { - CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount) - } - - Section { - DatePicker("Début", selection: $startDate) - .onChange(of: startDate) { - if endDate < startDate { - endDate = startDate.addingTimeInterval(90*60) - } - } - DatePicker("Fin", selection: $endDate) - .onChange(of: endDate) { - if startDate > endDate { - startDate = endDate.addingTimeInterval(-90*60) - } - - } - } footer: { - FooterButtonView("jour entier") { - startDate = startDate.startOfDay - endDate = startDate.endOfDay() - } - } - } - .toolbar { - ButtonValidateView { - if editingSlot == nil { - let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) - do { - try dataStore.dateIntervals.addOrUpdate(instance: dateInterval) - } catch { - Logger.error(error) - } - } else { - editingSlot?.courtIndex = courtIndex - editingSlot?.endDate = endDate - editingSlot?.startDate = startDate - do { - try dataStore.dateIntervals.addOrUpdate(instance: editingSlot!) - } catch { - Logger.error(error) - } - } - showingPopover = false - } - } - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Nouveau créneau") - .tint(.master) - } - .onAppear { - UIDatePicker.appearance().minuteInterval = 5 - } - .onDisappear { - UIDatePicker.appearance().minuteInterval = 1 - } + CourtAvailabilityEditorView(event: event) + } + .sheet(item: $editingSlot) { editingSlot in + CourtAvailabilityEditorView(editingSlot: editingSlot, event: event) } } } @@ -210,6 +143,135 @@ struct CourtPicker: View { } } -//#Preview { -// CourtAvailabilitySettingsView(event: Event.mock()) -//} +struct CourtAvailabilityEditorView: View { + @Environment(Tournament.self) var tournament: Tournament + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss + var editingSlot: DateInterval? + let event: Event + + @State private var courtIndex: Int + @State private var startDate: Date + @State private var endDate: Date + + init(editingSlot: DateInterval, event: Event) { + self.editingSlot = editingSlot + self.event = event + _courtIndex = .init(wrappedValue: editingSlot.courtIndex) + _startDate = .init(wrappedValue: editingSlot.startDate) + _endDate = .init(wrappedValue: editingSlot.endDate) + } + + init(event: Event) { + self.event = event + _courtIndex = .init(wrappedValue: 0) + let startDate = event.eventStartDate() + _startDate = .init(wrappedValue: event.eventStartDate()) + _endDate = .init(wrappedValue: startDate.addingTimeInterval(5400)) + } + + var body: some View { + NavigationStack { + Form { + Section { + CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount) + } + + Section { + DatePicker("Début", selection: $startDate) + .onChange(of: startDate) { + if endDate < startDate { + endDate = startDate.addingTimeInterval(90*60) + } + } + DatePicker("Fin", selection: $endDate) + .onChange(of: endDate) { + if startDate > endDate { + startDate = endDate.addingTimeInterval(-90*60) + } + + } + } footer: { + FooterButtonView("jour entier") { + startDate = startDate.startOfDay + endDate = startDate.tomorrowAtNine.startOfDay + } + } + + + Section { + DateAdjusterView(date: $startDate) + } header: { + Text("Modifier rapidement l'horaire de début") + } + Section { + DateAdjusterView(date: $endDate) + } header: { + Text("Modifier rapidement l'horaire de fin") + } + } + .toolbar { + ButtonValidateView { + if editingSlot == nil { + let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) + do { + try dataStore.dateIntervals.addOrUpdate(instance: dateInterval) + } catch { + Logger.error(error) + } + } else { + editingSlot?.courtIndex = courtIndex + editingSlot?.endDate = endDate + editingSlot?.startDate = startDate + do { + try dataStore.dateIntervals.addOrUpdate(instance: editingSlot!) + } catch { + Logger.error(error) + } + } + + dismiss() + } + + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle(_navigationTitle()) + .tint(.master) + } + } + + private func _navigationTitle() -> String { + editingSlot == nil ? "Nouveau créneau" : "Édition du créneau" + } +} + +struct DateAdjusterView: View { + @Binding var date: Date + + var body: some View { + HStack { + _createButton(label: "-1h", timeOffset: -1, component: .hour) + _createButton(label: "-30m", timeOffset: -30, component: .minute) + _createButton(label: "+30m", timeOffset: 30, component: .minute) + _createButton(label: "+1h", timeOffset: 1, component: .hour) + } + .font(.headline) + } + + private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View { + Button(action: { + date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date + }) { + Text(label) + .frame(maxWidth: .infinity) // Make buttons take equal space + } + .buttonStyle(.borderedProminent) + .tint(.master) + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 35311cb..0024d88 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -73,7 +73,8 @@ struct RoundSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") } } - + + let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft() Section { NavigationLink { DrawLogsView() @@ -82,7 +83,7 @@ struct RoundSettingsView: View { Text("Gestionnaire des tirages au sort") } - if tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount(), tournament.lastDrawnDate() != nil, tournament.seedSpotsLeft() { + if previewAvailable { NavigationLink { PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs()) @@ -94,7 +95,9 @@ struct RoundSettingsView: View { } } } footer: { - Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + if previewAvailable { + Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + } } // Section { From b219fef6c48c946e7d32ac1e651d9b4f5633350c Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 12:40:18 +0200 Subject: [PATCH 049/106] v1.0.24 b1 --- PadelClub.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index af8fb8f..d952dea 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3198,7 +3198,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3222,7 +3222,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3243,7 +3243,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3266,7 +3266,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.23; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3382,7 +3382,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3425,7 +3425,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3447,7 +3447,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3469,7 +3469,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3489,7 +3489,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3510,7 +3510,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.21; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 978f90dfcb5bcc5f86fd6d86df52bf40a04156c8 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 12:50:45 +0200 Subject: [PATCH 050/106] fix private settings in debug --- PadelClub/Data/Tournament.swift | 14 ++++++- .../CourtAvailabilitySettingsView.swift | 38 ++++++++++--------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3dc56c1..fd17325 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -120,7 +120,11 @@ final class Tournament : ModelObject, Storable { self.startDate = startDate self.endDate = endDate self.creationDate = creationDate +#if DEBUG + self.isPrivate = false +#else self.isPrivate = Guard.main.purchasedTransactions.isEmpty +#endif self.groupStageFormat = groupStageFormat self.roundFormat = roundFormat self.loserRoundFormat = loserRoundFormat @@ -142,16 +146,24 @@ final class Tournament : ModelObject, Storable { self.entryFee = entryFee self.additionalEstimationDuration = additionalEstimationDuration self.isDeleted = isDeleted +#if DEBUG + self.publishTeams = true + self.publishSummons = true + self.publishBrackets = true + self.publishGroupStages = true + self.publishRankings = true +#else self.publishTeams = publishTeams self.publishSummons = publishSummons self.publishBrackets = publishBrackets self.publishGroupStages = publishGroupStages + self.publishRankings = publishRankings +#endif self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyGroupStage = shouldVerifyGroupStage self.hideTeamsWeight = hideTeamsWeight self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned - self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode self.initialSeedRound = initialSeedRound self.initialSeedCount = initialSeedCount diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 93d52fa..8f05d8a 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -211,26 +211,28 @@ struct CourtAvailabilityEditorView: View { } } .toolbar { - ButtonValidateView { - if editingSlot == nil { - let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) - do { - try dataStore.dateIntervals.addOrUpdate(instance: dateInterval) - } catch { - Logger.error(error) - } - } else { - editingSlot?.courtIndex = courtIndex - editingSlot?.endDate = endDate - editingSlot?.startDate = startDate - do { - try dataStore.dateIntervals.addOrUpdate(instance: editingSlot!) - } catch { - Logger.error(error) + ToolbarItem(placement: .topBarTrailing) { + ButtonValidateView { + if editingSlot == nil { + let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) + do { + try dataStore.dateIntervals.addOrUpdate(instance: dateInterval) + } catch { + Logger.error(error) + } + } else { + editingSlot?.courtIndex = courtIndex + editingSlot?.endDate = endDate + editingSlot?.startDate = startDate + do { + try dataStore.dateIntervals.addOrUpdate(instance: editingSlot!) + } catch { + Logger.error(error) + } } + + dismiss() } - - dismiss() } ToolbarItem(placement: .topBarLeading) { From ea807778fe964b5a9607ad62cfa8c8a7457f886c Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 15:06:11 +0200 Subject: [PATCH 051/106] fix draw labeling add position only draw option fix broadcast view --- PadelClub/Data/Tournament.swift | 2 +- .../Views/Components/FortuneWheelView.swift | 58 +++++++++++++------ PadelClub/Views/Round/RoundView.swift | 11 +++- .../Tournament/Screen/BroadcastView.swift | 12 ++-- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index fd17325..512ba1a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1966,7 +1966,7 @@ defer { func labelIndexOf(team: TeamRegistration) -> String? { if let teamIndex = indexOf(team: team) { - return "#" + (teamIndex + 1).formatted() + return "Tête de série #" + (teamIndex + 1).formatted() } else { return nil } diff --git a/PadelClub/Views/Components/FortuneWheelView.swift b/PadelClub/Views/Components/FortuneWheelView.swift index ac659d1..795773a 100644 --- a/PadelClub/Views/Components/FortuneWheelView.swift +++ b/PadelClub/Views/Components/FortuneWheelView.swift @@ -8,20 +8,20 @@ import SwiftUI protocol SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] } extension String: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [self] } } extension Match: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { let teams = teams() - if teams.count == 1 { - return teams.first!.segmentLabel(displayStyle) + if teams.count == 1, hideNames == false { + return teams.first!.segmentLabel(displayStyle, hideNames: hideNames) } else { return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } } @@ -29,12 +29,16 @@ extension Match: SpinDrawable { } extension TeamRegistration: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { var strings: [String] = [] let indexLabel = tournamentObject()?.labelIndexOf(team: self) if let indexLabel { strings.append(indexLabel) + if hideNames { + return strings + } } + strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) return strings } @@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable { let initialIndex: Int let option: SpinDrawable - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { - option.segmentLabel(displayStyle) + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { + option.segmentLabel(displayStyle, hideNames: hideNames) } } @@ -62,6 +66,7 @@ struct SpinDrawView: View { let drawees: [any SpinDrawable] @State var segments: [any SpinDrawable] var autoMode: Bool = false + var hideNames: Bool = false let completion: ([DrawResult]) async -> Void // Completion closure @State private var drawCount: Int = 0 @@ -89,12 +94,12 @@ struct SpinDrawView: View { } } else if drawCount < drawees.count { Section { - _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } Section { ZStack { - FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in + FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in self.selectedIndex = index self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.drawOptions.remove(at: index) @@ -209,8 +214,8 @@ struct SpinDrawView: View { private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { VStack(alignment: horizontalAlignment, spacing: 0.0) { - ForEach(segment, id: \.self) { string in - Text(string).font(.title3) + ForEach(segment.indices, id: \.self) { lineIndex in + Text(segment[lineIndex]).font(.title3) .frame(maxWidth: .infinity) .lineLimit(1) } @@ -221,13 +226,13 @@ struct SpinDrawView: View { private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { VStack(spacing: 0.0) { let draw = drawees[drawee] - _segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: draw.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) if result as? TeamRegistration != nil { Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed) } else { Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed) } - _segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: result.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } } } @@ -236,10 +241,11 @@ struct FortuneWheelContainerView: View { @State private var rotation: Double = 0 let segments: [any SpinDrawable] let autoMode: Bool + let hideNames: Bool let completion: (Int) -> Void // Completion closure var body: some View { - FortuneWheelView(segments: segments) + FortuneWheelView(segments: segments, hideNames: hideNames) .rotationEffect(.degrees(rotation)) .aspectRatio(contentMode: .fill) .padding(.top, 5) @@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View { struct FortuneWheelView: View { let segments: [any SpinDrawable] + let hideNames: Bool let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] func getColor(forIndex index: Int) -> Color { @@ -330,12 +337,12 @@ struct FortuneWheelView: View { path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.closeSubpath() } - .fill(getColor(forIndex:index)) + .fill(getColor(forIndex: index)) VStack(alignment: .trailing, spacing: 0.0) { - let strings = segments[index].segmentLabel(.short) - ForEach(strings, id: \.self) { string in - Text(string).bold() + let strings = labels(forIndex: index) + ForEach(strings.indices, id: \.self) { lineIndex in + Text(strings[lineIndex]).bold() .font(.subheadline) } } @@ -349,6 +356,19 @@ struct FortuneWheelView: View { } } + private func labels(forIndex index: Int) -> [String] { + if segments.count < 5 { + return segments[index].segmentLabel(.short, hideNames: hideNames) + } else { + let values = segments[index].segmentLabel(.short, hideNames: hideNames) + if values.count < 3 { + return values + } else { + return Array(segments[index].segmentLabel(.short, hideNames: hideNames).prefix(1)) + } + } + } + // Calculate the position for the text in the middle of the arc segment private func arcPosition(index: Int, radius: Double) -> CGPoint { let segmentAngle = 360.0 / Double(segments.count) diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 3470ab4..bfe0e13 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -18,6 +18,7 @@ struct RoundView: View { @State private var selectedSeedGroup: SeedInterval? @State private var showPrintScreen: Bool = false + @State private var hideNames: Bool = true var upperRound: UpperRound @@ -141,6 +142,12 @@ struct RoundView: View { if (availableSeedGroup.isFixed() == false) { Section { + Toggle(isOn: $hideNames) { + Text("Masquer les noms") + if hideNames { + Text("Réalise un tirage des positions.") + } + } RowButtonView("Tirage au sort \(availableSeedGroup.localizedInterval()) visuel") { self.selectedSeedGroup = availableSeedGroup } @@ -266,7 +273,7 @@ struct RoundView: View { let seeds = _seeds(availableSeedGroup: availableSeedGroup) let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup) NavigationStack { - SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in + SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true, hideNames: hideNames) { draws in Task { draws.forEach { drawResult in seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) @@ -397,7 +404,7 @@ struct MatchSpot: SpinDrawable { let match: Match let teamPosition: TeamPosition - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 } } diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index d687e29..4e12408 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -102,18 +102,14 @@ struct BroadcastView: View { } Section { - Toggle(isOn: $tournament.isPrivate) { - Text("Tournoi privé") - } + Toggle("Visible sur Padel Club", isOn: Binding( + get: { !tournament.isPrivate }, + set: { tournament.isPrivate = !$0 } + )) Toggle(isOn: $tournament.hideTeamsWeight) { Text("Masquer les poids des équipes") } - - } footer: { - let verb : String = tournament.isPrivate ? "est" : "sera" - let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" - Text(.init(footerString)) } if tournament.isPrivate == false { From 883fdaea187a1936a8cb546ea9f064467963f996 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 24 Oct 2024 20:17:28 +0200 Subject: [PATCH 052/106] wip --- PadelClub/Data/GroupStage.swift | 108 +++++++++++++++--- .../Components/GroupStageSettingsView.swift | 6 + 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 15eee50..e00435d 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -115,6 +115,25 @@ final class GroupStage: ModelObject, Storable { return match } + func addReturnMatches() { + var teamScores = [TeamScore]() + var matches = [Match]() + + for i in 0..<_numberOfMatchesToBuild() { + let newMatch = self._createMatch(index: i + matchCount) +// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) + teamScores.append(contentsOf: newMatch.createTeamScores()) + matches.append(newMatch) + } + + do { + try self.tournamentStore.matches.addOrUpdate(contentOfs: matches) + try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) + } catch { + Logger.error(error) + } + } + func buildMatches(keepExistingMatches: Bool = false) { var teamScores = [TeamScore]() var matches = [Match]() @@ -291,40 +310,58 @@ final class GroupStage: ModelObject, Storable { return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() } + func isReturnMatchEnabled() -> Bool { + _matches().count > matchCount + } + private func _matchOrder() -> [Int] { + var order: [Int] + switch size { case 3: - return [1, 2, 0] + order = [1, 2, 0] case 4: - return [2, 3, 1, 4, 5, 0] + order = [2, 3, 1, 4, 5, 0] case 5: -// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] - return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] + order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: - //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] - return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: - return [] + order = [] + } + + if isReturnMatchEnabled() { + let arraySize = order.count + // Duplicate and increment each value by the array size + order += order.map { $0 + arraySize } } + + return order } - + + func indexOf(_ matchIndex: Int) -> Int { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } private func _matchUp(for matchIndex: Int) -> [Int] { - Array((0.. String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) vs #\(index2 + 1)" + return "#\(index + 1) vs #\(index2 + 1)" + (matchIndex >= matchCount ? " - retour" : "") } else { return "--" } } + var matchCount: Int { + (size * size - 1) / 2 + } + func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { @@ -337,7 +374,7 @@ final class GroupStage: ModelObject, Storable { private func _teams(for matchIndex: Int) -> [TeamRegistration?] { let combinations = Array(0.. Bool { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. 1 { + let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) + let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) + + let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in + let predicates: [TeamScoreAreInIncreasingOrder] = [ + { $0.wins < $1.wins }, + { $0.setDifference < $1.setDifference }, + { $0.gameDifference < $1.gameDifference}, + { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + }.map({ $0.team }) + + return teamsSorted.first == teamPosition } else { - return false + + if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { + return teamPosition.id == match.losingTeamId + } else { + return false + } + } } @@ -428,16 +495,19 @@ final class GroupStage: ModelObject, Storable { guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) if matches.isEmpty && nilIfEmpty { return nil } + let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition) + scoreCache[groupStagePosition] = score + return score + } + + private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore { let wins = matches.filter { $0.winningTeamId == team.id }.count let loses = matches.filter { $0.losingTeamId == team.id }.count let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } let setDifference = differences.map { $0.set }.reduce(0,+) let gameDifference = differences.map { $0.game }.reduce(0,+) - // Calculate the score and store it in the cache - let score = (team, wins, loses, setDifference, gameDifference) - scoreCache[groupStagePosition] = score - return score + return (team, wins, loses, setDifference, gameDifference) } // Clear the cache if necessary, for example when starting a new step or when matches update diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index ad0d943..9f7851a 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -149,6 +149,12 @@ struct GroupStageSettingsView: View { Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") } + Section { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStage.addReturnMatches() + } + } + Section { RowButtonView("Rafraichir", role: .destructive) { let playedMatches = groupStage.playedMatches() From 52c8162a2df581ba73b9b00107a9b4e260990503 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 25 Oct 2024 10:04:40 +0200 Subject: [PATCH 053/106] Adds timezone for clubs --- PadelClub/Data/Club.swift | 6 +++++- PadelClub/Data/README.md | 4 ++-- PadelClub/Views/Club/ClubDetailView.swift | 6 ++++++ PadelClubTests/ServerDataTests.swift | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 58ef226..c124f28 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -39,9 +39,10 @@ final class Club : ModelObject, Storable, Hashable { var longitude: Double? var courtCount: Int = 2 var broadcastCode: String? + var timezone: String? // var alphabeticalName: Bool = false - internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, broadcastCode: String? = nil) { + internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, broadcastCode: String? = nil, timezone: String? = nil) { self.name = name self.creator = creator self.acronym = acronym ?? name.acronym() @@ -54,6 +55,7 @@ final class Club : ModelObject, Storable, Hashable { self.longitude = longitude self.courtCount = courtCount self.broadcastCode = broadcastCode + self.timezone = TimeZone.current.identifier } override func copyFromServerInstance(_ instance: any Storable) -> Bool { @@ -102,6 +104,7 @@ final class Club : ModelObject, Storable, Hashable { case _longitude = "longitude" case _courtCount = "courtCount" case _broadcastCode = "broadcastCode" + case _timezone = "timezone" // case _alphabeticalName = "alphabeticalName" } @@ -121,6 +124,7 @@ final class Club : ModelObject, Storable, Hashable { try container.encode(longitude, forKey: ._longitude) try container.encode(courtCount, forKey: ._courtCount) try container.encode(broadcastCode, forKey: ._broadcastCode) + try container.encode(timezone, forKey: ._timezone) } } diff --git a/PadelClub/Data/README.md b/PadelClub/Data/README.md index 334dd01..d4a670e 100644 --- a/PadelClub/Data/README.md +++ b/PadelClub/Data/README.md @@ -4,13 +4,13 @@ Dans Swift: - Ajouter le champ dans classe - Ajouter le champ dans le constructeur si possible - Ajouter la codingKey correspondante -- Ajouter le champ dans l'encoding +- Ajouter le champ dans l'encoding/decoding - Ouvrir **ServerDataTests** et ajouter un test sur le champ - Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple Dans Django: - Ajouter le champ dans la classe -- S'il c'est un champ dans **CustomUser**: +- Si c'est un champ dans **CustomUser**: - Ajouter le champ à la méthode fields_for_update - Ajouter le champ dans UserSerializer > create > create_user dans serializers.py - L'ajouter aussi dans admin.py si nécéssaire diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index b16e079..285c564 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -19,6 +19,8 @@ struct ClubDetailView: View { @Bindable var club: Club @State private var clubDeleted: Bool = false @State private var confirmDeletion: Bool = false + @State private var timezone: String = TimeZone.current.identifier + var displayContext: DisplayContext var selection: ((Club) -> ())? = nil @@ -29,6 +31,10 @@ struct ClubDetailView: View { _acronymMode = State(wrappedValue: club.shortNameMode()) _city = State(wrappedValue: club.city ?? "") _zipCode = State(wrappedValue: club.zipCode ?? "") + + if let timezone = club.timezone { + self.timezone = timezone + } } var body: some View { diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index f1a988e..1eca68f 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -57,6 +57,7 @@ final class ServerDataTests: XCTestCase { assert(inserted_club.phone == club.phone) assert(inserted_club.courtCount == club.courtCount) assert(inserted_club.broadcastCode != nil) + assert(inserted_club.timezone != club.timezone) inserted_club.phone = "123456" From 8fbafd8c08cdd4419b5d64d5ae64e769e47905d3 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 25 Oct 2024 11:14:52 +0200 Subject: [PATCH 054/106] wip --- PadelClub/Data/GroupStage.swift | 9 +++++ .../Components/GroupStageSettingsView.swift | 10 ++++-- .../GroupStage/GroupStagesSettingsView.swift | 36 ++++++++++++++++--- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index e00435d..b7bd594 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -115,6 +115,15 @@ final class GroupStage: ModelObject, Storable { return match } + func removeReturnMatches() { + let returnMatches = _matches().filter({ $0.index >= matchCount }) + do { + try self.tournamentStore.matches.delete(contentOfs: returnMatches) + } catch { + Logger.error(error) + } + } + func addReturnMatches() { var teamScores = [TeamScore]() var matches = [Match]() diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 9f7851a..7bf706d 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -150,8 +150,14 @@ struct GroupStageSettingsView: View { } Section { - RowButtonView("Rajouter les matchs retours", role: .destructive) { - groupStage.addReturnMatches() + if groupStage.isReturnMatchEnabled() { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStage.removeReturnMatches() + } + } else { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStage.addReturnMatches() + } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 8f31945..bafec28 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -12,7 +12,7 @@ struct GroupStagesSettingsView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss @Environment(Tournament.self) var tournament: Tournament - @State private var generationDone: Bool = false + @State private var generationDoneMessage: String? let step: Int var tournamentStore: TournamentStore { @@ -167,6 +167,32 @@ struct GroupStagesSettingsView: View { } footer: { Text("Redistribue les équipes par la méthode du serpentin") } + + let groupStages = tournament.groupStages() + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() }) { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() }).forEach { groupStage in + groupStage.removeReturnMatches() + } + generationDoneMessage = "Matchs retours effacés" + } + } + + } + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() == false }) { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() == false }).forEach { groupStage in + groupStage.addReturnMatches() + } + + generationDoneMessage = "Matchs retours créés" + } + } + } Section { RowButtonView("Nommer les poules alphabétiquement", role: .destructive) { @@ -220,8 +246,8 @@ struct GroupStagesSettingsView: View { } .overlay(alignment: .bottom) { - if generationDone { - Label("Poules mises à jour", systemImage: "checkmark.circle.fill") + if let generationDoneMessage { + Label(generationDoneMessage, systemImage: "checkmark.circle.fill") .toastFormatted() .deferredRendering(for: .seconds(2)) } @@ -238,7 +264,7 @@ struct GroupStagesSettingsView: View { RowButtonView("Refaire les poules", role: .destructive) { tournament.deleteGroupStages() tournament.buildGroupStages() - generationDone = true + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() } @@ -249,7 +275,7 @@ struct GroupStagesSettingsView: View { RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) { tournament.groupStageOrderingMode = mode tournament.refreshGroupStages(keepExistingMatches: true) - generationDone = true + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() } From 2195bb6037bf27d1ca3664e2b9a949731b20b04c Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 25 Oct 2024 11:15:23 +0200 Subject: [PATCH 055/106] b2 --- PadelClub.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index d952dea..b10499b 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3198,7 +3198,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3243,7 +3243,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3359,7 +3359,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3403,7 +3403,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3447,7 +3447,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3489,7 +3489,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; From dced9bb0be0f5167dd7df84afe7945a9adba2ffb Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 25 Oct 2024 11:52:03 +0200 Subject: [PATCH 056/106] fix --- PadelClub/Data/GroupStage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index b7bd594..7b21f1b 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -255,7 +255,7 @@ final class GroupStage: ModelObject, Storable { matchIndexes.append(index) } } - return _matches().filter { matchIndexes.contains($0.index) } + return _matches().filter { matchIndexes.contains($0.index%matchCount) } } func initialStartDate(forTeam team: TeamRegistration) -> Date? { From b7f2b338167427b1762ebef77cc9fc83343207ea Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 25 Oct 2024 15:18:03 +0200 Subject: [PATCH 057/106] fix issue with match deletion in groupstage --- PadelClub/Data/GroupStage.swift | 1 + .../Views/GroupStage/Components/GroupStageSettingsView.swift | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 7b21f1b..da476cb 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -146,6 +146,7 @@ final class GroupStage: ModelObject, Storable { func buildMatches(keepExistingMatches: Bool = false) { var teamScores = [TeamScore]() var matches = [Match]() + clearScoreCache() if keepExistingMatches == false { _removeMatches() diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 7bf706d..5bfa715 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -143,7 +143,11 @@ struct GroupStageSettingsView: View { Section { RowButtonView("Recommencer tous les matchs", role: .destructive) { + let isReturnMatchesEnabled = groupStage.isReturnMatchEnabled() groupStage.buildMatches() + if isReturnMatchesEnabled { + groupStage.addReturnMatches() + } } } footer: { Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") From 4143236154e97e03b10b07003b1834d86bae00a4 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 26 Oct 2024 10:16:18 +0200 Subject: [PATCH 058/106] enhance and clean format selection --- PadelClub.xcodeproj/project.pbxproj | 72 ++++-- PadelClub/Utils/PadelRule.swift | 77 ++++-- .../Toolbox/GlobalSettingsView.swift | 128 +++++++--- .../Toolbox/MatchFormatStorageView.swift | 4 +- .../Planning/Components/DatePickingView.swift | 93 ++++++++ .../DatePickingViewWithFormat.swift | 108 +++++++++ .../Components/DateUpdateManagerView.swift | 224 +----------------- .../GroupStageDatePickingView.swift | 93 ++++++++ .../Components/MatchFormatPickingView.swift | 47 ++++ PadelClub/Views/Score/EditScoreView.swift | 18 +- .../Views/Shared/MatchFormatPickerView.swift | 63 ----- .../Views/Shared/MatchFormatRowView.swift | 46 ++++ .../Shared/MatchFormatSelectionView.swift | 33 +++ .../Views/Shared/MatchTypeSelectionView.swift | 11 +- .../Shared/MatchTypeSmallSelectionView.swift | 23 -- .../TournamentFormatSelectionView.swift | 6 +- 16 files changed, 654 insertions(+), 392 deletions(-) create mode 100644 PadelClub/Views/Planning/Components/DatePickingView.swift create mode 100644 PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift create mode 100644 PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift create mode 100644 PadelClub/Views/Planning/Components/MatchFormatPickingView.swift delete mode 100644 PadelClub/Views/Shared/MatchFormatPickerView.swift create mode 100644 PadelClub/Views/Shared/MatchFormatRowView.swift create mode 100644 PadelClub/Views/Shared/MatchFormatSelectionView.swift delete mode 100644 PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b10499b..10ac1be 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -79,7 +79,7 @@ FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; - FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; + FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; @@ -294,8 +294,8 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; - FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; - FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; + FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; @@ -351,7 +351,7 @@ FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; - FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; @@ -606,8 +606,8 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; - FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; - FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; + FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FF70FB642C90584900129CC2 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; @@ -663,7 +663,7 @@ FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; - FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; @@ -709,6 +709,15 @@ FF70FBC82C90584900129CC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */; }; FF70FBC92C90584900129CC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; FF70FBCB2C90584900129CC2 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; + FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; + FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; @@ -728,7 +737,7 @@ FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */; }; FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; }; FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; - FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; }; FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; }; @@ -797,7 +806,7 @@ FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; }; FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; }; FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; - FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; }; FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; @@ -999,7 +1008,7 @@ FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = ""; }; FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; - FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; + FF1162892BD05247000C4809 /* DatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = ""; }; @@ -1093,6 +1102,9 @@ FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = ""; }; FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = ""; }; FF70FBCF2C90584900129CC2 /* PadelClub TestFlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PadelClub TestFlight.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickingView.swift; sourceTree = ""; }; + FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingViewWithFormat.swift; sourceTree = ""; }; + FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = ""; }; FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = ""; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; @@ -1110,7 +1122,7 @@ FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLevelPickerView.swift; sourceTree = ""; }; FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = ""; }; FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = ""; }; - FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = ""; }; + FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatSelectionView.swift; sourceTree = ""; }; FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = ""; }; FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = ""; }; FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = ""; }; @@ -1178,7 +1190,7 @@ FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = ""; }; FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = ""; }; FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = ""; }; - FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = ""; }; + FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatRowView.swift; sourceTree = ""; }; FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = ""; }; FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; @@ -1546,7 +1558,10 @@ FF1162882BD0523B000C4809 /* Components */ = { isa = PBXGroup; children = ( - FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, + FF1162892BD05247000C4809 /* DatePickingView.swift */, + FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */, + FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */, + FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */, FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */, ); path = Components; @@ -1724,11 +1739,11 @@ isa = PBXGroup; children = ( FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */, - FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */, + FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */, FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, - FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */, + FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */, FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */, @@ -2264,6 +2279,7 @@ FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, + FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */, FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */, @@ -2416,6 +2432,7 @@ FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, + FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, @@ -2423,8 +2440,8 @@ FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, - FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, + FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */, + FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */, FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */, FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */, @@ -2485,8 +2502,9 @@ FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */, FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, - FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, + FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */, FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */, + FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */, @@ -2541,6 +2559,7 @@ FF4CBF452C996C0600151637 /* TabDestination.swift in Sources */, FF4CBF462C996C0600151637 /* CashierView.swift in Sources */, FF4CBF472C996C0600151637 /* Event.swift in Sources */, + FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF4CBF482C996C0600151637 /* PlayerHolder.swift in Sources */, FF4CBF492C996C0600151637 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF4CBF4A2C996C0600151637 /* ClubCourtSetupView.swift in Sources */, @@ -2693,6 +2712,7 @@ FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, + FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */, @@ -2700,8 +2720,8 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */, - FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */, + FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */, + FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */, FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */, FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */, FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */, @@ -2762,8 +2782,9 @@ FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */, FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */, - FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */, + FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */, FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */, + FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */, FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */, FF4CC0202C996C0600151637 /* DebugSettingsView.swift in Sources */, @@ -2797,6 +2818,7 @@ FF70FAC42C90584900129CC2 /* TabDestination.swift in Sources */, FF70FAC52C90584900129CC2 /* CashierView.swift in Sources */, FF70FAC62C90584900129CC2 /* Event.swift in Sources */, + FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF70FAC72C90584900129CC2 /* PlayerHolder.swift in Sources */, FF70FAC82C90584900129CC2 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF70FAC92C90584900129CC2 /* ClubCourtSetupView.swift in Sources */, @@ -2949,6 +2971,7 @@ FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, + FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */, @@ -2956,8 +2979,8 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */, - FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */, + FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */, + FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */, FF70FB642C90584900129CC2 /* MonthData.swift in Sources */, FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */, FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */, @@ -3018,8 +3041,9 @@ FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */, FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */, - FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */, + FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */, FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */, + FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */, FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */, FF70FB9F2C90584900129CC2 /* DebugSettingsView.swift in Sources */, diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index d3b1d45..404899b 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1135,19 +1135,24 @@ enum MatchType: String { case loserBracket = "loserBracket" } -enum MatchFormat: Int, Hashable, Codable, CaseIterable { +enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { + var id: Int { self.rawValue } case twoSets case twoSetsSuperTie case twoSetsOfFourGames case nineGames case superTie case megaTie - + case twoSetsDecisivePoint case twoSetsDecisivePointSuperTie case twoSetsOfFourGamesDecisivePoint case nineGamesDecisivePoint + case twoSetsOfSuperTie + case singleSet + case singleSetDecisivePoint + init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) @@ -1156,7 +1161,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] { Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin) } - + var weight: Int { switch self { case .twoSets, .twoSetsDecisivePoint: @@ -1171,6 +1176,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return 4 case .megaTie: return 5 + case .twoSetsOfSuperTie: + return 6 + case .singleSet, .singleSetDecisivePoint: + return 7 } } @@ -1196,6 +1205,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return 4 case .megaTie: return 5 + case .twoSetsOfSuperTie: + return 6 + case .singleSet, .singleSetDecisivePoint: + return 7 } } @@ -1211,7 +1224,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } static var allCases: [MatchFormat] { - [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] + [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint] } func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { @@ -1242,7 +1255,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String { Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes])) } - + func formattedEstimatedBreakDuration() -> String { var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) if breakTime.matchCount > 1 { @@ -1251,7 +1264,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } return label } - + var defaultEstimatedDuration: Int { switch self { case .twoSets: @@ -1274,9 +1287,15 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return 20 case .superTie: return 15 + case .twoSetsOfSuperTie: + return 25 + case .singleSet: + return 30 + case .singleSetDecisivePoint: + return 25 } } - + var estimatedTimeWithBreak: Int { estimatedDuration + breakTime.breakTime @@ -1294,6 +1313,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return (15, 3) case .megaTie: return (5, 1) + case .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: + return (10, 1) } } @@ -1305,7 +1326,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return matchCount < 6 ? 3 : 0 case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint: return matchCount < 7 ? 6 : 2 - case .superTie: + case .superTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: return 7 case .megaTie: return 7 @@ -1314,7 +1335,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var hasDecisivePoint: Bool { switch self { - case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint: return true default: return false @@ -1328,9 +1349,13 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return setFormat } + func formatTitle() -> String { + ["Format ", shortFormat, suffix].joined() + } + var suffix: String { switch self { - case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint: + case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint, .singleSetDecisivePoint: return " [Point Décisif]" default: return "" @@ -1344,8 +1369,20 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var shortPrefix: String { return "\(format) : " } + + var isFederal: Bool { + switch self { + case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: + return false + default: + return true + } + } var format: String { + shortFormat + (isFederal ? "" : " (non officiel)") + } + var shortFormat: String { switch self { case .twoSets: return "A1" @@ -1357,8 +1394,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return "D1" case .superTie: return "E" + case .twoSetsOfSuperTie: + return "G" case .megaTie: - return "F (non officiel)" + return "F" + case .singleSet: + return "H1" + case .singleSetDecisivePoint: + return "H2" case .twoSetsDecisivePoint: return "A2" case .twoSetsDecisivePointSuperTie: @@ -1372,6 +1415,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var longLabel: String { switch self { + case .singleSet, .singleSetDecisivePoint: + return "1 set de 6" case .twoSets, .twoSetsDecisivePoint: return "2 sets de 6" case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: @@ -1380,6 +1425,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return "2 sets de 4, tiebreak à 4/4, supertie au 3ème" case .nineGames, .nineGamesDecisivePoint: return "9 jeux, tiebreak à 8/8" + case .twoSetsOfSuperTie: + return "2 sets de supertie de 10 points" case .superTie: return "supertie de 10 points" case .megaTie: @@ -1401,22 +1448,22 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var setsToWin: Int { switch self { - case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie: return 2 - case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie: + case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint: return 1 } } var setFormat: SetFormat { switch self { - case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint: return .six case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: return .four case .nineGames, .nineGamesDecisivePoint: return .nine - case .superTie: + case .superTie, .twoSetsOfSuperTie: return .superTieBreak case .megaTie: return .megaTieBreak diff --git a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift index d39811d..ed0e831 100644 --- a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift +++ b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift @@ -11,49 +11,115 @@ import LeStorage struct GlobalSettingsView: View { @EnvironmentObject var dataStore : DataStore + var groupStageMatchFormat: Binding { + Binding { + dataStore.user.groupStageMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.groupStageMatchFormatPreference = value + } + } + + var groupStageMatchFormatPreference: Binding { + Binding { + dataStore.user.groupStageMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.groupStageMatchFormatPreference = nil + } else { + dataStore.user.groupStageMatchFormatPreference = .nineGames + } + } + + } + + var bracketMatchFormat: Binding { + Binding { + dataStore.user.bracketMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.bracketMatchFormatPreference = value + } + } + + var bracketMatchFormatPreference: Binding { + Binding { + dataStore.user.bracketMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.bracketMatchFormatPreference = nil + } else { + dataStore.user.bracketMatchFormatPreference = .nineGames + } + } + + } + + var loserBracketMatchFormat: Binding { + Binding { + dataStore.user.loserBracketMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.loserBracketMatchFormatPreference = value + } + } + + var loserBracketMatchFormatPreference: Binding { + Binding { + dataStore.user.loserBracketMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.loserBracketMatchFormatPreference = nil + } else { + dataStore.user.loserBracketMatchFormatPreference = .nineGames + } + } + + } + var body: some View { @Bindable var user = dataStore.user List { Section { - Picker(selection: $user.groupStageMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Poule") - Spacer() - } + Toggle(isOn: groupStageMatchFormatPreference) { + Text("Automatique") } - Picker(selection: $user.bracketMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Tableau") - Spacer() - } + + if groupStageMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: groupStageMatchFormat) + } + } header: { + Text("Poule") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + + Section { + Toggle(isOn: bracketMatchFormatPreference) { + Text("Automatique") + } + + if bracketMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: bracketMatchFormat) + } + } header: { + Text("Tableau") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + + Section { + Toggle(isOn: loserBracketMatchFormatPreference) { + Text("Automatique") } - Picker(selection: $user.loserBracketMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Match de classement") - Spacer() - } + + if loserBracketMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: loserBracketMatchFormat) } } header: { - Text("Vos formats préférés") + Text("Match de classement") } footer: { Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") } } + .headerProminence(.increased) .onChange(of: [ user.bracketMatchFormatPreference, user.groupStageMatchFormatPreference, diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift index b389892..7d90518 100644 --- a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -24,9 +24,7 @@ struct MatchFormatStorageView: View { LabeledContent { StepperView(title: "minutes", count: $estimatedDuration, step: 5) } label: { - Text("Durée \(matchFormat.format)" + matchFormat.suffix) - Text(matchFormat.longLabel) - Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) + MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) } } footer: { if estimatedDuration != matchFormat.defaultEstimatedDuration { diff --git a/PadelClub/Views/Planning/Components/DatePickingView.swift b/PadelClub/Views/Planning/Components/DatePickingView.swift new file mode 100644 index 0000000..9c865e3 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DatePickingView.swift @@ -0,0 +1,93 @@ +// +// DatePickingView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct DatePickingView: View { + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: (() async -> ()) + + @State private var confirmFollowingScheduleUpdate: Bool = false + @State private var updatingInProgress: Bool = false + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmFollowingScheduleUpdate { + RowButtonView("Modifier la suite du programme") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmFollowingScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier la suite") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + } + .onChange(of: startDate) { + confirmFollowingScheduleUpdate = true + } + .headerProminence(.increased) + } +} diff --git a/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift b/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift new file mode 100644 index 0000000..6eca161 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift @@ -0,0 +1,108 @@ +// +// DatePickingViewWithFormat.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct DatePickingViewWithFormat: View { + @Environment(Tournament.self) var tournament + @Binding var matchFormat: MatchFormat + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: ((Bool) async -> ()) + + @State private var confirmScheduleUpdate: Bool = false + @State private var updatingInProgress : Bool = false + + var body: some View { + Section { + MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration) + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmScheduleUpdate { + RowButtonView("Sauver et modifier la suite") { + updatingInProgress = true + await validateAction(true) + updatingInProgress = false + confirmScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + HStack { + FooterButtonView("sauver sans modifier la suite") { + Task { + await validateAction(false) + confirmScheduleUpdate = false + } + } + Text("ou") + FooterButtonView("annuler") { + confirmScheduleUpdate = false + } + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + + } + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true + } + .onChange(of: startDate) { + confirmScheduleUpdate = true + } + } +} diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index 3cdb16d..9c865e3 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -1,5 +1,5 @@ // -// DateUpdateManagerView.swift +// DatePickingView.swift // PadelClub // // Created by Razmig Sarkissian on 17/04/2024. @@ -91,225 +91,3 @@ struct DatePickingView: View { .headerProminence(.increased) } } - -struct MatchFormatPickingView: View { - var title: String? = nil - @Binding var matchFormat: MatchFormat - var validateAction: (() async -> ()) - - @State private var confirmScheduleUpdate: Bool = false - @State private var updatingInProgress : Bool = false - - var body: some View { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - if confirmScheduleUpdate { - RowButtonView("Recalculer les horaires") { - updatingInProgress = true - await validateAction() - updatingInProgress = false - confirmScheduleUpdate = false - } - } - } header: { - if let title { - Text(title) - } - } footer: { - if confirmScheduleUpdate && updatingInProgress == false { - FooterButtonView("non, ne pas modifier les horaires") { - confirmScheduleUpdate = false - } - } - } - .headerProminence(.increased) - .onChange(of: matchFormat) { - confirmScheduleUpdate = true - } - } -} - - -struct DatePickingViewWithFormat: View { - @Binding var matchFormat: MatchFormat - let title: String - @Binding var startDate: Date - @Binding var currentDate: Date? - var duration: Int? - var validateAction: ((Bool) async -> ()) - - @State private var confirmScheduleUpdate: Bool = false - @State private var updatingInProgress : Bool = false - - var body: some View { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - if confirmScheduleUpdate { - RowButtonView("Sauver et modifier la suite") { - updatingInProgress = true - await validateAction(true) - updatingInProgress = false - confirmScheduleUpdate = false - } - } - } header: { - Text(title) - } footer: { - if confirmScheduleUpdate && updatingInProgress == false { - HStack { - FooterButtonView("sauver sans modifier la suite") { - Task { - await validateAction(false) - confirmScheduleUpdate = false - } - } - Text("ou") - FooterButtonView("annuler") { - confirmScheduleUpdate = false - } - } - } else { - HStack { - Menu { - Button("de 30 minutes") { - startDate = startDate.addingTimeInterval(1800) - } - - Button("d'une heure") { - startDate = startDate.addingTimeInterval(3600) - } - - Button("à 9h") { - startDate = startDate.atNine() - } - - Button("à demain 9h") { - startDate = startDate.tomorrowAtNine - } - - if let duration { - Button("à la prochaine rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * 60) - } - Button("à la précédente rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * -60) - } - } - } label: { - Text("décaler") - .underline() - } - .buttonStyle(.borderless) - Spacer() - - if currentDate != nil { - FooterButtonView("retirer l'horaire bloqué") { - currentDate = nil - } - } else { - FooterButtonView("bloquer l'horaire") { - currentDate = startDate - } - } - } - .buttonStyle(.borderless) - } - - } - .headerProminence(.increased) - .onChange(of: matchFormat) { - confirmScheduleUpdate = true - } - .onChange(of: startDate) { - confirmScheduleUpdate = true - } - } -} - -struct GroupStageDatePickingView: View { - let title: String - @Binding var startDate: Date - @Binding var currentDate: Date? - var duration: Int? - var validateAction: (() async -> ()) - - @State private var confirmFollowingScheduleUpdate: Bool = false - @State private var updatingInProgress: Bool = false - - var body: some View { - Section { - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - if confirmFollowingScheduleUpdate { - RowButtonView("Confirmer et modifier les matchs") { - updatingInProgress = true - await validateAction() - updatingInProgress = false - confirmFollowingScheduleUpdate = false - } - } - } header: { - Text(title) - } footer: { - if confirmFollowingScheduleUpdate && updatingInProgress == false { - FooterButtonView("Modifier juste l'horaire de la poule") { - currentDate = startDate - confirmFollowingScheduleUpdate = false - } - } else { - HStack { - Menu { - Button("de 30 minutes") { - startDate = startDate.addingTimeInterval(1800) - } - - Button("d'une heure") { - startDate = startDate.addingTimeInterval(3600) - } - - Button("à 9h") { - startDate = startDate.atNine() - } - - Button("à demain 9h") { - startDate = startDate.tomorrowAtNine - } - - if let duration { - Button("à la prochaine rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * 60) - } - Button("à la précédente rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * -60) - } - } - } label: { - Text("décaler") - .underline() - } - .buttonStyle(.borderless) - Spacer() - - if currentDate != nil { - FooterButtonView("retirer l'horaire bloqué") { - currentDate = nil - } - } else { - FooterButtonView("bloquer l'horaire") { - currentDate = startDate - } - } - } - .buttonStyle(.borderless) - } - } - .onChange(of: startDate) { - confirmFollowingScheduleUpdate = true - } - .headerProminence(.increased) - } -} diff --git a/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift b/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift new file mode 100644 index 0000000..29c380f --- /dev/null +++ b/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift @@ -0,0 +1,93 @@ +// +// GroupStageDatePickingView.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct GroupStageDatePickingView: View { + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: (() async -> ()) + + @State private var confirmFollowingScheduleUpdate: Bool = false + @State private var updatingInProgress: Bool = false + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmFollowingScheduleUpdate { + RowButtonView("Confirmer et modifier les matchs") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmFollowingScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("Modifier juste l'horaire de la poule") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + } + .onChange(of: startDate) { + confirmFollowingScheduleUpdate = true + } + .headerProminence(.increased) + } +} diff --git a/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift b/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift new file mode 100644 index 0000000..8b36c13 --- /dev/null +++ b/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift @@ -0,0 +1,47 @@ +// +// MatchFormatPickingView.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct MatchFormatPickingView: View { + @Environment(Tournament.self) var tournament + var title: String? = nil + @Binding var matchFormat: MatchFormat + var validateAction: (() async -> ()) + + @State private var confirmScheduleUpdate: Bool = false + @State private var updatingInProgress : Bool = false + + var body: some View { + Section { + MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration) + if confirmScheduleUpdate { + RowButtonView("Recalculer les horaires") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmScheduleUpdate = false + } + } + } header: { + if let title { + Text(title) + } + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier les horaires") { + confirmScheduleUpdate = false + } + } + } + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true + } + } +} + diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 639d2bf..600b771 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -13,6 +13,7 @@ struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore @StateObject var matchDescriptor: MatchDescriptor + @State private var presentMatchFormatSelection: Bool = false @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss @@ -89,11 +90,9 @@ struct EditScoreView: View { } Spacer() - MatchTypeSmallSelectionView(selectedFormat: $matchDescriptor.matchFormat, format: "Format") - .onChange(of: matchDescriptor.matchFormat) { - matchDescriptor.setDescriptors.removeAll() - matchDescriptor.addNewSet() - } + FooterButtonView("Format : \(matchDescriptor.matchFormat.shortFormat)") { + presentMatchFormatSelection = true + } } } ForEach($matchDescriptor.setDescriptors) { $setDescriptor in @@ -143,6 +142,15 @@ struct EditScoreView: View { } } } + .sheet(isPresented: $presentMatchFormatSelection) { + MatchFormatSelectionView(matchFormat: $matchDescriptor.matchFormat, additionalEstimationDuration: matchDescriptor.match?.currentTournament()?.additionalEstimationDuration) + .tint(.master) + } + .onChange(of: matchDescriptor.matchFormat) { + presentMatchFormatSelection = false + matchDescriptor.setDescriptors.removeAll() + matchDescriptor.addNewSet() + } } func save() { diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift deleted file mode 100644 index c50c3a7..0000000 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// MatchFormatPickerView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 22/03/2024. -// - -import SwiftUI - -struct MatchFormatPickerView: View { - @Environment(Tournament.self) var tournament: Tournament - let headerLabel: String - @Binding var matchFormat: MatchFormat - @State private var isExpanded: Bool = false - - var body: some View { - DisclosureGroup(isExpanded: $isExpanded) { - Picker(selection: $matchFormat) { - ForEach(MatchFormat.allCases, id: \.rawValue) { format in - LabeledContent { - Text("~" + format.formattedEstimatedDuration(tournament.additionalEstimationDuration)) - } label: { - Text(format.format + " " + format.suffix) - Text(format.longLabel) - } - .tag(format) - } - } label: { - } - .pickerStyle(.inline) - .onChange(of: matchFormat) { - isExpanded = false - } - } label: { - descriptionView - } - } - - var descriptionView: some View { - VStack(alignment: .leading) { - HStack { - Text(headerLabel).font(.footnote) - Spacer() - Text("Durée").font(.footnote) - } - HStack { - Text(matchFormat.format).font(.title).fontWeight(.semibold) - Spacer() - VStack(alignment: .trailing) { - Text("~" + matchFormat.formattedEstimatedDuration(tournament.additionalEstimationDuration)) - Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) - } - } - } - } -} - - -//#Preview { -// List { -// MatchFormatPickerView(headerLabel: "Test", matchFormat: .constant(MatchFormat.superTie)) -// } -//} diff --git a/PadelClub/Views/Shared/MatchFormatRowView.swift b/PadelClub/Views/Shared/MatchFormatRowView.swift new file mode 100644 index 0000000..8698ea4 --- /dev/null +++ b/PadelClub/Views/Shared/MatchFormatRowView.swift @@ -0,0 +1,46 @@ +// +// MatchFormatRowView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct MatchFormatRowView: View { + let matchFormat: MatchFormat + var headerLabel: String? = nil + var hideExplanation: Bool = false + var hideDuration: Bool = false + var additionalEstimationDuration: Int? + + var body: some View { + VStack(alignment: .leading) { + if let headerLabel { + HStack { + Text(headerLabel).font(.footnote) + if hideDuration == false { + Spacer() + Text("Durée").font(.footnote) + } + } + } + HStack { + Text(matchFormat.formatTitle()) + if hideDuration == false { + Spacer() + Text("~" + matchFormat.formattedEstimatedDuration(additionalEstimationDuration ?? 0)) + } + } + .font(.headline) + + if hideExplanation == false { + Text(matchFormat.longLabel) + Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").font(.footnote) + if matchFormat.isFederal == false { + Text("Non officiel").foregroundStyle(.logoRed).font(.footnote) + } + } + } + } +} diff --git a/PadelClub/Views/Shared/MatchFormatSelectionView.swift b/PadelClub/Views/Shared/MatchFormatSelectionView.swift new file mode 100644 index 0000000..3249090 --- /dev/null +++ b/PadelClub/Views/Shared/MatchFormatSelectionView.swift @@ -0,0 +1,33 @@ +// +// MatchFormatSelectionView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 22/03/2024. +// + +import SwiftUI + +struct MatchFormatSelectionView: View { + @Environment(\.dismiss) private var dismiss + @Binding var matchFormat: MatchFormat + var additionalEstimationDuration: Int? + + var body: some View { + List { + Section { + Picker(selection: $matchFormat) { + ForEach(MatchFormat.allCases) { format in + MatchFormatRowView(matchFormat: format, additionalEstimationDuration: additionalEstimationDuration).tag(format) + } + } label: { + + } + .labelsHidden() + .pickerStyle(.inline) + } + } + .onChange(of: matchFormat) { + dismiss() + } + } +} diff --git a/PadelClub/Views/Shared/MatchTypeSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSelectionView.swift index 2d3ba3c..54bc81c 100644 --- a/PadelClub/Views/Shared/MatchTypeSelectionView.swift +++ b/PadelClub/Views/Shared/MatchTypeSelectionView.swift @@ -9,9 +9,16 @@ import SwiftUI struct MatchTypeSelectionView: View { @Binding var selectedFormat: MatchFormat - let format: String + var format: String? + var additionalEstimationDuration: Int? var body: some View { - MatchFormatPickerView(headerLabel: format, matchFormat: $selectedFormat) + NavigationLink { + MatchFormatSelectionView(matchFormat: $selectedFormat) + .navigationTitle("Choix du format") + .toolbarBackground(.visible, for: .navigationBar) + } label: { + MatchFormatRowView(matchFormat: selectedFormat, headerLabel: format, hideExplanation: true, additionalEstimationDuration: additionalEstimationDuration) + } } } diff --git a/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift deleted file mode 100644 index cc30267..0000000 --- a/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// MatchTypeSmallSelectionView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 02/04/2024. -// - -import SwiftUI - -struct MatchTypeSmallSelectionView: View { - @Binding var selectedFormat: MatchFormat - let format: String - - var body: some View { - Picker(selection: $selectedFormat) { - ForEach(MatchFormat.allCases, id: \.rawValue) { matchFormat in - Text(format + " " + matchFormat.format) - .tag(matchFormat) - } - } label: { - } - } -} diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift index 74ef46c..5f6f301 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift @@ -15,9 +15,9 @@ struct TournamentFormatSelectionView: View { @Bindable var tournament = tournament Section { - MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule") - MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau") - MatchTypeSelectionView(selectedFormat: $tournament.loserBracketMatchFormat, format: "Match de classement") + MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration) + MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration) + MatchTypeSelectionView(selectedFormat: $tournament.loserBracketMatchFormat, format: "Match de classement", additionalEstimationDuration: tournament.additionalEstimationDuration) } footer: { Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") } From c016e5e0c8f9a488286a215fad20ef9c80ac0f84 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 26 Oct 2024 10:31:19 +0200 Subject: [PATCH 059/106] fix game difference calculation for format b / c --- PadelClub/Data/Match.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 4573827..9d3b181 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -816,8 +816,24 @@ defer { } else { setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count } - let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) - return (setDifference * reverseValue, gameDifference * reverseValue) + + // si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu + + if matchFormat.canSuperTie, endedSetsOne.count == 3 { + let games = zip.map { ($0, $1) } + let gameDifference = games.enumerated().map({ index, pair in + if index < 2 { + return pair.0 - pair.1 + } else { + return pair.0 < pair.1 ? -1 : 1 + } + }) + .reduce(0,+) + return (setDifference * reverseValue, gameDifference * reverseValue) + } else { + let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) + return (setDifference * reverseValue, gameDifference * reverseValue) + } } func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { From 566c5c1eacd4fb9e60d5f6813178e502ccdbefe1 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 26 Oct 2024 11:54:09 +0200 Subject: [PATCH 060/106] enhance score input to handle first team to input score switching --- PadelClub/ViewModel/MatchDescriptor.swift | 19 --- PadelClub/ViewModel/SetDescriptor.swift | 4 + PadelClub/Views/Score/EditScoreView.swift | 68 ++++++-- PadelClub/Views/Score/SetInputView.swift | 183 ++++++++++++++++------ 4 files changed, 196 insertions(+), 78 deletions(-) diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index bbb444a..dd88446 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -62,25 +62,6 @@ class MatchDescriptor: ObservableObject { return true } - func getColor() -> Color { - guard let setDescriptor = setDescriptors.last else { - return .master - } - - if setDescriptor.valueTeamOne == nil { - return colorTeamOne - } else if setDescriptor.valueTeamTwo == nil { - return colorTeamTwo - } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { - return colorTeamOne - } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { - return colorTeamTwo - } - - return colorTeamTwo - } - - var showSetInputView: Bool { return setDescriptors.anySatisfy({ $0.showSetInputView }) } diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index fef0db5..6ef5bfe 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -17,6 +17,10 @@ struct SetDescriptor: Identifiable, Equatable { var showSetInputView: Bool = true var showTieBreakInputView: Bool = false + var isTeamOneSet: Bool { + return valueTeamOne != nil || tieBreakValueTeamOne != nil + } + var hasEnded: Bool { if let valueTeamTwo, let valueTeamOne { return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo) diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 600b771..52e3225 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -16,12 +16,29 @@ struct EditScoreView: View { @State private var presentMatchFormatSelection: Bool = false @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss + @State private var firstTeamIsFirstScoreToEnter: Bool = true init(match: Match, confirmScoreEdition: Binding) { let matchDescriptor = MatchDescriptor(match: match) _matchDescriptor = .init(wrappedValue: matchDescriptor) _confirmScoreEdition = confirmScoreEdition } + + var defaultTeamIsActive: Bool { + if firstTeamIsFirstScoreToEnter { + matchDescriptor.teamOneSetupIsActive + } else { + matchDescriptor.teamTwoSetupIsActive + } + } + + var otherTeamIsActive: Bool { + if firstTeamIsFirstScoreToEnter { + matchDescriptor.teamTwoSetupIsActive + } else { + matchDescriptor.teamOneSetupIsActive + } + } func walkout(_ team: TeamPosition) { self.matchDescriptor.match?.setWalkOut(team) @@ -45,20 +62,48 @@ struct EditScoreView: View { return winner ? tournament.tournamentLevel.points(for: seedInterval.first - 1, count: teamsCount) : tournament.tournamentLevel.points(for: seedInterval.last - 1, count: teamsCount) } + func getColor(setDescriptor: SetDescriptor) -> Color { + switch (firstTeamIsFirstScoreToEnter, setDescriptor.isTeamOneSet == false) { + case (true, true), (false, false): + return matchDescriptor.colorTeamOne + default: + return matchDescriptor.colorTeamTwo + } + } + + func teamScorePositionLabel(teamPosition: TeamPosition) -> String { + switch (firstTeamIsFirstScoreToEnter, teamPosition == .one) { + case (true, true), (false, false): + return "score de gauche" + default: + return "score de droite" + } + } + var body: some View { Form { Section { - VStack(alignment: .leading) { - Text(matchDescriptor.teamLabelOne) - if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .one) { - Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") - .bold() + HStack { + VStack(alignment: .leading) { + Text(teamScorePositionLabel(teamPosition: .one)).font(.caption) + Text(matchDescriptor.teamLabelOne) + if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .one) { + Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") + .bold() + } } + Spacer() + } + .listRowView(isActive: defaultTeamIsActive, color: matchDescriptor.colorTeamOne, hideColorVariation: false) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity) + .onTapGesture { + firstTeamIsFirstScoreToEnter.toggle() } - .listRowView(isActive: matchDescriptor.teamOneSetupIsActive, color: matchDescriptor.colorTeamOne, hideColorVariation: false) HStack { Spacer() VStack(alignment: .trailing) { + Text(teamScorePositionLabel(teamPosition: .two)).font(.caption) Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .two) { Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") @@ -66,7 +111,12 @@ struct EditScoreView: View { } } } - .listRowView(isActive: matchDescriptor.teamTwoSetupIsActive, color: matchDescriptor.colorTeamTwo, hideColorVariation: false, alignment: .trailing) + .listRowView(isActive: otherTeamIsActive, color: matchDescriptor.colorTeamTwo, hideColorVariation: false, alignment: .trailing) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity) + .onTapGesture { + firstTeamIsFirstScoreToEnter.toggle() + } } header: { if let roundTitle = matchDescriptor.match?.roundAndMatchTitle() { Text(roundTitle) @@ -96,7 +146,7 @@ struct EditScoreView: View { } } ForEach($matchDescriptor.setDescriptors) { $setDescriptor in - SetInputView(setDescriptor: $setDescriptor) + SetInputView(setDescriptor: $setDescriptor, firstTeamIsFirstScoreToEnter: firstTeamIsFirstScoreToEnter) .onChange(of: setDescriptor.hasEnded) { if setDescriptor.hasEnded { if matchDescriptor.hasEnded == false { @@ -107,7 +157,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } - .tint(matchDescriptor.getColor()) + .tint(getColor(setDescriptor: setDescriptor)) } if matchDescriptor.hasEnded { diff --git a/PadelClub/Views/Score/SetInputView.swift b/PadelClub/Views/Score/SetInputView.swift index 231e168..ff87f39 100644 --- a/PadelClub/Views/Score/SetInputView.swift +++ b/PadelClub/Views/Score/SetInputView.swift @@ -11,6 +11,7 @@ struct SetInputView: View { @Binding var setDescriptor: SetDescriptor @State private var showSetInputView: Bool = true @State private var showTieBreakInputView: Bool = false + var firstTeamIsFirstScoreToEnter: Bool = true var setFormat: SetFormat { setDescriptor.setFormat } @@ -22,56 +23,138 @@ struct SetInputView: View { setFormat == .superTieBreak || setFormat == .megaTieBreak } + var defaultTeamValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.valueTeamOne + } else { + return setDescriptor.valueTeamTwo + } + } + + var defaultTeamValueBinding: Binding { + Binding { + defaultTeamValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.valueTeamOne = value + } else { + setDescriptor.valueTeamTwo = value + } + } + } + + var otherTeamValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.valueTeamTwo + } else { + return setDescriptor.valueTeamOne + } + } + + var otherTeamValueBinding: Binding { + Binding { + otherTeamValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.valueTeamTwo = value + } else { + setDescriptor.valueTeamOne = value + } + } + + } + + var defaultTeamTieBreakValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.tieBreakValueTeamOne + } else { + return setDescriptor.tieBreakValueTeamTwo + } + } + + var defaultTeamTieBreakValueBinding: Binding { + Binding { + defaultTeamTieBreakValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.tieBreakValueTeamOne = value + } else { + setDescriptor.tieBreakValueTeamTwo = value + } + } + } + + var otherTeamTieBreakValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.tieBreakValueTeamTwo + } else { + return setDescriptor.tieBreakValueTeamOne + } + } + + var otherTeamTieBreakValueBinding: Binding { + Binding { + otherTeamTieBreakValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.tieBreakValueTeamTwo = value + } else { + setDescriptor.tieBreakValueTeamOne = value + } + } + + } + private var currentValue: Binding { Binding { - if setDescriptor.valueTeamOne != nil { - return setDescriptor.valueTeamTwo + if defaultTeamValue != nil { + return otherTeamValue } else { - return setDescriptor.valueTeamOne + return defaultTeamValue } } set: { newValue, _ in - if setDescriptor.valueTeamOne != nil { - setDescriptor.valueTeamTwo = newValue + if defaultTeamValue != nil { + otherTeamValueBinding.wrappedValue = newValue } else { - setDescriptor.valueTeamOne = newValue + defaultTeamValueBinding.wrappedValue = newValue } } } private var currentTiebreakValue: Binding { Binding { - if setDescriptor.tieBreakValueTeamOne != nil { - return setDescriptor.tieBreakValueTeamTwo + if defaultTeamTieBreakValue != nil { + return otherTeamTieBreakValue } else { - return setDescriptor.tieBreakValueTeamOne + return defaultTeamTieBreakValue } } set: { newValue, _ in - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if tieBreakValueTeamOne < tieBreakValueTeamTwo && tieBreakValueTeamTwo > 6 { - setDescriptor.tieBreakValueTeamOne = newValue + defaultTeamTieBreakValueBinding.wrappedValue = newValue } else if tieBreakValueTeamOne > tieBreakValueTeamTwo && tieBreakValueTeamOne > 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + otherTeamTieBreakValueBinding.wrappedValue = newValue } } - else if setDescriptor.tieBreakValueTeamOne != nil { - setDescriptor.tieBreakValueTeamTwo = newValue + else if defaultTeamTieBreakValue != nil { + otherTeamTieBreakValueBinding.wrappedValue = newValue } else { - setDescriptor.tieBreakValueTeamOne = newValue + defaultTeamTieBreakValueBinding.wrappedValue = newValue } } } private var disableValues: [Int] { - if let valueTeamOne = setDescriptor.valueTeamOne { + if let valueTeamOne = defaultTeamValue { return setFormat.disableValuesForTeamTwo(with: valueTeamOne) } return [] } private var disableTieBreakValues: [Int] { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue { if tieBreakValueTeamOne == 7 { return [7,6] } @@ -90,7 +173,7 @@ struct SetInputView: View { } func possibleValues() -> [Int] { - if let valueTeamOne = setDescriptor.valueTeamOne { + if let valueTeamOne = defaultTeamValue { if valueTeamOne == 7 && setFormat == .six { return [6,5] } @@ -102,7 +185,7 @@ struct SetInputView: View { } func tieBreakPossibleValues() -> [Int] { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if tieBreakValueTeamOne == 6 && tieBreakValueTeamTwo == 8 { return [] } @@ -114,7 +197,7 @@ struct SetInputView: View { } return Array(((max(tieBreakValueTeamOne, tieBreakValueTeamTwo)+2).. 10 && setFormat == .superTieBreak { - setDescriptor.valueTeamTwo = newValue - 2 + otherTeamValueBinding.wrappedValue = newValue - 2 } else if newValue > 15 && setFormat == .megaTieBreak { - setDescriptor.valueTeamTwo = newValue - 2 + otherTeamValueBinding.wrappedValue = newValue - 2 } } } - .onChange(of: setDescriptor.valueTeamTwo) { - if setDescriptor.valueTeamOne != nil && setDescriptor.valueTeamTwo != nil { + .onChange(of: otherTeamValue) { + if defaultTeamValue != nil && otherTeamValue != nil { showSetInputView = false } } - .onChange(of: setDescriptor.tieBreakValueTeamOne) { - if let newValue = setDescriptor.tieBreakValueTeamOne, setDescriptor.tieBreakValueTeamTwo == nil { + .onChange(of: defaultTeamTieBreakValue) { + if let newValue = defaultTeamTieBreakValue, otherTeamTieBreakValue == nil { if newValue > 7 { - setDescriptor.tieBreakValueTeamTwo = newValue - 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue - 2 } if newValue == 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue <= 5 { - setDescriptor.tieBreakValueTeamTwo = 7 + otherTeamTieBreakValueBinding.wrappedValue = 7 } } - else if let newValue = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + else if let newValue = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if newValue > 6 && tieBreakValueTeamTwo < newValue { - setDescriptor.tieBreakValueTeamTwo = newValue - 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue - 2 } if newValue > 6 && tieBreakValueTeamTwo > newValue { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue == 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue <= 5 { - setDescriptor.tieBreakValueTeamTwo = 7 + otherTeamTieBreakValueBinding.wrappedValue = 7 showTieBreakInputView = false } } } - .onChange(of: setDescriptor.tieBreakValueTeamTwo) { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, tieBreakValueTeamOne <= 5 { + .onChange(of: otherTeamTieBreakValue) { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, tieBreakValueTeamOne <= 5 { showTieBreakInputView = false } else { - if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { - if tieBreakValueTeamTwo > 6 && tieBreakValueTeamTwo > setDescriptor.tieBreakValueTeamOne ?? 0 { - setDescriptor.tieBreakValueTeamOne = tieBreakValueTeamTwo - 2 + if let tieBreakValueTeamTwo = otherTeamTieBreakValue { + if tieBreakValueTeamTwo > 6 && tieBreakValueTeamTwo > defaultTeamTieBreakValue ?? 0 { + defaultTeamTieBreakValueBinding.wrappedValue = tieBreakValueTeamTwo - 2 } - if tieBreakValueTeamTwo > 4 && tieBreakValueTeamTwo < setDescriptor.tieBreakValueTeamOne ?? 0 { - setDescriptor.tieBreakValueTeamOne = tieBreakValueTeamTwo + 2 + if tieBreakValueTeamTwo > 4 && tieBreakValueTeamTwo < defaultTeamTieBreakValue ?? 0 { + defaultTeamTieBreakValueBinding.wrappedValue = tieBreakValueTeamTwo + 2 } } - if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo, let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if let tieBreakValueTeamTwo = otherTeamTieBreakValue, let tieBreakValueTeamOne = defaultTeamTieBreakValue { if tieBreakValueTeamTwo < 6 && tieBreakValueTeamOne == 7 { showTieBreakInputView = false } From af1ea610b4766d04e54c3e0043f34cf5b0b2b0dd Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 28 Oct 2024 11:47:50 +0100 Subject: [PATCH 061/106] minor fixes --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Data/GroupStage.swift | 4 ++-- PadelClub/Data/Tournament.swift | 3 ++- .../Extensions/FixedWidthInteger+Extensions.swift | 10 +++++++++- PadelClub/Utils/PadelRule.swift | 9 +++++++-- .../GroupStage/Components/GroupStageSettingsView.swift | 8 ++++++++ PadelClub/Views/GroupStage/GroupStageView.swift | 2 -- PadelClub/Views/Shared/MatchFormatRowView.swift | 5 +++-- PadelClub/Views/Shared/MatchTypeSelectionView.swift | 3 ++- 9 files changed, 37 insertions(+), 15 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 10ac1be..4c90f36 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3383,7 +3383,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3406,7 +3406,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.24; + MARKETING_VERSION = 1.0.25; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3427,7 +3427,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3449,7 +3449,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.24; + MARKETING_VERSION = 1.0.25; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index da476cb..e89d98a 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -221,8 +221,8 @@ final class GroupStage: ModelObject, Storable { func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? { if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) { let hideSetDifference = matchFormat.setsToWin == 1 - let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) - let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false))) + let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " set" + scoreData.setDifference.pluralSuffix + let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x") return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference) // return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString } else { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 512ba1a..8334da2 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -152,17 +152,18 @@ final class Tournament : ModelObject, Storable { self.publishBrackets = true self.publishGroupStages = true self.publishRankings = true + self.publishTournament = true #else self.publishTeams = publishTeams self.publishSummons = publishSummons self.publishBrackets = publishBrackets self.publishGroupStages = publishGroupStages self.publishRankings = publishRankings + self.publishTournament = publishTournament #endif self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyGroupStage = shouldVerifyGroupStage self.hideTeamsWeight = hideTeamsWeight - self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned self.loserBracketMode = loserBracketMode self.initialSeedRound = initialSeedRound diff --git a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift index f9c0d6d..c40c74a 100644 --- a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift +++ b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift @@ -19,8 +19,16 @@ public extension FixedWidthInteger { return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine) } + private var isMany: Bool { + self > 1 || self < -1 + } + var pluralSuffix: String { - return self > 1 ? "s" : "" + return isMany ? "s" : "" + } + + func localizedPluralSuffix(_ plural: String = "s") -> String { + return isMany ? plural : "" } func formattedAsRawString() -> String { diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 404899b..81bb541 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1349,8 +1349,13 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return setFormat } - func formatTitle() -> String { - ["Format ", shortFormat, suffix].joined() + func formatTitle(_ displayStyle: DisplayStyle = .wide) -> String { + switch displayStyle { + case .short: + return ["Format ", shortFormat].joined() + default: + return ["Format ", shortFormat, suffix].joined() + } } var suffix: String { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 5bfa715..6dcd8f9 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -181,6 +181,10 @@ struct GroupStageSettingsView: View { presentConfirmationButton = true } } + .onChange(of: groupStage.matchFormat) { + _save() + groupStage.updateAllMatchesFormat() + } .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { @@ -190,6 +194,10 @@ struct GroupStageSettingsView: View { } } } + + ToolbarItem(placement: .topBarTrailing) { + MatchTypeSelectionView(selectedFormat: $groupStage.matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration, displayStyle: .short) + } }) .navigationTitle("Paramètres") .toolbarBackground(.visible, for: .navigationBar) diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 63abc49..dd5007a 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -165,13 +165,11 @@ struct GroupStageView: View { if let setsDifference = score.setsDifference { HStack(spacing: 4.0) { Text(setsDifference) - Text("sets") }.font(.footnote) } if let gamesDifference = score.gamesDifference { HStack(spacing: 4.0) { Text(gamesDifference) - Text("jeux") }.font(.footnote) } } diff --git a/PadelClub/Views/Shared/MatchFormatRowView.swift b/PadelClub/Views/Shared/MatchFormatRowView.swift index 8698ea4..c03a13b 100644 --- a/PadelClub/Views/Shared/MatchFormatRowView.swift +++ b/PadelClub/Views/Shared/MatchFormatRowView.swift @@ -13,6 +13,7 @@ struct MatchFormatRowView: View { var hideExplanation: Bool = false var hideDuration: Bool = false var additionalEstimationDuration: Int? + var displayStyle: DisplayStyle = .wide var body: some View { VStack(alignment: .leading) { @@ -26,8 +27,8 @@ struct MatchFormatRowView: View { } } HStack { - Text(matchFormat.formatTitle()) - if hideDuration == false { + Text(matchFormat.formatTitle(displayStyle)) + if hideDuration == false && displayStyle != .short { Spacer() Text("~" + matchFormat.formattedEstimatedDuration(additionalEstimationDuration ?? 0)) } diff --git a/PadelClub/Views/Shared/MatchTypeSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSelectionView.swift index 54bc81c..2801067 100644 --- a/PadelClub/Views/Shared/MatchTypeSelectionView.swift +++ b/PadelClub/Views/Shared/MatchTypeSelectionView.swift @@ -11,6 +11,7 @@ struct MatchTypeSelectionView: View { @Binding var selectedFormat: MatchFormat var format: String? var additionalEstimationDuration: Int? + var displayStyle: DisplayStyle = .wide var body: some View { NavigationLink { @@ -18,7 +19,7 @@ struct MatchTypeSelectionView: View { .navigationTitle("Choix du format") .toolbarBackground(.visible, for: .navigationBar) } label: { - MatchFormatRowView(matchFormat: selectedFormat, headerLabel: format, hideExplanation: true, additionalEstimationDuration: additionalEstimationDuration) + MatchFormatRowView(matchFormat: selectedFormat, headerLabel: format, hideExplanation: true, additionalEstimationDuration: additionalEstimationDuration, displayStyle: displayStyle) } } } From 9a47f8c87def81119d53700d370f17061d10d71a Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 28 Oct 2024 12:23:33 +0100 Subject: [PATCH 062/106] fix teams sorting in edge case scenario --- PadelClub/Data/Tournament.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 8334da2..0165676 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -867,7 +867,7 @@ defer { let defaultSorting : [MySortDescriptor] = _defaultSorting() - let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(by: \.initialWeight) + let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending) @@ -2100,7 +2100,7 @@ defer { private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: - [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)] + [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] case .inscriptionDate: [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] } @@ -2112,7 +2112,7 @@ defer { && federalTournamentAge == build.age } - private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.id)] + private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.id)] private func _matchSchedulers() -> [MatchScheduler] { return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } From 31f093e60e69628666d1601e8a63210f880737f7 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 29 Oct 2024 08:44:15 +0100 Subject: [PATCH 063/106] v1.0.25 b2 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4c90f36..c383318 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.24; + MARKETING_VERSION = 1.0.25; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.24; + MARKETING_VERSION = 1.0.25; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 835bf8fd3fe7927443c542eebe45eb2fb9137c1b Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 1 Nov 2024 08:30:39 +0100 Subject: [PATCH 064/106] fix bugs with smart planning fix bug with planning view by court fix bug with seed deletion --- PadelClub.xcodeproj/project.pbxproj | 8 +- PadelClub/Data/Match.swift | 2 + PadelClub/Data/MatchScheduler.swift | 4 +- PadelClub/Views/Match/MatchDetailView.swift | 20 ++ PadelClub/Views/Match/MatchSetupView.swift | 2 +- .../Views/Planning/PlanningByCourtView.swift | 13 +- .../Views/Planning/PlanningSettingsView.swift | 222 ++++++++++++------ PadelClub/Views/Round/RoundView.swift | 6 +- 8 files changed, 190 insertions(+), 87 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c383318..8d9035b 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3222,7 +3222,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3267,7 +3267,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 9d3b181..3e522eb 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -551,6 +551,8 @@ defer { } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) + } else if let startDate, let endDate, startDate >= endDate { + self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } let teamOne = team(matchDescriptor.winner) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 3eb7450..f277d11 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -673,7 +673,7 @@ final class MatchScheduler : ModelObject, Storable { @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { let upperRounds: [Round] = tournament.rounds() - let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false }) + let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) var rounds = [Round]() @@ -694,7 +694,7 @@ final class MatchScheduler : ModelObject, Storable { } let flattenedMatches = rounds.flatMap { round in - round._matches().filter({ $0.disabled == false && $0.hasEnded() == false }).sorted(by: \.index) + round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index) } flattenedMatches.forEach({ diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 0548811..145ba90 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -354,6 +354,26 @@ struct MatchDetailView: View { Text("Remise-à-zéro") } + if match.teamScores.isEmpty == false { + Divider() + Menu { + ForEach(match.teamScores) { teamScore in + Button(role: .destructive) { + do { + try tournamentStore.teamScores.delete(instance: teamScore) + } catch { + Logger.error(error) + } + match.confirmed = false + _saveMatch() + } label: { + Text(teamScore.team?.teamLabel() ?? "Aucun nom") + } + } + } label: { + Text("Supprimer une équipe") + } + } } label: { LabelOptions() } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index b804de0..1c7e833 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -205,7 +205,7 @@ struct MatchSetupView: View { } catch { Logger.error(error) } - match.previousMatches().forEach { previousMatch in + if let previousMatch = match.previousMatch(teamPosition) { if previousMatch.disabled { previousMatch.enableMatch() do { diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 2ed79c0..f90edc6 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -16,6 +16,7 @@ struct PlanningByCourtView: View { @State private var viewByCourt: Bool = false @State private var selectedDay: Date @State private var selectedCourt: Int = 0 + @State private var uuid: UUID = UUID() var timeSlots: [Date:[Match]] { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } @@ -48,8 +49,14 @@ struct PlanningByCourtView: View { let noStartDate = matches.allSatisfy({ $0.startDate == nil }) List { - _byCourtView(noStartDate: noStartDate) - .id(selectedCourt) + _byCourtView(selectedCourt: selectedCourt, selectedDay: selectedDay, noStartDate: noStartDate) + .id(uuid) + } + .onChange(of: selectedCourt) { + uuid = UUID() + } + .onChange(of: selectedDay) { + uuid = UUID() } .overlay { if noStartDate { @@ -98,7 +105,7 @@ struct PlanningByCourtView: View { } @ViewBuilder - func _byCourtView(noStartDate: Bool) -> some View { + func _byCourtView(selectedCourt: Int, selectedDay: Date, noStartDate: Bool) -> some View { if let _matches = courtSlots[selectedCourt]?.filter({ $0.startDate?.dayInt == selectedDay.dayInt }) { let _sortedMatches = _matches.sorted(by: \.computedStartDateForSorting) if _sortedMatches.isEmpty == false { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 70957f6..d2ba9f9 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -143,82 +143,7 @@ struct PlanningSettingsView: View { } } - let allMatches = tournament.allMatches() - let allGroupStages = tournament.allGroupStages() - let allRounds = tournament.allRounds() - let matchesWithDate = allMatches.filter({ $0.startDate != nil }) - - let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) - - let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) - - _formatPerDayView(matchCountPerDay: countedSet) - - let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) - let roundsWithDate = allRounds.filter({ $0.startDate != nil }) - if matchesWithDate.isEmpty == false || groupStagesWithDate.isEmpty == false || roundsWithDate.isEmpty == false { - Section { - RowButtonView("Supprimer les horaires des matches", role: .destructive) { - do { - deletingDateMatchesDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - deletingDateMatchesDone = true - } catch { - Logger.error(error) - } - } - } footer: { - Text("Garde les horaires définis pour les poules et les manches.") - } - - Section { - RowButtonView("Supprimer tous les horaires", role: .destructive) { - do { - deletingDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - - allGroupStages.forEach({ $0.startDate = nil }) - try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) - - allRounds.forEach({ $0.startDate = nil }) - try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) - deletingDone = true - } catch { - Logger.error(error) - } - } - } - } - - Section { - if groupStagesWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") - } - if roundsWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") - } - RowButtonView("Horaire intelligent", role: .destructive) { - await MainActor.run { - issueFound = false - schedulingDone = false - } - self.issueFound = await _setupSchedule() - await MainActor.run { - _save() - schedulingDone = true - } - } - } footer: { - Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() - } + _smartView() } .headerProminence(.increased) .onAppear { @@ -265,6 +190,151 @@ struct PlanningSettingsView: View { } } + @ViewBuilder + private func _smartView() -> some View { + let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) + let allGroupStages = tournament.allGroupStages() + let allRounds = tournament.allRounds() + let matchesWithDate = allMatches.filter({ $0.startDate != nil }) + + let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) + + let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) + + _formatPerDayView(matchCountPerDay: countedSet) + + let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) + let roundsWithDate = allRounds.filter({ $0.startDate != nil }) + if matchesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des matches", role: .destructive) { + do { + deletingDateMatchesDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + deletingDateMatchesDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs restants non démarrés. Garde les horaires définis pour les poules et les manches du tableau.") + } + } + + if groupStagesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des poules", role: .destructive) { + do { + deletingDone = false + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + deletingDone = true + } catch { + Logger.error(error) + } + } + } + } + + if roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires du tableau", role: .destructive) { + do { + deletingDone = false + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires définis pour les manches du tableau.") + } + } + + if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer tous les horaires", role: .destructive) { + do { + deletingDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.") + } + } + + #if DEBUG + Section { + RowButtonView("Supprimer tous les horaires", role: .destructive) { + do { + deletingDone = false + tournament.allMatches().forEach({ + $0.startDate = nil + $0.endDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: tournament.allMatches()) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs, les horaires définis pour les poules et les manches du tableau.") + } + #endif + + + Section { + if groupStagesWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") + } + if roundsWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") + } + RowButtonView("Horaire intelligent", role: .destructive) { + await MainActor.run { + issueFound = false + schedulingDone = false + } + self.issueFound = await _setupSchedule() + await MainActor.run { + _save() + schedulingDone = true + } + } + } footer: { + Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() + } + } + @ViewBuilder private func _optionsView() -> some View { List { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index bfe0e13..68c0418 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -109,7 +109,11 @@ struct RoundView: View { LabeledContent { let status = upperRound.status() if status.0 == status.1 { - Image(systemName: "checkmark").foregroundStyle(.green) + if status.0 == 0 { + Text("aucun match") + } else { + Image(systemName: "checkmark").foregroundStyle(.green) + } } else { Text("\(status.0) terminé\(status.0.pluralSuffix) sur \(status.1)") } From a5e09d2c06249dfd468b11f3128b7f29d62db337 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 1 Nov 2024 08:30:39 +0100 Subject: [PATCH 065/106] fix bugs with smart planning fix bug with planning view by court fix bug with seed deletion --- PadelClub.xcodeproj/project.pbxproj | 8 +- PadelClub/Data/Match.swift | 2 + PadelClub/Data/MatchScheduler.swift | 4 +- PadelClub/Views/Match/MatchDetailView.swift | 20 ++ PadelClub/Views/Match/MatchSetupView.swift | 2 +- .../Views/Planning/PlanningByCourtView.swift | 13 +- .../Views/Planning/PlanningSettingsView.swift | 220 ++++++++++++------ PadelClub/Views/Round/RoundView.swift | 6 +- 8 files changed, 188 insertions(+), 87 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c383318..8d9035b 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3222,7 +3222,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3267,7 +3267,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 9d3b181..3e522eb 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -551,6 +551,8 @@ defer { } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) + } else if let startDate, let endDate, startDate >= endDate { + self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } let teamOne = team(matchDescriptor.winner) diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 3eb7450..f277d11 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -673,7 +673,7 @@ final class MatchScheduler : ModelObject, Storable { @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { let upperRounds: [Round] = tournament.rounds() - let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false }) + let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) var rounds = [Round]() @@ -694,7 +694,7 @@ final class MatchScheduler : ModelObject, Storable { } let flattenedMatches = rounds.flatMap { round in - round._matches().filter({ $0.disabled == false && $0.hasEnded() == false }).sorted(by: \.index) + round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index) } flattenedMatches.forEach({ diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 0548811..145ba90 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -354,6 +354,26 @@ struct MatchDetailView: View { Text("Remise-à-zéro") } + if match.teamScores.isEmpty == false { + Divider() + Menu { + ForEach(match.teamScores) { teamScore in + Button(role: .destructive) { + do { + try tournamentStore.teamScores.delete(instance: teamScore) + } catch { + Logger.error(error) + } + match.confirmed = false + _saveMatch() + } label: { + Text(teamScore.team?.teamLabel() ?? "Aucun nom") + } + } + } label: { + Text("Supprimer une équipe") + } + } } label: { LabelOptions() } diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index b804de0..1c7e833 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -205,7 +205,7 @@ struct MatchSetupView: View { } catch { Logger.error(error) } - match.previousMatches().forEach { previousMatch in + if let previousMatch = match.previousMatch(teamPosition) { if previousMatch.disabled { previousMatch.enableMatch() do { diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 2ed79c0..f90edc6 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -16,6 +16,7 @@ struct PlanningByCourtView: View { @State private var viewByCourt: Bool = false @State private var selectedDay: Date @State private var selectedCourt: Int = 0 + @State private var uuid: UUID = UUID() var timeSlots: [Date:[Match]] { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } @@ -48,8 +49,14 @@ struct PlanningByCourtView: View { let noStartDate = matches.allSatisfy({ $0.startDate == nil }) List { - _byCourtView(noStartDate: noStartDate) - .id(selectedCourt) + _byCourtView(selectedCourt: selectedCourt, selectedDay: selectedDay, noStartDate: noStartDate) + .id(uuid) + } + .onChange(of: selectedCourt) { + uuid = UUID() + } + .onChange(of: selectedDay) { + uuid = UUID() } .overlay { if noStartDate { @@ -98,7 +105,7 @@ struct PlanningByCourtView: View { } @ViewBuilder - func _byCourtView(noStartDate: Bool) -> some View { + func _byCourtView(selectedCourt: Int, selectedDay: Date, noStartDate: Bool) -> some View { if let _matches = courtSlots[selectedCourt]?.filter({ $0.startDate?.dayInt == selectedDay.dayInt }) { let _sortedMatches = _matches.sorted(by: \.computedStartDateForSorting) if _sortedMatches.isEmpty == false { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 70957f6..5ded88c 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -143,82 +143,7 @@ struct PlanningSettingsView: View { } } - let allMatches = tournament.allMatches() - let allGroupStages = tournament.allGroupStages() - let allRounds = tournament.allRounds() - let matchesWithDate = allMatches.filter({ $0.startDate != nil }) - - let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) - - let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) - - _formatPerDayView(matchCountPerDay: countedSet) - - let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) - let roundsWithDate = allRounds.filter({ $0.startDate != nil }) - if matchesWithDate.isEmpty == false || groupStagesWithDate.isEmpty == false || roundsWithDate.isEmpty == false { - Section { - RowButtonView("Supprimer les horaires des matches", role: .destructive) { - do { - deletingDateMatchesDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - deletingDateMatchesDone = true - } catch { - Logger.error(error) - } - } - } footer: { - Text("Garde les horaires définis pour les poules et les manches.") - } - - Section { - RowButtonView("Supprimer tous les horaires", role: .destructive) { - do { - deletingDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - - allGroupStages.forEach({ $0.startDate = nil }) - try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) - - allRounds.forEach({ $0.startDate = nil }) - try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) - deletingDone = true - } catch { - Logger.error(error) - } - } - } - } - - Section { - if groupStagesWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") - } - if roundsWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") - } - RowButtonView("Horaire intelligent", role: .destructive) { - await MainActor.run { - issueFound = false - schedulingDone = false - } - self.issueFound = await _setupSchedule() - await MainActor.run { - _save() - schedulingDone = true - } - } - } footer: { - Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() - } + _smartView() } .headerProminence(.increased) .onAppear { @@ -265,6 +190,149 @@ struct PlanningSettingsView: View { } } + @ViewBuilder + private func _smartView() -> some View { + let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) + let allGroupStages = tournament.allGroupStages() + let allRounds = tournament.allRounds() + let matchesWithDate = allMatches.filter({ $0.startDate != nil }) + + let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) + + let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) + + _formatPerDayView(matchCountPerDay: countedSet) + + let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) + let roundsWithDate = allRounds.filter({ $0.startDate != nil }) + if matchesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des matches", role: .destructive) { + do { + deletingDateMatchesDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + deletingDateMatchesDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs restants non démarrés. Garde les horaires définis pour les poules et les manches du tableau.") + } + } + + if groupStagesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des poules", role: .destructive) { + do { + deletingDone = false + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + deletingDone = true + } catch { + Logger.error(error) + } + } + } + } + + if roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires du tableau", role: .destructive) { + do { + deletingDone = false + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires définis pour les manches du tableau.") + } + } + + if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer tous les horaires", role: .destructive) { + do { + deletingDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.") + } + } + + #if DEBUG + Section { + RowButtonView("Debug delete all dates", role: .destructive) { + do { + deletingDone = false + tournament.allMatches().forEach({ + $0.startDate = nil + $0.endDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: tournament.allMatches()) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } + #endif + + + Section { + if groupStagesWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") + } + if roundsWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") + } + RowButtonView("Horaire intelligent", role: .destructive) { + await MainActor.run { + issueFound = false + schedulingDone = false + } + self.issueFound = await _setupSchedule() + await MainActor.run { + _save() + schedulingDone = true + } + } + } footer: { + Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() + } + } + @ViewBuilder private func _optionsView() -> some View { List { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index bfe0e13..68c0418 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -109,7 +109,11 @@ struct RoundView: View { LabeledContent { let status = upperRound.status() if status.0 == status.1 { - Image(systemName: "checkmark").foregroundStyle(.green) + if status.0 == 0 { + Text("aucun match") + } else { + Image(systemName: "checkmark").foregroundStyle(.green) + } } else { Text("\(status.0) terminé\(status.0.pluralSuffix) sur \(status.1)") } From ef20c838c44037ef4262cf13edd52e6cad86ee2a Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 1 Nov 2024 08:53:37 +0100 Subject: [PATCH 066/106] fix wording --- .../Views/Planning/PlanningSettingsView.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 33a2162..3edfed5 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -190,6 +190,20 @@ struct PlanningSettingsView: View { } } + private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String { + let base = "Supprime les horaires des matchs restants non démarrés." + let extend = " Garde les horaires définis pour les " + if groupStagesWithDateIsEmpty && roundsWithDateIsEmpty { + return base + } else if groupStagesWithDateIsEmpty, roundsWithDateIsEmpty == false { + return base + extend + "manches du tableau." + } else if roundsWithDateIsEmpty, groupStagesWithDateIsEmpty == false { + return base + extend + "poules." + } else { + return base + extend + "poules et les manches du tableau." + } + } + @ViewBuilder private func _smartView() -> some View { let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) @@ -221,7 +235,7 @@ struct PlanningSettingsView: View { } } } footer: { - Text("Supprime les horaires des matchs restants non démarrés. Garde les horaires définis pour les poules et les manches du tableau.") + Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty)) } } From 407d73a7653c76d191e43ae2a8c68e5eee0e9014 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 1 Nov 2024 11:29:38 +0100 Subject: [PATCH 067/106] wording fix --- PadelClub/Data/Round.swift | 3 +-- PadelClub/Views/Match/MatchSetupView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index e29e884..c9a8669 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -119,8 +119,7 @@ final class Round: ModelObject, Storable { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return self.tournamentStore.teamRegistrations.first(where: { - $0.tournament == tournament - && $0.bracketPosition != nil + $0.bracketPosition != nil && ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! % 2) == team.rawValue }) diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 1c7e833..dc932bd 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -9,7 +9,7 @@ import SwiftUI import LeStorage struct MatchSetupView: View { - static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe sur ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées." + static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées." @EnvironmentObject var dataStore: DataStore From e25bcb47b46d303b3c092ff15a51937eaac70643 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 1 Nov 2024 12:45:33 +0100 Subject: [PATCH 068/106] wording debug option --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Views/Match/MatchDetailView.swift | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 8d9035b..c002b9e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3222,7 +3222,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3267,7 +3267,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 145ba90..9af3d85 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -340,6 +340,7 @@ struct MatchDetailView: View { match.resetScores() match.resetMatch() match.confirmed = false + match.updateFollowingMatchTeamScore() save() } label: { Text("Supprimer les scores") From 1f77e287e08e6a48852e51af38a57ac304181761 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 4 Nov 2024 09:00:23 +0100 Subject: [PATCH 069/106] prod test --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c002b9e..1aa7c37 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3383,7 +3383,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3406,7 +3406,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3427,7 +3427,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3449,7 +3449,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.25; + MARKETING_VERSION = 1.0.26; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From b6e8144bfd9517531845ee9aff92e7e5fbed10b6 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 4 Nov 2024 10:22:50 +0100 Subject: [PATCH 070/106] fix issue with women in men tournament --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Data/PlayerRegistration.swift | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 1aa7c37..fb6dd99 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3222,7 +3222,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.26; + MARKETING_VERSION = 1.0.27; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3267,7 +3267,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.26; + MARKETING_VERSION = 1.0.27; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index c0874bf..d137e2d 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -452,14 +452,8 @@ final class PlayerRegistration: ModelObject, Storable { case 0: return 0 case womanMax: return manMax - womanMax case manMax: return 0 - case 1...10: return 400 - case 11...30: return 1000 - case 31...60: return 2000 - case 61...100: return 3000 - case 101...200: return 8000 - case 201...500: return 12000 default: - return 15000 + return TournamentCategory.femaleInMaleAssimilationAddition(playerRank) } } From 0c0b492d8f7465275a1707f1018958b404880d36 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 5 Nov 2024 07:48:29 +0100 Subject: [PATCH 071/106] add locale currency fix grammar --- PadelClub.xcodeproj/project.pbxproj | 4 +- PadelClub/Data/Tournament.swift | 4 +- PadelClub/Extensions/Locale+Extensions.swift | 5 + .../Views/Cashier/CashierDetailView.swift | 8 +- .../Views/Cashier/CashierSettingsView.swift | 117 +++++++++--------- .../Views/Planning/PlanningByCourtView.swift | 8 +- .../TournamentGeneralSettingsView.swift | 4 +- 7 files changed, 75 insertions(+), 75 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index fb6dd99..9b1b10e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.27; + MARKETING_VERSION = 1.0.28; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.27; + MARKETING_VERSION = 1.0.28; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 0165676..f79366b 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1592,10 +1592,10 @@ defer { var entryFeeMessage: String { if let entryFee { - let message: String = "Inscription: \(entryFee.formatted(.currency(code: "EUR"))) par joueur." + let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur." return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") } else { - return "Inscription: gratuite." + return "Inscription : gratuite." } } diff --git a/PadelClub/Extensions/Locale+Extensions.swift b/PadelClub/Extensions/Locale+Extensions.swift index 5e6534b..646be4c 100644 --- a/PadelClub/Extensions/Locale+Extensions.swift +++ b/PadelClub/Extensions/Locale+Extensions.swift @@ -20,4 +20,9 @@ extension Locale { return countries.sorted() } + + static func defaultCurrency() -> String { +// return "EUR" + Locale.current.currency?.identifier ?? "EUR" + } } diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index 074a67e..068d17f 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -26,7 +26,7 @@ struct CashierDetailView: View { Section { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -95,7 +95,7 @@ struct CashierDetailView: View { Section { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -144,7 +144,7 @@ struct CashierDetailView: View { var body: some View { LabeledContent { if let value { - Text(value.formatted(.currency(code: "EUR"))) + Text(value.formatted(.currency(code: Locale.defaultCurrency()))) } else { ProgressView() } @@ -173,7 +173,7 @@ struct CashierDetailView: View { LabeledContent { if let entryFee = tournament.entryFee { let sum = Double(count) * entryFee - Text(sum.formatted(.currency(code: "EUR"))) + Text(sum.formatted(.currency(code: Locale.defaultCurrency()))) } } label: { Text(type.localizedLabel()) diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index fd51e73..098c78b 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -24,85 +24,72 @@ struct CashierSettingsView: View { var body: some View { List { Section { - LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) - .keyboardType(.decimalPad) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity) - .focused($focusedField, equals: ._entryFee) - } label: { - Text("Inscription") - } + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($focusedField, equals: ._entryFee) + } header: { + Text("Prix de l'inscription") } footer: { Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") } - Section { - RowButtonView("Tout le monde est arrivé", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = true - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) + + let players = tournament.selectedPlayers() + + if players.anySatisfy({ $0.hasArrived == false }) { + Section { + RowButtonView("Tout le monde est arrivé", role: .destructive) { + players.forEach { player in + player.hasArrived = true + } + _save(players: players) } + } footer: { + Text("Indique tous les joueurs sont là") } - } footer: { - Text("Indique tous les joueurs sont là") } - Section { - RowButtonView("Personne n'est là", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = false - } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) + if players.anySatisfy({ $0.hasArrived == true }) { + Section { + RowButtonView("Personne n'est là", role: .destructive) { + players.forEach { player in + player.hasArrived = false + } + _save(players: players) } + } footer: { + Text("Indique qu'aucun joueur n'est arrivé") } - } footer: { - Text("Indique qu'aucun joueur n'est arrivé") } - Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift + if players.anySatisfy({ $0.hasPaid() == false }) { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift + } } + _save(players: players) } - do { - try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") } - } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") } - Section { - RowButtonView("Personne n'a réglé", role: .destructive) { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() - players.forEach { player in - player.paymentType = nil - } - do { - try store.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) + if players.anySatisfy({ $0.hasPaid() == true }) { + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + players.forEach { player in + player.paymentType = nil + } + _save(players: players) } + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") } - } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") } } .navigationBarBackButtonHidden(focusedField != nil) @@ -119,7 +106,7 @@ struct CashierSettingsView: View { HStack { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: "EUR"))) { + Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil @@ -150,6 +137,14 @@ struct CashierSettingsView: View { } } + private func _save(players: [PlayerRegistration]) { + do { + try tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } catch { + Logger.error(error) + } + } + private func _save() { do { try dataStore.tournaments.addOrUpdate(instance: tournament) diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index f90edc6..25592c6 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -132,17 +132,17 @@ struct PlanningByCourtView: View { } } else if noStartDate == false { ContentUnavailableView { - Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") + Label("Aucun match planifié", systemImage: "clock.badge.questionmark") } description: { - Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") + Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") } actions: { } } } else if noStartDate == false { ContentUnavailableView { - Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") + Label("Aucun match planifié", systemImage: "clock.badge.questionmark") } description: { - Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") + Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") } actions: { } } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 1d8efa3..7b2f6fe 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -45,7 +45,7 @@ struct TournamentGeneralSettingsView: View { TournamentDatePickerView() TournamentDurationManagerView() LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -125,7 +125,7 @@ struct TournamentGeneralSettingsView: View { if focusedField == ._entryFee { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: "EUR"))) { + Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil From 619702bcff7ae998eee2b8209c668155968f8655 Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 5 Nov 2024 14:06:39 +0100 Subject: [PATCH 072/106] add new variable to drawlog and fix filterByStoreIdentifier improve ranking data debug export --- PadelClub/Data/DrawLog.swift | 71 +++++++++++++++-- PadelClub/Data/TeamRegistration.swift | 2 +- PadelClub/Utils/FileImportManager.swift | 3 +- PadelClub/Utils/SourceFileManager.swift | 4 +- .../GroupStageQualificationManagerView.swift | 72 ++++++++++++++++++ .../Navigation/Umpire/PadelClubView.swift | 76 ++++++++++++++++++- PadelClub/Views/Round/RoundSettingsView.swift | 2 +- .../Shared/TournamentCellView.swift | 4 + PadelClubTests/ServerDataTests.swift | 3 +- 9 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift diff --git a/PadelClub/Data/DrawLog.swift b/PadelClub/Data/DrawLog.swift index 29cf948..8673aaf 100644 --- a/PadelClub/Data/DrawLog.swift +++ b/PadelClub/Data/DrawLog.swift @@ -13,7 +13,7 @@ import LeStorage final class DrawLog: ModelObject, Storable { static func resourceName() -> String { return "draw-logs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - static func filterByStoreIdentifier() -> Bool { return false } + static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() @@ -22,14 +22,16 @@ final class DrawLog: ModelObject, Storable { var drawSeed: Int var drawMatchIndex: Int var drawTeamPosition: TeamPosition + var drawType: DrawType - internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition) { + internal init(id: String = Store.randomId(), tournament: String, drawDate: Date = Date(), drawSeed: Int, drawMatchIndex: Int, drawTeamPosition: TeamPosition, drawType: DrawType) { self.id = id self.tournament = tournament self.drawDate = drawDate self.drawSeed = drawSeed self.drawMatchIndex = drawMatchIndex self.drawTeamPosition = drawTeamPosition + self.drawType = drawType } func tournamentObject() -> Tournament? { @@ -48,24 +50,34 @@ final class DrawLog: ModelObject, Storable { } func exportedDrawLog() -> String { - [drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].joined(separator: " ") + [drawType.localizedDrawType(), drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].filter({ $0.isEmpty == false }).joined(separator: " ") } func localizedDrawSeedLabel() -> String { - return "Tête de série #\(drawSeed + 1)" + return "\(drawType.localizedDrawType()) #\(drawSeed + 1)" } func localizedDrawLogLabel() -> String { - return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ") + return [localizedDrawSeedLabel(), positionLabel()].filter({ $0.isEmpty == false }).joined(separator: " -> ") } func localizedDrawBranch() -> String { - drawTeamPosition.localizedBranchLabel() + switch drawType { + case .seed: + return drawTeamPosition.localizedBranchLabel() + default: + return "" + } } func drawMatch() -> Match? { - let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) - return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) + switch drawType { + case .seed: + let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) + return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) + default: + return nil + } } func positionLabel() -> String { @@ -94,6 +106,32 @@ final class DrawLog: ModelObject, Storable { case _drawSeed = "drawSeed" case _drawMatchIndex = "drawMatchIndex" case _drawTeamPosition = "drawTeamPosition" + case _drawType = "drawType" + } + + required 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) + drawDate = try container.decode(Date.self, forKey: ._drawDate) + drawSeed = try container.decode(Int.self, forKey: ._drawSeed) + drawMatchIndex = try container.decode(Int.self, forKey: ._drawMatchIndex) + drawTeamPosition = try container.decode(TeamPosition.self, forKey: ._drawTeamPosition) + drawType = try container.decodeIfPresent(DrawType.self, forKey: ._drawType) ?? .seed + } + + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: ._id) + try container.encode(tournament, forKey: ._tournament) + try container.encode(drawDate, forKey: ._drawDate) + try container.encode(drawSeed, forKey: ._drawSeed) + try container.encode(drawMatchIndex, forKey: ._drawMatchIndex) + try container.encode(drawTeamPosition, forKey: ._drawTeamPosition) + try container.encode(drawType, forKey: ._drawType) } func insertOnServer() throws { @@ -101,3 +139,20 @@ final class DrawLog: ModelObject, Storable { } } + +enum DrawType: Int, Codable { + case seed + case groupStage + case court + + func localizedDrawType() -> String { + switch self { + case .seed: + return "Tête de série" + case .groupStage: + return "Poule" + case .court: + return "Terrain" + } + } +} diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 1d8d414..14909a2 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -140,7 +140,7 @@ final class TeamRegistration: ModelObject, Storable { } if let tournament = tournamentObject() { if let index = index(in: tournament.selectedSortedTeams()) { - let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition) + let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed) do { try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) } catch { diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index deb82db..f67228b 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -64,9 +64,10 @@ class FileImportManager { importedPlayer.firstName = firstName.trimmed.capitalized } } - playersLeft.removeAll(where: { $0.lastName.isEmpty == false }) } }) + + players = playersLeft } func foundInWomenData(license: String?) -> Bool { diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index ac06446..80c330c 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -60,9 +60,9 @@ class SourceFileManager { } } - func exportToCSV(players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { + func exportToCSV(_ prefix: String = "", players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { let lastDateString = URL.importDateFormatter.string(from: date) - let dateString = ["CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].joined(separator: "-") + "." + "csv" + let dateString = [prefix, "CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].filter({ $0.isEmpty == false }).joined(separator: "-") + "." + "csv" let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") diff --git a/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift b/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift new file mode 100644 index 0000000..671a96a --- /dev/null +++ b/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift @@ -0,0 +1,72 @@ +// +// GroupStageQualificationManager.swift +// PadelClub +// +// Created by razmig on 05/11/2024. +// + + +class GroupStageQualificationManager { + private let tournament: Tournament + private let tournamentStore: TournamentStore + + init(tournament: Tournament, tournamentStore: TournamentStore) { + self.tournament = tournament + self.tournamentStore = tournamentStore + } + + func qualificationSection() -> some View { + guard tournament.groupStageAdditionalQualified > 0 else { return EmptyView() } + + let name = "\(tournament.qualifiedPerGroupStage + 1).ordinalFormatted()" + let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() + + return Section { + NavigationLink { + SpinDrawView( + drawees: missingQualifiedFromGroupStages.isEmpty + ? tournament.groupStageAdditionalQualifiedPreDraw() + : ["Qualification d'un \(name) de poule"], + segments: missingQualifiedFromGroupStages.isEmpty + ? tournament.groupStageAdditionalLeft() + : missingQualifiedFromGroupStages + ) { results in + if !missingQualifiedFromGroupStages.isEmpty { + self.handleDrawResults(results, missingQualifiedFromGroupStages) + } + } + } label: { + Label { + Text("Qualifier un \(name) de poule par tirage au sort") + } icon: { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.logoBackground) + } + } + .disabled(tournament.moreQualifiedToDraw() == 0) + } footer: { + footerText(missingQualifiedFromGroupStages.isEmpty) + } + } + + private func handleDrawResults(_ results: [DrawResult], _ missingQualifiedFromGroupStages: [Team]) { + results.forEach { drawResult in + var team = missingQualifiedFromGroupStages[drawResult.drawIndex] + team.qualified = true + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + } + } + + private func footerText(_ noMoreTeams: Bool) -> Text { + if tournament.moreQualifiedToDraw() == 0 { + return Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifiant le paramètre dans structure.") + } else if noMoreTeams { + return Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") + } + return Text("") + } +} diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index 95311c8..56e46d8 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -7,6 +7,7 @@ import SwiftUI import LeStorage +import Foundation struct PadelClubView: View { @State private var uuid: UUID = UUID() @@ -74,9 +75,14 @@ struct PadelClubView: View { print("before anonymousPlayers.count", anonymousPlayers.count) FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) - print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count) + + await fetchPlayersDataSequentially(for: &anonymousPlayers) + + print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } .count) SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) + SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) } catch { Logger.error(error) } @@ -241,3 +247,71 @@ struct PadelClubView: View { //#Preview { // PadelClubView() //} + +// Function to fetch data for a single license ID +func fetchPlayerData(for licenseID: String) async throws -> [Player]? { + guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept") + request.setValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site") + request.setValue("fr-FR,fr;q=0.9", forHTTPHeaderField: "Accept-Language") + request.setValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") + request.setValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode") + request.setValue("beach-padel.app.fft.fr", forHTTPHeaderField: "Host") + request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", forHTTPHeaderField: "User-Agent") + request.setValue("keep-alive", forHTTPHeaderField: "Connection") + request.setValue("https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation=82477107", forHTTPHeaderField: "Referer") + request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With") + + // Add cookies if needed (example cookie header value shown, replace with valid cookies) + request.setValue("JSESSIONID=F4ED2A1BCF3CD2694FE0B111B8027999; AWSALB=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; AWSALBCORS=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; datadome=KlbIdnrCgaY1zLVIZ5CfLJm~KXv9_YnXGhaQdqMEn6Ja9R6imBH~vhzmyuiLxGi1D0z90v5x2EiGDvQ7zsw~fajWLbOupFEajulc86PSJ7RIHpOiduCQ~cNoITQYJOXa; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQLNQOPLOSLJOZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQKSMOZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOQMSLNZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQNSJMZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOSJMLJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLRPQMQQNRQRZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLRPQNKSLOMSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLSNSOPMSOPJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMJQSRLJSOOJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMJRJPJMSSKRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; incap_ses_2222_2712217=ui9wOOAjNziUTlU3gCHWHtv/KWcAAAAAhSzbpyITRp7YwRT3vJB2vg==; incap_ses_2224_2712217=NepDAr2kUDShMiCJaDzdHqbjKWcAAAAA0kLlk3lgvGnwWSTMceZoEw==; xtan=-; xtant=1; incap_ses_1350_2712217=g+XhSJRwOS8JlWTYCSq8EtOBJGcAAAAAffg2IobkPUW2BtvgJGHbMw==; TCSESSION=124101910177775608913; nlbi_2712217=jnhtOC5KDiLvfpy/b9lUTgAAAAA7zduh8JyZOVrEfGsEdFlq; TCID=12481811494814553052; xtvrn=$548419$; TCPID=12471746148351334672; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie") + + let (data, _) = try await URLSession.shared.data(for: request) + let decoder = JSONDecoder() + + // Debug: Print raw JSON data for inspection + if let jsonString = String(data: data, encoding: .utf8) { + print("Raw JSON response: \(jsonString)") + } + + // Decode the response + let response = try decoder.decode(Response.self, from: data) + let players = response.object.listeJoueurs + + // Cast the JSON object to [String: Any] dictionary + return players +} + +// Function to fetch data for multiple license IDs using TaskGroup +func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async { + for licenseID in licenseIDs.filter({ $0.firstName.isEmpty && $0.lastName.isEmpty }) { + do { + if let playerData = try await fetchPlayerData(for: licenseID.license)?.first { + licenseID.lastName = playerData.nom + licenseID.firstName = playerData.prenom + } + } catch { + print(error) + } + } +} + + +struct Player: Codable { + let licence: Int + let nom: String + let prenom: String + let sexe: String +} + +struct Response: Codable { + let object: PlayerList +} + +struct PlayerList: Codable { + let listeJoueurs: [Player] +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 0024d88..42a45b4 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -86,7 +86,7 @@ struct RoundSettingsView: View { if previewAvailable { NavigationLink { - PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs()) + PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs().filter({ $0.drawType == .seed })) } label: { Text("Aperçu du repositionnement") } diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index be746d3..c4426d7 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -76,6 +76,10 @@ struct TournamentCellView: View { // .frame(width: 2) VStack(alignment: .leading, spacing: 0.0) { if let tournament = tournament as? Tournament { +#if targetEnvironment(simulator) + Text(tournament.id) +#endif + HStack { Text(tournament.locationLabel(displayStyle)) .lineLimit(1) diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 40f0feb..15887ad 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -378,7 +378,7 @@ final class ServerDataTests: XCTestCase { return } - let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two) + let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two, drawType: .court) let d: DrawLog = try await StoreCenter.main.service().post(drawLog) assert(d.tournament == drawLog.tournament) @@ -386,6 +386,7 @@ final class ServerDataTests: XCTestCase { assert(d.drawSeed == drawLog.drawSeed) assert(d.drawTeamPosition == drawLog.drawTeamPosition) assert(d.drawMatchIndex == drawLog.drawMatchIndex) + assert(d.drawType == drawLog.drawType) } } From 884f1d9186cd118b069be89e82e097971f46c242 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 6 Nov 2024 08:37:01 +0100 Subject: [PATCH 073/106] add multi waves of group stage matchs management --- PadelClub.xcodeproj/project.pbxproj | 16 ++--- PadelClub/Data/GroupStage.swift | 59 ++++++++++++++----- .../Components/GroupStageSettingsView.swift | 16 ++++- .../GroupStage/GroupStagesSettingsView.swift | 8 +++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 9b1b10e..c8384c1 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3222,7 +3222,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3246,7 +3246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.28; + MARKETING_VERSION = 1.0.29; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3267,7 +3267,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3290,7 +3290,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.28; + MARKETING_VERSION = 1.0.29; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3383,7 +3383,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3406,7 +3406,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.26; + MARKETING_VERSION = 1.0.29; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3427,7 +3427,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3449,7 +3449,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.26; + MARKETING_VERSION = 1.0.29; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index e89d98a..b8093f2 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -112,11 +112,17 @@ final class GroupStage: ModelObject, Storable { matchFormat: self.matchFormat, name: self.localizedMatchUpLabel(for: index)) match.store = self.store + print("_createMatch(index)", index) return match } - func removeReturnMatches() { - let returnMatches = _matches().filter({ $0.index >= matchCount }) + func removeReturnMatches(onlyLast: Bool = false) { + + var returnMatches = _matches().filter({ $0.index >= matchCount }) + if onlyLast { + let matchPhaseCount = matchPhaseCount - 1 + returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) + } do { try self.tournamentStore.matches.delete(contentOfs: returnMatches) } catch { @@ -124,12 +130,21 @@ final class GroupStage: ModelObject, Storable { } } + var matchPhaseCount: Int { + let count = _matches().count + if matchCount > 0 { + return count / matchCount + } else { + return 0 + } + } + func addReturnMatches() { var teamScores = [TeamScore]() var matches = [Match]() - + let matchPhaseCount = matchPhaseCount for i in 0..<_numberOfMatchesToBuild() { - let newMatch = self._createMatch(index: i + matchCount) + let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) teamScores.append(contentsOf: newMatch.createTeamScores()) matches.append(newMatch) @@ -174,10 +189,15 @@ final class GroupStage: ModelObject, Storable { func playedMatches() -> [Match] { let ordered = _matches() - if ordered.isEmpty == false && ordered.count == _matchOrder().count { - return _matchOrder().map { - ordered[$0] + let order = _matchOrder() + let matchCount = max(1, matchCount) + let count = ordered.count / matchCount + if ordered.isEmpty == false && ordered.count % order.count == 0 { + let repeatedArray = (0.. String { + if matchCount > 0 { + let count = _matches().count + if count > matchCount * 2 { + return " - vague \((matchIndex / matchCount) + 1)" + } + + if matchIndex >= matchCount { + return " - retour" + } + } + + return "" + } + func localizedMatchUpLabel(for matchIndex: Int) -> String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) vs #\(index2 + 1)" + (matchIndex >= matchCount ? " - retour" : "") + return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex) } else { return "--" } } var matchCount: Int { - (size * size - 1) / 2 + (size * (size - 1)) / 2 } func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 6dcd8f9..521b328 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -154,14 +154,26 @@ struct GroupStageSettingsView: View { } Section { - if groupStage.isReturnMatchEnabled() { + if groupStage.matchPhaseCount > 2 { + RowButtonView("Effacer la dernière vague", role: .destructive) { + groupStage.removeReturnMatches(onlyLast: true) + } + } else if groupStage.isReturnMatchEnabled() { RowButtonView("Effacer les matchs retours", role: .destructive) { groupStage.removeReturnMatches() } - } else { + } + } + + Section { + if groupStage.isReturnMatchEnabled() == false { RowButtonView("Rajouter les matchs retours", role: .destructive) { groupStage.addReturnMatches() } + } else { + RowButtonView("Rajouter une vague de matchs", role: .destructive) { + groupStage.addReturnMatches() + } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index bafec28..3bf6939 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -191,6 +191,14 @@ struct GroupStagesSettingsView: View { generationDoneMessage = "Matchs retours créés" } + } else if groupStages.allSatisfy({ $0.isReturnMatchEnabled() }) { + RowButtonView("Rajouter une vague de matchs", role: .destructive) { + groupStages.forEach { groupStage in + groupStage.addReturnMatches() + } + + generationDoneMessage = "Nouveaux matchs créés" + } } } From e12dbff90d1741dcdce6241fe10545ee2d8adf2e Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 6 Nov 2024 18:14:00 +0100 Subject: [PATCH 074/106] fix issue with smart planning save --- .../Umpire/UmpireStatisticView.swift | 18 ++++++++++++++++ .../GroupStageScheduleEditorView.swift | 10 +++++++-- .../LoserRoundScheduleEditorView.swift | 18 +++++++++++++--- .../Planning/MatchScheduleEditorView.swift | 21 ++++++++++++++++++- .../Planning/RoundScheduleEditorView.swift | 9 ++++++-- .../Views/Player/PlayerStatisticView.swift | 18 ++++++++++++++++ 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift create mode 100644 PadelClub/Views/Player/PlayerStatisticView.swift diff --git a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift new file mode 100644 index 0000000..dcea4eb --- /dev/null +++ b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift @@ -0,0 +1,18 @@ +// +// UmpireStatisticView.swift +// PadelClub +// +// Created by razmig on 06/11/2024. +// + +import SwiftUI + +struct UmpireStatisticView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + UmpireStatisticView() +} diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index 50b7cf4..ec29423 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -15,7 +15,8 @@ struct GroupStageScheduleEditorView: View { @Bindable var groupStage: GroupStage var tournament: Tournament @State private var startDate: Date - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -24,14 +25,19 @@ struct GroupStageScheduleEditorView: View { self.groupStage = groupStage self.tournament = tournament self._startDate = State(wrappedValue: groupStage.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: groupStage.startDate) } var body: some View { - GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $currentDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { groupStage.startDate = startDate tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage) _save() } + .onChange(of: currentDate) { + groupStage.startDate = currentDate + _save() + } } private func _save() { diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index f280dd2..e14ad4b 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -17,7 +17,8 @@ struct LoserRoundScheduleEditorView: View { var loserRounds: [Round] @State private var startDate: Date @State private var matchFormat: MatchFormat - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -27,8 +28,11 @@ struct LoserRoundScheduleEditorView: View { self.tournament = tournament let _loserRounds = upperRound.loserRounds() self.loserRounds = _loserRounds - self._startDate = State(wrappedValue: _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate ?? tournament.startDate) + let startDate = _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate + + self._startDate = State(wrappedValue: startDate ?? tournament.startDate) self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat) + self._currentDate = State(wrappedValue: startDate) } var body: some View { @@ -37,9 +41,17 @@ struct LoserRoundScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: .constant(nil), duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $currentDate, duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) + for loserRound in enabledLoserRounds { + loserRound.startDate = currentDate + } + + _save() + } let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) ForEach(enabledLoserRounds.indices, id: \.self) { index in diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 95a1bce..592a4ac 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -6,16 +6,23 @@ // import SwiftUI +import LeStorage struct MatchScheduleEditorView: View { @Bindable var match: Match var tournament: Tournament @State private var startDate: Date + @State private var currentDate: Date? + + var tournamentStore: TournamentStore { + return self.tournament.tournamentStore + } init(match: Match, tournament: Tournament) { self.match = match self.tournament = tournament self._startDate = State(wrappedValue: match.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: match.startDate) } var title: String { @@ -31,15 +38,27 @@ struct MatchScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: title, startDate: $startDate, currentDate: $currentDate, duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + match.startDate = currentDate + _save() + } } private func _updateSchedule() async { let scheduler: MatchScheduler? = tournament.matchScheduler() scheduler?.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) } + + private func _save() { + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } } //#Preview { diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index b2ad186..679b306 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -14,7 +14,8 @@ struct RoundScheduleEditorView: View { var round: Round var tournament: Tournament @State private var startDate: Date - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -23,6 +24,7 @@ struct RoundScheduleEditorView: View { self.round = round self.tournament = tournament self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: round.startDate) } var body: some View { @@ -32,9 +34,12 @@ struct RoundScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $round.startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $currentDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + round.startDate = currentDate + } ForEach(round.playedMatches()) { match in MatchScheduleEditorView(match: match, tournament: tournament) diff --git a/PadelClub/Views/Player/PlayerStatisticView.swift b/PadelClub/Views/Player/PlayerStatisticView.swift new file mode 100644 index 0000000..e396964 --- /dev/null +++ b/PadelClub/Views/Player/PlayerStatisticView.swift @@ -0,0 +1,18 @@ +// +// PlayerStatisticView.swift +// PadelClub +// +// Created by razmig on 06/11/2024. +// + +import SwiftUI + +struct PlayerStatisticView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + PlayerStatisticView() +} From 877bc33ea94e749555d10424ba3c63a9e313e599 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 6 Nov 2024 18:15:10 +0100 Subject: [PATCH 075/106] add penalty check views --- PadelClub.xcodeproj/project.pbxproj | 16 ++++ .../Umpire/UmpireStatisticView.swift | 76 +++++++++++++++++-- .../Views/Navigation/Umpire/UmpireView.swift | 8 ++ PadelClub/Views/Player/PlayerDetailView.swift | 18 +++-- .../Views/Player/PlayerStatisticView.swift | 35 +++++++-- 5 files changed, 138 insertions(+), 15 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c8384c1..4a2c01b 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -771,6 +771,12 @@ FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; }; FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */; }; FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */; }; + FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; @@ -1156,6 +1162,8 @@ FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = ""; }; FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamReplacementView.swift; sourceTree = ""; }; FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = ""; }; + FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = ""; }; + FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -1491,6 +1499,7 @@ children = ( FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, FF1162842BD00279000C4809 /* PlayerDetailView.swift */, + FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */, FF089EB02BB001EA00F0AEC7 /* Components */, ); path = Player; @@ -1693,6 +1702,7 @@ isa = PBXGroup; children = ( FF3F74F52B919E45004CFE0E /* UmpireView.swift */, + FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, ); path = Umpire; @@ -2320,6 +2330,7 @@ FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, + FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -2474,6 +2485,7 @@ FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, + FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, @@ -2600,6 +2612,7 @@ FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */, FF4CBF6D2C996C0600151637 /* EditablePlayerView.swift in Sources */, FF4CBF6E2C996C0600151637 /* PlayerDetailView.swift in Sources */, + FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */, FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */, FF4CBF712C996C0600151637 /* AppSettings.swift in Sources */, @@ -2754,6 +2767,7 @@ FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, + FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, @@ -2859,6 +2873,7 @@ FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */, FF70FAEC2C90584900129CC2 /* EditablePlayerView.swift in Sources */, FF70FAED2C90584900129CC2 /* PlayerDetailView.swift in Sources */, + FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */, FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */, FF70FAF02C90584900129CC2 /* AppSettings.swift in Sources */, @@ -3013,6 +3028,7 @@ FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, + FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, diff --git a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift index dcea4eb..cf1aea4 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift @@ -6,13 +6,79 @@ // import SwiftUI +import LeStorage struct UmpireStatisticView: View { + @EnvironmentObject var dataStore: DataStore + + let walkoutTeams: [TeamRegistration] + let players: [PlayerRegistration] + let countedPlayers: [String: Int] + let countedWalkoutPlayers: [String: Int] + + init() { + let teams = DataStore.shared.tournaments.flatMap({ $0.unsortedTeams() }) + let wos = teams.filter({ $0.walkOut }) + self.walkoutTeams = wos + + var uniquePlayersDict = [String: PlayerRegistration]() + var playerCountDict = [String: Int]() + var playerWalkOutCountDict = [String: Int]() + + for team in teams { + for player in team.unsortedPlayers() { + if let licenceId = player.licenceId?.strippedLicense { + if team.walkOut { + uniquePlayersDict[licenceId] = player + playerWalkOutCountDict[licenceId, default: 0] += 1 + } + playerCountDict[licenceId, default: 0] += 1 + } + } + } + + self.players = Array(uniquePlayersDict.values).sorted(by: { a, b in + playerCountDict[a.licenceId!.strippedLicense!]! > playerCountDict[b.licenceId!.strippedLicense!]! + }) + self.countedPlayers = playerCountDict + self.countedWalkoutPlayers = playerWalkOutCountDict + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + List { + Section { + LabeledContent { + Text(dataStore.tournaments.count.formatted()) + } label: { + Text("Tournois") + } + + LabeledContent { + Text(walkoutTeams.count.formatted()) + } label: { + Text("Équipes forfaites") + } + } + + if players.isEmpty == false { + Section { + ForEach(players) { player in + LabeledContent { + if let licenceId = player.licenceId?.strippedLicense, let count = countedPlayers[licenceId], let walkoutCount = countedWalkoutPlayers[licenceId] { + Text(walkoutCount.formatted() + " / " + count.formatted()) + .font(.title3) + } + } label: { + Text(player.playerLabel()) + } + } + } header: { + Text("Nombre de forfaits / participations") + } + } + } + .navigationTitle("Statistiques") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } - -#Preview { - UmpireStatisticView() -} diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 347c4ba..7fb1421 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -121,6 +121,14 @@ struct UmpireView: View { Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.") } + Section { + NavigationLink { + UmpireStatisticView() + } label: { + Text("Statistiques de participations") + } + } + Section { @Bindable var user = dataStore.user diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 45cb3e7..69c153a 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -34,7 +34,7 @@ struct PlayerDetailView: View { Form { Section { Toggle("Joueur sur place", isOn: $player.hasArrived) - + LabeledContent { TextField("Nom", text: $player.lastName) .keyboardType(.alphabet) @@ -47,7 +47,7 @@ struct PlayerDetailView: View { } label: { Text("Nom") } - + LabeledContent { TextField("Prénom", text: $player.firstName) .keyboardType(.alphabet) @@ -60,7 +60,7 @@ struct PlayerDetailView: View { } label: { Text("Prénom") } - + PlayerSexPickerView(player: player) if let birthdate = player.birthdate { @@ -109,7 +109,7 @@ struct PlayerDetailView: View { Text("Calculé en fonction du sexe") } } - + Section { LabeledContent { TextField("Licence", text: $licenceId) @@ -160,7 +160,7 @@ struct PlayerDetailView: View { Text("Téléphone") } } - + LabeledContent { TextField("Email", text: $email) .focused($focusedField, equals: ._email) @@ -207,6 +207,14 @@ struct PlayerDetailView: View { } } } + + Section { + NavigationLink { + PlayerStatisticView(player: player) + } label: { + Text("Statistiques de participations") + } + } } .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/PadelClub/Views/Player/PlayerStatisticView.swift b/PadelClub/Views/Player/PlayerStatisticView.swift index e396964..08fa3c4 100644 --- a/PadelClub/Views/Player/PlayerStatisticView.swift +++ b/PadelClub/Views/Player/PlayerStatisticView.swift @@ -6,13 +6,38 @@ // import SwiftUI +import LeStorage struct PlayerStatisticView: View { + let player: PlayerRegistration + + let teams: [TeamRegistration] + + init(player: PlayerRegistration) { + self.player = player + self.teams = DataStore.shared.tournaments.flatMap({ $0.unsortedTeams() }).filter({ $0.includes(player: player) + }) + } + + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + List { + Section { + LabeledContent { + Text(teams.count.formatted()) + } label: { + Text("Participations") + } + LabeledContent { + Text(teams.filter({ $0.walkOut }).count.formatted()) + } label: { + Text("Forfaits") + } + } + } + .headerProminence(.increased) + .navigationTitle("Statistiques") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } - -#Preview { - PlayerStatisticView() -} From 969fa5094f1827d6e42cf125dd707fc9c554f435 Mon Sep 17 00:00:00 2001 From: Raz Date: Wed, 6 Nov 2024 18:26:44 +0100 Subject: [PATCH 076/106] filter tournaments by deleted false --- PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift | 4 ++-- PadelClub/Views/Player/PlayerStatisticView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift index cf1aea4..a82674b 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift @@ -17,7 +17,7 @@ struct UmpireStatisticView: View { let countedWalkoutPlayers: [String: Int] init() { - let teams = DataStore.shared.tournaments.flatMap({ $0.unsortedTeams() }) + let teams = DataStore.shared.tournaments.filter { $0.isDeleted == false }.flatMap({ $0.unsortedTeams() }) let wos = teams.filter({ $0.walkOut }) self.walkoutTeams = wos @@ -38,7 +38,7 @@ struct UmpireStatisticView: View { } self.players = Array(uniquePlayersDict.values).sorted(by: { a, b in - playerCountDict[a.licenceId!.strippedLicense!]! > playerCountDict[b.licenceId!.strippedLicense!]! + playerWalkOutCountDict[a.licenceId!.strippedLicense!]! > playerWalkOutCountDict[b.licenceId!.strippedLicense!]! }) self.countedPlayers = playerCountDict self.countedWalkoutPlayers = playerWalkOutCountDict diff --git a/PadelClub/Views/Player/PlayerStatisticView.swift b/PadelClub/Views/Player/PlayerStatisticView.swift index 08fa3c4..5a2faf3 100644 --- a/PadelClub/Views/Player/PlayerStatisticView.swift +++ b/PadelClub/Views/Player/PlayerStatisticView.swift @@ -15,7 +15,7 @@ struct PlayerStatisticView: View { init(player: PlayerRegistration) { self.player = player - self.teams = DataStore.shared.tournaments.flatMap({ $0.unsortedTeams() }).filter({ $0.includes(player: player) + self.teams = DataStore.shared.tournaments.filter { $0.isDeleted == false }.flatMap({ $0.unsortedTeams() }).filter({ $0.includes(player: player) }) } From d6e87daa3dd1567cf5c07afb52a1067b343d3964 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 8 Nov 2024 11:20:27 +0100 Subject: [PATCH 077/106] overhault ongoing view --- PadelClub.xcodeproj/project.pbxproj | 16 ++ PadelClub/Data/DataStore.swift | 16 +- PadelClub/Data/Match.swift | 95 +++++++- PadelClub/Data/MatchScheduler.swift | 2 +- PadelClub/Data/Tournament.swift | 34 ++- PadelClub/Extensions/Date+Extensions.swift | 16 ++ .../GenericDestinationPickerView.swift | 4 +- .../Views/Components/MatchListView.swift | 31 ++- .../Views/GroupStage/GroupStagesView.swift | 8 +- .../Match/Components/MatchDateView.swift | 24 +- .../Match/Components/PlayerBlockView.swift | 30 ++- PadelClub/Views/Match/MatchDetailView.swift | 18 +- PadelClub/Views/Match/MatchSummaryView.swift | 29 ++- .../Navigation/Agenda/CalendarView.swift | 2 +- PadelClub/Views/Navigation/MainView.swift | 2 +- .../Ongoing/OngoingContainerView.swift | 100 ++++++++ .../Ongoing/OngoingDestination.swift | 121 ++++++++++ .../Navigation/Ongoing/OngoingView.swift | 146 +++++++----- .../Views/Planning/PlanningByCourtView.swift | 23 +- PadelClub/Views/Planning/PlanningView.swift | 22 +- PadelClub/Views/Score/FollowUpMatchView.swift | 225 ++++++++++++------ PadelClub/Views/Team/TeamRestingView.swift | 8 +- .../Screen/TournamentRankView.swift | 4 +- .../Tournament/TournamentRunningView.swift | 25 +- 24 files changed, 763 insertions(+), 238 deletions(-) create mode 100644 PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift create mode 100644 PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 4a2c01b..cae5d4a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -777,6 +777,12 @@ FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; @@ -1164,6 +1170,8 @@ FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = ""; }; FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = ""; }; FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = ""; }; + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingContainerView.swift; sourceTree = ""; }; + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -1741,6 +1749,8 @@ isa = PBXGroup; children = ( FF5D30552BD95B1100F2B93D /* OngoingView.swift */, + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */, + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */, ); path = Ongoing; sourceTree = ""; @@ -2433,6 +2443,7 @@ FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, @@ -2482,6 +2493,7 @@ C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, @@ -2715,6 +2727,7 @@ FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, @@ -2764,6 +2777,7 @@ FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFD2C996C0600151637 /* User.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, @@ -2976,6 +2990,7 @@ FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, @@ -3025,6 +3040,7 @@ FF70FB7C2C90584900129CC2 /* User.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 4ebdaad..2e203a6 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -304,10 +304,24 @@ class DataStore: ObservableObject { var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.confirmed && match.startDate != nil && match.endDate == nil } + match.isRunning() } runningMatches.append(contentsOf: matches) } return runningMatches } + + func runningAndNextMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.startDate != nil && match.endDate == nil } + runningMatches.append(contentsOf: matches) + } + return runningMatches + } + } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 3e522eb..2fa5ba0 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -151,7 +151,7 @@ defer { case .wide, .title: return "Match \(indexInRound(in: matches) + 1)" case .short: - return "#\(indexInRound(in: matches) + 1)" + return "n˚\(indexInRound(in: matches) + 1)" } } @@ -212,7 +212,7 @@ defer { } func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { - startDate = targetStartDate + startDate = targetStartDate ?? startDate confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) @@ -452,14 +452,14 @@ defer { } } - func roundTitle() -> String? { + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? { if groupStage != nil { return groupStageObject?.groupStageTitle() } else if let roundObject { return roundObject.roundTitle() } else { return nil } } - func roundAndMatchTitle() -> String { - [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") + func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String { + [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ") } func topPreviousRoundMatchIndex() -> Int { @@ -496,6 +496,15 @@ defer { } } + func loserMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { + return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) + } else { + return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil) + } + + } + var computedOrder: Int { if let groupStageObject { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) @@ -681,6 +690,14 @@ defer { } } + func courtName(for selectedIndex: Int) -> String { + if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) { + return courtName + } else { + return Court.courtIndexedTitle(atIndex: selectedIndex) + } + } + func courtCount() -> Int { return currentTournament()?.courtCount ?? 1 } @@ -948,6 +965,74 @@ defer { previousMatches().allSatisfy({ $0.isSeeded() == false }) } + func expectedToBeRunning() -> Bool { + guard let startDate else { return false } + return confirmed == false && startDate.timeIntervalSinceNow < 0 + } + + func expectedFormattedStartDate() -> String { + guard let startDate else { return "" } + return "était prévu à " + startDate.formattedAsHourMinute() + } + + func runningDuration() -> String { + guard let startDate else { return "" } + return " depuis " + startDate.timeElapsedString() + } + + func canBePlayedInSpecifiedCourt() -> Bool { + guard let courtIndex else { return false } + if expectedToBeRunning() { + return courtIsAvailable(courtIndex) + } else { + return true + } + } + + typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) + + func nextCourtsAvailable() -> [CourtIndexAndDate] { + guard let tournament = currentTournament() else { return [] } + let availableCourts = availableCourts() + let runningMatches = Tournament.runningMatches(tournament.allMatches()) + let startDate = Date().withoutSeconds() + if runningMatches.isEmpty { + return availableCourts.map { + ($0, startDate) + } + } + + let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in + guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil } + guard let courtIndex = match.courtIndex else { return nil } + if endDate <= startDate { + return (courtIndex, startDate.addingTimeInterval(600)) + } else { + return (courtIndex, endDate) + } + }) + + let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in + a.1 < b.1 + } + return dates + } + + func estimatedStartDate() -> CourtIndexAndDate? { + guard isReady() else { return nil } + guard let tournament = currentTournament() else { return nil } + let availableCourts = nextCourtsAvailable() + return availableCourts.first(where: { (courtIndex, startDate) in + let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60) + if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false { + return true + } + + return false + }) + } + + enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index f277d11..265e03f 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -575,7 +575,7 @@ final class MatchScheduler : ModelObject, Storable { print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") - return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) + return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) } func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index f79366b..0899530 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -514,7 +514,7 @@ final class Tournament : ModelObject, Storable { } func courtUsed() -> [Int] { -#if DEBUG //DEBUGING TIME +#if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) @@ -522,7 +522,7 @@ defer { } #endif - let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() } + let runningMatches: [Match] = DataStore.shared.runningMatches() return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } @@ -1169,7 +1169,9 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { + static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] + + static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1177,10 +1179,10 @@ defer { print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending) } - func runningMatches(_ allMatches: [Match]) -> [Match] { + static func runningMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1188,10 +1190,10 @@ defer { print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - func readyMatches(_ allMatches: [Match]) -> [Match] { + static func readyMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1199,10 +1201,10 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - func matchesLeft(_ allMatches: [Match]) -> [Match] { + static func matchesLeft(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1210,11 +1212,11 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { + static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -2397,6 +2399,16 @@ defer { return logs.joined() } + + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { + guard let source = eventObject()?.courtsUnavailability else { return false } + let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex }) + return courtLockedSchedule.anySatisfy({ dateInterval in + let range = startDate.. Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: calendar.component(.hour, from: self), + minute: calendar.component(.minute, from: self), + second: 0, + of: self)! + } + func localizedDate() -> String { self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() } @@ -232,6 +240,14 @@ extension Date { self.formatted(.dateTime.weekday(.wide)) } + func timeElapsedString() -> String { + let timeInterval = abs(Date().timeIntervalSince(self)) + let duration = Duration.seconds(timeInterval) + + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } + static var hourMinuteFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute] // Customize units diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 59079c5..86e23c8 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -73,7 +73,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count, count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( @@ -93,7 +93,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 412ee38..c084661 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -10,7 +10,6 @@ import SwiftUI struct MatchListView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament let section: String let matches: [Match]? @@ -30,24 +29,22 @@ struct MatchListView: View { @ViewBuilder var body: some View { if _shouldHide() == false { - Section { - DisclosureGroup(isExpanded: $isExpanded) { - if let matches { - ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: matchViewStyle) - .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) - } + DisclosureGroup(isExpanded: $isExpanded) { + if let matches { + ForEach(matches) { match in + MatchRowView(match: match, matchViewStyle: matchViewStyle) + .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) } - } label: { - LabeledContent { - if matches == nil { - ProgressView() - } else { - Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) - } - } label: { - Text(section.firstCapitalized) + } + } label: { + LabeledContent { + if matches == nil { + ProgressView() + } else { + Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) } + } label: { + Text(section.firstCapitalized) } } } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 6f3355d..ebc9e2a 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -111,7 +111,7 @@ struct GroupStagesView: View { GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) switch selectedDestination { case .all: - let finishedMatches = tournament.finishedMatches(allMatches, limit: nil) + let finishedMatches = Tournament.finishedMatches(allMatches, limit: nil) List { if tournament.groupStageAdditionalQualified > 0 { @@ -148,10 +148,10 @@ struct GroupStagesView: View { } } - let runningMatches = tournament.runningMatches(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "prêt à démarrer", matches: tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) } .navigationTitle("Toutes les poules") diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index d71d326..9d33fe2 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -26,7 +26,17 @@ struct MatchDateView: View { self.isReady = match.isReady() self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasEnded = match.hasEnded() - self.updatedField = updatedField + if updatedField == nil, match.canBePlayedInSpecifiedCourt() { + self.updatedField = match.courtIndex + } else if let updatedField { + self.updatedField = updatedField + } else { + self.updatedField = match.availableCourts().first + } + } + + var currentDate: Date { + Date().withoutSeconds() } var body: some View { @@ -41,7 +51,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Date() + match.startDate = currentDate match.endDate = nil match.confirmed = true _save() @@ -50,7 +60,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate) match.endDate = nil match.confirmed = true _save() @@ -59,7 +69,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) match.endDate = nil match.confirmed = true _save() @@ -68,13 +78,15 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) + match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate) match.endDate = nil match.confirmed = true _save() } } header: { - Text("Le match apparaîtra dans les en cours") + if let updatedField { + Text(match.courtName(for: updatedField)) + } } } else { Button("Décaler de \(estimatedDuration) minutes") { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 5901d37..eca6643 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -46,8 +46,26 @@ struct PlayerBlockView: View { teamScore?.score?.components(separatedBy: ",") ?? [] } - private func _defaultLabel() -> String { - teamPosition.localizedLabel() + private func _defaultLabel() -> [String] { + var defaultLabels = [String]() + if let previous = match.previousMatch(teamPosition) { + defaultLabels.append("Gagnant \(previous.roundAndMatchTitle(.short))") + if previous.isReady() == true { + if let courtName = previous.courtName(), previous.isRunning() { + defaultLabels.append(courtName + "\(previous.runningDuration())") + } + } + } else if let loser = match.loserMatch(teamPosition) { + defaultLabels.append("Perdant \(loser.roundAndMatchTitle(.short))") + if loser.isReady() == true { + if let courtName = loser.courtName(), loser.isRunning() { + defaultLabels.append(courtName + "\(loser.runningDuration())") + } + } + } else { + defaultLabels.append(teamPosition.localizedLabel()) + } + return defaultLabels } var body: some View { @@ -74,7 +92,13 @@ struct PlayerBlockView: View { Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) - Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) + VStack(alignment: .leading) { + ForEach(_defaultLabel(), id: \.self) { name in + Text(name) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } } } diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 9af3d85..4adaa02 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -162,8 +162,11 @@ struct MatchDetailView: View { dismiss() } }) { - FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) - .tint(.master) + NavigationStack { + + FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) + } + .tint(.master) } .sheet(isPresented: $presentRanking, content: { if let currentTournament = match.currentTournament() { @@ -492,19 +495,20 @@ struct MatchDetailView: View { Text("Horaire") } .onChange(of: startDateSetup) { + let date = Date().withoutSeconds() switch startDateSetup { case .customDate: break case .now: - startDate = Date() + startDate = date case .nextRotation: - let baseDate = match.startDate ?? Date() + let baseDate = match.startDate ?? date startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60) case .previousRotation: - let baseDate = match.startDate ?? Date() + let baseDate = match.startDate ?? date startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60) case .inMinutes(let minutes): - startDate = Date().addingTimeInterval(Double(minutes) * 60) + startDate = date.addingTimeInterval(Double(minutes) * 60) } } } @@ -546,7 +550,7 @@ struct MatchDetailView: View { } RowButtonView("Valider") { - match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) + match.validateMatch(fromStartDate: startDateSetup == .now ? Date().withoutSeconds() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) save() diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index fc03111..199fa43 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -59,12 +59,26 @@ struct MatchSummaryView: View { } } Spacer() - if let courtName { - Spacer() - Text(courtName) - .foregroundStyle(.gray) - .font(.caption) + VStack(alignment: .trailing, spacing: 0) { + if let courtName, match.canBePlayedInSpecifiedCourt() { + if match.isRunning() == false { + Text("prévu") + } + Text(courtName) + } else if let first = match.availableCourts().first { + Text("possible") + Text(match.courtName(for: first)) + } else { + if let estimatedStartDate = match.estimatedStartDate() { + Text(match.courtName(for: estimatedStartDate.0) + " possible") + Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) + } else { + Text("aucun terrain disponible") + } + } } + .foregroundStyle(.secondary) + .font(.footnote) } .lineLimit(1) } @@ -91,6 +105,11 @@ struct MatchSummaryView: View { if matchViewStyle != .plainStyle { HStack { + if match.expectedToBeRunning() { + Text(match.expectedFormattedStartDate()) + .font(.footnote) + .foregroundStyle(.secondary) + } Spacer() MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField) } diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index b700129..1dab0aa 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -124,7 +124,7 @@ struct CalendarView: View { ) .overlay(alignment: .bottomTrailing) { if let count = counts[day.dayInt] { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(count).circle.fill" : "ellipsis.circle.fill") .foregroundColor(.secondary) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index 6fc905d..ee01859 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -79,7 +79,7 @@ struct MainView: View { TournamentOrganizerView() .tabItem(for: .tournamentOrganizer) .toolbarBackground(.visible, for: .tabBar) - OngoingView() + OngoingContainerView() .tabItem(for: .ongoing) .badge(self.dataStore.runningMatches().count) .toolbarBackground(.visible, for: .tabBar) diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift new file mode 100644 index 0000000..1946c24 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -0,0 +1,100 @@ +// +// OngoingContainerView.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// + +import SwiftUI +import LeStorage + +@Observable +class OngoingViewModel { + static let shared = OngoingViewModel() + + var destination: OngoingDestination? = .running + var hideUnconfirmedMatches: Bool = false + var hideNotReadyMatches: Bool = false + + func areFiltersEnabled() -> Bool { + hideUnconfirmedMatches || hideNotReadyMatches + } + + let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.index), .keyPath(\Match.courtIndexForSorting)] + + var runningAndNextMatches: [Match] { + DataStore.shared.runningAndNextMatches().sorted(using: defaultSorting, order: .ascending) + } + + var filteredRunningAndNextMatches: [Match] { + if destination == .followUp { + return runningAndNextMatches.filter({ + (hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed) + && (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() ) + }) + } else { + return runningAndNextMatches + } + } +} + +struct OngoingContainerView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var showMatchPicker: Bool = false + + var body: some View { + @Bindable var navigation = navigation + @Bindable var ongoingViewModel = OngoingViewModel.shared + NavigationStack(path: $navigation.ongoingPath) { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false) + + switch ongoingViewModel.destination! { + case .running, .followUp: + OngoingView() + case .court, .free: + OngoingCourtView() + } + } + .toolbarBackground(.visible, for: .bottomBar) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Programmation") + .toolbar { + if ongoingViewModel.destination == .followUp { + ToolbarItem(placement: .topBarLeading) { + Menu { + Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) { + Text("masquer non confirmés") + } + Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) { + Text("masquer incomplets") + } + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + showMatchPicker = true + } label: { + Image(systemName: "rectangle.stack.badge.plus") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + } + } + } + .environment(ongoingViewModel) + .sheet(isPresented: $showMatchPicker, content: { + FollowUpMatchView(selectedCourt: nil, allMatches: ongoingViewModel.runningAndNextMatches, autoDismiss: false) + .tint(.master) + }) + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift new file mode 100644 index 0000000..f97e021 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift @@ -0,0 +1,121 @@ +// +// OngoingDestination.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// +import SwiftUI + +enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { + var id: Int { self.rawValue } + + static func == (lhs: OngoingDestination, rhs: OngoingDestination) -> Bool { + return lhs.id == rhs.id + } + + case running + case followUp + case court + case free + + var runningAndNextMatches: [Match] { + if self == .followUp { + OngoingViewModel.shared.filteredRunningAndNextMatches + } else { + OngoingViewModel.shared.runningAndNextMatches + } + } + + var sortedMatches: [Match] { + return runningAndNextMatches.filter({ self.shouldDisplay($0) }) + } + + var filteredMatches: [Match] { + sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + } + + var sortedCourtIndex: [Int?] { + let courtUsed = sortedMatches.grouped(by: { $0.courtIndex }).keys + let sortedNumbers = courtUsed.sorted { (a, b) -> Bool in + switch (a, b) { + case (nil, _): return false + case (_, nil): return true + case let (a?, b?): return a < b + } + } + return sortedNumbers + } + + func contentUnavailable() -> some View { + switch self { + case .running: + ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) + case .followUp: + ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi.")) + case .court: + ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi.")) + case .free: + ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi.")) + } + } + + func localizedFilterModeLabel() -> String { + switch self { + case .running: + return "En cours" + case .followUp: + return "À suivre" + case .court: + return "Terrains" + case .free: + return "Libres" + } + } + + func shouldDisplay(_ match: Match) -> Bool { + switch self { + case .running: + return match.isRunning() + case .court, .free: + return true + case .followUp: + return match.isRunning() == false + } + } + + func selectionLabel(index: Int) -> String { + localizedFilterModeLabel() + } + + func systemImage() -> String? { + switch self { + default: + return nil + } + } + + func badgeValue() -> Int? { + switch self { + case .running: + sortedMatches.count + case .followUp: + sortedMatches.count + case .court: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false + }).count + case .free: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty + }).count + } + } + + func badgeValueColor() -> Color? { + nil + } + + func badgeImage() -> Badge? { + nil + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 226b052..d36fd40 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -8,82 +8,110 @@ import SwiftUI import LeStorage +extension Int: @retroactive Identifiable { + public var id: Int { + return self + } +} + + + struct OngoingView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel - @State private var sortByField: Bool = false - - let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)] - let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndexForSorting)] - - var matches: [Match] { - let sorting = self.sortByField ? fieldSorting : defaultSorting - return self.dataStore.runningMatches().sorted(using: sorting, order: .ascending) + var filterMode: OngoingDestination { + ongoingViewModel.destination! } - + var body: some View { - @Bindable var navigation = navigation - NavigationStack(path: $navigation.ongoingPath) { - List { - ForEach(matches) { match in - - if let tournament = match.currentTournament() { - - Section { - MatchRowView(match: match, matchViewStyle: .standardStyle) - } header: { - HStack { - Text(tournament.tournamentTitle(.short)) - Spacer() - if let club = tournament.club() { - Text("@" + club.clubTitle(.short)) - } + let filteredMatches = filterMode.sortedMatches + List { + ForEach(filteredMatches) { match in + let tournament = match.currentTournament() + Section { + MatchRowView(match: match, matchViewStyle: .standardStyle) + } header: { + if let tournament { + HStack { + Text(tournament.tournamentTitle(.short)) + Spacer() + if let club = tournament.club() { + Text("@" + club.clubTitle(.short)) } - } footer: { - HStack { - Text(tournament.eventLabel()) + } + } + } footer: { + HStack { + if let tournament { + Text(tournament.eventLabel()) + } #if DEBUG - Spacer() - FooterButtonView("copier l'id") { - let pasteboard = UIPasteboard.general - pasteboard.string = match.id - } -#endif - } + Spacer() + FooterButtonView("copier l'id") { + let pasteboard = UIPasteboard.general + pasteboard.string = match.id } - +#endif } - } } - .headerProminence(.increased) - .overlay { - if matches.isEmpty { - ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) - } + } + .headerProminence(.increased) + .overlay { + if filteredMatches.isEmpty { + filterMode.contentUnavailable() } - .navigationTitle("En cours") - .toolbarBackground(.visible, for: .bottomBar) - .toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .status) { - Picker(selection: $sortByField) { - Text("tri par date").tag(false) - Text("tri par terrain").tag(true) - } label: { - + } + } +} + +struct OngoingCourtView: View { + + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel + + var filterMode: OngoingDestination { + ongoingViewModel.destination! + } + + @State private var selectedCourtForFollowUp: Int? + + var body: some View { + let sortedMatches = filterMode.sortedMatches + let filteredMatches = sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + + List { + ForEach(filterMode.sortedCourtIndex, id: \.self) { index in + let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index }) + let title : String = (index == nil ? "Aucun terrain défini" : "Terrain #\(index! + 1)") + if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) { + Section { + MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false) + if courtFilteredMatches.isEmpty { + Button("Choisir un match") { + selectedCourtForFollowUp = index + } + } + MatchListView(section: "À venir", matches: sortedMatches.filter({ $0.courtIndex == index && $0.hasStarted() == false }), isExpanded: false) + } header: { + Text(title) } - .pickerStyle(.segmented) - .fixedSize() - .offset(y: -3) } } } + .sheet(item: $selectedCourtForFollowUp, content: { selectedCourtForFollowUp in + FollowUpMatchView(selectedCourt: selectedCourtForFollowUp, allMatches: filterMode.runningAndNextMatches) + .tint(.master) + }) + .headerProminence(.increased) + .overlay { + if (filteredMatches.isEmpty && filterMode != .free) || (filterMode == .free && filterMode.sortedCourtIndex.allSatisfy({ index in filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false })) { + filterMode.contentUnavailable() + } + } } } - -//#Preview { -// OngoingView() -//} diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 25592c6..15a1a9e 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -19,7 +19,7 @@ struct PlanningByCourtView: View { @State private var uuid: UUID = UUID() var timeSlots: [Date:[Match]] { - Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + Dictionary(grouping: matches.filter({ $0.startDate != nil })) { $0.startDate! } } var days: [Date] { @@ -146,24 +146,5 @@ struct PlanningByCourtView: View { } actions: { } } - } - - private func _matchesCount(inDayInt dayInt: Int) -> Int { - timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count - } - - private func _timeSlotView(key: Date, matches: [Match]) -> some View { - LabeledContent { - Text(self._formattedMatchCount(matches.count)) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", ")) - } - } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" - } - - + } } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 03ae980..1e738b6 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -79,7 +79,11 @@ struct PlanningView: View { Picker(selection: $selectedDay) { Text("Tous les jours").tag(nil as Date?) ForEach(days, id: \.self) { day in - Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire").tag(day as Date?) + } else { + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } } } label: { Text("Jour") @@ -177,7 +181,11 @@ struct PlanningView: View { } } header: { HStack { - Text(day.formatted(.dateTime.day().weekday().month())) + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire") + } else { + Text(day.formatted(.dateTime.day().weekday().month())) + } Spacer() let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) if showFinishedMatches { @@ -186,6 +194,10 @@ struct PlanningView: View { Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") } } + } footer: { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") + } } .headerProminence(.increased) } @@ -201,7 +213,11 @@ struct PlanningView: View { LabeledContent { Text(self._formattedMatchCount(matches.count)) } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Aucun horaire") + } else { + Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + } if matches.count <= tournament.courtCount { let names = matches.sorted(by: \.computedOrder) .compactMap({ $0.roundTitle() }) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 3ae74cc..2356a69 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -10,17 +10,43 @@ import SwiftUI struct FollowUpMatchView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss - let match: Match + let match: Match? let readyMatches: [Match] let matchesLeft: [Match] let isFree: Bool + var autoDismiss: Bool = true - @State private var sortingMode: SortingMode = .index + @State private var sortingMode: SortingMode? = .index @State private var selectedCourt: Int? @State private var checkCanPlay: Bool = false + @State private var seeAll: Bool = false @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool - enum SortingMode: Int, Identifiable, CaseIterable { + var matches: [Match] { + seeAll ? matchesLeft : readyMatches + } + + enum SortingMode: Int, Identifiable, CaseIterable, Selectable, Equatable { + func selectionLabel(index: Int) -> String { + localizedSortingModeLabel() + } + + func badgeValue() -> Int? { + nil + } + + func badgeImage() -> Badge? { + nil + } + + func badgeValueColor() -> Color? { + nil + } + + static func == (lhs: SortingMode, rhs: SortingMode) -> Bool { + return lhs.id == rhs.id + } + var id: Int { self.rawValue } case winner case loser @@ -28,14 +54,23 @@ struct FollowUpMatchView: View { case restingTime case court + func canHaveSeeAllOption() -> Bool { + switch self { + case .index, .restingTime: + return true + case .winner, .loser, .court: + return false + } + } + func localizedSortingModeLabel() -> String { switch self { case .index: - return "Ordre" + return "Ordre prévu" case .court: return "Terrain" case .restingTime: - return "Repos" + return "Temps de repos" case .winner: return "Gagnant" case .loser: @@ -50,37 +85,50 @@ struct FollowUpMatchView: View { _selectedCourt = .init(wrappedValue: match.courtIndex) let currentTournament = match.currentTournament() let allMatches = currentTournament?.allMatches() ?? [] - self.matchesLeft = currentTournament?.matchesLeft(allMatches) ?? [] - let runningMatches = currentTournament?.runningMatches(allMatches) ?? [] - let readyMatches = currentTournament?.readyMatches(allMatches) ?? [] - self.readyMatches = currentTournament?.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) ?? [] + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) self.isFree = currentTournament?.isFree() ?? true } + + init(selectedCourt: Int?, allMatches: [Match], autoDismiss: Bool = true) { + _dismissWhenPresentFollowUpMatchIsDismissed = .constant(false) + _selectedCourt = .init(wrappedValue: selectedCourt) + self.match = nil + self.autoDismiss = autoDismiss + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) + self.isFree = false + } + var winningTeam: TeamRegistration? { - match.winner() + match?.winner() } var losingTeam: TeamRegistration? { - match.loser() + match?.loser() } var sortingModeCases: [SortingMode] { var sortingModes = [SortingMode]() - if let winningTeam { + if winningTeam != nil { sortingModes.append(.winner) } - if let losingTeam { + if losingTeam != nil { sortingModes.append(.loser) } sortingModes.append(.index) sortingModes.append(.restingTime) - sortingModes.append(.court) +// sortingModes.append(.court) return sortingModes } func contentUnavailableDescriptionLabel() -> String { - switch sortingMode { + switch sortingMode! { case .winner: if let winningTeam { return "Aucun match à suivre pour \(winningTeam.teamLabel())" @@ -103,13 +151,13 @@ struct FollowUpMatchView: View { } var sortedMatches: [Match] { - switch sortingMode { + switch sortingMode! { case .index: - return readyMatches + return matches case .restingTime: - return readyMatches.sorted(by: \.restingTimeForSorting) + return matches.sorted(by: \.restingTimeForSorting) case .court: - return readyMatches.sorted(using: [.keyPath(\.courtIndexForSorting), .keyPath(\.restingTimeForSorting)], order: .ascending) + return matchesLeft.filter({ $0.courtIndex == selectedCourt }) case .winner: if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) { return [followUpMatch] @@ -127,77 +175,98 @@ struct FollowUpMatchView: View { var body: some View { NavigationStack { - List { - Section { - Picker(selection: $selectedCourt) { - Text("Aucun").tag(nil as Int?) - if let tournament = match.currentTournament() { - ForEach(0.. Date: Fri, 8 Nov 2024 13:09:47 +0100 Subject: [PATCH 078/106] fix groupstage sorting --- PadelClub/Data/GroupStage.swift | 2 +- .../Views/GroupStage/GroupStageView.swift | 22 ++++++++++---- .../Views/GroupStage/GroupStagesView.swift | 29 ++++++++++++------- PadelClub/Views/Match/MatchSummaryView.swift | 18 ++++++------ 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index b8093f2..4738c2c 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -304,7 +304,7 @@ final class GroupStage: ModelObject, Storable { print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }) + return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) } func runningMatches(playedMatches: [Match]) -> [Match] { diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index dd5007a..9fc489f 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -58,12 +58,24 @@ struct GroupStageView: View { } let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) - MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) + Section { + MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) + } + let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches) - MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) - .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) - MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) - MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) + Section { + MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) + .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) + } + Section { + + MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) + } + + Section { + + MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) + } if playedMatches.isEmpty { RowButtonView("Créer les matchs de poules") { diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index ebc9e2a..f3e2a58 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -131,12 +131,7 @@ struct GroupStagesView: View { } } } label: { - Label { - Text("Qualifier un \(name) de poule par tirage au sort") - } icon: { - Image(systemName: "exclamationmark.circle.fill") - .foregroundStyle(.logoBackground) - } + Text("Qualifier un \(name) de poule par tirage au sort") } .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) } footer: { @@ -149,10 +144,24 @@ struct GroupStagesView: View { } let runningMatches = Tournament.runningMatches(allMatches) - MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) + Section { + + MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) + } + Section { + + MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) + } + + Section { + + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) + } + + Section { + + MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) + } } .navigationTitle("Toutes les poules") case .groupStage(let groupStage): diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 199fa43..3f63c16 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -60,21 +60,21 @@ struct MatchSummaryView: View { } Spacer() VStack(alignment: .trailing, spacing: 0) { - if let courtName, match.canBePlayedInSpecifiedCourt() { - if match.isRunning() == false { + if match.hasEnded() == false, match.isRunning() == false { + if let courtName, match.canBePlayedInSpecifiedCourt() { Text("prévu") - } - Text(courtName) - } else if let first = match.availableCourts().first { - Text("possible") - Text(match.courtName(for: first)) - } else { - if let estimatedStartDate = match.estimatedStartDate() { + Text(courtName) + } else if let first = match.availableCourts().first { + Text("possible") + Text(match.courtName(for: first)) + } else if let estimatedStartDate = match.estimatedStartDate() { Text(match.courtName(for: estimatedStartDate.0) + " possible") Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) } else { Text("aucun terrain disponible") } + } else if let courtName { + Text(courtName) } } .foregroundStyle(.secondary) From 42a7487c631709e4034ec13746caa8167f05378e Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 8 Nov 2024 15:41:58 +0100 Subject: [PATCH 079/106] fix issues --- PadelClub/Views/Match/MatchSummaryView.swift | 11 +++++--- .../Ongoing/OngoingContainerView.swift | 13 +++------- .../Navigation/Ongoing/OngoingView.swift | 9 +++---- PadelClub/Views/Score/FollowUpMatchView.swift | 26 ++++++++++++++++++- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 3f63c16..84ec569 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -19,6 +19,7 @@ struct MatchSummaryView: View { let color: Color let width: CGFloat let updatedField: Int? + let estimatedStartDate: Match.CourtIndexAndDate? init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil, updatedField: Int? = nil) { self.match = match @@ -44,6 +45,8 @@ struct MatchSummaryView: View { } else { self.courtName = nil } + + self.estimatedStartDate = match.estimatedStartDate() } var body: some View { @@ -67,11 +70,13 @@ struct MatchSummaryView: View { } else if let first = match.availableCourts().first { Text("possible") Text(match.courtName(for: first)) - } else if let estimatedStartDate = match.estimatedStartDate() { + } else if let estimatedStartDate { Text(match.courtName(for: estimatedStartDate.0) + " possible") Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) + } else if let courtName { + Text(courtName) } else { - Text("aucun terrain disponible") + Text("") } } else if let courtName { Text(courtName) @@ -111,7 +116,7 @@ struct MatchSummaryView: View { .foregroundStyle(.secondary) } Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField) + MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField ?? estimatedStartDate?.0) } } } diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift index 1946c24..aa16926 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -27,14 +27,10 @@ class OngoingViewModel { } var filteredRunningAndNextMatches: [Match] { - if destination == .followUp { - return runningAndNextMatches.filter({ - (hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed) - && (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() ) - }) - } else { - return runningAndNextMatches - } + return runningAndNextMatches.filter({ + (hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed) + && (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() ) + }) } } @@ -56,7 +52,6 @@ struct OngoingContainerView: View { OngoingCourtView() } } - .toolbarBackground(.visible, for: .bottomBar) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Programmation") diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index d36fd40..4335f39 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -91,14 +91,13 @@ struct OngoingCourtView: View { if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) { Section { MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false) - if courtFilteredMatches.isEmpty { - Button("Choisir un match") { - selectedCourtForFollowUp = index - } - } MatchListView(section: "À venir", matches: sortedMatches.filter({ $0.courtIndex == index && $0.hasStarted() == false }), isExpanded: false) } header: { Text(title) + } footer: { + FooterButtonView("Ajouter un match à suivre") { + selectedCourtForFollowUp = index + } } } } diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 2356a69..30bb84d 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -196,8 +196,32 @@ struct FollowUpMatchView: View { if sortedMatches.isEmpty == false { ForEach(sortedMatches) { match in + let tournament = match.currentTournament() Section { MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) + } header: { + if let tournament { + HStack { + Text(tournament.tournamentTitle(.short)) + Spacer() + if let club = tournament.club() { + Text("@" + club.clubTitle(.short)) + } + } + } + } footer: { + HStack { + if let tournament { + Text(tournament.eventLabel()) + } + #if DEBUG + Spacer() + FooterButtonView("copier l'id") { + let pasteboard = UIPasteboard.general + pasteboard.string = match.id + } + #endif + } } } } else { @@ -243,7 +267,7 @@ struct FollowUpMatchView: View { ToolbarItem(placement: .topBarTrailing) { Picker(selection: $selectedCourt) { - Text("choix du terrain").tag(nil as Int?) + Image(systemName: "sportscourt").tag(nil as Int?) if let tournament = match?.currentTournament() { ForEach(0.. Date: Fri, 8 Nov 2024 15:46:53 +0100 Subject: [PATCH 080/106] v1.0.30 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index cae5d4a..357871a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3254,7 +3254,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3278,7 +3278,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.29; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3299,7 +3299,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3322,7 +3322,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.29; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From f60daee81f7195968b377b8b337db29fb07af650 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 8 Nov 2024 18:49:22 +0100 Subject: [PATCH 081/106] add rotation knowledge --- PadelClub/Data/DataStore.swift | 13 +++++ PadelClub/Data/Match.swift | 47 +++++++++++++++++-- .../FixedWidthInteger+Extensions.swift | 6 +++ PadelClub/Views/Match/MatchSummaryView.swift | 36 +++++++------- .../Ongoing/OngoingContainerView.swift | 2 +- .../Ongoing/OngoingDestination.swift | 22 ++++++--- PadelClub/Views/Score/FollowUpMatchView.swift | 4 +- 7 files changed, 98 insertions(+), 32 deletions(-) diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 2e203a6..9fc3e62 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -324,4 +324,17 @@ class DataStore: ObservableObject { return runningMatches } + func endMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.hasEnded() } + runningMatches.append(contentsOf: matches) + } + return runningMatches.sorted(by: \.endDate!, order: .descending) + } + } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 2fa5ba0..7bd6547 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -496,6 +496,11 @@ defer { } } + func loserMatches() -> [Match] { + guard let roundObject else { return [] } + return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 }) + } + func loserMatch(_ teamPosition: TeamPosition) -> Match? { if teamPosition == .one { return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) @@ -718,7 +723,7 @@ defer { func availableCourts() -> [Int] { let courtUsed = currentTournament()?.courtUsed() ?? [] - return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed))) + return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted() } func removeCourt() { @@ -970,9 +975,27 @@ defer { return confirmed == false && startDate.timeIntervalSinceNow < 0 } - func expectedFormattedStartDate() -> String { + func expectedFormattedStartDate(updatedField: Int?) -> String { guard let startDate else { return "" } - return "était prévu à " + startDate.formattedAsHourMinute() + guard hasEnded() == false, isRunning() == false else { return "" } + let depthReadiness = depthReadiness() + if depthReadiness == 0 { + let availableCourts = availableCourts() + if canBePlayedInSpecifiedCourt() { + return "possible tout de suite" + } else if let updatedField, availableCourts.contains(updatedField) { + return "possible tout de suite \(courtName(for: updatedField))" + } else if let first = availableCourts.first { + return "possible tout de suite \(courtName(for: first))" + } else if let estimatedStartDate = estimatedStartDate() { + return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0) + } + return "était prévu à " + startDate.formattedAsHourMinute() + } else if depthReadiness == 1 { + return "possible prochaine rotation" + } else { + return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())" + } } func runningDuration() -> String { @@ -1031,7 +1054,23 @@ defer { return false }) } - + + func depthReadiness() -> Int { + // Base case: If this match is ready, the depth is 0 + if isReady() { + return 0 + } + + // Recursive case: If not ready, check the maximum depth of readiness among previous matches + // If previousMatches() is empty, return a default depth of -1 + let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1 + return previousDepth + 1 + } + + func ancestors() -> [Match] { + previousMatches() + loserMatches() + } + enum CodingKeys: String, CodingKey { case _id = "id" diff --git a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift index c40c74a..37815d3 100644 --- a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift +++ b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift @@ -34,4 +34,10 @@ public extension FixedWidthInteger { func formattedAsRawString() -> String { String(self) } + + func durationInHourMinutes() -> String { + let duration = Duration.seconds(self*60) + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 84ec569..9bdb2d8 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -63,23 +63,9 @@ struct MatchSummaryView: View { } Spacer() VStack(alignment: .trailing, spacing: 0) { - if match.hasEnded() == false, match.isRunning() == false { - if let courtName, match.canBePlayedInSpecifiedCourt() { - Text("prévu") - Text(courtName) - } else if let first = match.availableCourts().first { - Text("possible") - Text(match.courtName(for: first)) - } else if let estimatedStartDate { - Text(match.courtName(for: estimatedStartDate.0) + " possible") - Text("dans ~ " + estimatedStartDate.1.timeElapsedString()) - } else if let courtName { - Text(courtName) - } else { - Text("") - } - } else if let courtName { + if let courtName { Text(courtName) + .strikethrough(match.courtIndex! != updatedField && match.isReady() && match.canBePlayedInSpecifiedCourt() == false) } } .foregroundStyle(.secondary) @@ -111,18 +97,32 @@ struct MatchSummaryView: View { if matchViewStyle != .plainStyle { HStack { if match.expectedToBeRunning() { - Text(match.expectedFormattedStartDate()) + Text(match.expectedFormattedStartDate(updatedField: updatedField)) .font(.footnote) .foregroundStyle(.secondary) } Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: updatedField ?? estimatedStartDate?.0) + MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: possibleCourtIndex) } } } .padding(.vertical, padding) .monospacedDigit() } + + var possibleCourtIndex: Int? { + let availableCourts = match.availableCourts() + if match.canBePlayedInSpecifiedCourt() { + return nil + } else if let updatedField, availableCourts.contains(updatedField) { + return updatedField + } else if let first = availableCourts.first { + return first + } else if let estimatedStartDate { + return estimatedStartDate.0 + } + return updatedField + } } //#Preview { diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift index aa16926..15e4bb9 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -46,7 +46,7 @@ struct OngoingContainerView: View { GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false) switch ongoingViewModel.destination! { - case .running, .followUp: + case .running, .followUp, .over: OngoingView() case .court, .free: OngoingCourtView() diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift index f97e021..ebf38f4 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift @@ -17,12 +17,16 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable case followUp case court case free + case over var runningAndNextMatches: [Match] { - if self == .followUp { - OngoingViewModel.shared.filteredRunningAndNextMatches - } else { - OngoingViewModel.shared.runningAndNextMatches + switch self { + case .running, .court, .free: + return OngoingViewModel.shared.runningAndNextMatches + case .followUp: + return OngoingViewModel.shared.filteredRunningAndNextMatches + case .over: + return DataStore.shared.endMatches() } } @@ -56,6 +60,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi.")) case .free: ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi.")) + case .over: + ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi.")) } } @@ -69,6 +75,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable return "Terrains" case .free: return "Libres" + case .over: + return "Finis" } } @@ -80,6 +88,8 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable return true case .followUp: return match.isRunning() == false + case .over: + return match.hasEnded() } } @@ -96,9 +106,7 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable func badgeValue() -> Int? { switch self { - case .running: - sortedMatches.count - case .followUp: + case .running, .followUp, .over: sortedMatches.count case .court: sortedCourtIndex.filter({ index in diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 30bb84d..9fd6867 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -14,12 +14,12 @@ struct FollowUpMatchView: View { let readyMatches: [Match] let matchesLeft: [Match] let isFree: Bool - var autoDismiss: Bool = true + var autoDismiss: Bool = false @State private var sortingMode: SortingMode? = .index @State private var selectedCourt: Int? @State private var checkCanPlay: Bool = false - @State private var seeAll: Bool = false + @State private var seeAll: Bool = true @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool var matches: [Match] { From 41e356bf28acc069a81b0c501d39389acb4912ed Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 8 Nov 2024 18:49:50 +0100 Subject: [PATCH 082/106] v1.0.30 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 357871a..3547bc7 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3438,7 +3438,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.29; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3481,7 +3481,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.29; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 3d00c58eb23e5b258de8f1e11a841fbc2ce4257d Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 8 Nov 2024 19:58:35 +0100 Subject: [PATCH 083/106] update --- PadelClub/Views/Score/FollowUpMatchView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 9fd6867..0ee7f20 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -286,8 +286,10 @@ struct FollowUpMatchView: View { } } .onChange(of: readyMatches) { + dismissWhenPresentFollowUpMatchIsDismissed = true if autoDismiss { - dismissWhenPresentFollowUpMatchIsDismissed = true + dismiss() + } else if readyMatches.isEmpty && matchesLeft.isEmpty { dismiss() } } From 87e4adf270974ca1ed6b3ef09d6a5c9e2d7fdd97 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 10 Nov 2024 11:00:37 +0100 Subject: [PATCH 084/106] fix stuff --- PadelClub.xcodeproj/project.pbxproj | 8 +++--- .../Match/Components/PlayerBlockView.swift | 10 ++++--- PadelClub/Views/Match/MatchRowView.swift | 19 +++++++++++++ PadelClub/Views/Match/MatchSummaryView.swift | 27 ++++++++++++++++++- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 3547bc7..05fb0bd 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3254,7 +3254,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3299,7 +3299,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3415,7 +3415,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3459,7 +3459,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index eca6643..5813ab9 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -71,16 +71,18 @@ struct PlayerBlockView: View { var body: some View { HStack { VStack(alignment: .leading) { - if let names { + if let team { if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false { Text("Repêchée").italic().font(.caption) } - if let teamName = team?.name { + if let teamName = team.name { Text(teamName).foregroundStyle(.secondary).font(.footnote) } - ForEach(names, id: \.self) { name in - Text(name).lineLimit(1) + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .italic(player.hasArrived == false) + .foregroundStyle(player.hasArrived == false ? .secondary : .primary) } } else { ZStack(alignment: .leading) { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 1cde456..1a778c1 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import LeStorage struct MatchRowView: View { @@ -63,6 +64,24 @@ struct MatchRowView: View { } label: { MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title, updatedField: updatedField) .contextMenu { + Section { + ForEach(match.teams().flatMap({ $0.players() })) { player in + Button { + player.hasArrived.toggle() + do { + try player.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } label: { + Label(player.playerLabel(), systemImage: player.hasArrived ? "checkmark" : "xmark") + } + } + } header: { + Text("Présence") + } + + Divider() NavigationLink { EditSharingView(match: match) } label: { diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 9bdb2d8..5e3a86f 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -65,7 +65,7 @@ struct MatchSummaryView: View { VStack(alignment: .trailing, spacing: 0) { if let courtName { Text(courtName) - .strikethrough(match.courtIndex! != updatedField && match.isReady() && match.canBePlayedInSpecifiedCourt() == false) + .strikethrough(courtIsNotValid()) } } .foregroundStyle(.secondary) @@ -123,6 +123,31 @@ struct MatchSummaryView: View { } return updatedField } + + func courtIsNotValid() -> Bool { + if match.courtIndex == updatedField { + return false + } + + if match.isReady() == false { + return false + } + + if match.canBePlayedInSpecifiedCourt() { + return false + } + + if let estimatedStartDate, estimatedStartDate.0 == updatedField { + return false + } + + if let estimatedStartDate, estimatedStartDate.0 == match.courtIndex { + return false + } + + + return true + } } //#Preview { From 41b62a73aedca0b74b14e1cb99f6b0bcf08cb39c Mon Sep 17 00:00:00 2001 From: Raz Date: Tue, 12 Nov 2024 10:27:50 +0100 Subject: [PATCH 085/106] update matches --- .../Views/Tournament/TournamentRunningView.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 0ce2fa0..d28620a 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -22,17 +22,22 @@ struct TournamentRunningView: View { let runningMatches = Tournament.runningMatches(allMatches) let matchesLeft = Tournament.matchesLeft(allMatches) let readyMatches = Tournament.readyMatches(allMatches) + let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true) Section { - MatchListView(section: "prêt à démarrer", matches: readyMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: true) + MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: tournament.hasEnded(), isExpanded: true) } Section { - MatchListView(section: "à venir", matches: matchesLeft, hideWhenEmpty: true, isExpanded: false) + MatchListView(section: "à lancer", matches: readyMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: availableToStart.isEmpty) + } + + Section { + MatchListView(section: "à venir", matches: matchesLeft, hideWhenEmpty: true, isExpanded: readyMatches.isEmpty) } Section { - MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: false) + MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: matchesLeft.isEmpty) } Section { From 17e0c85e0bd797cb8ffcb88ea6e5f5bdf29352a5 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 15 Nov 2024 07:41:27 +0100 Subject: [PATCH 086/106] fix next matches display --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Data/DataStore.swift | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 05fb0bd..57ea77b 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3254,7 +3254,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3278,7 +3278,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.30; + MARKETING_VERSION = 1.0.31; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3299,7 +3299,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3322,7 +3322,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.30; + MARKETING_VERSION = 1.0.31; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 9fc3e62..b83f56d 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -299,12 +299,13 @@ class DataStore: ObservableObject { func runningMatches() -> [Match] { let dateNow : Date = Date() - let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.isRunning() } + match.disabled == false && match.isRunning() + } runningMatches.append(contentsOf: matches) } return runningMatches @@ -313,12 +314,12 @@ class DataStore: ObservableObject { func runningAndNextMatches() -> [Match] { let dateNow : Date = Date() - let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.startDate != nil && match.endDate == nil } + match.disabled == false && match.startDate != nil && match.endDate == nil } runningMatches.append(contentsOf: matches) } return runningMatches @@ -331,7 +332,7 @@ class DataStore: ObservableObject { var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.hasEnded() } + match.disabled == false && match.hasEnded() } runningMatches.append(contentsOf: matches) } return runningMatches.sorted(by: \.endDate!, order: .descending) From e85299973903cdec6094257987d9d44e8fdf2884 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 07:46:07 +0100 Subject: [PATCH 087/106] clean matchviewstyle to make it environment variable fix lag due to court used calculation on each match cell --- PadelClub.xcodeproj/project.pbxproj | 8 +++ PadelClub/Data/Match.swift | 15 ++--- PadelClub/Data/PlayerRegistration.swift | 4 ++ PadelClub/Utils/DisplayContext.swift | 26 -------- PadelClub/ViewModel/MatchViewStyle.swift | 51 ++++++++++++++++ .../Views/Components/MatchListView.swift | 4 +- .../Views/GroupStage/GroupStagesView.swift | 10 +-- .../LoserBracketFromGroupStageView.swift | 3 +- .../Match/Components/MatchDateView.swift | 8 +-- .../Match/Components/PlayerBlockView.swift | 23 ++++--- PadelClub/Views/Match/MatchDetailView.swift | 10 +-- PadelClub/Views/Match/MatchRowView.swift | 6 +- PadelClub/Views/Match/MatchSummaryView.swift | 61 +++++++++++-------- .../Navigation/Ongoing/OngoingView.swift | 3 +- .../Views/Planning/PlanningByCourtView.swift | 3 +- PadelClub/Views/Planning/PlanningView.swift | 4 +- PadelClub/Views/Round/LoserRoundView.swift | 3 +- PadelClub/Views/Round/RoundView.swift | 3 +- PadelClub/Views/Score/FollowUpMatchView.swift | 3 +- PadelClub/Views/Team/TeamRestingView.swift | 3 +- 20 files changed, 151 insertions(+), 100 deletions(-) create mode 100644 PadelClub/ViewModel/MatchViewStyle.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 57ea77b..a6803c2 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -789,6 +789,9 @@ FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; + FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; + FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; + FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; @@ -1178,6 +1181,7 @@ FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = ""; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; + FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = ""; }; FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = ""; }; FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = ""; }; FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; @@ -1733,6 +1737,7 @@ FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */, + FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */, ); path = ViewModel; sourceTree = ""; @@ -2321,6 +2326,7 @@ FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, + FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */, FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */, FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, @@ -2605,6 +2611,7 @@ FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */, FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */, FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */, + FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */, FF4CBF5E2C996C0600151637 /* TournamentBroadcastRowView.swift in Sources */, FF4CBF5F2C996C0600151637 /* TeamWeightView.swift in Sources */, @@ -2868,6 +2875,7 @@ FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */, FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */, FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */, + FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */, FF70FADD2C90584900129CC2 /* TournamentBroadcastRowView.swift in Sources */, FF70FADE2C90584900129CC2 /* TeamWeightView.swift in Sources */, diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7bd6547..9084607 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -975,19 +975,18 @@ defer { return confirmed == false && startDate.timeIntervalSinceNow < 0 } - func expectedFormattedStartDate(updatedField: Int?) -> String { + func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String { guard let startDate else { return "" } guard hasEnded() == false, isRunning() == false else { return "" } let depthReadiness = depthReadiness() if depthReadiness == 0 { - let availableCourts = availableCourts() - if canBePlayedInSpecifiedCourt() { + if canBePlayedInSpecifiedCourt { return "possible tout de suite" } else if let updatedField, availableCourts.contains(updatedField) { return "possible tout de suite \(courtName(for: updatedField))" } else if let first = availableCourts.first { return "possible tout de suite \(courtName(for: first))" - } else if let estimatedStartDate = estimatedStartDate() { + } else if let estimatedStartDate { return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0) } return "était prévu à " + startDate.formattedAsHourMinute() @@ -1014,10 +1013,8 @@ defer { typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) - func nextCourtsAvailable() -> [CourtIndexAndDate] { + func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] { guard let tournament = currentTournament() else { return [] } - let availableCourts = availableCourts() - let runningMatches = Tournament.runningMatches(tournament.allMatches()) let startDate = Date().withoutSeconds() if runningMatches.isEmpty { return availableCourts.map { @@ -1041,10 +1038,10 @@ defer { return dates } - func estimatedStartDate() -> CourtIndexAndDate? { + func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? { guard isReady() else { return nil } guard let tournament = currentTournament() else { return nil } - let availableCourts = nextCourtsAvailable() + let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches) return availableCourts.first(where: { (courtIndex, startDate) in let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60) if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false { diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index d137e2d..4dbc043 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -188,6 +188,10 @@ final class PlayerRegistration: ModelObject, Storable { return self.tournamentStore.teamRegistrations.findById(teamRegistration) } + func isHere() -> Bool { + hasArrived + } + func hasPaid() -> Bool { paymentType != nil } diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index 7eafd7d..e9d0338 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -26,32 +26,6 @@ enum SummoningDisplayContext { case menu } -enum MatchViewStyle { - case standardStyle // vue normal - case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche - case feedStyle // vue programmation - case plainStyle // vue detail - case tournamentResultStyle //vue resultat tournoi - case followUpStyle // vue normal - - func displayRestingTime() -> Bool { - switch self { - case .standardStyle: - return false - case .sectionedStandardStyle: - return false - case .feedStyle: - return false - case .plainStyle: - return false - case .tournamentResultStyle: - return false - case .followUpStyle: - return true - } - } -} - struct DeviceHelper { static func isBigScreen() -> Bool { switch UIDevice.current.userInterfaceIdiom { diff --git a/PadelClub/ViewModel/MatchViewStyle.swift b/PadelClub/ViewModel/MatchViewStyle.swift new file mode 100644 index 0000000..5917fc7 --- /dev/null +++ b/PadelClub/ViewModel/MatchViewStyle.swift @@ -0,0 +1,51 @@ +// +// MatchViewStyle.swift +// PadelClub +// +// Created by razmig on 17/11/2024. +// + +import SwiftUI + +enum MatchViewStyle { + case standardStyle // vue normal + case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche + case feedStyle // vue programmation + case plainStyle // vue detail + //case tournamentResultStyle //vue resultat tournoi + case followUpStyle // vue normal + + func displayRestingTime() -> Bool { + switch self { + case .standardStyle: + return false + case .sectionedStandardStyle: + return false + case .feedStyle: + return false + case .plainStyle: + return false +// case .tournamentResultStyle: +// return false + case .followUpStyle: + return true + } + } +} + +struct MatchViewStyleKey: EnvironmentKey { + static let defaultValue: MatchViewStyle = .standardStyle +} + +extension EnvironmentValues { + var matchViewStyle: MatchViewStyle { + get { self[MatchViewStyleKey.self] } + set { self[MatchViewStyleKey.self] = newValue } + } +} + +extension View { + func matchViewStyle(_ style: MatchViewStyle) -> some View { + environment(\.matchViewStyle, style) + } +} diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index c084661..0f98f01 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -10,10 +10,10 @@ import SwiftUI struct MatchListView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.matchViewStyle) private var matchViewStyle let section: String let matches: [Match]? - var matchViewStyle: MatchViewStyle = .standardStyle var hideWhenEmpty: Bool = false @State var isExpanded: Bool = true @@ -32,7 +32,7 @@ struct MatchListView: View { DisclosureGroup(isExpanded: $isExpanded) { if let matches { ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: matchViewStyle) + MatchRowView(match: match) .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) } } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index f3e2a58..13cd9e2 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -146,23 +146,25 @@ struct GroupStagesView: View { let runningMatches = Tournament.runningMatches(allMatches) Section { - MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "en cours", matches: runningMatches, isExpanded: false) } Section { - MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), isExpanded: false) + } Section { - MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false) } Section { - MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) + MatchListView(section: "terminés", matches: finishedMatches, isExpanded: false) } } + .matchViewStyle(.standardStyle) .navigationTitle("Toutes les poules") case .groupStage(let groupStage): GroupStageView(groupStage: groupStage).id(groupStage.id) diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index aeb532a..bd0fe30 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -45,7 +45,8 @@ struct LoserBracketFromGroupStageView: View { ForEach(displayableMatches) { match in Section { - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + MatchRowView(match: match) + .matchViewStyle(.sectionedStandardStyle) .environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage) } header: { let tournamentTeamCount = tournament.teamCount diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 9d33fe2..51ccc19 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -26,13 +26,7 @@ struct MatchDateView: View { self.isReady = match.isReady() self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasEnded = match.hasEnded() - if updatedField == nil, match.canBePlayedInSpecifiedCourt() { - self.updatedField = match.courtIndex - } else if let updatedField { - self.updatedField = updatedField - } else { - self.updatedField = match.availableCourts().first - } + self.updatedField = updatedField } var currentDate: Date { diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 5813ab9..c98a724 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -8,26 +8,29 @@ import SwiftUI struct PlayerBlockView: View { + @Environment(\.matchViewStyle) private var matchViewStyle @State var match: Match let teamPosition: TeamPosition let team: TeamRegistration? - let color: Color - let width: CGFloat let teamScore: TeamScore? let isWalkOut: Bool - let displayRestingTime: Bool - init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat, displayRestingTime: Bool) { + var displayRestingTime: Bool { + matchViewStyle.displayRestingTime() + } + + var width: CGFloat { + matchViewStyle == .plainStyle ? 1 : 2 + } + + init(match: Match, teamPosition: TeamPosition) { self.match = match self.teamPosition = teamPosition let theTeam = match.team(teamPosition) self.team = theTeam - self.color = color - self.width = width let theTeamScore = match.teamScore(ofTeam: theTeam) self.teamScore = theTeamScore self.isWalkOut = theTeamScore?.isWalkOut() == true - self.displayRestingTime = displayRestingTime } var names: [String]? { @@ -81,8 +84,8 @@ struct PlayerBlockView: View { } ForEach(team.players()) { player in Text(player.playerLabel()).lineLimit(1) - .italic(player.hasArrived == false) - .foregroundStyle(player.hasArrived == false ? .secondary : .primary) + .italic(player.isHere() == false) + .foregroundStyle(player.isHere() == false ? .secondary : .primary) } } else { ZStack(alignment: .leading) { @@ -122,7 +125,7 @@ struct PlayerBlockView: View { if width == 1 { Divider() } else { - Divider().frame(width: width).overlay(color) + Divider().frame(width: width).overlay(Color(white: 0.9)) } Text(string) .font(.title3) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 4adaa02..45f0444 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -14,8 +14,8 @@ struct MatchDetailView: View { @EnvironmentObject var networkMonitor: NetworkMonitor @Environment(\.dismiss) var dismiss - let matchViewStyle: MatchViewStyle - + @Environment(\.matchViewStyle) private var matchViewStyle + @State private var showLiveScore: Bool = false @State private var editScore: Bool = false @State private var scoreType: ScoreType? @@ -54,9 +54,8 @@ struct MatchDetailView: View { var match: Match - init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle, updatedField: Int? = nil) { + init(match: Match, updatedField: Int? = nil) { self.match = match - self.matchViewStyle = matchViewStyle if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) { _isEditing = State(wrappedValue: true) @@ -89,7 +88,8 @@ struct MatchDetailView: View { } Section { - MatchSummaryView(match: match, matchViewStyle: .plainStyle) + MatchSummaryView(match: match) + .matchViewStyle(.plainStyle) } footer: { if match.isEmpty() == false { HStack { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 1a778c1..643078e 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -11,8 +11,8 @@ import LeStorage struct MatchRowView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.matchViewStyle) private var matchViewStyle @State var match: Match - let matchViewStyle: MatchViewStyle var title: String? = nil var updatedField: Int? = nil @@ -60,9 +60,9 @@ struct MatchRowView: View { // }) NavigationLink { - MatchDetailView(match: match, matchViewStyle: matchViewStyle, updatedField: updatedField) + MatchDetailView(match: match, updatedField: updatedField) } label: { - MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title, updatedField: updatedField) + MatchSummaryView(match: match, title: title, updatedField: updatedField) .contextMenu { Section { ForEach(match.teams().flatMap({ $0.players() })) { player in diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5e3a86f..cdb7471 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -10,30 +10,25 @@ import SwiftUI struct MatchSummaryView: View { @EnvironmentObject var dataStore: DataStore @State var match: Match - let matchViewStyle: MatchViewStyle + @Environment(\.matchViewStyle) private var matchViewStyle let matchTitle: String let roundTitle: String? let courtName: String? - let spacing: CGFloat - let padding: CGFloat - let color: Color - let width: CGFloat let updatedField: Int? let estimatedStartDate: Match.CourtIndexAndDate? + let availableCourts: [Int] + let canBePlayedInSpecifiedCourt: Bool - init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil, updatedField: Int? = nil) { + init(match: Match, title: String? = nil, updatedField: Int? = nil) { self.match = match - self.matchViewStyle = matchViewStyle - self.padding = matchViewStyle == .plainStyle ? 0 : 8 - self.spacing = matchViewStyle == .plainStyle ? 8 : 0 - self.width = matchViewStyle == .plainStyle ? 1 : 2 - self.color = Color(white: 0.9) self.updatedField = updatedField + let currentAvailableCourts = match.availableCourts() + self.availableCourts = currentAvailableCourts if let groupStage = match.groupStageObject { self.roundTitle = groupStage.groupStageTitle(.title) } else if let round = match.roundObject { - self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short) + self.roundTitle = round.roundTitle(.short) } else { self.roundTitle = nil } @@ -45,10 +40,23 @@ struct MatchSummaryView: View { } else { self.courtName = nil } - - self.estimatedStartDate = match.estimatedStartDate() + let runningMatches = Tournament.runningMatches(match.currentTournament()?.allMatches() ?? []) + self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches) + self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt() + } + + var spacing: CGFloat { + matchViewStyle == .plainStyle ? 8 : 0 + } + + var padding: CGFloat { + matchViewStyle == .plainStyle ? 0 : 8 } + var width: CGFloat { + matchViewStyle == .plainStyle ? 1 : 2 + } + var body: some View { VStack(alignment: .leading) { if matchViewStyle != .plainStyle { @@ -76,33 +84,35 @@ struct MatchSummaryView: View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: spacing) { - PlayerBlockView(match: match, teamPosition: .one, color: color, width: width, displayRestingTime: matchViewStyle.displayRestingTime()) + PlayerBlockView(match: match, teamPosition: .one) .padding(padding) if width == 1 { Divider() } else { - Divider().frame(height: width).overlay(color) + Divider().frame(height: width).overlay(Color(white: 0.9)) } - PlayerBlockView(match: match, teamPosition: .two, color: color, width: width, displayRestingTime: matchViewStyle.displayRestingTime()) + PlayerBlockView(match: match, teamPosition: .two) .padding(padding) } } .overlay { if matchViewStyle != .plainStyle { RoundedRectangle(cornerRadius: 8) - .stroke(color, lineWidth: 2) + .stroke(Color(white: 0.9), lineWidth: 2) } } if matchViewStyle != .plainStyle { HStack { - if match.expectedToBeRunning() { - Text(match.expectedFormattedStartDate(updatedField: updatedField)) - .font(.footnote) - .foregroundStyle(.secondary) + if matchViewStyle == .followUpStyle { + if match.expectedToBeRunning() { + Text(match.expectedFormattedStartDate(canBePlayedInSpecifiedCourt: canBePlayedInSpecifiedCourt, availableCourts: availableCourts, estimatedStartDate: estimatedStartDate, updatedField: updatedField)) + .font(.footnote) + .foregroundStyle(.secondary) + } } Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle, updatedField: possibleCourtIndex) + MatchDateView(match: match, showPrefix: false, updatedField: possibleCourtIndex) } } } @@ -111,8 +121,7 @@ struct MatchSummaryView: View { } var possibleCourtIndex: Int? { - let availableCourts = match.availableCourts() - if match.canBePlayedInSpecifiedCourt() { + if canBePlayedInSpecifiedCourt { return nil } else if let updatedField, availableCourts.contains(updatedField) { return updatedField @@ -133,7 +142,7 @@ struct MatchSummaryView: View { return false } - if match.canBePlayedInSpecifiedCourt() { + if canBePlayedInSpecifiedCourt { return false } diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 4335f39..309885a 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -32,7 +32,8 @@ struct OngoingView: View { ForEach(filteredMatches) { match in let tournament = match.currentTournament() Section { - MatchRowView(match: match, matchViewStyle: .standardStyle) + MatchRowView(match: match) + .matchViewStyle(.followUpStyle) } header: { if let tournament { HStack { diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 15a1a9e..10b4f92 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -112,7 +112,8 @@ struct PlanningByCourtView: View { ForEach(_sortedMatches.indices, id: \.self) { index in let match = _sortedMatches[index] Section { - MatchRowView(match: match, matchViewStyle: .feedStyle) + MatchRowView(match: match) + .matchViewStyle(.feedStyle) } header: { if let startDate = match.startDate { if index > 0 { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index 1e738b6..3a6e07f 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -158,7 +158,9 @@ struct PlanningView: View { DisclosureGroup { ForEach(_matches) { match in NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) + MatchDetailView(match: match) + .matchViewStyle(.sectionedStandardStyle) + } label: { LabeledContent { if let courtName = match.courtName() { diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index 843765d..fba3563 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -40,7 +40,8 @@ struct LoserRoundView: View { if matches.isEmpty == false { Section { ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + MatchRowView(match: match) + .matchViewStyle(.sectionedStandardStyle) .overlay { if match.disabled && isEditingTournamentSeed.wrappedValue == true { Image(systemName: "xmark") diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 68c0418..23bf3d5 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -215,7 +215,8 @@ struct RoundView: View { ForEach(displayableMatches) { match in let matchTitle = match.matchTitle(.short, inMatches: displayableMatches) Section { - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle, title: matchTitle) + MatchRowView(match: match, title: matchTitle) + .matchViewStyle(.sectionedStandardStyle) } header: { HStack { Text(upperRound.round.roundTitle(.wide)) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift index 0ee7f20..78cda7f 100644 --- a/PadelClub/Views/Score/FollowUpMatchView.swift +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -198,7 +198,8 @@ struct FollowUpMatchView: View { ForEach(sortedMatches) { match in let tournament = match.currentTournament() Section { - MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) + MatchRowView(match: match, updatedField: selectedCourt) + .matchViewStyle(.followUpStyle) } header: { if let tournament { HStack { diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift index 1438f59..712cb05 100644 --- a/PadelClub/Views/Team/TeamRestingView.swift +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -62,7 +62,8 @@ struct TeamRestingView: View { case .restingTime: if sortedMatches.isEmpty == false { ForEach(sortedMatches) { match in - MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt) + MatchRowView(match: match, updatedField: selectedCourt) + .matchViewStyle(.followUpStyle) } } else { ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) From 16de4ee01261c021349ded9039cba88c70b86c76 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 08:34:30 +0100 Subject: [PATCH 088/106] optimize running matches gathering --- PadelClub/Data/Match.swift | 15 ++++++++------- PadelClub/Data/Tournament.swift | 4 +--- PadelClub/Views/Match/MatchSummaryView.swift | 8 +++++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 9084607..a2338b4 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -667,7 +667,8 @@ defer { setCourt(_courtIndex) } case .random: - if let _courtIndex = availableCourts().randomElement() { + let runningMatches: [Match] = DataStore.shared.runningMatches() + if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() { setCourt(_courtIndex) } case .field(let _courtIndex): @@ -707,8 +708,8 @@ defer { return currentTournament()?.courtCount ?? 1 } - func courtIsAvailable(_ courtIndex: Int) -> Bool { - let courtUsed = currentTournament()?.courtUsed() ?? [] + func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool { + let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return courtUsed.contains(courtIndex) == false } @@ -721,8 +722,8 @@ defer { return availableCourts } - func availableCourts() -> [Int] { - let courtUsed = currentTournament()?.courtUsed() ?? [] + func availableCourts(runningMatches: [Match]) -> [Int] { + let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted() } @@ -1002,10 +1003,10 @@ defer { return " depuis " + startDate.timeElapsedString() } - func canBePlayedInSpecifiedCourt() -> Bool { + func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool { guard let courtIndex else { return false } if expectedToBeRunning() { - return courtIsAvailable(courtIndex) + return courtIsAvailable(courtIndex, in: runningMatches) } else { return true } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 0899530..132db89 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -513,7 +513,7 @@ final class Tournament : ModelObject, Storable { return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path) } - func courtUsed() -> [Int] { + func courtUsed(runningMatches: [Match]) -> [Int] { #if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { @@ -521,8 +521,6 @@ defer { print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - - let runningMatches: [Match] = DataStore.shared.runningMatches() return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index cdb7471..51942d3 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -22,7 +22,10 @@ struct MatchSummaryView: View { init(match: Match, title: String? = nil, updatedField: Int? = nil) { self.match = match self.updatedField = updatedField - let currentAvailableCourts = match.availableCourts() + + let runningMatches = DataStore.shared.runningMatches() + + let currentAvailableCourts = match.availableCourts(runningMatches: runningMatches) self.availableCourts = currentAvailableCourts if let groupStage = match.groupStageObject { @@ -40,9 +43,8 @@ struct MatchSummaryView: View { } else { self.courtName = nil } - let runningMatches = Tournament.runningMatches(match.currentTournament()?.allMatches() ?? []) self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches) - self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt() + self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt(runningMatches: runningMatches) } var spacing: CGFloat { From c5bc2fb0d5f56503f5734551f55b6e59b8eb679f Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 09:12:48 +0100 Subject: [PATCH 089/106] add set of four games --- PadelClub/Utils/PadelRule.swift | 43 +++++++++++++------ .../Views/Shared/MatchFormatRowView.swift | 2 +- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 81bb541..e69e5ee 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -1152,6 +1152,9 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { case twoSetsOfSuperTie case singleSet case singleSetDecisivePoint + case singleSetOfFourGames + case singleSetOfFourGamesDecisivePoint + init?(rawValue: Int?) { guard let value = rawValue else { return nil } @@ -1180,6 +1183,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return 6 case .singleSet, .singleSetDecisivePoint: return 7 + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return 8 } } @@ -1209,6 +1214,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return 6 case .singleSet, .singleSetDecisivePoint: return 7 + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return 8 } } @@ -1224,7 +1231,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { } static var allCases: [MatchFormat] { - [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint] + [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint] } func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { @@ -1237,7 +1244,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { var canSuperTie: Bool { switch self { - case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return true default: return false @@ -1259,8 +1266,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { func formattedEstimatedBreakDuration() -> String { var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) if breakTime.matchCount > 1 { - label += " après \(breakTime.matchCount) match" + label += " de pause après \(breakTime.matchCount) match" label += breakTime.matchCount.pluralSuffix + } else { + label += " de pause" } return label } @@ -1293,6 +1302,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return 30 case .singleSetDecisivePoint: return 25 + case .singleSetOfFourGames: + return 15 + case .singleSetOfFourGamesDecisivePoint: + return 10 } } @@ -1311,10 +1324,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return (30, 1) case .superTie: return (15, 3) - case .megaTie: + default: return (5, 1) - case .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: - return (10, 1) } } @@ -1326,16 +1337,16 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return matchCount < 6 ? 3 : 0 case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint: return matchCount < 7 ? 6 : 2 - case .superTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: - return 7 - case .megaTie: + case .superTie: return 7 + default: + return 10 } } var hasDecisivePoint: Bool { switch self { - case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint: + case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint: return true default: return false @@ -1377,7 +1388,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { var isFederal: Bool { switch self { - case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint: + case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return false default: return true @@ -1415,6 +1426,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return "C2" case .nineGamesDecisivePoint: return "D2" + case .singleSetOfFourGames: + return "I1" + case .singleSetOfFourGamesDecisivePoint: + return "I2" } } @@ -1436,6 +1451,8 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { return "supertie de 10 points" case .megaTie: return "supertie de 15 points" + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return "1 set de 4 jeux, tiebreak à 4/4" } } @@ -1455,7 +1472,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { switch self { case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie: return 2 - case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint: + case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return 1 } } @@ -1464,7 +1481,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { switch self { case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint: return .six - case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: + case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return .four case .nineGames, .nineGamesDecisivePoint: return .nine diff --git a/PadelClub/Views/Shared/MatchFormatRowView.swift b/PadelClub/Views/Shared/MatchFormatRowView.swift index c03a13b..b253e63 100644 --- a/PadelClub/Views/Shared/MatchFormatRowView.swift +++ b/PadelClub/Views/Shared/MatchFormatRowView.swift @@ -37,7 +37,7 @@ struct MatchFormatRowView: View { if hideExplanation == false { Text(matchFormat.longLabel) - Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").font(.footnote) + Text(matchFormat.formattedEstimatedBreakDuration()).font(.footnote) if matchFormat.isFederal == false { Text("Non officiel").foregroundStyle(.logoRed).font(.footnote) } From e8665a3e8be0d390e4fda6228ec177a0193069e1 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 09:33:08 +0100 Subject: [PATCH 090/106] add a way to know if the team is from beach --- .../Components/InscriptionInfoView.swift | 59 +++++++++++++------ .../Screen/InscriptionManagerView.swift | 13 ++++ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index a52b8c5..3fb55c6 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -27,33 +27,56 @@ struct InscriptionInfoView: View { var body: some View { List { Section { - let footerString = "via [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" - - LabeledContent { - Text(entriesFromBeachPadel.count.formatted()) - } label: { - Text("Paires importées") - Text(.init(footerString)) - } - .listRowView(color: .indigo) - LabeledContent { Text(selectedTeams.filter { $0.called() }.count.formatted()) } label: { Text("Paires convoquées") - Text("Vous avez envoyé une convocation par sms ou email") } .listRowView(color: .cyan) -// LabeledContent { -// Text(selectedTeams.filter { $0.confirmed() }.count.formatted()) -// } label: { -// Text("Paires ayant confirmées") -// Text("Vous avez noté la confirmation de l'équipe") -// } -// .listRowView(color: .green) + LabeledContent { + Text(selectedTeams.filter { $0.confirmed() }.count.formatted()) + } label: { + Text("Paires ayant confirmées") + } + .listRowView(color: .green) } + + if tournament.isAnimation() == false, entriesFromBeachPadel.count > 0 { + let notFromBeach = selectedTeams.filter({ $0.isImported() == false }) + Section { + let subtitle = "via [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" + + LabeledContent { + Text(entriesFromBeachPadel.count.formatted()) + } label: { + Text("Paires importées") + Text(.init(subtitle)) + } + .listRowView(color: .indigo) + + DisclosureGroup { + ForEach(notFromBeach) { team in + TeamDetailView(team: team) + } + } label: { + LabeledContent { + Text(notFromBeach.count.formatted()) + } label: { + Text("Paires non importées") + } + } + .listRowView(color: .brown) + } footer: { + if notFromBeach.isEmpty == false { + let footerString = "Au moins un joueur ne provient pas du fichier. Il est possible que vous ayez à mettre à jour les équipes sur [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" + + Text(.init(footerString)) + } + } + } + Section { DisclosureGroup { ForEach(callDateIssue) { team in diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 81a6368..7dd6dc2 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -78,6 +78,7 @@ struct InscriptionManagerView: View { case groupStage case wildcardGroupStage case wildcardBracket + case notImported func emptyLocalizedLabelDescription() -> String { switch self { @@ -95,6 +96,8 @@ struct InscriptionManagerView: View { return "Vous n'avez placé aucune équipe dans le tableau." case .groupStage: return "Vous n'avez placé aucune équipe en poule." + case .notImported: + return "Vous n'avez aucune équipe non importé. Elles proviennent toutes du fichier." } } @@ -114,6 +117,8 @@ struct InscriptionManagerView: View { return "Aucune équipe dans le tableau" case .groupStage: return "Aucune équipe en poule" + case .notImported: + return "Aucune équipe non importée" } } @@ -133,6 +138,8 @@ struct InscriptionManagerView: View { return displayStyle == .wide ? "Forfaits" : "forfait" case .waiting: return displayStyle == .wide ? "Liste d'attente" : "attente" + case .notImported: + return "Non importées" } } } @@ -470,6 +477,8 @@ struct InscriptionManagerView: View { teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) case .groupStage: teams = teams.filter({ $0.inGroupStage() }) + case .notImported: + teams = teams.filter({ $0.isImported() == false }) default: break } @@ -652,6 +661,10 @@ struct InscriptionManagerView: View { case .waiting: let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) return waiting.formatted() + case .notImported: + let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) + return notImported.formatted() + } } From 928b3510e2db0779468e0362977e1265cf6678d7 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 10:08:55 +0100 Subject: [PATCH 091/106] add simple contact option in call/recall menu --- PadelClub/Views/Calling/CallView.swift | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 85fb5cb..86f12af 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -137,8 +137,8 @@ struct CallView: View { } } - func finalMessage(reSummon: Bool) -> String { - if simpleMode { + func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String { + if simpleMode || forcedEmptyMessage { let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature() return "\n\n\n\n" + signature } @@ -325,6 +325,13 @@ struct CallView: View { Button("Re-convoquer") { self._summon(byMessage: byMessage, reSummon: true) } + + if simpleMode == false { + Divider() + Button("Contacter") { + self._summon(byMessage: byMessage, reSummon: false, forcedEmptyMessage: true) + } + } } label: { Text(byMessage ? "sms" : "mail") @@ -337,15 +344,15 @@ struct CallView: View { } } - private func _summon(byMessage: Bool, reSummon: Bool) { + private func _summon(byMessage: Bool, reSummon: Bool, forcedEmptyMessage: Bool = false) { self.summonParamByMessage = byMessage self.summonParamReSummon = reSummon self._verifyUser { self._payTournamentAndExecute { if byMessage { - self._contactByMessage(reSummon: reSummon) + self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } else { - self._contactByMail(reSummon: reSummon) + self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } } } @@ -368,18 +375,18 @@ struct CallView: View { } } - fileprivate func _contactByMessage(reSummon: Bool) { - self.contactType = .message(date: callDate, + fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) { + self.contactType = .message(date: callDate, recipients: teams.flatMap { $0.getPhoneNumbers() }, - body: finalMessage(reSummon: reSummon), + body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), tournamentBuild: nil) } - fileprivate func _contactByMail(reSummon: Bool) { + fileprivate func _contactByMail(reSummon: Bool, forcedEmptyMessage: Bool) { self.contactType = .mail(date: callDate, recipients: tournament.umpireMail(), bccRecipients: teams.flatMap { $0.getMail() }, - body: finalMessage(reSummon: reSummon), + body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), subject: tournament.tournamentTitle(), tournamentBuild: nil) } From 58ffb56ef5b910dc7fb01d4ac6b61c04c415d02d Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 18 Nov 2024 10:49:23 +0100 Subject: [PATCH 092/106] add ranking to register message --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift | 4 ++-- .../Views/Navigation/Agenda/TournamentSubscriptionView.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a6803c2..acd4aa9 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.31; + MARKETING_VERSION = 1.0.32; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.31; + MARKETING_VERSION = 1.0.32; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index ba05ab0..d08eaa6 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -58,8 +58,8 @@ extension ImportedPlayer: PlayerHolder { male } - func pasteData() -> String { - return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense].compactMap({ $0 }).joined(separator: " ") + func pasteData(withRank: Bool = false) -> String { + return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense, withRank ? "(\(rank.ordinalFormatted(feminine: isMalePlayer() == false)))" : nil].compactMap({ $0 }).joined(separator: " ") } func isNotFromCurrentDate() -> Bool { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index 91dedc7..b9050ed 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -286,7 +286,7 @@ struct TournamentSubscriptionView: View { } var teamsString: String { - selectedPlayers.map { $0.pasteData() }.joined(separator: "\n") + selectedPlayers.map { $0.pasteData(withRank: true) }.joined(separator: "\n") } var messageBody: String { From 7a4628208c1b604b4d40b48baf1c98b80d52ec0b Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 28 Nov 2024 18:46:57 +0100 Subject: [PATCH 093/106] v1.0.33 --- PadelClub.xcodeproj/project.pbxproj | 4 +- PadelClub/Data/GroupStage.swift | 6 ++- PadelClub/Data/Match.swift | 2 +- PadelClub/Data/MatchScheduler.swift | 50 ++++++++++++++----- PadelClub/ViewModel/SearchViewModel.swift | 7 +-- .../Views/Planning/PlanningSettingsView.swift | 8 +++ 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index acd4aa9..5129223 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.32; + MARKETING_VERSION = 1.0.33; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.32; + MARKETING_VERSION = 1.0.33; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 4738c2c..75e6946 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -203,6 +203,10 @@ final class GroupStage: ModelObject, Storable { } } + func orderedIndexOfMatch(_ match: Match) -> Int { + _matchOrder()[safe: match.index] ?? match.index + } + func updateGroupStageState() { clearScoreCache() @@ -368,7 +372,7 @@ final class GroupStage: ModelObject, Storable { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } - private func _matchUp(for matchIndex: Int) -> [Int] { + func _matchUp(for matchIndex: Int) -> [Int] { let combinations = Array((0.. Bool { let teams = teamScores guard teams.count == 2 else { - print("teams.count != 2") + //print("teams.count != 2") return false } guard hasEnded() == false else { return false } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 265e03f..f45f08f 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -32,6 +32,7 @@ final class MatchScheduler : ModelObject, Storable { var overrideCourtsUnavailability: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = true var courtsAvailable: Set = Set() + var simultaneousStart: Bool = true init(tournament: String, timeDifferenceLimit: Int = 5, @@ -46,7 +47,8 @@ final class MatchScheduler : ModelObject, Storable { groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = true, - courtsAvailable: Set = Set()) { + courtsAvailable: Set = Set(), + simultaneousStart: Bool = true) { self.tournament = tournament self.timeDifferenceLimit = timeDifferenceLimit self.loserBracketRotationDifference = loserBracketRotationDifference @@ -61,6 +63,7 @@ final class MatchScheduler : ModelObject, Storable { self.overrideCourtsUnavailability = overrideCourtsUnavailability self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.courtsAvailable = courtsAvailable + self.simultaneousStart = simultaneousStart } enum CodingKeys: String, CodingKey { @@ -79,6 +82,7 @@ final class MatchScheduler : ModelObject, Storable { case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _courtsAvailable = "courtsAvailable" + case _simultaneousStart = "simultaneousStart" } var courtsUnavailability: [DateInterval]? { @@ -185,14 +189,18 @@ final class MatchScheduler : ModelObject, Storable { // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 - - // Flatten matches in a round-robin order by cycling through each group - let flattenedMatches = (0.. 0 { rotationMatches = rotationMatches.sorted(by: { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { - return $0.groupStageObject!.index < $1.groupStageObject!.index + if simultaneousStart { + return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1) + } else { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } } else { return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 } @@ -245,7 +257,7 @@ final class MatchScheduler : ModelObject, Storable { return false } - let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) + let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) }) if !teamsAvailable { print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") return false @@ -259,7 +271,7 @@ final class MatchScheduler : ModelObject, Storable { print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") slots.append(timeMatch) - teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) + teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp()) rotationMatches.removeAll(where: { $0.id == first.id }) availableMatches.removeAll(where: { $0.id == first.id }) @@ -891,4 +903,16 @@ extension Match { func containsTeamId(_ id: String) -> Bool { return teamIds().contains(id) } + + func containsTeamIndex(_ id: String) -> Bool { + matchUp().contains(id) + } + + func matchUp() -> [String] { + guard let groupStageObject else { + return [] + } + + return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" } + } } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 0b066c3..e55dff7 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -69,9 +69,6 @@ class SearchViewModel: ObservableObject, Identifiable { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") - if filterOption == .male { - message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.") - } } return message.joined(separator: "\n") } @@ -231,7 +228,7 @@ class SearchViewModel: ObservableObject, Identifiable { ] if let mostRecentDate { - //predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if hideAssimilation { @@ -344,7 +341,7 @@ class SearchViewModel: ObservableObject, Identifiable { } if let mostRecentDate { - //andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if nameComponents.count > 1 { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 3edfed5..8a0d080 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -385,6 +385,14 @@ struct PlanningSettingsView: View { } footer: { Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") } + + Section { + Toggle(isOn: $matchScheduler.simultaneousStart) { + Text("Démarrage simultané") + } + } footer: { + Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.") + } } Section { From acacfe1de26d67ace5e913efb14dd1b3c3a9db1f Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 28 Nov 2024 22:42:07 +0100 Subject: [PATCH 094/106] fix stuff --- PadelClub.xcodeproj/project.pbxproj | 4 +-- PadelClub/Data/AppSettings.swift | 12 +++++++ PadelClub/ViewModel/SearchViewModel.swift | 3 +- .../Navigation/Toolbox/ToolboxView.swift | 2 +- .../Shared/SelectablePlayerListView.swift | 36 +++++++++++++------ 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5129223..f2b27ae 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3262,7 +3262,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3307,7 +3307,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 60be3e3..4dbc2db 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -29,6 +29,18 @@ final class AppSettings: MicroStorable { var nationalCup: Bool var dayDuration: Int? var dayPeriod: DayPeriod + + func lastDataSourceDate() -> Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + + func localizedLastDataSource() -> String? { + guard let lastDataSource else { return nil } + guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil } + + return date.monthYearFormatted + } func resetSearch() { tournamentAges = Set() diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index e55dff7..be8a85c 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable { @Published var filterSelectionEnabled: Bool = false @Published var isPresented: Bool = false @Published var selectedAgeCategory: FederalTournamentAge = .unlisted - - var mostRecentDate: Date? = nil + @Published var mostRecentDate: Date? = nil var selectionIsOver: Bool { if allowSingleSelection && selectedPlayers.count == 1 { diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 8c9dd36..e8fbfd0 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -129,7 +129,7 @@ struct ToolboxView: View { Section { NavigationLink { - SelectablePlayerListView(isPresented: false) + SelectablePlayerListView(isPresented: false, lastDataSource: true) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 9b61575..00a4836 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -24,23 +24,19 @@ struct SelectablePlayerListView: View { @StateObject private var searchViewModel: SearchViewModel @Environment(\.dismiss) var dismiss - var lastDataSource: String? { - dataStore.appSettings.lastDataSource - } - @State private var searchText: String = "" - var mostRecentDate: Date? { - guard let lastDataSource else { return nil } - return URL.importDateFormatter.date(from: lastDataSource) - } - - init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + + init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, lastDataSource: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction self.searchText = searchField ?? "" let searchViewModel = SearchViewModel() searchViewModel.tokens = tokens + if lastDataSource { + searchViewModel.mostRecentDate = DataStore.shared.appSettings.lastDataSourceDate() + } + searchViewModel.searchText = searchField ?? "" searchViewModel.debouncableText = searchField ?? "" searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation @@ -59,6 +55,18 @@ struct SelectablePlayerListView: View { _searchViewModel = StateObject(wrappedValue: searchViewModel) } + var enableSourceCheck: Binding { + Binding { + searchViewModel.mostRecentDate != nil + } set: { value in + if value == false { + searchViewModel.mostRecentDate = nil + } else { + searchViewModel.mostRecentDate = dataStore.appSettings.lastDataSourceDate() + } + } + } + var body: some View { VStack(spacing: 0) { if importObserver.isImportingFile() == false { @@ -73,6 +81,13 @@ struct SelectablePlayerListView: View { } .pickerStyle(.segmented) Menu { + if let lastDataSource = dataStore.appSettings.localizedLastDataSource() { + Section { + Toggle(isOn: enableSourceCheck) { + Text("Limité à \(lastDataSource)") + } + } + } Section { ForEach(SourceFileManager.getSortOption()) { option in Toggle(isOn: .init(get: { @@ -191,7 +206,6 @@ struct SelectablePlayerListView: View { } } .onAppear { - searchViewModel.mostRecentDate = mostRecentDate if searchViewModel.tokens.isEmpty && searchText.isEmpty { searchViewModel.debouncableText.removeAll() searchViewModel.searchText.removeAll() From a0a6200dc91e8c5902b1f36619535a91f775b8d9 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 2 Dec 2024 12:00:36 +0100 Subject: [PATCH 095/106] v1.0.34 --- PadelClub.xcodeproj/project.pbxproj | 8 ++++---- PadelClub/Views/Navigation/Toolbox/ToolboxView.swift | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f2b27ae..b4b2873 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3262,7 +3262,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.33; + MARKETING_VERSION = 1.0.34; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3307,7 +3307,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.33; + MARKETING_VERSION = 1.0.34; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index e8fbfd0..114112d 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -196,10 +196,17 @@ struct ToolboxView: View { Text("Contrat d'utilisation") } } + + Section { + RowButtonView("Effacer les logs", role: .destructive) { + StoreCenter.main.resetLoggingCollections() + didResetApiCalls = true + } + } } .overlay(alignment: .bottom) { if didResetApiCalls { - Label("failed api calls deleted", systemImage: "checkmark") + Label("logs effacés", systemImage: "checkmark") .toastFormatted() .deferredRendering(for: .seconds(3)) .onAppear { From 11acae92b0e85c04e53bff9ae73e6b164ca5fd23 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 2 Dec 2024 13:13:57 +0100 Subject: [PATCH 096/106] fix animation settings --- .../Screen/InscriptionManagerView.swift | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 7dd6dc2..fc7c297 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -320,25 +320,27 @@ struct InscriptionManagerView: View { .symbolVariant(filterMode == .all ? .none : .fill) } Menu { + if tournament.inscriptionClosed() == false { + Menu { + _sortingTypePickerView() + } label: { + Text("Méthode de sélection") + Text(tournament.teamSorting.localizedLabel()) + } + Divider() + rankingDateSourcePickerView(showDateInLabel: true) + + Divider() + Button { + tournament.lockRegistration() + _save() + } label: { + Label("Clôturer", systemImage: "lock") + } + + } if tournament.isAnimation() == false { if tournament.inscriptionClosed() == false { - Menu { - _sortingTypePickerView() - } label: { - Text("Méthode de sélection") - Text(tournament.teamSorting.localizedLabel()) - } - Divider() - rankingDateSourcePickerView(showDateInLabel: true) - - Divider() - Button { - tournament.lockRegistration() - _save() - } label: { - Label("Clôturer", systemImage: "lock") - } - Divider() Section { @@ -391,7 +393,7 @@ struct InscriptionManagerView: View { Text("Masquer les poids des équipes") } - rankingDateSourcePickerView(showDateInLabel: true) + //rankingDateSourcePickerView(showDateInLabel: true) Divider() From c27a524fc5f0d7e3b4c57773398ec2914e6a59f7 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 5 Dec 2024 16:32:30 +0100 Subject: [PATCH 097/106] Adds patch to reset logs --- PadelClub/Utils/Patcher.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 66a71ee..7bbd46b 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -16,6 +16,7 @@ enum Patch: String, CaseIterable { case alexisLeDu case importDataFromDevToProd case fixMissingMatches + case cleanLogs var id: String { return "padelclub.app.patch.\(self.rawValue)" @@ -31,7 +32,7 @@ class Patcher { } static func patchIfPossible(_ patch: Patch) { - if UserDefaults.standard.value(forKey: patch.id) == nil { +// if UserDefaults.standard.value(forKey: patch.id) == nil { do { Logger.log(">>> Patches \(patch.rawValue)...") try self._applyPatch(patch) @@ -39,7 +40,7 @@ class Patcher { } catch { Logger.error(error) } - } +// } } fileprivate static func _applyPatch(_ patch: Patch) throws { @@ -47,6 +48,7 @@ class Patcher { case .alexisLeDu: self._patchAlexisLeDu() case .importDataFromDevToProd: try self._importDataFromDev() case .fixMissingMatches: self._patchMissingMatches() + case .cleanLogs: self._cleanLogs() } } @@ -161,4 +163,7 @@ class Patcher { } + fileprivate static func _cleanLogs() { + StoreCenter.main.resetLoggingCollections() + } } From f6cf2c45b3f29bd4b1ceb732b6efcddced9eb3b8 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 5 Dec 2024 16:40:57 +0100 Subject: [PATCH 098/106] v1.0.35 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b4b2873..f9760af 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.34; + MARKETING_VERSION = 1.0.35; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.34; + MARKETING_VERSION = 1.0.35; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 033901d97c8f1ab46386654f48bae9ff72d8b8ee Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 6 Dec 2024 18:32:37 +0100 Subject: [PATCH 099/106] Fix issue --- PadelClub/Utils/Patcher.swift | 232 +++++++++++++++++----------------- 1 file changed, 113 insertions(+), 119 deletions(-) diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 7bbd46b..6841bf4 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -13,9 +13,6 @@ enum PatchError: Error { } enum Patch: String, CaseIterable { - case alexisLeDu - case importDataFromDevToProd - case fixMissingMatches case cleanLogs var id: String { @@ -32,7 +29,7 @@ class Patcher { } static func patchIfPossible(_ patch: Patch) { -// if UserDefaults.standard.value(forKey: patch.id) == nil { + if UserDefaults.standard.value(forKey: patch.id) == nil { do { Logger.log(">>> Patches \(patch.rawValue)...") try self._applyPatch(patch) @@ -40,128 +37,125 @@ class Patcher { } catch { Logger.error(error) } -// } + } } fileprivate static func _applyPatch(_ patch: Patch) throws { switch patch { - case .alexisLeDu: self._patchAlexisLeDu() - case .importDataFromDevToProd: try self._importDataFromDev() - case .fixMissingMatches: self._patchMissingMatches() case .cleanLogs: self._cleanLogs() } } - - fileprivate static func _patchAlexisLeDu() { - guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return } - - let clubs = DataStore.shared.clubs - StoreCenter.main.resetApiCalls(collection: clubs) -// clubs.resetApiCalls() - - for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) { - club.creator = StoreCenter.main.userId - clubs.writeChangeAndInsertOnServer(instance: club) - } - - } - - fileprivate static func _importDataFromDev() throws { - - let devServices = Services(url: "https://xlr.alwaysdata.net/roads/") - guard devServices.hasToken() else { - return - } - guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else { - return - } - - guard let userId = StoreCenter.main.userId else { - return - } - - try StoreCenter.main.migrateToken(devServices) - - - let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId } - let clubIds: [String] = myClubs.map { $0.id } - - myClubs.forEach { club in - DataStore.shared.clubs.insertIntoCurrentService(item: club) - - let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) } - for court in courts { - DataStore.shared.courts.insertIntoCurrentService(item: court) - } - } - - DataStore.shared.user.clubs = Array(clubIds) - DataStore.shared.saveUser() - - DataStore.shared.events.insertAllIntoCurrentService() - DataStore.shared.tournaments.insertAllIntoCurrentService() - DataStore.shared.dateIntervals.insertAllIntoCurrentService() - - for tournament in DataStore.shared.tournaments { - let store = tournament.tournamentStore - - Task { // need to wait for the collections to load - try await Task.sleep(until: .now + .seconds(2)) - - store.teamRegistrations.insertAllIntoCurrentService() - store.rounds.insertAllIntoCurrentService() - store.groupStages.insertAllIntoCurrentService() - store.matches.insertAllIntoCurrentService() - store.playerRegistrations.insertAllIntoCurrentService() - store.teamScores.insertAllIntoCurrentService() - - } - } - - } - - fileprivate static func _patchMissingMatches() { - - guard let url = StoreCenter.main.synchronizationApiURL else { - return - } - guard url == "https://padelclub.app/roads/" else { - return - } - let services = Services(url: url) - - for tournament in DataStore.shared.tournaments { - - let store = tournament.tournamentStore - let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament") - - Task { - - do { - // if nothing is online we upload the data - let matches: [Match] = try await services.get(identifier: identifier) - if matches.isEmpty { - store.matches.insertAllIntoCurrentService() - } - - let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier) - if playerRegistrations.isEmpty { - store.playerRegistrations.insertAllIntoCurrentService() - } - - let teamScores: [TeamScore] = try await services.get(identifier: identifier) - if teamScores.isEmpty { - store.teamScores.insertAllIntoCurrentService() - } - - } catch { - Logger.error(error) - } - - } - } - - } +// +// fileprivate static func _patchAlexisLeDu() { +// guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return } +// +// let clubs = DataStore.shared.clubs +// StoreCenter.main.resetApiCalls(collection: clubs) +//// clubs.resetApiCalls() +// +// for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) { +// club.creator = StoreCenter.main.userId +// clubs.writeChangeAndInsertOnServer(instance: club) +// } +// +// } +// +// fileprivate static func _importDataFromDev() throws { +// +// let devServices = Services(url: "https://xlr.alwaysdata.net/roads/") +// guard devServices.hasToken() else { +// return +// } +// guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else { +// return +// } +// +// guard let userId = StoreCenter.main.userId else { +// return +// } +// +// try StoreCenter.main.migrateToken(devServices) +// +// +// let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId } +// let clubIds: [String] = myClubs.map { $0.id } +// +// myClubs.forEach { club in +// DataStore.shared.clubs.insertIntoCurrentService(item: club) +// +// let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) } +// for court in courts { +// DataStore.shared.courts.insertIntoCurrentService(item: court) +// } +// } +// +// DataStore.shared.user.clubs = Array(clubIds) +// DataStore.shared.saveUser() +// +// DataStore.shared.events.insertAllIntoCurrentService() +// DataStore.shared.tournaments.insertAllIntoCurrentService() +// DataStore.shared.dateIntervals.insertAllIntoCurrentService() +// +// for tournament in DataStore.shared.tournaments { +// let store = tournament.tournamentStore +// +// Task { // need to wait for the collections to load +// try await Task.sleep(until: .now + .seconds(2)) +// +// store.teamRegistrations.insertAllIntoCurrentService() +// store.rounds.insertAllIntoCurrentService() +// store.groupStages.insertAllIntoCurrentService() +// store.matches.insertAllIntoCurrentService() +// store.playerRegistrations.insertAllIntoCurrentService() +// store.teamScores.insertAllIntoCurrentService() +// +// } +// } +// +// } +// +// fileprivate static func _patchMissingMatches() { +// +// guard let url = StoreCenter.main.synchronizationApiURL else { +// return +// } +// guard url == "https://padelclub.app/roads/" else { +// return +// } +// let services = Services(url: url) +// +// for tournament in DataStore.shared.tournaments { +// +// let store = tournament.tournamentStore +// let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament") +// +// Task { +// +// do { +// // if nothing is online we upload the data +// let matches: [Match] = try await services.get(identifier: identifier) +// if matches.isEmpty { +// store.matches.insertAllIntoCurrentService() +// } +// +// let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier) +// if playerRegistrations.isEmpty { +// store.playerRegistrations.insertAllIntoCurrentService() +// } +// +// let teamScores: [TeamScore] = try await services.get(identifier: identifier) +// if teamScores.isEmpty { +// store.teamScores.insertAllIntoCurrentService() +// } +// +// } catch { +// Logger.error(error) +// } +// +// } +// } +// +// } fileprivate static func _cleanLogs() { StoreCenter.main.resetLoggingCollections() From fc3708fbc3640e819c1835f0e981d527cf6c188e Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 6 Dec 2024 20:21:17 +0100 Subject: [PATCH 100/106] fix mobile number detection --- PadelClub/Extensions/String+Extensions.swift | 3 +-- .../Views/Calling/Components/PlayersWithoutContactView.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 37094b9..4655383 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -166,8 +166,7 @@ extension String { // MARK: - FFT Source Importing extension String { enum RegexStatic { - static let mobileNumber = /^0[6-7]/ - //static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/ + static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ } func isMobileNumber() -> Bool { diff --git a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift index 4e8d78e..cffae6e 100644 --- a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift +++ b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift @@ -31,7 +31,7 @@ struct PlayersWithoutContactView: View { } } - let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil }) + let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false }) DisclosureGroup { ForEach(withoutPhones) { player in NavigationLink { @@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View { LabeledContent { Text(withoutPhones.count.formatted()) } label: { - Text("Joueurs sans téléphone") + Text("Joueurs sans téléphone portable") } } } header: { From 607a5e65bd8e1b57d485042d29386a43df7a5221 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 6 Dec 2024 20:21:58 +0100 Subject: [PATCH 101/106] v1.0.37 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f9760af..97a0eb4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.37; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.37; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 2cf955f378354a07e9514830960c1875c32467f4 Mon Sep 17 00:00:00 2001 From: Raz Date: Sat, 7 Dec 2024 16:44:42 +0100 Subject: [PATCH 102/106] v1.0.38 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- PadelClub/Utils/PadelRule.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 97a0eb4..91e2516 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.37; + MARKETING_VERSION = 1.0.38; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.37; + MARKETING_VERSION = 1.0.38; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index e69e5ee..9a4e040 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -540,7 +540,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case .p25: switch count { case 9...12: - return [17, 13, 11, 9, 7, 5, 4, 3, 2, 1] + return [17, 15, 13, 11, 9, 7, 5, 4, 3, 2, 1] case 13...16: return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1] case 17...20: From afe7aedb29299dfc2de7fa5229c635ee468ba8a5 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 14:01:18 +0100 Subject: [PATCH 103/106] fix ranking (cherry picked from commit 3c0fa45153adea38a597b8a952569fbcfa873e06) --- PadelClub/Data/TeamRegistration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 14909a2..da189ec 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -466,7 +466,7 @@ final class TeamRegistration: ModelObject, Storable { self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in let predicates: [AreInIncreasingOrder] = [ { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, - { $0.rank ?? 0 < $1.rank ?? 0 }, + { $0.rank ?? Int.max < $1.rank ?? Int.max }, { $0.lastName < $1.lastName}, { $0.firstName < $1.firstName } ] From d5a1449b3f0bd631059fe5f0fd202166d6a6dcb0 Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 15:39:44 +0100 Subject: [PATCH 104/106] fix sharelink lag --- PadelClub.xcodeproj/project.pbxproj | 4 ++ .../Data/Federal/FederalTournament.swift | 2 +- PadelClub/Data/Tournament.swift | 4 +- PadelClub/Info.plist | 2 - PadelClub/Utils/PadelRule.swift | 10 +-- PadelClub/Utils/URLs.swift | 3 + .../ViewModel/FederalDataViewModel.swift | 2 +- .../Event/TournamentConfiguratorView.swift | 2 +- .../GroupStage/GroupStagesSettingsView.swift | 6 +- .../Agenda/TournamentLookUpView.swift | 4 +- .../Navigation/Toolbox/ToolboxView.swift | 35 +++++++--- PadelClub/Views/Round/RoundSettingsView.swift | 7 +- .../Shared/SelectablePlayerListView.swift | 4 +- .../Views/Shared/TournamentFilterView.swift | 2 +- .../TournamentLevelPickerView.swift | 2 +- .../Screen/InscriptionManagerView.swift | 68 ++++++++++++++++--- .../Shared/TournamentCellView.swift | 4 +- 17 files changed, 120 insertions(+), 41 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 91e2516..89222c8 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3434,6 +3434,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3477,6 +3478,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3521,6 +3523,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3562,6 +3565,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index e61f36c..003218e 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -239,7 +239,7 @@ struct CategorieAge: Codable { return FederalTournamentAge(rawValue: id) } if let libelle { - return FederalTournamentAge.allCases.first(where: { $0.localizedLabel().localizedCaseInsensitiveContains(libelle) }) + return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) } return nil } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 132db89..b55e575 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1494,7 +1494,7 @@ defer { return tournamentLevel.localizedLevelLabel(.title) } } - let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") + let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { @@ -2537,7 +2537,7 @@ extension Tournament: FederalTournamentHolder { func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { if isAnimation() { if displayAgeAndCategory(forBuild: build) == false { - return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") + return [build.category.localizedLabel(), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") } else if name != nil { return build.level.localizedLevelLabel(.title) } else { diff --git a/PadelClub/Info.plist b/PadelClub/Info.plist index 40756ec..7b68d4a 100644 --- a/PadelClub/Info.plist +++ b/PadelClub/Info.plist @@ -33,7 +33,5 @@ ITSAppUsesNonExemptEncryption - UIFileSharingEnabled - diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 9a4e040..4b21a3d 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } var identifier: String { - level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() + level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel() } func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { @@ -65,7 +65,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { - age.localizedLabel(displayStyle) + age.localizedFederalAgeLabel(displayStyle) } } @@ -252,7 +252,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } } - func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + func localizedFederalAgeLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" @@ -265,7 +265,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case .a17_18: return "17/18 ans" case .senior: - return "Senior" + return displayStyle == .short ? "" : "Senior" case .a45: return "+45 ans" case .a55: @@ -274,7 +274,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } var tournamentDescriptionLabel: String { - return localizedLabel() + return localizedFederalAgeLabel() } func isAgeValid(age: Int?) -> Bool { diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index 16e0fba..20e1013 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -50,6 +50,7 @@ enum URLs: String, Identifiable { } enum PageLink: String, Identifiable, CaseIterable { + case info = "Informations" case teams = "Équipes" case summons = "Convocations" case groupStages = "Poules" @@ -68,6 +69,8 @@ enum PageLink: String, Identifiable, CaseIterable { switch self { case .matches: return "" + case .info: + return "info" case .teams: return "teams" case .summons: diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index a5579b7..2ca3b2f 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -28,7 +28,7 @@ class FederalDataViewModel { var labels: [String] = [] labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) - labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) + labels.append(contentsOf: ageCategories.map { $0.localizedFederalAgeLabel() }.formatList()) let clubNames = selectedClubs.compactMap { codeClub in let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub }) return club?.clubTitle(.short) diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 9333ee9..30ec0f6 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -46,7 +46,7 @@ struct TournamentConfigurationView: View { } Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } LabeledContent { diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 3bf6939..6961bae 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -262,11 +262,15 @@ struct GroupStagesSettingsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + ShareLink(item: groupStagesPaste(), preview: .init("Données des poules")) } } } + func groupStagesPaste() -> TournamentGroupStageShareContent { + TournamentGroupStageShareContent(tournament: tournament) + } + var menuBuildAllGroupStages: some View { RowButtonView("Refaire les poules", role: .destructive) { diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 93e9aa6..783359c 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -369,7 +369,7 @@ struct TournamentLookUpView: View { NavigationLink { List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in - Text(type.localizedLabel()) + Text(type.localizedFederalAgeLabel()) } .navigationTitle("Limites d'âge") .environment(\.editMode, Binding.constant(EditMode.active)) @@ -381,7 +381,7 @@ struct TournamentLookUpView: View { Text("Tous les âges") .foregroundStyle(.secondary) } else { - Text(ages.map({ $0.localizedLabel()}).joined(separator: ", ")) + Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", ")) .foregroundStyle(.secondary) } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 114112d..487f80c 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -228,10 +228,9 @@ struct ToolboxView: View { ShareLink(item: URLs.appStore.url) { Label("Lien AppStore", systemImage: "link") } - if let zip = _getZip() { - ShareLink(item: zip) { - Label("Mes données", systemImage: "server.rack") - } + + ShareLink(item: ZipLog(), preview: .init("Mon archive")) { + Label("Mes données", systemImage: "server.rack") } } label: { Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) @@ -240,7 +239,14 @@ struct ToolboxView: View { } } } - +} + +//#Preview { +// ToolboxView() +//} + + +struct ZipLog: Transferable { private func _getZip() -> URL? { do { let filePath = try Club.storageDirectoryPath() @@ -250,8 +256,19 @@ struct ToolboxView: View { return nil } } -} -//#Preview { -// ToolboxView() -//} + func shareFile() -> URL? { + print("Generating URL...") + return _getZip() + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .zip) { transferable in + return SentTransferredFile(transferable.shareFile()!) + }.exportingCondition { $0.shareFile() != nil } + + ProxyRepresentation { transferable in + return transferable.shareFile()! + }.exportingCondition { $0.shareFile() != nil } + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 42a45b4..2928a49 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -147,11 +147,16 @@ struct RoundSettingsView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + ShareLink(item: roundsPaste(), preview: .init("Données du tableau")) } } } + func roundsPaste() -> TournamentRoundShareContent { + TournamentRoundShareContent(tournament: tournament) + } + + private func _removeAllSeeds() async { await tournament.removeAllSeeds() self.isEditingTournamentSeed.wrappedValue = true diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 00a4836..352251d 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -109,7 +109,7 @@ struct SelectablePlayerListView: View { Section { Picker(selection: $searchViewModel.selectedAgeCategory) { ForEach(FederalTournamentAge.allCases) { ageCategory in - Text(ageCategory.localizedLabel(.title)).tag(ageCategory) + Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory) } } label: { Text("Catégorie d'âge") @@ -142,7 +142,7 @@ struct SelectablePlayerListView: View { VStack(alignment: .trailing) { Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") if searchViewModel.selectedAgeCategory != .unlisted { - Text(searchViewModel.selectedAgeCategory.localizedLabel()).font(.caption) + Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption) } } } diff --git a/PadelClub/Views/Shared/TournamentFilterView.swift b/PadelClub/Views/Shared/TournamentFilterView.swift index 6af4d7c..3686aff 100644 --- a/PadelClub/Views/Shared/TournamentFilterView.swift +++ b/PadelClub/Views/Shared/TournamentFilterView.swift @@ -107,7 +107,7 @@ struct TournamentFilterView: View { } } } label: { - Text(category.localizedLabel(.title)) + Text(category.localizedFederalAgeLabel(.title)) } } } header: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index bc81b41..524a65a 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -42,7 +42,7 @@ struct TournamentLevelPickerView: View { Picker(selection: $tournament.federalTournamentAge, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } Picker(selection: $tournament.groupStageOrderingMode, label: Text("Répartition en poule")) { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index fc7c297..be075c8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -426,15 +426,11 @@ struct InscriptionManagerView: View { private func _sharingTeamsMenuView() -> some View { Menu { - if let teamPaste = teamPaste() { - ShareLink(item: teamPaste) { - Text("En texte") - } + ShareLink(item: teamPaste(), preview: .init("Inscriptions")) { + Text("En texte") } - if let teamPaste = teamPaste(.csv) { - ShareLink(item: teamPaste) { - Text("En csv") - } + ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) { + Text("En csv") } } label: { Label("Exporter les paires", systemImage: "square.and.arrow.up") @@ -449,8 +445,8 @@ struct InscriptionManagerView: View { tournament.unsortedTeamsWithoutWO() } - func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? { - tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat) + func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile { + TournamentShareFile(tournament: tournament, exportFormat: exportFormat) } var unsortedPlayers: [PlayerRegistration] { @@ -1015,3 +1011,55 @@ struct InscriptionManagerView: View { // .environment(Tournament.mock()) // } //} + +struct TournamentRoundShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentGroupStageShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentShareFile: Transferable { + let tournament: Tournament + let exportFormat: ExportFormat + + func shareFile() -> URL { + print("Generating URL...") + return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat) + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .utf8PlainText) { transferable in + return SentTransferredFile(transferable.shareFile()) + } + + ProxyRepresentation { transferable in + return transferable.shareFile() + } + } +} diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index c4426d7..e3b862d 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -107,7 +107,7 @@ struct TournamentCellView: View { if displayStyle == .wide, tournament.displayAgeAndCategory(forBuild: build) { VStack(alignment: .leading, spacing: 0) { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } .font(.caption) } @@ -155,7 +155,7 @@ struct TournamentCellView: View { } } else { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } } } From 107eabf8ff22942045fcfeb1e0aac4f9e273b7eb Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 16:10:24 +0100 Subject: [PATCH 105/106] fix unecessary count check of month player data --- PadelClub/Views/Navigation/MainView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index ee01859..62f9ceb 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -263,7 +263,7 @@ struct MainView: View { await _startImporting(importingDate: mostRecentDateImported) } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { await _startImporting(importingDate: mostRecentDateImported) - } else if current.incompleteMode == false || updated == 0 { + } else if updated == 0 { await _calculateMonthData(dataSource: current.monthKey) } } From 75a721179f746b53341fc30caac0abf36a7002be Mon Sep 17 00:00:00 2001 From: Raz Date: Fri, 13 Dec 2024 16:11:04 +0100 Subject: [PATCH 106/106] v1.0.39 --- PadelClub.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 89222c8..7cba62e 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3286,7 +3286,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.38; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3330,7 +3330,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.38; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";