Laurent 1 year ago
commit 28b2d79d2a
  1. 44
      PadelClub.xcodeproj/project.pbxproj
  2. 103
      PadelClub/Data/DrawLog.swift
  3. 25
      PadelClub/Data/Match.swift
  4. 19
      PadelClub/Data/MatchScheduler.swift
  5. 56
      PadelClub/Data/TeamRegistration.swift
  6. 148
      PadelClub/Data/Tournament.swift
  7. 2
      PadelClub/Data/TournamentStore.swift
  8. 9
      PadelClub/Utils/PadelRule.swift
  9. 58
      PadelClub/Views/Components/FortuneWheelView.swift
  10. 12
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  11. 180
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  12. 69
      PadelClub/Views/Round/DrawLogsView.swift
  13. 107
      PadelClub/Views/Round/PreviewBracketPositionView.swift
  14. 90
      PadelClub/Views/Round/RoundSettingsView.swift
  15. 156
      PadelClub/Views/Round/RoundView.swift
  16. 101
      PadelClub/Views/Team/TeamRestingView.swift
  17. 138
      PadelClub/Views/Team/TeamRowView.swift
  18. 12
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  19. 18
      PadelClubTests/ServerDataTests.swift

@ -424,6 +424,15 @@
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 */; };
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 */; };
@ -1066,6 +1075,9 @@
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; };
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; };
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; };
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; };
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; };
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -1356,6 +1368,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */,
);
@ -1871,6 +1884,8 @@
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
);
path = Round;
sourceTree = "<group>";
@ -2331,6 +2346,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 +2407,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 */,
@ -2442,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 */,
@ -2605,6 +2623,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 +2684,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 */,
@ -2716,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 */,
@ -2858,6 +2879,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 +2940,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 */,
@ -2969,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 */,
@ -3174,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\"";
@ -3198,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 = "";
@ -3219,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;
@ -3242,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 = "";
@ -3358,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 = "";
@ -3401,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 = "";
@ -3423,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\"";
@ -3445,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 = "";
@ -3465,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;
@ -3486,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 = "";

@ -0,0 +1,103 @@
//
// 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 drawMatchIndex: Int
var 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.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 {
return "Tête de série #\(drawSeed + 1)"
}
func localizedDrawLogLabel() -> String {
return [localizedDrawSeedLabel(), positionLabel()].joined(separator: " -> ")
}
func localizedDrawBranch() -> String {
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 {
return drawMatch()?.roundAndMatchTitle() ?? ""
}
func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
}
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 _drawMatchIndex = "drawMatchIndex"
case _drawTeamPosition = "drawTeamPosition"
}
func insertOnServer() throws {
self.tournamentStore.drawLogs.writeChangeAndInsertOnServer(instance: self)
}
}

@ -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
}
@ -247,6 +233,7 @@ defer {
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
}
func resetScores() {
@ -548,7 +535,7 @@ defer {
if endDate == nil {
endDate = Date()
}
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState()
@ -571,6 +558,8 @@ defer {
teamOne?.hasArrived()
teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id
@ -937,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"

@ -493,8 +493,16 @@ 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 }
@ -651,7 +659,12 @@ 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

@ -116,13 +116,39 @@ 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() {
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)
}
}
func expectedSummonDate() -> Date? {
@ -532,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 {
@ -544,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"

@ -1,5 +1,5 @@
//
// Tournament.swift
// swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
@ -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
@ -335,6 +347,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()
@ -552,7 +570,7 @@ defer {
return endDate != nil
}
func state() -> Tournament.State {
func state() -> State {
if self.isCanceled == true {
return .canceled
}
@ -1948,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
}
@ -2257,6 +2275,128 @@ defer {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
}
func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
func drawLogs() -> [DrawLog] {
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..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
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 {

@ -26,6 +26,7 @@ class TournamentStore: Store, ObservableObject {
fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder()
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder()
fileprivate(set) var drawLogs: StoredCollection<DrawLog> = 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: synchronized, indexed: indexed)
self.loadCollectionsFromServerIfNoFile()

@ -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 {

@ -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)

@ -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)

@ -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,34 +119,99 @@ 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)
}
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
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)
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
}
}
}
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)
}
} footer: {
FooterButtonView("jour entier") {
startDate = startDate.startOfDay
endDate = startDate.endOfDay()
}
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
}
}
.toolbar {
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 {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View {
Logger.error(error)
}
}
showingPopover = false
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau")
.tint(.master)
}
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle(_navigationTitle())
.tint(.master)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
}
}
let title: String
@Binding var selection: Int
let maxCourt: Int
struct DateAdjusterView: View {
@Binding var date: Date
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
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)
}
}
//#Preview {
// CourtAvailabilitySettingsView(event: Event.mock())
//}
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)
}
}

@ -0,0 +1,69 @@
//
// 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
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)
}
}
}
}
.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)
}
Divider()
Button("Tout effacer", role: .destructive) {
do {
try tournament.tournamentStore.drawLogs.deleteAll()
} catch {
Logger.error(error)
}
}
} label: {
LabelOptions()
}
}
})
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Journal des tirages")
}
}

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

@ -74,6 +74,32 @@ struct RoundSettingsView: View {
}
}
let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft()
Section {
NavigationLink {
DrawLogsView()
.environment(tournament)
} label: {
Text("Gestionnaire des tirages au sort")
}
if previewAvailable {
NavigationLink {
PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs())
} label: {
Text("Aperçu du repositionnement")
}
RowButtonView("Replacer toutes les têtes de série", role: .destructive) {
await tournament.updateSeedsBracketPosition()
}
}
} footer: {
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 {
// RowButtonView("Enabled", role: .destructive) {
// let allMatches = tournament._allMatchesIncludingDisabled()
@ -127,72 +153,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..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = self.tournament.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
await tournament.addNewRound(roundIndex)
}
private func _removeRound(_ lastRound: Round) async {

@ -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
@ -37,14 +38,14 @@ struct RoundView: View {
let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in
match.teamScores.count == 1
}
}.filter({ $0.isValidSpot() })
}
private var seedSpaceLeft: [Match] {
let displayableMatches: [Match] = self.upperRound.round.playedMatches()
return displayableMatches.filter { match in
match.teamScores.count == 0
}
}.filter({ $0.isValidSpot() })
}
private var availableSeedGroup: SeedInterval? {
@ -121,6 +122,7 @@ struct RoundView: View {
}
}
} else {
let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index)
let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -140,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
}
@ -152,87 +160,46 @@ 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)
}
}
@ -259,11 +226,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 {
@ -306,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)
@ -399,18 +366,45 @@ struct RoundView: View {
}
}
}
}
//#Preview {
// RoundView(round: Round.mock())
// .environment(Tournament.mock())
//}
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)
}
}
}
}
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 }
}

@ -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..<tournament.courtCount, id: \.self) { courtIndex in
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex as Int?)
switch displayMode {
case .teams:
if sortedTeams.isEmpty == false {
ForEach(sortedTeams) { team in
TeamRowView(team: team, displayRestingTime: true)
}
} else {
ContentUnavailableView("Aucune équipe en repos", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
}
} label: {
Text("Sur le terrain")
}
//
// Toggle(isOn: $checkCanPlay) {
// if isFree {
// Text("Vérifier le paiement ou la présence")
// } else {
// Text("Vérifier la présence")
// }
// }
// } footer: {
// if isFree {
// Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé")
// } else {
// Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé")
// }
}
Section {
if sortedMatches.isEmpty == false {
ForEach(sortedMatches) { match in
MatchRowView(match: match, matchViewStyle: .followUpStyle, updatedField: selectedCourt)
case .restingTime:
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()))
}
} else {
ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel()))
}
} header: {
Picker(selection: $sortingMode) {
ForEach(sortingModeCases) { sortingMode in
Picker(selection: $displayMode) {
ForEach(DisplayMode.allCases) { sortingMode in
Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode)
}
} label: {
Text("Méthode de tri")
Text("Affichage")
}
.labelsHidden()
.pickerStyle(.segmented)
}
.headerProminence(.increased)
.textCase(nil)
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Match à suivre")
.navigationTitle("Temps de repos")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.onAppear {
let allMatches = tournament.allMatches()
let matchesLeft = tournament.matchesLeft(allMatches)
@ -121,6 +91,7 @@ struct TeamRestingView: View {
let readyMatches = tournament.readyMatches(allMatches)
self.readyMatches = tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)
}
}
}

@ -12,66 +12,114 @@ struct TeamRowView: View {
var team: TeamRegistration
var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false
var displayRestingTime: Bool = false
var body: some View {
LabeledContent {
TeamWeightView(team: team, teamPosition: teamPosition)
} label: {
VStack(alignment: .leading) {
HStack {
if let groupStage = team.groupStageObject() {
HStack {
Text(groupStage.groupStageTitle(.title))
if let finalPosition = groupStage.finalPosition(ofTeam: team) {
Text((finalPosition + 1).ordinalFormatted())
}
}
} else if let round = team.initialRound() {
Text(round.roundTitle(.wide))
}
TeamHeadlineView(team: team)
TeamView(team: team)
}
if displayCallDate {
TeamCallDateView(team: team)
}
if displayRestingTime {
TeamRestingView(team: team)
}
}
}
if let wildcardLabel = team.wildcardLabel() {
Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption)
}
struct TeamRestingView: View {
let team: TeamRegistration
@ViewBuilder
var body: some View {
if 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 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)
}
}
struct TeamView: View {
let team: TeamRegistration
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 {
if team.players().isEmpty == false {
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1).truncationMode(.tail)
CompactTeamView(team: team)
}
} else {
if team.players().isEmpty == false {
CompactTeamView(team: team)
} else {
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)
}
}
}
}

@ -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 {

@ -370,4 +370,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)
}
}

Loading…
Cancel
Save