diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index ab7dc72..9a82ca4 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1830,7 +1830,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1868,7 +1868,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 50c361d..de2babd 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -79,7 +79,7 @@ class GroupStage: ModelObject, Storable { var _matches = [Match]() for i in 0..<_numberOfMatchesToBuild() { - let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat) + let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat, name: localizedMatchUpLabel(for: i)) _matches.append(newMatch) } diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 4fd7b0b..ab0c4a3 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -12,7 +12,12 @@ import LeStorage class Match: ModelObject, Storable { static func resourceName() -> String { "matches" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - + + static func setServerTitle(upperRound: Round, matchIndex: Int) -> String { + if upperRound.index == 0 { return upperRound.roundTitle() } + return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted() + } + var byeState: Bool = false var id: String = Store.randomId() @@ -27,7 +32,7 @@ class Match: ModelObject, Storable { var winningTeamId: String? var losingTeamId: String? //var broadcasted: Bool - private var name: String? + var name: String? //var order: Int var disabled: Bool = false private(set) var courtIndex: Int? diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 801a988..28dacee 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -26,7 +26,8 @@ class MatchScheduler : ModelObject, Storable { var rotationDifferenceIsImportant: Bool var shouldHandleUpperRoundSlice: Bool var shouldEndRoundBeforeStartingNext: Bool - + var groupStageChunkCount: Int? + init(tournament: String, timeDifferenceLimit: Int = 5, loserBracketRotationDifference: Int = 0, @@ -36,7 +37,8 @@ class MatchScheduler : ModelObject, Storable { randomizeCourts: Bool = true, rotationDifferenceIsImportant: Bool = false, shouldHandleUpperRoundSlice: Bool = true, - shouldEndRoundBeforeStartingNext: Bool = true) { + shouldEndRoundBeforeStartingNext: Bool = true, + groupStageChunkCount: Int? = nil) { self.tournament = tournament self.timeDifferenceLimit = timeDifferenceLimit self.loserBracketRotationDifference = loserBracketRotationDifference @@ -47,6 +49,7 @@ class MatchScheduler : ModelObject, Storable { self.rotationDifferenceIsImportant = rotationDifferenceIsImportant self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext + self.groupStageChunkCount = groupStageChunkCount } enum CodingKeys: String, CodingKey { @@ -61,6 +64,7 @@ class MatchScheduler : ModelObject, Storable { case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant" case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice" case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext" + case _groupStageChunkCount = "groupStageChunkCount" } var courtsUnavailability: [DateInterval]? { @@ -77,7 +81,7 @@ class MatchScheduler : ModelObject, Storable { @discardableResult func updateGroupStageSchedule(tournament: Tournament) -> Date { - let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 + let computedGroupStageChunkCount = groupStageChunkCount ?? 1 let groupStages = tournament.groupStages() let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount @@ -88,7 +92,35 @@ class MatchScheduler : ModelObject, Storable { }) var lastDate : Date = tournament.startDate - groupStages.chunked(into: groupStageCourtCount).forEach { groups in + let times = Set(groupStages.compactMap { $0.startDate }).sorted() + + if let first = times.first { + if first.isEarlierThan(tournament.startDate) { + tournament.startDate = first + try? DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } + } + + times.forEach({ time in + lastDate = time + let groups = groupStages.filter({ $0.startDate == time }) + let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) + + dispatch.timedMatches.forEach { matchSchedule in + if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { + let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) + let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60 + if let startDate = match.groupStageObject?.startDate { + let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) + match.startDate = matchStartDate + lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60) + } + match.setCourt(matchSchedule.courtIndex) + } + } + }) + + groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in groups.forEach({ $0.startDate = lastDate }) try? DataStore.shared.groupStages.addOrUpdate(contentOfs: groups) diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index f5b38e8..8b9da7a 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -431,12 +431,8 @@ class Round: ModelObject, Storable { let matches = (0..) throws { @@ -295,6 +293,61 @@ class Tournament : ModelObject, Storable { case canceled } + func publishedTeamsDate() -> Date { + startDate + } + + func areTeamsPublished() -> Bool { + Date() >= startDate || publishTeams + } + + func areSummonsPublished() -> Bool { + Date() >= startDate || publishSummons + } + + func publishedGroupStagesDate() -> Date? { + if let first = groupStages().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atNine() { + if first.isEarlierThan(startDate) { + return startDate + } else { + return first + } + } else { + return nil + } + } + + func areGroupStagesPublished() -> Bool { + if publishGroupStages { return true } + if let publishedGroupStagesDate = publishedGroupStagesDate() { + return Date() >= publishedGroupStagesDate + } else { + return false + } + } + + func publishedBracketsDate() -> Date? { + if let first = rounds().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atNine() { + if first.isEarlierThan(startDate) { + return startDate + } else { + return first + } + } else { + return nil + } + } + + func areBracketsPublished() -> Bool { + if publishBrackets { return true } + if let publishedBracketsDate = publishedBracketsDate() { + return Date() >= publishedBracketsDate + } else { + return false + } + } + + func shareURL() -> URL? { return URLs.main.url.appending(path: "tournament/\(id)") } @@ -1096,7 +1149,7 @@ class Tournament : ModelObject, Storable { let matches = (0.. Tournament { - return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, groupStageCourtCount: nil, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil) + return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil) } } diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index a54c678..f62d8d4 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -11,7 +11,7 @@ enum URLs: String, Identifiable { case subscriptions = "https://apple.co/2Th4vqI" case main = "https://xlr.alwaysdata.net/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" - case padelClub = "https://padelclub.app" + //case padelClub = "https://padelclub.app" var id: String { return self.rawValue } diff --git a/PadelClub/ViewModel/NavigationViewModel.swift b/PadelClub/ViewModel/NavigationViewModel.swift index dfe589c..722f686 100644 --- a/PadelClub/ViewModel/NavigationViewModel.swift +++ b/PadelClub/ViewModel/NavigationViewModel.swift @@ -16,4 +16,21 @@ class NavigationViewModel { var selectedTab: TabDestination? var agendaDestination: AgendaDestination? = .activity var tournament: Tournament? + var organizerTournament: Tournament? + + func isTournamentAlreadyOpenInOrganizer(_ tournament: Tournament) -> Bool { + organizerTournament?.id == tournament.id + } + + func closeTournamentFromOrganizer(_ tournament: Tournament) { + tournament.navigationPath.removeAll() + organizerTournament = nil + } + + func openTournamentInOrganizer(_ tournament: Tournament) { + organizerTournament = tournament + if selectedTab != .tournamentOrganizer { + selectedTab = .tournamentOrganizer + } + } } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index dc5d794..cb9059d 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -69,7 +69,7 @@ class SearchViewModel: ObservableObject, Identifiable { } func codeClubs() -> [String] { - DataStore.shared.clubs.compactMap { $0.code } + DataStore.shared.user.clubsObjects().compactMap { $0.code } } func getCodeClub() -> String? { diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index a98034b..be2b761 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -27,21 +27,37 @@ struct ClubDetailView: View { var body: some View { Form { + + Section { + NavigationLink { + ClubSearchView(displayContext: .edition, club: club) + } label: { + Label("Chercher dans la base fédérale", systemImage: "magnifyingglass") + } + } footer: { + Text("Vous pouvez chercher un club dans la base fédérale et importer les informations directement.") + } + Section { - VStack(alignment: .leading, spacing: 0) { - Text("Nom du club").foregroundStyle(.secondary).font(.caption) + LabeledContent { TextField("Nom du club", text: $club.name) - .fixedSize() + .autocorrectionDisabled() + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) .focused($focusedField, equals: ._name) .submitLabel( displayContext == .addition ? .next : .done) .onSubmit { if club.acronym.isEmpty { club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) + focusedField = ._city } if displayContext == .addition { focusedField = ._acronym } } + } label: { + Text("Nom du club") } .onTapGesture { focusedField = ._name @@ -92,10 +108,12 @@ struct ClubDetailView: View { } if club.code == nil { - VStack(alignment: .leading, spacing: 0) { - Text("Ville").foregroundStyle(.secondary).font(.caption) + LabeledContent { TextField("Ville", text: $city) - .fixedSize() + .autocorrectionDisabled() + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) .focused($focusedField, equals: ._city) .submitLabel( displayContext == .addition ? .next : .done) .onSubmit { @@ -104,20 +122,26 @@ struct ClubDetailView: View { } club.city = city } + } label: { + Text("Ville") } .onTapGesture { focusedField = ._city } - VStack(alignment: .leading, spacing: 0) { - Text("Code Postal").foregroundStyle(.secondary).font(.caption) + LabeledContent { TextField("Code Postal", text: $zipCode) - .fixedSize() + .autocorrectionDisabled() + .keyboardType(.alphabet) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) .focused($focusedField, equals: ._zipCode) .submitLabel( displayContext == .addition ? .next : .done) .onSubmit { club.zipCode = zipCode } + } label: { + Text("Code Postal") } .onTapGesture { focusedField = ._zipCode diff --git a/PadelClub/Views/Club/CourtView.swift b/PadelClub/Views/Club/CourtView.swift index 38b992b..1835cf8 100644 --- a/PadelClub/Views/Club/CourtView.swift +++ b/PadelClub/Views/Club/CourtView.swift @@ -22,6 +22,7 @@ struct CourtView: View { Section { LabeledContent { TextField("Nom", text: $name) + .autocorrectionDisabled() .keyboardType(.alphabet) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index 61942c7..3011b04 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -103,7 +103,7 @@ struct EventListView: View { } .contextMenu { Button { - + navigation.openTournamentInOrganizer(tournament) } label: { Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") } diff --git a/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift b/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift index ff48983..7d7a54b 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift @@ -8,21 +8,15 @@ import SwiftUI struct TournamentButtonView: View { + @Environment(NavigationViewModel.self) private var navigation let tournament: Tournament - @Binding var selectedId: String? - + var body: some View { Button { - if selectedId == tournament.id { - tournament.navigationPath.removeAll() - selectedId = nil -// if tournament.navigationPath.isEmpty { -// selectedId = nil -// } else { -// tournament.navigationPath.removeLast() -// } + if navigation.isTournamentAlreadyOpenInOrganizer(tournament) { + navigation.closeTournamentFromOrganizer(tournament) } else { - selectedId = tournament.id + navigation.openTournamentInOrganizer(tournament) } } label: { TournamentCellView(tournament: tournament, displayStyle: .short) @@ -34,7 +28,7 @@ struct TournamentButtonView: View { .fixedSize(horizontal: false, vertical: true) } .overlay(alignment: .top) { - if selectedId == tournament.id { + if navigation.isTournamentAlreadyOpenInOrganizer(tournament) { Image(systemName: "ellipsis") .offset(y: -10) } diff --git a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift index 6c8f037..72f6afe 100644 --- a/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift +++ b/PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift @@ -10,17 +10,13 @@ import LeStorage struct TournamentOrganizerView: View { @EnvironmentObject var dataStore: DataStore - @State private var selectedTournamentId: String? - + @Environment(NavigationViewModel.self) private var navigation + var body: some View { VStack(spacing: 0) { - ForEach(dataStore.tournaments) { tournament in - if tournament.id == selectedTournamentId { - OrganizedTournamentView(tournament: tournament) - } - } - - if selectedTournamentId == nil { + if let tournament = navigation.organizerTournament { + OrganizedTournamentView(tournament: tournament) + } else { NavigationStack { let userClubsEmpty = dataStore.user.clubs.isEmpty ContentUnavailableView( @@ -39,7 +35,7 @@ struct TournamentOrganizerView: View { ScrollView(.horizontal) { HStack { ForEach(dataStore.tournaments) { tournament in - TournamentButtonView(tournament: tournament, selectedId: $selectedTournamentId) + TournamentButtonView(tournament: tournament) } } .padding() @@ -48,7 +44,7 @@ struct TournamentOrganizerView: View { } } .onChange(of: Store.main.currentUserUUID) { - selectedTournamentId = nil + navigation.organizerTournament = nil } } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index 75c22df..fde3f8c 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -13,7 +13,7 @@ struct PlanningSettingsView: View { @Bindable var tournament: Tournament @Bindable var matchScheduler: MatchScheduler - @State private var groupStageCourtCount: Int + @State private var groupStageChunkCount: Int @State private var isScheduling: Bool = false @State private var schedulingDone: Bool = false @State private var showOptions: Bool = false @@ -22,10 +22,11 @@ struct PlanningSettingsView: View { self.tournament = tournament if let matchScheduler = tournament.matchScheduler() { self.matchScheduler = matchScheduler + self._groupStageChunkCount = State(wrappedValue: matchScheduler.groupStageChunkCount ?? 1) } else { self.matchScheduler = MatchScheduler(tournament: tournament.id) + self._groupStageChunkCount = State(wrappedValue: 1) } - self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1) } var body: some View { @@ -48,7 +49,7 @@ struct PlanningSettingsView: View { TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) if tournament.groupStages().isEmpty == false { - TournamentFieldsManagerView(localizedStringKey: "Nombre de poule en même temps", count: $groupStageCourtCount, max: tournament.groupStageCount) + TournamentFieldsManagerView(localizedStringKey: "Nombre de poule en même temps", count: $groupStageChunkCount, max: tournament.groupStageCount) } if let event = tournament.eventObject() { @@ -59,28 +60,27 @@ struct PlanningSettingsView: View { Text("Préciser la disponibilité des terrains") } } + } footer: { + FooterButtonView((showOptions ? "masquer" : "voir") + " les réglages avancées") { + showOptions.toggle() + } + } + + if showOptions { + _optionsView() } Section { RowButtonView("Horaire intelligent", role: .destructive) { schedulingDone = false await _setupSchedule() + _save() schedulingDone = true } } footer: { - Button { - showOptions.toggle() - } label: { - Text((showOptions ? "masquer" : "voir") + " les réglages avancées") - .underline() - } - .buttonStyle(.borderless) + 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() } - if showOptions { - _optionsView() - } - Section { RowButtonView("Supprimer tous les horaires", role: .destructive) { do { @@ -101,6 +101,7 @@ struct PlanningSettingsView: View { } } } + .headerProminence(.increased) .onAppear { do { try dataStore.matchSchedulers.addOrUpdate(instance: matchScheduler) @@ -115,9 +116,8 @@ struct PlanningSettingsView: View { .deferredRendering(for: .seconds(2)) } } - .onChange(of: groupStageCourtCount) { - tournament.groupStageCourtCount = groupStageCourtCount - _save() + .onChange(of: groupStageChunkCount) { + matchScheduler.groupStageChunkCount = groupStageChunkCount } .onChange(of: tournament.startDate) { _save() @@ -125,9 +125,6 @@ struct PlanningSettingsView: View { .onChange(of: tournament.courtCount) { _save() } - .onChange(of: tournament.groupStageCourtCount) { - _save() - } .onChange(of: tournament.dayDuration) { _save() } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index f57f499..b3d5aff 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -42,7 +42,7 @@ struct RoundSettingsView: View { let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) let matches = (0..