Laurent 2 years ago
commit dd7b6c8b34
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/GroupStage.swift
  3. 7
      PadelClub/Data/Match.swift
  4. 38
      PadelClub/Data/MatchScheduler.swift
  5. 6
      PadelClub/Data/Round.swift
  6. 83
      PadelClub/Data/Tournament.swift
  7. 2
      PadelClub/Utils/URLs.swift
  8. 17
      PadelClub/ViewModel/NavigationViewModel.swift
  9. 2
      PadelClub/ViewModel/SearchViewModel.swift
  10. 42
      PadelClub/Views/Club/ClubDetailView.swift
  11. 1
      PadelClub/Views/Club/CourtView.swift
  12. 2
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  13. 16
      PadelClub/Views/Navigation/Organizer/TournamentButtonView.swift
  14. 16
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  15. 37
      PadelClub/Views/Planning/PlanningSettingsView.swift
  16. 2
      PadelClub/Views/Round/RoundSettingsView.swift
  17. 28
      PadelClub/Views/Round/RoundView.swift
  18. 146
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  19. 7
      PadelClubTests/ServerDataTests.swift

@ -1830,7 +1830,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1868,7 +1868,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -79,7 +79,7 @@ class GroupStage: ModelObject, Storable {
var _matches = [Match]()
for i in 0..<_numberOfMatchesToBuild() {
let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat)
let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat, name: localizedMatchUpLabel(for: i))
_matches.append(newMatch)
}

@ -13,6 +13,11 @@ class Match: ModelObject, Storable {
static func resourceName() -> String { "matches" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func setServerTitle(upperRound: Round, matchIndex: Int) -> String {
if upperRound.index == 0 { return upperRound.roundTitle() }
return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted()
}
var byeState: Bool = false
var id: String = Store.randomId()
@ -27,7 +32,7 @@ class Match: ModelObject, Storable {
var winningTeamId: String?
var losingTeamId: String?
//var broadcasted: Bool
private var name: String?
var name: String?
//var order: Int
var disabled: Bool = false
private(set) var courtIndex: Int?

@ -26,6 +26,7 @@ class MatchScheduler : ModelObject, Storable {
var rotationDifferenceIsImportant: Bool
var shouldHandleUpperRoundSlice: Bool
var shouldEndRoundBeforeStartingNext: Bool
var groupStageChunkCount: Int?
init(tournament: String,
timeDifferenceLimit: Int = 5,
@ -36,7 +37,8 @@ class MatchScheduler : ModelObject, Storable {
randomizeCourts: Bool = true,
rotationDifferenceIsImportant: Bool = false,
shouldHandleUpperRoundSlice: Bool = true,
shouldEndRoundBeforeStartingNext: Bool = true) {
shouldEndRoundBeforeStartingNext: Bool = true,
groupStageChunkCount: Int? = nil) {
self.tournament = tournament
self.timeDifferenceLimit = timeDifferenceLimit
self.loserBracketRotationDifference = loserBracketRotationDifference
@ -47,6 +49,7 @@ class MatchScheduler : ModelObject, Storable {
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
self.groupStageChunkCount = groupStageChunkCount
}
enum CodingKeys: String, CodingKey {
@ -61,6 +64,7 @@ class MatchScheduler : ModelObject, Storable {
case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant"
case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice"
case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext"
case _groupStageChunkCount = "groupStageChunkCount"
}
var courtsUnavailability: [DateInterval]? {
@ -77,7 +81,7 @@ class MatchScheduler : ModelObject, Storable {
@discardableResult
func updateGroupStageSchedule(tournament: Tournament) -> Date {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let computedGroupStageChunkCount = groupStageChunkCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
@ -88,7 +92,35 @@ class MatchScheduler : ModelObject, Storable {
})
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
let times = Set(groupStages.compactMap { $0.startDate }).sorted()
if let first = times.first {
if first.isEarlierThan(tournament.startDate) {
tournament.startDate = first
try? DataStore.shared.tournaments.addOrUpdate(instance: tournament)
}
}
times.forEach({ time in
lastDate = time
let groups = groupStages.filter({ $0.startDate == time })
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex)
}
}
})
groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
try? DataStore.shared.groupStages.addOrUpdate(contentOfs: groups)

@ -431,13 +431,9 @@ class Round: ModelObject, Storable {
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat, name: round.roundTitle())
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in

@ -32,7 +32,6 @@ class Tournament : ModelObject, Storable {
var federalCategory: TournamentCategory
var federalLevelCategory: TournamentLevel
var federalAgeCategory: FederalTournamentAge
var groupStageCourtCount: Int?
var closedRegistrationDate: Date?
var groupStageAdditionalQualified: Int
var courtCount: Int = 2
@ -44,9 +43,8 @@ class Tournament : ModelObject, Storable {
var additionalEstimationDuration: Int = 0
var isDeleted: Bool = false
var isCanceled: Bool = false
var publishManually: Bool = false
var publishTeams: Bool = false
var publishWaitingList: Bool = false
//var publishWaitingList: Bool = false
var publishSummons: Bool = false
var publishGroupStages: Bool = false
var publishBrackets: Bool = false
@ -75,7 +73,6 @@ class Tournament : ModelObject, Storable {
case _federalCategory = "federalCategory"
case _federalLevelCategory = "federalLevelCategory"
case _federalAgeCategory = "federalAgeCategory"
case _groupStageCourtCount = "groupStageCourtCount"
case _seedCount = "seedCount"
case _closedRegistrationDate = "closedRegistrationDate"
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
@ -88,15 +85,14 @@ class Tournament : ModelObject, Storable {
case _isDeleted = "isDeleted"
case _isCanceled = "localId"
case _payment = "globalId"
case _publishManually = "publishManually"
case _publishTeams = "publishTeams"
case _publishWaitingList = "publishWaitingList"
//case _publishWaitingList = "publishWaitingList"
case _publishSummons = "publishSummons"
case _publishGroupStages = "publishGroupStages"
case _publishBrackets = "publishBrackets"
}
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishManually: Bool = false, publishTeams: Bool = false, publishWaitingList: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false) {
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false) {
self.event = event
self.name = name
self.startDate = startDate
@ -115,7 +111,6 @@ class Tournament : ModelObject, Storable {
self.federalCategory = federalCategory
self.federalLevelCategory = federalLevelCategory
self.federalAgeCategory = federalAgeCategory
self.groupStageCourtCount = groupStageCourtCount
self.closedRegistrationDate = closedRegistrationDate
self.groupStageAdditionalQualified = groupStageAdditionalQualified
self.courtCount = courtCount
@ -125,6 +120,10 @@ class Tournament : ModelObject, Storable {
self.entryFee = entryFee
self.additionalEstimationDuration = additionalEstimationDuration
self.isDeleted = isDeleted
self.publishTeams = publishTeams
self.publishSummons = publishSummons
self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages
}
required init(from decoder: Decoder) throws {
@ -148,7 +147,6 @@ class Tournament : ModelObject, Storable {
federalCategory = try container.decode(TournamentCategory.self, forKey: ._federalCategory)
federalLevelCategory = try container.decode(TournamentLevel.self, forKey: ._federalLevelCategory)
federalAgeCategory = try container.decode(FederalTournamentAge.self, forKey: ._federalAgeCategory)
groupStageCourtCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCourtCount)
closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate)
groupStageAdditionalQualified = try container.decode(Int.self, forKey: ._groupStageAdditionalQualified)
courtCount = try container.decode(Int.self, forKey: ._courtCount)
@ -160,9 +158,7 @@ class Tournament : ModelObject, Storable {
additionalEstimationDuration = try container.decode(Int.self, forKey: ._additionalEstimationDuration)
isDeleted = try container.decode(Bool.self, forKey: ._isDeleted)
isCanceled = try Tournament._decodeCanceled(container: container)
publishManually = try container.decodeIfPresent(Bool.self, forKey: ._publishManually) ?? false
publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false
publishWaitingList = try container.decodeIfPresent(Bool.self, forKey: ._publishWaitingList) ?? false
publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false
publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false
publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false
@ -222,7 +218,6 @@ class Tournament : ModelObject, Storable {
try container.encode(federalCategory, forKey: ._federalCategory)
try container.encode(federalLevelCategory, forKey: ._federalLevelCategory)
try container.encode(federalAgeCategory, forKey: ._federalAgeCategory)
try container.encodeIfPresent(groupStageCourtCount, forKey: ._groupStageCourtCount)
try container.encodeIfPresent(closedRegistrationDate, forKey: ._closedRegistrationDate)
try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
try container.encode(courtCount, forKey: ._courtCount)
@ -234,7 +229,10 @@ class Tournament : ModelObject, Storable {
try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration)
try container.encode(isDeleted, forKey: ._isDeleted)
try self._encodeIsCanceled(container: &container)
try container.encode(publishTeams, forKey: ._publishTeams)
try container.encode(publishSummons, forKey: ._publishSummons)
try container.encode(publishBrackets, forKey: ._publishBrackets)
try container.encode(publishGroupStages, forKey: ._publishGroupStages)
}
fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
@ -295,6 +293,61 @@ class Tournament : ModelObject, Storable {
case canceled
}
func publishedTeamsDate() -> Date {
startDate
}
func areTeamsPublished() -> Bool {
Date() >= startDate || publishTeams
}
func areSummonsPublished() -> Bool {
Date() >= startDate || publishSummons
}
func publishedGroupStagesDate() -> Date? {
if let first = groupStages().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atNine() {
if first.isEarlierThan(startDate) {
return startDate
} else {
return first
}
} else {
return nil
}
}
func areGroupStagesPublished() -> Bool {
if publishGroupStages { return true }
if let publishedGroupStagesDate = publishedGroupStagesDate() {
return Date() >= publishedGroupStagesDate
} else {
return false
}
}
func publishedBracketsDate() -> Date? {
if let first = rounds().flatMap({ $0.playedMatches() }).compactMap({ $0.startDate }).sorted().first?.atNine() {
if first.isEarlierThan(startDate) {
return startDate
} else {
return first
}
} else {
return nil
}
}
func areBracketsPublished() -> Bool {
if publishBrackets { return true }
if let publishedBracketsDate = publishedBracketsDate() {
return Date() >= publishedBracketsDate
} else {
return false
}
}
func shareURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)")
}
@ -1096,7 +1149,7 @@ class Tournament : ModelObject, Storable {
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: matchFormat)
return Match(round: round.id, index: $0, matchFormat: matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: RoundRule.matchIndexWithinRound(fromMatchIndex: $0)))
}
print(matches.map {
@ -1579,7 +1632,7 @@ extension Tournament {
}
static func fake() -> Tournament {
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, groupStageCourtCount: nil, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
}
}

@ -11,7 +11,7 @@ enum URLs: String, Identifiable {
case subscriptions = "https://apple.co/2Th4vqI"
case main = "https://xlr.alwaysdata.net/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
case padelClub = "https://padelclub.app"
//case padelClub = "https://padelclub.app"
var id: String { return self.rawValue }

@ -16,4 +16,21 @@ class NavigationViewModel {
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity
var tournament: Tournament?
var organizerTournament: Tournament?
func isTournamentAlreadyOpenInOrganizer(_ tournament: Tournament) -> Bool {
organizerTournament?.id == tournament.id
}
func closeTournamentFromOrganizer(_ tournament: Tournament) {
tournament.navigationPath.removeAll()
organizerTournament = nil
}
func openTournamentInOrganizer(_ tournament: Tournament) {
organizerTournament = tournament
if selectedTab != .tournamentOrganizer {
selectedTab = .tournamentOrganizer
}
}
}

@ -69,7 +69,7 @@ class SearchViewModel: ObservableObject, Identifiable {
}
func codeClubs() -> [String] {
DataStore.shared.clubs.compactMap { $0.code }
DataStore.shared.user.clubsObjects().compactMap { $0.code }
}
func getCodeClub() -> String? {

@ -27,21 +27,37 @@ struct ClubDetailView: View {
var body: some View {
Form {
Section {
NavigationLink {
ClubSearchView(displayContext: .edition, club: club)
} label: {
Label("Chercher dans la base fédérale", systemImage: "magnifyingglass")
}
} footer: {
Text("Vous pouvez chercher un club dans la base fédérale et importer les informations directement.")
}
Section {
VStack(alignment: .leading, spacing: 0) {
Text("Nom du club").foregroundStyle(.secondary).font(.caption)
LabeledContent {
TextField("Nom du club", text: $club.name)
.fixedSize()
.autocorrectionDisabled()
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._name)
.submitLabel( displayContext == .addition ? .next : .done)
.onSubmit {
if club.acronym.isEmpty {
club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
focusedField = ._city
}
if displayContext == .addition {
focusedField = ._acronym
}
}
} label: {
Text("Nom du club")
}
.onTapGesture {
focusedField = ._name
@ -92,10 +108,12 @@ struct ClubDetailView: View {
}
if club.code == nil {
VStack(alignment: .leading, spacing: 0) {
Text("Ville").foregroundStyle(.secondary).font(.caption)
LabeledContent {
TextField("Ville", text: $city)
.fixedSize()
.autocorrectionDisabled()
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._city)
.submitLabel( displayContext == .addition ? .next : .done)
.onSubmit {
@ -104,20 +122,26 @@ struct ClubDetailView: View {
}
club.city = city
}
} label: {
Text("Ville")
}
.onTapGesture {
focusedField = ._city
}
VStack(alignment: .leading, spacing: 0) {
Text("Code Postal").foregroundStyle(.secondary).font(.caption)
LabeledContent {
TextField("Code Postal", text: $zipCode)
.fixedSize()
.autocorrectionDisabled()
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._zipCode)
.submitLabel( displayContext == .addition ? .next : .done)
.onSubmit {
club.zipCode = zipCode
}
} label: {
Text("Code Postal")
}
.onTapGesture {
focusedField = ._zipCode

@ -22,6 +22,7 @@ struct CourtView: View {
Section {
LabeledContent {
TextField("Nom", text: $name)
.autocorrectionDisabled()
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)

@ -103,7 +103,7 @@ struct EventListView: View {
}
.contextMenu {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
}

@ -8,21 +8,15 @@
import SwiftUI
struct TournamentButtonView: View {
@Environment(NavigationViewModel.self) private var navigation
let tournament: Tournament
@Binding var selectedId: String?
var body: some View {
Button {
if selectedId == tournament.id {
tournament.navigationPath.removeAll()
selectedId = nil
// if tournament.navigationPath.isEmpty {
// selectedId = nil
// } else {
// tournament.navigationPath.removeLast()
// }
if navigation.isTournamentAlreadyOpenInOrganizer(tournament) {
navigation.closeTournamentFromOrganizer(tournament)
} else {
selectedId = tournament.id
navigation.openTournamentInOrganizer(tournament)
}
} label: {
TournamentCellView(tournament: tournament, displayStyle: .short)
@ -34,7 +28,7 @@ struct TournamentButtonView: View {
.fixedSize(horizontal: false, vertical: true)
}
.overlay(alignment: .top) {
if selectedId == tournament.id {
if navigation.isTournamentAlreadyOpenInOrganizer(tournament) {
Image(systemName: "ellipsis")
.offset(y: -10)
}

@ -10,17 +10,13 @@ import LeStorage
struct TournamentOrganizerView: View {
@EnvironmentObject var dataStore: DataStore
@State private var selectedTournamentId: String?
@Environment(NavigationViewModel.self) private var navigation
var body: some View {
VStack(spacing: 0) {
ForEach(dataStore.tournaments) { tournament in
if tournament.id == selectedTournamentId {
OrganizedTournamentView(tournament: tournament)
}
}
if selectedTournamentId == nil {
if let tournament = navigation.organizerTournament {
OrganizedTournamentView(tournament: tournament)
} else {
NavigationStack {
let userClubsEmpty = dataStore.user.clubs.isEmpty
ContentUnavailableView(
@ -39,7 +35,7 @@ struct TournamentOrganizerView: View {
ScrollView(.horizontal) {
HStack {
ForEach(dataStore.tournaments) { tournament in
TournamentButtonView(tournament: tournament, selectedId: $selectedTournamentId)
TournamentButtonView(tournament: tournament)
}
}
.padding()
@ -48,7 +44,7 @@ struct TournamentOrganizerView: View {
}
}
.onChange(of: Store.main.currentUserUUID) {
selectedTournamentId = nil
navigation.organizerTournament = nil
}
}
}

@ -13,7 +13,7 @@ struct PlanningSettingsView: View {
@Bindable var tournament: Tournament
@Bindable var matchScheduler: MatchScheduler
@State private var groupStageCourtCount: Int
@State private var groupStageChunkCount: Int
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
@State private var showOptions: Bool = false
@ -22,10 +22,11 @@ struct PlanningSettingsView: View {
self.tournament = tournament
if let matchScheduler = tournament.matchScheduler() {
self.matchScheduler = matchScheduler
self._groupStageChunkCount = State(wrappedValue: matchScheduler.groupStageChunkCount ?? 1)
} else {
self.matchScheduler = MatchScheduler(tournament: tournament.id)
self._groupStageChunkCount = State(wrappedValue: 1)
}
self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1)
}
var body: some View {
@ -48,7 +49,7 @@ struct PlanningSettingsView: View {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
if tournament.groupStages().isEmpty == false {
TournamentFieldsManagerView(localizedStringKey: "Nombre de poule en même temps", count: $groupStageCourtCount, max: tournament.groupStageCount)
TournamentFieldsManagerView(localizedStringKey: "Nombre de poule en même temps", count: $groupStageChunkCount, max: tournament.groupStageCount)
}
if let event = tournament.eventObject() {
@ -59,26 +60,25 @@ struct PlanningSettingsView: View {
Text("Préciser la disponibilité des terrains")
}
}
} footer: {
FooterButtonView((showOptions ? "masquer" : "voir") + " les réglages avancées") {
showOptions.toggle()
}
}
if showOptions {
_optionsView()
}
Section {
RowButtonView("Horaire intelligent", role: .destructive) {
schedulingDone = false
await _setupSchedule()
_save()
schedulingDone = true
}
} footer: {
Button {
showOptions.toggle()
} label: {
Text((showOptions ? "masquer" : "voir") + " les réglages avancées")
.underline()
}
.buttonStyle(.borderless)
}
if showOptions {
_optionsView()
Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline()
}
Section {
@ -101,6 +101,7 @@ struct PlanningSettingsView: View {
}
}
}
.headerProminence(.increased)
.onAppear {
do {
try dataStore.matchSchedulers.addOrUpdate(instance: matchScheduler)
@ -115,9 +116,8 @@ struct PlanningSettingsView: View {
.deferredRendering(for: .seconds(2))
}
}
.onChange(of: groupStageCourtCount) {
tournament.groupStageCourtCount = groupStageCourtCount
_save()
.onChange(of: groupStageChunkCount) {
matchScheduler.groupStageChunkCount = groupStageChunkCount
}
.onChange(of: tournament.startDate) {
_save()
@ -125,9 +125,6 @@ struct PlanningSettingsView: View {
.onChange(of: tournament.courtCount) {
_save()
}
.onChange(of: tournament.groupStageCourtCount) {
_save()
}
.onChange(of: tournament.dayDuration) {
_save()
}

@ -42,7 +42,7 @@ struct RoundSettingsView: View {
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let matches = (0..<matchCount).map { //0 is final match
return Match(round: round.id, index: $0 + matchStartIndex, matchFormat: round.matchFormat)
return Match(round: round.id, index: $0 + matchStartIndex, matchFormat: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: $0))
}
try? dataStore.rounds.addOrUpdate(instance: round)
try? dataStore.matches.addOrUpdate(contentOfs: matches)

@ -52,9 +52,10 @@ struct RoundView: View {
print(spaceLeft[drawResult.drawIndex].matchTitle())
availableQualifiedTeams[drawResult.drawee].setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true)
}
try? dataStore.matches.addOrUpdate(contentOfs: spaceLeft)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: availableQualifiedTeams)
isEditingTournamentSeed.wrappedValue.toggle()
_save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
}
}
} else if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) {
@ -62,8 +63,8 @@ struct RoundView: View {
Section {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup)
if tournament.availableSeeds().isEmpty {
_save()
_save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
}
@ -109,8 +110,10 @@ struct RoundView: View {
print(availableSeedSpot[drawResult.drawIndex].matchTitle())
seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: false)
}
try? dataStore.matches.addOrUpdate(contentOfs: availableSeedSpot)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
_save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false
}
}
}
}
@ -129,7 +132,16 @@ struct RoundView: View {
}
private func _save() {
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams())
//todo should be done server side
let rounds = tournament.rounds()
rounds.forEach { round in
let matches = round.playedMatches()
matches.forEach { match in
match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound())
}
}
let allRoundMatches = tournament.allRoundMatches()
try? DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches)
}

@ -30,7 +30,7 @@ struct BroadcastView: View {
List {
Section {
TipView(tournamentPublishingTip) { action in
UIApplication.shared.open(URLs.padelClub.url)
UIApplication.shared.open(URLs.main.url)
}
.tipStyle(tint: nil)
}
@ -39,67 +39,144 @@ struct BroadcastView: View {
.tipStyle(tint: nil)
}
if tournament.publishManually == false {
Section {
Text("En mode automatique, Padel Club détermine le meilleur moment pour publier les informations du tournoi.\n\n• La liste des équipes sera visible au maximum 2 jours après la clôture des inscriptions. À partir des p500, la liste d'attente à la clôture sera également publiée.\n\n• Les convocations ne seront visibles qu'à partir du premier jour du tournoi.\n\n• Les poules ne seront visibles qu'au premier jour des poules.\n\n• Le tableau ne sera visible qu'au premier jour du tableau.")
} header: {
Text("Mode Automatique")
} footer: {
FooterButtonView("désactiver le mode automatique") {
tournament.publishManually.toggle()
Section {
LabeledContent {
if tournament.areTeamsPublished() {
Image(systemName:"checkmark").foregroundStyle(.green)
} else {
Text(tournament.publishedTeamsDate().formatted())
}
} label: {
if tournament.areTeamsPublished() {
Text("Publiée")
} else {
Text("Publication prévue")
}
Text("Les horaires de convocations ne seront pas publiés")
}
} header: {
Text("Liste des équipes")
} footer: {
if Date() < tournament.publishedTeamsDate() {
HStack {
Spacer()
FooterButtonView(tournament.publishTeams ? "masquer sur le site" : "publier maintenant") {
tournament.publishTeams.toggle()
}
}
}
}
if tournament.publishManually == true {
Section {
Text("En mode manuel, vous devez indiquer à Padel Club le moment où vous souhaitez publier les informations du tournoi")
Toggle(isOn: $tournament.publishTeams) {
Text("Publier la liste des équipes")
Text("Les horaires de convocations ne seront pas publiés")
Section {
LabeledContent {
if tournament.areSummonsPublished() {
Image(systemName:"checkmark").foregroundStyle(.green)
} else {
Text(tournament.publishedTeamsDate().formatted())
}
Toggle(isOn: $tournament.publishWaitingList) {
Text("Inclure les équipes en liste d'attente")
} label: {
if tournament.areSummonsPublished() {
Text("Publiées")
} else {
Text("Publication prévue")
}
Toggle(isOn: $tournament.publishSummons) {
Text("Publier les convocations")
}
} header: {
Text("Convocations")
} footer: {
if Date() < tournament.publishedTeamsDate() {
HStack {
Spacer()
FooterButtonView(tournament.publishSummons ? "masquer sur le site" : "publier maintenant") {
tournament.publishSummons.toggle()
}
}
Toggle(isOn: $tournament.publishGroupStages) {
Text("Publier les poules")
}
}
if let publishedGroupStagesDate = tournament.publishedGroupStagesDate() {
Section {
let areGroupStagesPublished = tournament.areGroupStagesPublished()
LabeledContent {
if areGroupStagesPublished {
Image(systemName:"checkmark").foregroundStyle(.green)
} else {
Text(publishedGroupStagesDate.formatted())
}
} label: {
if areGroupStagesPublished {
Text("Publiées")
} else {
Text("Publication prévue")
}
}
Toggle(isOn: $tournament.publishBrackets) {
Text("Publier le tableau")
} header: {
Text("Poules")
} footer: {
if Date() < publishedGroupStagesDate {
HStack {
Spacer()
FooterButtonView(tournament.publishGroupStages ? "masquer sur le site" : "publier maintenant") {
tournament.publishGroupStages.toggle()
}
}
}
}
}
if let publishedBracketsDate = tournament.publishedBracketsDate() {
Section {
let areBracketsPublished = tournament.areBracketsPublished()
LabeledContent {
if areBracketsPublished {
Image(systemName:"checkmark").foregroundStyle(.green)
} else {
Text(publishedBracketsDate.formatted())
}
} label: {
if areBracketsPublished {
Text("Publié")
} else {
Text("Publication prévue")
}
}
} header: {
Text("Mode Manuel")
Text("Tableau")
} footer: {
FooterButtonView("activer le mode automatique") {
tournament.publishManually.toggle()
if Date() < publishedBracketsDate {
HStack {
Spacer()
FooterButtonView(tournament.publishBrackets ? "masquer sur le site" : "publier maintenant") {
tournament.publishBrackets.toggle()
}
}
}
}
}
//todo waitinglist & info
Section {
Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privé")
}
} footer: {
Text("Le tournoi sera masqué sur le site \(URLs.main.rawValue)")
let footerString = "Le tournoi sera masqué sur le site [Padel Club](\(URLs.main.rawValue))"
Text(.init(footerString))
}
Section {
LabeledContent {
actionForURL(URLs.main.url)
} label: {
Text("Lien Padel Club")
Text("Padel Club")
}
if let club = tournament.club(), let clubURL = club.shareURL() {
LabeledContent {
actionForURL(clubURL)
} label: {
Text("Lien du club")
Text("Club")
}
}
@ -107,7 +184,7 @@ struct BroadcastView: View {
LabeledContent {
actionForURL(url)
} label: {
Text("Lien du tournoi")
Text("Tournoi")
}
}
@ -115,7 +192,7 @@ struct BroadcastView: View {
LabeledContent {
actionForURL(url)
} label: {
Text("Lien TV")
Text("TV")
}
}
@ -139,7 +216,7 @@ struct BroadcastView: View {
UIPasteboard.general.string = urlToShow
}
}
.onChange(of: tournament.isPrivate) {
.onChange(of: [tournament.isPrivate, tournament.publishTeams, tournament.publishSummons, tournament.publishBrackets, tournament.publishGroupStages]) {
_save()
}
}
@ -185,8 +262,7 @@ struct BroadcastView: View {
} label: {
HStack {
Spacer()
Text("lien")
.underline()
Image(systemName: "square.and.arrow.up")
}
}
.frame(maxWidth: .infinity)

@ -96,7 +96,7 @@ final class ServerDataTests: XCTestCase {
return
}
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, groupStageCourtCount: 6, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true)
let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true)
let t = try await Store.main.service().post(tournament)
assert(t.event == tournament.event)
@ -117,7 +117,6 @@ final class ServerDataTests: XCTestCase {
assert(t.federalCategory == tournament.federalCategory)
assert(t.federalLevelCategory == tournament.federalLevelCategory)
assert(t.federalAgeCategory == tournament.federalAgeCategory)
assert(t.groupStageCourtCount == tournament.groupStageCourtCount)
assert(t.closedRegistrationDate?.formatted() == tournament.closedRegistrationDate?.formatted())
assert(t.groupStageAdditionalQualified == tournament.groupStageAdditionalQualified)
assert(t.courtCount == tournament.courtCount)
@ -127,6 +126,10 @@ final class ServerDataTests: XCTestCase {
assert(t.entryFee == tournament.entryFee)
assert(t.additionalEstimationDuration == tournament.additionalEstimationDuration)
assert(t.isDeleted == tournament.isDeleted)
assert(t.publishTeams == tournament.publishTeams)
assert(t.publishSummons == tournament.publishSummons)
assert(t.publishGroupStages == tournament.publishGroupStages)
assert(t.publishBrackets == tournament.publishBrackets)
}

Loading…
Cancel
Save