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) + } + }