From 230181fe31bd5201d0d8c00c07063503cb952a79 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 23 Mar 2024 13:50:38 +0100 Subject: [PATCH] match in progress --- PadelClub.xcodeproj/project.pbxproj | 68 ++++ PadelClub/Data/DataStore.swift | 12 + PadelClub/Data/GroupStage.swift | 108 +++++ PadelClub/Data/Match.swift | 107 +++++ PadelClub/Data/MockData.swift | 6 + PadelClub/Data/PlayerRegistration.swift | 46 +++ PadelClub/Data/Round.swift | 53 +++ PadelClub/Data/TeamRegistration.swift | 68 ++++ PadelClub/Data/TeamScore.swift | 52 +++ PadelClub/Data/Tournament.swift | 158 +++++++- PadelClub/Manager/DisplayContext.swift | 8 + PadelClub/Views/Components/Labels.swift | 26 ++ .../Views/GroupStage/GroupStageView.swift | 376 ++++++++++++++++++ .../Views/GroupStage/GroupStagesView.swift | 304 ++++++++++++++ PadelClub/Views/Match/MatchDetailView.swift | 23 ++ PadelClub/Views/Match/MatchRowView.swift | 35 ++ PadelClub/Views/Match/MatchSummaryView.swift | 24 ++ .../Navigation/Agenda/ActivityView.swift | 3 +- .../Organizer/OrganizedTournamentView.swift | 21 +- .../Screen/TableStructureView.swift | 19 +- .../Shared/TournamentCellView.swift | 8 +- .../Views/Tournament/TournamentInitView.swift | 4 +- .../Tournament/TournamentRunningView.swift | 30 ++ .../Views/Tournament/TournamentView.swift | 31 +- 24 files changed, 1532 insertions(+), 58 deletions(-) create mode 100644 PadelClub/Data/GroupStage.swift create mode 100644 PadelClub/Data/Match.swift create mode 100644 PadelClub/Data/PlayerRegistration.swift create mode 100644 PadelClub/Data/Round.swift create mode 100644 PadelClub/Data/TeamRegistration.swift create mode 100644 PadelClub/Data/TeamScore.swift create mode 100644 PadelClub/Views/Components/Labels.swift create mode 100644 PadelClub/Views/GroupStage/GroupStageView.swift create mode 100644 PadelClub/Views/GroupStage/GroupStagesView.swift create mode 100644 PadelClub/Views/Match/MatchDetailView.swift create mode 100644 PadelClub/Views/Match/MatchRowView.swift create mode 100644 PadelClub/Views/Match/MatchSummaryView.swift create mode 100644 PadelClub/Views/Tournament/TournamentRunningView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index f8ad0e0..b0d8ad0 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -81,6 +81,19 @@ FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; + FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CE72BAEC70100A9A3BD /* GroupStage.swift */; }; + FF967CEC2BAECB9900A9A3BD /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CEB2BAECB9900A9A3BD /* Match.swift */; }; + FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CED2BAECBD700A9A3BD /* Round.swift */; }; + FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */; }; + FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */; }; + FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */; }; + FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */; }; + FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CF72BAEDF0000A9A3BD /* Labels.swift */; }; + FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */; }; + FF967CFD2BAEE5F500A9A3BD /* GroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */; }; + FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */; }; + FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */; }; + FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; @@ -220,6 +233,19 @@ FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = ""; }; FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = ""; }; FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = ""; }; + FF967CE72BAEC70100A9A3BD /* GroupStage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStage.swift; sourceTree = ""; }; + FF967CEB2BAECB9900A9A3BD /* Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Match.swift; sourceTree = ""; }; + FF967CED2BAECBD700A9A3BD /* Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Round.swift; sourceTree = ""; }; + FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamScore.swift; sourceTree = ""; }; + FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRegistration.swift; sourceTree = ""; }; + FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRegistration.swift; sourceTree = ""; }; + FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentRunningView.swift; sourceTree = ""; }; + FF967CF72BAEDF0000A9A3BD /* Labels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Labels.swift; sourceTree = ""; }; + FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageView.swift; sourceTree = ""; }; + FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStagesView.swift; sourceTree = ""; }; + FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchRowView.swift; sourceTree = ""; }; + FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchSummaryView.swift; sourceTree = ""; }; + FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDetailView.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -349,6 +375,12 @@ C4A47D752B73787D00ADC637 /* Migration */, C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */, C4A47D592B6D383C00ADC637 /* Tournament.swift */, + FF967CE72BAEC70100A9A3BD /* GroupStage.swift */, + FF967CED2BAECBD700A9A3BD /* Round.swift */, + FF967CEB2BAECB9900A9A3BD /* Match.swift */, + FF967CF12BAECC0B00A9A3BD /* PlayerRegistration.swift */, + FF967CF02BAECC0B00A9A3BD /* TeamRegistration.swift */, + FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */, FF8F263E2BAD7D5C00650388 /* Event.swift */, FF1DC5522BAB354A00FD8220 /* MockData.swift */, @@ -366,6 +398,8 @@ FF39719B2B8DE04B004C4E75 /* Navigation */, FF8F26392BAD526A00650388 /* Event */, FF1DC54D2BAB34FA00FD8220 /* Club */, + FF967CF92BAEE11500A9A3BD /* GroupStage */, + FF967CFE2BAEEF5A00A9A3BD /* Match */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -415,6 +449,7 @@ children = ( C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */, FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */, + FF967CF72BAEDF0000A9A3BD /* Labels.swift */, ); path = Components; sourceTree = ""; @@ -449,6 +484,7 @@ children = ( FF70916B2B91005400AB08DA /* TournamentView.swift */, FF8F26402BADFC8700650388 /* TournamentInitView.swift */, + FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */, FF3F74F92B91A018004CFE0E /* Screen */, FF3F74F82B919FB2004CFE0E /* Shared */, ); @@ -572,6 +608,25 @@ path = Components; sourceTree = ""; }; + FF967CF92BAEE11500A9A3BD /* GroupStage */ = { + isa = PBXGroup; + children = ( + FF967CFA2BAEE13800A9A3BD /* GroupStageView.swift */, + FF967CFB2BAEE13900A9A3BD /* GroupStagesView.swift */, + ); + path = GroupStage; + sourceTree = ""; + }; + FF967CFE2BAEEF5A00A9A3BD /* Match */ = { + isa = PBXGroup; + children = ( + FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */, + FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */, + FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */, + ); + path = Match; + sourceTree = ""; + }; FFD783FB2B91B919000F62A6 /* Agenda */ = { isa = PBXGroup; children = ( @@ -786,6 +841,7 @@ FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */, FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, + FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */, @@ -796,15 +852,20 @@ FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, + FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */, FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */, + FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, + FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, + FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */, FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */, FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */, C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */, C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */, FF3795662B9399AA004EA093 /* Persistence.swift in Sources */, + FF967CEC2BAECB9900A9A3BD /* Match.swift in Sources */, FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */, C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */, FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, @@ -814,6 +875,7 @@ FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, + FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, @@ -830,6 +892,7 @@ FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, + FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, @@ -840,18 +903,23 @@ C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */, + FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, + FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, + FF967CFD2BAEE5F500A9A3BD /* GroupStageView.swift in Sources */, FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */, FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */, FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */, FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */, + FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */, + FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */, FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */, C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */, diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 23fd265..fa6db7c 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -18,6 +18,12 @@ class DataStore: ObservableObject { fileprivate(set) var tournaments: StoredCollection fileprivate(set) var clubs: StoredCollection fileprivate(set) var events: StoredCollection + fileprivate(set) var groupStages: StoredCollection + fileprivate(set) var matches: StoredCollection + fileprivate(set) var teamRegistrations: StoredCollection + fileprivate(set) var playerRegistrations: StoredCollection + fileprivate(set) var rounds: StoredCollection + fileprivate(set) var teamScores: StoredCollection fileprivate var _userStorage: OptionalStorage = OptionalStorage(fileName: "user.json") @@ -40,6 +46,12 @@ class DataStore: ObservableObject { self.clubs = store.registerCollection(synchronized: false) self.tournaments = store.registerCollection(synchronized: false) self.events = store.registerCollection(synchronized: false) + self.groupStages = store.registerCollection(synchronized: false) + self.teamScores = store.registerCollection(synchronized: false) + self.teamRegistrations = store.registerCollection(synchronized: false) + self.playerRegistrations = store.registerCollection(synchronized: false) + self.rounds = store.registerCollection(synchronized: false) + self.matches = store.registerCollection(synchronized: false) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift new file mode 100644 index 0000000..35f0040 --- /dev/null +++ b/PadelClub/Data/GroupStage.swift @@ -0,0 +1,108 @@ +// +// GroupStage_v2.swift +// Padel Tournament +// +// Created by razmig on 10/03/2024. +// + +import Foundation +import LeStorage + +@Observable +class GroupStage: ModelObject, Storable { + static func resourceName() -> String { "group-stages" } + + var id: String = Store.randomId() + var tournament: String + var index: Int + var size: Int + var format: Int? + var startDate: Date? + + var matchFormat: MatchFormat { + get { + MatchFormat(rawValue: format ?? 0) ?? .defaultFormatForMatchType(.groupStage) + } + set { + format = newValue.rawValue + } + } + + internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil) { + self.tournament = tournament + self.index = index + self.size = size + self.format = matchFormat?.rawValue + self.startDate = startDate + } + + func title(_ displayStyle: DisplayStyle = .wide) -> String { + switch displayStyle { + case .wide: + return "Poule \(index + 1)" + case .short: + return "#\(index + 1)" + } + } + + func isBroadcasted() -> Bool { + false + } + + func isRunning() -> Bool { // at least a match has started + matches.anySatisfy({ $0.isRunning() }) + } + + func hasStarted() -> Bool { // meaning at least one match is over + matches.filter { $0.hasEnded() }.isEmpty == false + } + + func hasEnded() -> Bool { + if matches.isEmpty { return false } + return matches.allSatisfy { $0.hasEnded() } + } + + func buildMatches() { + removeMatches() + + var _matches = [Match]() + for i in 0.. String { "matches" } + + var id: String = Store.randomId() + var round: String? + var groupStage: String? + var startDate: Date? + var endDate: Date? + var index: Int + var format: Int? + var court: String? + var servingTeamId: String? + var winningTeamId: String? + var losingTeamId: String? + var broadcasted: Bool + var name: String? + var order: Int + + internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) { + self.round = round + self.groupStage = groupStage + self.startDate = startDate + self.endDate = endDate + self.index = index + self.format = matchFormat?.rawValue + self.court = court + self.servingTeamId = servingTeamId + self.winningTeamId = winningTeamId + self.losingTeamId = losingTeamId + self.broadcasted = broadcasted + self.name = name + self.order = order + } + + func isRunning() -> Bool { // at least a match has started + hasStarted() && hasEnded() == false + } + + func hasStarted() -> Bool { // meaning at least one match is over + if let startDate { + return startDate.timeIntervalSinceNow < 0 + } + if hasEnded() { + return true + } + return false + + //todo scores +// if let score { +// return score.hasEnded == false && score.sets.isEmpty == false +// } else { +// return false +// } + } + + func hasEnded() -> Bool { + endDate != nil + } + + var roundObject: Round? { + Store.main.filter { $0.id == self.round }.first + } + + var groupStageObject: GroupStage? { + Store.main.filter { $0.id == self.groupStage }.first + } + + var isLoserBracket: Bool { + roundObject?.loser != nil + } + + var teamScores: [TeamScore] { + Store.main.filter { $0.match == self.id } + } + + override func deleteDependencies() throws { + try Store.main.deleteDependencies(items: self.teamScores) + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _round = "round" + case _groupStage = "groupStage" + case _startDate = "startDate" + case _endDate = "endDate" + case _index = "index" + case _format = "format" + case _court = "court" + case _servingTeamId = "servingTeamId" + case _winningTeamId = "winningTeamId" + case _losingTeamId = "losingTeamId" + case _broadcasted = "broadcasted" + case _name = "name" + case _order = "order" + } +} diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index a385ec4..37a06c7 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -26,3 +26,9 @@ extension Tournament { Tournament(groupStageSortMode: .snake, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior) } } + +extension Match { + static func mock() -> Match { + Match(index: 0, broadcasted: false, order: 0) + } +} diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift new file mode 100644 index 0000000..8c45cad --- /dev/null +++ b/PadelClub/Data/PlayerRegistration.swift @@ -0,0 +1,46 @@ +// +// PlayerRegistration.swift +// Padel Tournament +// +// Created by razmig on 10/03/2024. +// + +import Foundation +import LeStorage + +@Observable +class PlayerRegistration: ModelObject, Storable { + static func resourceName() -> String { "player-registrations" } + + var id: String = Store.randomId() + var teamRegistration: String + var firstName: String + var lastName: String + var licenceId: String? + var rank: Int? + var hasPaid: Bool + var unranked: Bool + + internal init(teamRegistration: String, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, hasPaid: Bool, unranked: Bool) { + self.teamRegistration = teamRegistration + self.firstName = firstName + self.lastName = lastName + self.licenceId = licenceId + self.rank = rank + self.hasPaid = hasPaid + self.unranked = unranked + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _teamRegistration = "teamRegistration" + case _firstName = "firstName" + case _lastName = "lastName" + case _licenceId = "licenceId" + case _rank = "rank" + case _hasPaid = "hasPaid" + case _unranked = "unranked" + + + } +} diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift new file mode 100644 index 0000000..6f7c859 --- /dev/null +++ b/PadelClub/Data/Round.swift @@ -0,0 +1,53 @@ +// +// Round_v2.swift +// Padel Tournament +// +// Created by razmig on 10/03/2024. +// + +import Foundation +import LeStorage + +@Observable +class Round: ModelObject, Storable { + static func resourceName() -> String { "rounds" } + + var id: String = Store.randomId() + var tournament: String + var index: Int + var loser: String? + var format: Int? + + internal init(id: String = Store.randomId(), tournament: String, index: Int, loser: String? = nil, format: Int? = nil) { + self.id = id + self.tournament = tournament + self.index = index + self.loser = loser + self.format = format + } + + var matches: [Match] { + Store.main.filter { $0.round == self.id } + } + + + var loserRound: Round? { + guard let loser else { return nil } + return Store.main.findById(loser) + } + + override func deleteDependencies() throws { + try Store.main.deleteDependencies(items: self.matches) + if let loserRound { + try Store.main.deleteDependencies(items: [loserRound]) + } + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _index = "index" + case _loser = "loser" + case _format = "format" + } +} diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift new file mode 100644 index 0000000..23e6820 --- /dev/null +++ b/PadelClub/Data/TeamRegistration.swift @@ -0,0 +1,68 @@ +// +// TeamRegistration.swift +// Padel Tournament +// +// Created by razmig on 10/03/2024. +// + +import Foundation +import LeStorage + +@Observable +class TeamRegistration: ModelObject, Storable { + static func resourceName() -> String { "team-registrations" } + + var id: String = Store.randomId() + var tournament: String + var groupStage: String? + var registrationDate: Date? + var callDate: Date? + var bracketPosition: Int? + var groupStagePosition: Int? + var comment: String? + var source: String? + var sourceValue: String? + var logo: String? + var name: String? + + internal init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil) { + self.tournament = tournament + self.groupStage = groupStage + self.registrationDate = registrationDate + self.callDate = callDate + self.bracketPosition = bracketPosition + self.groupStagePosition = groupStagePosition + self.comment = comment + self.source = source + self.sourceValue = sourceValue + self.logo = logo + self.name = name + } + + func qualified() -> Bool { + groupStagePosition != nil && bracketPosition != nil + } + + var playerRegistrations: [PlayerRegistration] { + Store.main.filter { $0.teamRegistration == self.id } + } + + override func deleteDependencies() throws { + try Store.main.deleteDependencies(items: self.playerRegistrations) + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _groupStage = "groupStage" + case _registrationDate = "registrationDate" + case _callDate = "callDate" + case _bracketPosition = "bracketPosition" + case _groupStagePosition = "groupStagePosition" + case _comment = "comment" + case _source = "source" + case _sourceValue = "sourceValue" + case _logo = "logo" + case _name = "name" + } +} diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift new file mode 100644 index 0000000..03c120b --- /dev/null +++ b/PadelClub/Data/TeamScore.swift @@ -0,0 +1,52 @@ +// +// TeamScore.swift +// Padel Tournament +// +// Created by razmig on 10/03/2024. +// + +import Foundation +import LeStorage + +@Observable +class TeamScore: ModelObject, Storable { + + static func resourceName() -> String { "team-scores" } + + var id: String = Store.randomId() + var match: String + var teamRegistration: String? + var playerRegistrations: [String]? + var score: String? + var walkOut: Int? + var luckyLoser: Bool + + internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool) { + self.match = match + self.teamRegistration = teamRegistration + self.playerRegistrations = playerRegistrations + self.score = score + self.walkOut = walkOut + self.luckyLoser = luckyLoser + } + + + var team: TeamRegistration? { + guard let teamRegistration else { + return nil + } + return DataStore.shared.teamRegistrations.findById(teamRegistration) + + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _match = "match" + case _teamRegistration = "teamRegistration" + case _playerRegistrations = "playerRegistrations" + case _score = "score" + case _walkOut = "walkOut" + case _luckyLoser = "luckyLoser" + } + +} diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index a2bd0d0..f51a966 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -78,7 +78,27 @@ class Tournament : ModelObject, Storable { self.teamsPerGroupStage = teamsPerGroupStage self.entryFee = entryFee } + + enum State { + case initial + case build + } + func state() -> Tournament.State { + if groupStageCount > 0 && groupStages.isEmpty == false { + return .build + } + return .initial + } + + var groupStages: [GroupStage] { + Store.main.filter { $0.tournament == self.id }.sorted(by: \.index) + } + + var teamRegistrations: [TeamRegistration] { + Store.main.filter { $0.tournament == self.id } + } + var rounds: Int { 4 } @@ -87,6 +107,10 @@ class Tournament : ModelObject, Storable { [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ") } + func subtitle(_ displayStyle: DisplayStyle = .wide) -> String { + name ?? "" + } + func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide: @@ -96,6 +120,53 @@ class Tournament : ModelObject, Storable { } } + func qualifiedFromGroupStage() -> Int { + groupStageCount * qualifiedPerGroupStage + } + + + func qualifiedTeams() -> [TeamRegistration] { + teamRegistrations.filter({ $0.qualified() }) + } + + func moreQualifiedToDraw() -> Int { + max(qualifiedTeams().count - (qualifiedFromGroupStage() + groupStageAdditionalQualified), 0) + } + + func missingQualifiedFromGroupStages() -> [TeamRegistration] { + if groupStageAdditionalQualified > 0 { + return groupStages.filter { $0.hasEnded() }.compactMap { groupStage in + groupStage.teams[qualifiedPerGroupStage] + } + .filter({ $0.qualified() == false }) + } else { + return [] + } + } + + func groupStagesAreOver() -> Bool { + guard groupStages.isEmpty == false else { + return true + } + return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified + } + + + func groupStageStatus() -> String { + let runningGroupStages = groupStages.filter({ $0.isRunning() }) + if groupStagesAreOver() { return "terminées" } + if runningGroupStages.isEmpty { + + let ongoingGroupStages = runningGroupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }) + if ongoingGroupStages.isEmpty == false { + return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" + } + return groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix + } else { + return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours" + } + } + func settingsDescriptionLocalizedLabel() -> String { [dayDuration.formatted() + " jour\(dayDuration.pluralSuffix)", courtCount.formatted() + " terrain\(courtCount.pluralSuffix)"].joined(separator: ", ") } @@ -105,15 +176,84 @@ class Tournament : ModelObject, Storable { return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") } -} + func buildStructure() { + buildGroupStages() + } + + func buildGroupStages() { + groupStages.forEach { groupStage in + try? DataStore.shared.groupStages.delete(instance: groupStage) + } + + var _groupStages = [GroupStage]() + for index in 0.. 0 { +// entrant.resetBracketPosition() +// } +// } +// + if groupStageCount > 0 { + switch groupStageOrderingMode { + case .random: + setBrackets(randomize: true) + case .snake: + setBrackets(randomize: false) + case .swiss: + setBrackets(randomize: true) + } + } + } + + func setBrackets(randomize: Bool) { + let numberOfBracketsAsInt = groupStages.count +// let teamsPerBracket = Int(teamsPerBracket) + if groupStageCount != numberOfBracketsAsInt { + buildGroupStages() + return + } + let max = groupStages.map { $0.size }.reduce(0,+) +// var chunks = orderedEntries.filter { $0.wcFinalTable == false }.suffix(Int(max)).chunked(into: numberOfBracketsAsInt) +// for (index, _) in chunks.enumerated() { +// if randomize { +// chunks[index].shuffle() +// } else if index % 2 != 0 { +// chunks[index].reverse() +// } +// +// print("Equipes \(chunks[index].map { $0.initialRank })") +// for (jIndex, _) in chunks[index].enumerated() { +// print("Position \(index+1) Poule \(orderedBrackets[jIndex].index)") +// chunks[index][jIndex].bracketPosition = orderedBrackets[jIndex].index +// chunks[index][jIndex].bracketPositions = [Int(index + 1)] +// } +// } + } + -extension Tournament { func isFree() -> Bool { entryFee == nil || entryFee == 0 } -} -extension Tournament { var teamSortingType: TeamSortingType { get { TeamSortingType(rawValue: teamSorting) ?? tournamentLevel.defaultTeamSortingType @@ -255,16 +395,6 @@ extension Tournament { } } -extension Tournament { - func state() -> Tournament.State { - .initial - } - - enum State { - case initial - } -} - extension Tournament: Hashable { static func == (lhs: Tournament, rhs: Tournament) -> Bool { lhs.id == rhs.id diff --git a/PadelClub/Manager/DisplayContext.swift b/PadelClub/Manager/DisplayContext.swift index 1a22a0d..7ca52be 100644 --- a/PadelClub/Manager/DisplayContext.swift +++ b/PadelClub/Manager/DisplayContext.swift @@ -16,3 +16,11 @@ enum DisplayStyle { case wide case short } + +enum MatchViewStyle { + case standardStyle // vue normal + case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche + case feedStyle // vue programmation + case plainStyle // vue detail + case tournamentResultStyle //vue resultat tournoi +} diff --git a/PadelClub/Views/Components/Labels.swift b/PadelClub/Views/Components/Labels.swift new file mode 100644 index 0000000..807a275 --- /dev/null +++ b/PadelClub/Views/Components/Labels.swift @@ -0,0 +1,26 @@ +// +// Labels.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct LabelOptions: View { + var body: some View { + Label("Options", systemImage: "ellipsis.circle") + } +} + +struct LabelStructure: View { + var body: some View { + Label("Structure", systemImage: "hammer") + } +} + +struct LabelSettings: View { + var body: some View { + Label("Réglages", systemImage: "slider.horizontal.3") + } +} diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift new file mode 100644 index 0000000..8cc69c5 --- /dev/null +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -0,0 +1,376 @@ +// +// GroupStageView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 02/03/2023. +// + +import SwiftUI + +struct GroupStageView: View { + @Bindable var groupStage: GroupStage +// @State private var selectedMenuLink: MenuLink? +// @State private var canUpdateTournament: Bool = false +// @AppStorage("showLongLabel") private var showLongLabel: Bool = false +// @AppStorage("hideRank") private var hideRank: Bool = false + @State private var confirmGroupStageStart: Bool = false + + enum MenuLink: Int, Identifiable, Hashable { + var id: Int { self.rawValue } + case prepare + } + + var groupStageView: some View { + ForEach(0..<(groupStage.size), id: \.self) { index in +// let entrant : Entrant? = runningGroupStageOrderedByScore ? groupStage.orderedByScore[Int(index)] : groupStage.entrantAtIndex(Int(index)) +// if let entrant { +// GroupStageEntrantMenuView(entrant: entrant, groupStage: groupStage, index: index.intValue) +// } else { + Menu { +// Section { +// EntrantPickerView(groupStage: groupStage, index: Int(index)) +// } +// +// if let tournament = groupStage.tournament, let deltaLabel = tournament.deltaLabel(index.intValue, groupStageIndex: groupStage.index.intValue) { +// let date = tournament.localizedDate ?? "" +// Divider() +// Section { +// ShareLink(item: "\(tournament.localizedTitle)\n\(date)\nCherche une équipe dont le poids d'équipe " + deltaLabel) { +// Text(deltaLabel) +// } +// } header: { +// Text("Remplacer avec un poids d'équipe") +// } +// } + } label: { + HStack { + Text("#\(index+1)") + Text("Aucune équipe") + } + } +// } + } + } + + var body: some View { + Section { + groupStageView +// .disabled(canUpdateTournament == false) +// .sheet(item: $selectedMenuLink) { selectedMenuLink in +// switch selectedMenuLink { +// case .prepare: +// PrepareGroupStageView(groupStage: groupStage) +// } +// } + } header: { + HStack { + if groupStage.isBroadcasted() { + Label(groupStage.title(), systemImage: "airplayvideo") + } else { + Text(groupStage.title()) + } + Spacer() + if let startDate = groupStage.startDate { + Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) + } + } + } footer: { + HStack { + if groupStage.matches.isEmpty { + Button { + //groupStage.startGroupStage() + //save() + } label: { + Text("Créer les matchs") + } + .buttonStyle(.borderless) + } + Spacer() + Menu { +// Button { +// selectedMenuLink = .prepare +// } label: { +// Label("Préparer", systemImage: "calendar") +// } +// +// Menu { +// MenuWarnView(warningSender: groupStage) +// } label: { +// Label("Prévenir", systemImage: "person.crop.circle") +// } +// +// if groupStage.isBroadcasted() { +// Button { +// groupStage.refreshBroadcast() +// } label: { +// Label("Rafraîchir", systemImage: "arrow.up.circle.fill") +// } +// Button { +// groupStage.stopBroadcast() +// save() +// } label: { +// Label("Arrêter la diffusion", systemImage: "stop.circle.fill") +// } +// } else if groupStage.tournament?.canBroadcast() == true { +// Button { +// Task { +// try? await groupStage.broadcastGroupStage() +// save() +// } +// } label: { +// Label("Diffuser", systemImage: "airplayvideo") +// } +// } +// +// Divider() +// if groupStage.tournament?.canBroadcast() == true { +// Menu { +// Button { +// Task { +// try? await groupStage.broadcastGroupStageMatches() +// save() +// } +// } label: { +// Label("Diffuser", systemImage: "airplayvideo") +// } +// +// Button { +// groupStage.refreshBroadcastMatches() +// } label: { +// Label("Rafraîchir", systemImage: "arrow.up.circle.fill") +// } +// Button { +// groupStage.stopBroadcastMatches() +// save() +// } label: { +// Label("Arrêter la diffusion", systemImage: "stop.circle.fill") +// } +// } label: { +// Text("Diffusion des matchs") +// } +// } +// +// Divider() +// Menu { +// if groupStage.orderedMatches.isEmpty == false { +// Button(role: .destructive) { +// groupStage.startGroupStage() +// save() +// } label: { +// Label("Re-démarrer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") +// } +// } +// +// if groupStage.orderedMatches.isEmpty == false { +// Button(role: .destructive) { +// groupStage.removeMatches() +// save() +// } label: { +// Label("Supprimer les matchs de la \(groupStage.titleLabel.lowercased())", systemImage: "trash") +// } +// } +// +// Button(role: .destructive) { +// groupStage.tournament?.completeEntries.filter { $0.groupStagePosition == groupStage.index }.forEach { $0.resetGroupStagePosition() } +// groupStage.tournament?.removeFromGroupStages(groupStage) +// groupStage.tournament?.numberOfGroupStages -= 1 +// save() +// } label: { +// Label("Supprimer la \(groupStage.titleLabel.lowercased())", systemImage: "trash") +// } +// } label: { +// Text("Éditer") +// } + } label: { + HStack { + Spacer() + Label("Options", systemImage: "ellipsis.circle").labelStyle(.titleOnly) + } + } + .buttonStyle(.borderless) + } + } +// .onAppear { +// if let tournament = groupStage.tournament { +// canUpdateTournament = PersistenceController.shared.container.canUpdateRecord(forManagedObjectWith: tournament.objectID) +// } else { +// canUpdateTournament = true +// } +// } + } + +// func save() { +// do { +// groupStage.objectWillChange.send() +// groupStage.tournament?.orderedGroupStages.forEach { $0.objectWillChange.send() } +// groupStage.tournament?.objectWillChange.send() +// +// try viewContext.save() +// } catch { +// // Replace this implementation with code to handle the error appropriately. +// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// } +} + + +//struct GroupStageEntrantMenuView: View { +// @ObservedObject var entrant: Entrant +// @ObservedObject var groupStage: GroupStage +// @Environment(\.managedObjectContext) private var viewContext +// @AppStorage("showLongLabel") private var showLongLabel: Bool = false +// @AppStorage("hideRank") private var hideRank: Bool = false +// +// let index: Int +// +// var body: some View { +// Menu { +// ForEach(entrant.orderedPlayers) { player in +// Menu { +// Text(player.formattedRank) +// Text(player.localizedAge) +// if let computedClubName = player.computedClubName { +// Text(computedClubName) +// } +// } label: { +// Text(player.longLabel) +// } +// } +// +// if groupStage.tournament?.isOver == false { +// if entrant.qualified == false { +// Divider() +// Button { +// entrant.addToQualifiedGroup() +// entrant.objectWillChange.send() +// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() } +// entrant.currentTournament?.objectWillChange.send() +// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() } +// save() +// } label: { +// Label("Qualifier l'équipe", systemImage: "checkmark") +// } +// } +// +// Divider() +// if entrant.qualified { +// Menu { +// Button(role: .destructive) { +// entrant.unqualified() +// entrant.objectWillChange.send() +// entrant.orderedGroupStages.forEach { $0.objectWillChange.send() } +// entrant.currentTournament?.objectWillChange.send() +// entrant.currentTournament?.orderedMatches.forEach { $0.objectWillChange.send() } +// save() +// } label: { +// Label("Annuler la qualification", systemImage: "xmark") +// } +// } label: { +// Text("Qualification") +// } +// } +// +// Menu { +// if let deltaLabel = groupStage.tournament?.deltaLabel(index, groupStageIndex: groupStage.index.intValue) { +// Section { +// Button(role: .destructive) { +// entrant.resetGroupStagePosition() +// save() +// } label: { +// Text(deltaLabel) +// } +// Divider() +// } header: { +// Text("Toute l'équipe, poids: " + entrant.updatedRank.formatted()) +// } +// } +// +// ForEach(entrant.orderedPlayers) { player in +// if let deltaLabel = groupStage.tournament?.playerDeltaLabel(rank: entrant.otherPlayer(player)?.currentRank, positionInGroupStage: index, groupStageIndex: groupStage.index.intValue) { +// Section { +// Button(role: .destructive) { +// entrant.team?.removeFromPlayers(player) +// save() +// } label: { +// Text(deltaLabel) +// } +// Divider() +// } header: { +// Text(player.longLabel + ", rang: " + player.formattedRank) +// } +// } +// } +// } label: { +// Text("Remplacement") +// } +// +// Menu { +// Button(role: .destructive) { +// entrant.resetGroupStagePosition() +// save() +// } label: { +// Label("Retirer l'équipe", systemImage: "xmark") +// } +// } label: { +// Text("Retirer") +// } +// +// } +// } label: { +// HStack(alignment: .center) { +// if let tournament = groupStage.tournament, groupStage.hasEnded, groupStage.groupStageRound > 0 { +// Text("#\(index + Int((groupStage.index - tournament.numberOfGroupStages)*tournament.teamsPerGroupStage) + 1)") +// } else { +// Text("#\(index + 1)") +// } +// VStack(alignment: .leading, spacing: 0) { +// if hideRank == false { +// Text("Poids \(entrant.updatedRank)") +// .font(.caption) +// } +// +// HStack { +// if let brand = entrant.team?.brand?.title { +// Text(brand) +// } else { +// +// VStack(alignment: .leading) { +// Text(entrant.longLabelPlayerOne) +// Text(entrant.longLabelPlayerTwo) +// } +// +// } +// +// if groupStage.tournament?.isRoundSwissTournament() == true { +// if entrant.groupStagePosition == groupStage.index { +// Text("forcé") +// } else { +// Text("auto") +// } +// } else { +// if entrant.qualified { +// Image(systemName: "checkmark.seal") +// } +// } +// } +// } +// Spacer() +// Text(groupStage.scoreLabel(for: entrant.position(in: groupStage))) +// } +// } +// .buttonStyle(.plain) +// } +// +// func save() { +// do { +// try viewContext.save() +// } catch { +// // Replace this implementation with code to handle the error appropriately. +// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// } +//} diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift new file mode 100644 index 0000000..7ac0d7d --- /dev/null +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -0,0 +1,304 @@ +// +// GroupStagesView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 11/12/2023. +// + +import SwiftUI + +struct GroupStagesView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var selectedGroupStageIndex: Int = -1 + @State private var startAllGroupStageConfirmation: Bool = false + @State private var confirmGroupStageRebuild: Bool = false + + func displayGroupStage(_ groupStage: GroupStage) -> Bool { + selectedGroupStageIndex == groupStage.index || selectedGroupStageIndex == -1 + } + + var dynamicTitle: String { + switch selectedGroupStageIndex { + case -1: + return "Toutes les poules" + default: + return tournament.groupStages[selectedGroupStageIndex].title() + } + } +// +// init(tournament: Tournament) { +// _tournament = ObservedObject(wrappedValue: tournament) +// if let index = tournament.orderedGroupStages.firstIndex(where: { $0.isRunning }) { +// _selectedGroupStageIndex = State(wrappedValue: index.int64Value + 1) +// } +// } + + var body: some View { + List { + if tournament.missingQualifiedFromGroupStages().isEmpty == false && tournament.qualifiedTeams().count >= tournament.qualifiedFromGroupStage() && tournament.groupStageAdditionalQualified > 0 { + NavigationLink { + //DrawView(tournament: tournament) + } label: { + LabeledContent { + Text(tournament.moreQualifiedToDraw().formatted() + "/" + tournament.groupStageAdditionalQualified.formatted()) + } label: { + Text("Qualifié\(tournament.groupStageAdditionalQualified.pluralSuffix) supplémentaire\(tournament.groupStageAdditionalQualified.pluralSuffix)") + let message = [tournament.groupStageAdditionalQualified.formatted(), " meilleur", tournament.groupStageAdditionalQualified.pluralSuffix, " ", (tournament.qualifiedPerGroupStage + 1).ordinalFormatted()].joined() + Text(message) + } + } + } + +// if (tournament.groupStagesAreWrong || (tournament.emptySlotInGroupStages > 0 && tournament.entriesCount >= tournament.teamsFromGroupStages)) { +// Section { +// RowButtonView(title: "Reconstruire les poules") { +// confirmGroupStageRebuild = true +// } +// .modify { +// if UIDevice.current.userInterfaceIdiom == .pad { +// $0.alert("Êtes-vous sûr ?", isPresented: $confirmGroupStageRebuild) { +// +// Button(role: .destructive) { +// tournament.refreshGroupStages() +// save() +// } label: { +// Text("Reconstruire") +// } +// +// +// Button(role: .cancel) { +// +// } label: { +// Text("Annuler") +// } +// } message: { +// Text("Attention, cela peut modifier les poules existants.") +// +// } +// } else { +// $0.confirmationDialog("Êtes-vous sûr ?", isPresented: $confirmGroupStageRebuild) { +// Button(role: .destructive) { +// tournament.refreshGroupStages() +// save() +// } label: { +// Text("Reconstruire") +// } +// } message: { +// Text("Attention, cela peut modifier les poules existants.") +// } +// } +// } +// } header: { +// Text("Erreur détectée") +// } +// } +// + +// if tournament.isRoundSwissTournament() == false && (tournament.orderedGroupStages.allSatisfy({ $0.orderedMatches.count > 0 }) == false && tournament.groupStagesAreOver == false && tournament.groupStagesCount > 0) { +// Section { +// RowButtonView(title: "Générer les matchs de poules") { +// startAllGroupStageConfirmation = true +// } +// .modify { +// if UIDevice.current.userInterfaceIdiom == .pad { +// $0.alert("Êtes-vous sûr ?", isPresented: $startAllGroupStageConfirmation) { +// Button("Générer") { +// tournament.orderedGroupStages.forEach { +// if $0.orderedMatches.isEmpty { +// $0.startGroupStage() +// } +// } +// save() +// } +// Button(role: .cancel) { +// +// } label: { +// Text("Annuler") +// } +// } +// +// } else { +// $0.confirmationDialog("Êtes-vous sûr ?", isPresented: $startAllGroupStageConfirmation) { +// Button("Générer") { +// tournament.orderedGroupStages.forEach { +// if $0.orderedMatches.isEmpty { +// $0.startGroupStage() +// } +// } +// save() +// } +// } +// } +// } +// } +// } +// + if tournament.groupStagesAreOver() == false { +// Section { +// GroupStageMatchAvailableToStartView(tournament: tournament, groupStageIndex: selectedGroupStageIndex) +// } header: { +// if selectedGroupStageIndex == -1 { +// Text("Matchs de poules prêt à démarrer") +// } else { +// Text("Matchs de la poule \(selectedGroupStageIndex) prêt à démarrer") +// } +// } footer: { +// Text("présence d'au moins 2 équipes d'une même poule ayant réglé.") +// } + } + +// if tournament.teamsPerGroupStage == 3 && tournament.qualifiedPerGroupStage == 1 && tournament.numberOfGroupStages%2 == 0 && tournament.moreQualifiedFromGroupStages == 0 { +// Section { +// NavigationLink { +// GroupStageMissingMatchView(tournament: tournament) +// } label: { +// Text("Matchs de classement de poules") +// } +// } +// } + + ForEach(tournament.groupStages) { groupStage in + if displayGroupStage(groupStage) && groupStage.hasEnded() == false { + GroupStageView(groupStage: groupStage) + if groupStage.matches.isEmpty == false { + Section { + ForEach(groupStage.matches) { match in + MatchRowView(setupSeedContext: false, matchViewStyle: .sectionedStandardStyle) + .environment(match) + } + } header: { + Text("Matchs de la " + groupStage.title()) + } + } + } + } + } + .toolbar { + ToolbarItem(placement: .principal) { + if tournament.groupStages.count < 6 { + Picker(selection: $selectedGroupStageIndex) { + Text("Toutes").tag(-1) + ForEach(tournament.groupStages) { groupStage in + Text(groupStage.title(.short)).tag(groupStage.index) + } + } label: { + + } + .labelsHidden() + .pickerStyle(.segmented) + } else if tournament.groupStages.count < 8 { + Picker(selection: $selectedGroupStageIndex) { + Image(systemName: "square.stack").tag(-1) + ForEach(tournament.groupStages) { groupStage in + Text(groupStage.title(.short)).tag(groupStage.index) + } + } label: { + + } + .labelsHidden() + .pickerStyle(.segmented) + } else { + Picker(selection: $selectedGroupStageIndex) { + Text("Voir toutes les poules").tag(-1) + ForEach(tournament.groupStages) { groupStage in + Text(groupStage.title()).tag(groupStage.index) + } + } label: { + Text("\(tournament.groupStages.count.formatted()) poules") + } + } + } + + ToolbarItem(placement: .topBarTrailing) { + Menu { +// menuAddGroupStage +// menuBuildAllGroupStages +// menuGenerateGroupStage(.random) +// menuGenerateGroupStage(.snake) +// menuGenerateGroupStage(.swiss) + } label: { + LabelOptions() + } + } + } + } +// +// var menuBuildAllGroupStages: some View { +// Button(role: .destructive) { +// tournament.orderedEntries.forEach { entrant in +// if entrant.groupStagePosition > 0 { +// entrant.resetGroupStagePosition() +// } +// } +// tournament.buildGroupStages() +// do { +// try viewContext.save() +// } catch { +// // Replace this implementation with code to handle the error appropriately. +// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// +// } label: { +// Label("Refaire les poules", systemImage: "restart") +// } +// } +// +// @ViewBuilder +// func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View { +// Button(role: .destructive) { +// tournament.stopBroadcastGroupStages() +// tournament.groupStageOrderingMode = mode +// tournament.refreshGroupStages() +// save() +// } label: { +// Label("Poule \(mode.localizedLabel.lowercased())", systemImage: mode.systemImage) +// } +// } +// +// func addGroupStage(_ size: Int64) { +// let groupStage = GroupStage(context: viewContext) +// groupStage.index = tournament.firstIndexToUseForNewGroupStage +// groupStage.size = Int64(size) +// groupStage.matchFormatRawValue = tournament.groupStageMatchFormatRawValue +// print("addGroupStage groupStagesCount", tournament.groupStagesCount) +// print("addGroupStage numberOfGroupStages", tournament.numberOfGroupStages) +// if tournament.groupStagesCount >= tournament.numberOfGroupStages { +// tournament.numberOfGroupStages += 1 +// } +// tournament.addToGroupStages(groupStage) +// save() +// } +// +// var menuAddGroupStage: some View { +// Menu { +// ForEach(-1...1) { index in +// let i = tournament.teamsPerGroupStage + Int64(index) +// Button { +// addGroupStage(i) +// } label: { +// Text("Poule de \(i)") +// } +// .disabled(i < 2) +// } +// } label: { +// Label("Ajouter une poule", systemImage: "server.rack") +// } +// +// } +// +// +// func save() { +// do { +// tournament.objectWillChange.send() +// try viewContext.save() +// viewContext.refreshAllObjects() +// } catch { +// // Replace this implementation with code to handle the error appropriately. +// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// } +} diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift new file mode 100644 index 0000000..a11137d --- /dev/null +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -0,0 +1,23 @@ +// +// MatchDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct MatchDetailView: View { + @Environment(Match.self) var match: Match + let setupSeedContext: Bool + let matchViewStyle: MatchViewStyle + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + MatchDetailView(setupSeedContext: false, matchViewStyle: .standardStyle) + .environment(Match.mock()) +} diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift new file mode 100644 index 0000000..9e31259 --- /dev/null +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -0,0 +1,35 @@ +// +// MatchRowView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 25/11/2023. +// + +import SwiftUI + +struct MatchRowView: View { + @Environment(Match.self) var match: Match + let setupSeedContext: Bool + let matchViewStyle: MatchViewStyle + + @ViewBuilder + var body: some View { + if setupSeedContext { + MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) + } else { + NavigationLink { + MatchDetailView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) + .environment(match) + } label: { + MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle) + } + //.modifier(BroadcastViewModifier(isBroadcasted: match.isBroadcasted())) + } + } +} + + +#Preview { + MatchRowView(setupSeedContext: false, matchViewStyle: .standardStyle) + .environment(Match.mock()) +} diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift new file mode 100644 index 0000000..d540945 --- /dev/null +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -0,0 +1,24 @@ +// +// MatchSummaryView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct MatchSummaryView: View { + @Environment(Match.self) var match: Match + let setupSeedContext: Bool + let matchViewStyle: MatchViewStyle + + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + MatchSummaryView(setupSeedContext: false, matchViewStyle: .standardStyle) + .environment(Match.mock()) +} diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 28f08a2..40fe733 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -129,7 +129,8 @@ struct ActivityView: View { } .navigationTitle(TabDestination.activity.title) .navigationDestination(for: Tournament.self) { tournament in - TournamentView(tournament: tournament) + TournamentView() + .environment(tournament) } } } diff --git a/PadelClub/Views/Navigation/Organizer/OrganizedTournamentView.swift b/PadelClub/Views/Navigation/Organizer/OrganizedTournamentView.swift index 76bcb2f..78d6dfb 100644 --- a/PadelClub/Views/Navigation/Organizer/OrganizedTournamentView.swift +++ b/PadelClub/Views/Navigation/Organizer/OrganizedTournamentView.swift @@ -8,20 +8,15 @@ import SwiftUI struct OrganizedTournamentView: View { - let tournament: Tournament - @State private var navigationPath: [Screen] = [] - - init(tournament: Tournament) { - self.tournament = tournament - _navigationPath = State(wrappedValue: tournament.navigationPath) - } - + let tournament: Tournament var body: some View { - NavigationStack(path: $navigationPath) { - TournamentView(tournament: tournament, presentationContext: .organizer) - .onChange(of: navigationPath) { - tournament.navigationPath = navigationPath - } + @Bindable var tournament = tournament + NavigationStack(path: $tournament.navigationPath) { + TournamentView(presentationContext: .organizer) + .environment(tournament) +// .onChange(of: navigationPath) { +// tournament.navigationPath = navigationPath +// } } } } diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 1f0478b..85d5e60 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -193,18 +193,15 @@ struct TableStructureView: View { if tournament.state() == .initial { Button("Valider") { _save(rebuildEverything: true) - dismiss() } .clipShape(Capsule()) .buttonStyle(.bordered) - .disabled(updatedElements.isEmpty) } else { let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) Button("Valider", role: .destructive) { if requirements.isEmpty { _save(rebuildEverything: false) - dismiss() } else { presentRefreshStructureWarning = true } @@ -217,13 +214,11 @@ struct TableStructureView: View { if requirements.allSatisfy({ $0 == .groupStage }) { Button("Mettre à jour les poules") { _save(rebuildEverything: false) - dismiss() } } Button("Tout mettre à jour", role: .destructive) { _save(rebuildEverything: true) - dismiss() } }, message: { @@ -243,14 +238,7 @@ struct TableStructureView: View { let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { -// if let matches = tournament.matchs { -// tournament.removeFromMatchs(matches) -// } -// tournament.additionalRounds = 0 -// tournament.orderedEntries.forEach { entrant in -// entrant.initialPosition = 0 -// } -// tournament.hiddenRounds = nil + tournament.resetStructure() } tournament.teamCount = teamCount @@ -260,13 +248,14 @@ struct TableStructureView: View { tournament.groupStageAdditionalQualified = groupStageAdditionalQualified if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { -// tournament.build() + tournament.buildStructure() } else if (rebuildEverything == false && requirements.contains(.groupStage)) { - // tournament.buildGroupStages() + tournament.buildGroupStages() } try dataStore.tournaments.addOrUpdate(instance: tournament) + dismiss() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 90832db..1354c4b 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -19,11 +19,13 @@ struct TournamentCellView: View { .fill(color) .frame(width: 2) VStack(alignment: .leading, spacing: -2) { - Text("Homme") + Text(tournament.subtitle()) .font(.caption2) - Text(tournament.title()) + Text(tournament.tournamentLevel.localizedLabel()) .font(.title) - Text("Senior") + Text(tournament.tournamentCategory.localizedLabel()) + .font(.caption2) + Text(tournament.federalTournamentAge.localizedLabel()) .font(.caption2) } } diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index 5de7349..8e25141 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -17,7 +17,7 @@ struct TournamentInitView: View { LabeledContent { Text(tournament.settingsDescriptionLocalizedLabel()) } label: { - Label("Réglages", systemImage: "slider.horizontal.3") + LabelSettings() } } } footer: { @@ -29,7 +29,7 @@ struct TournamentInitView: View { LabeledContent { Text(tournament.structureDescriptionLocalizedLabel()) } label: { - Label("Structure", systemImage: "hammer") + LabelStructure() } } } footer: { diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift new file mode 100644 index 0000000..872e790 --- /dev/null +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -0,0 +1,30 @@ +// +// TournamentRunningView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 23/03/2024. +// + +import SwiftUI + +struct TournamentRunningView: View { + @Environment(Tournament.self) private var tournament: Tournament + + @ViewBuilder + var body: some View { + Section { + NavigationLink(value: Screen.groupStage) { + LabeledContent { + Text(tournament.groupStageStatus()) + } label: { + Text("Poules") + } + } + } + } +} + +#Preview { + TournamentRunningView() + .environment(Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 9057883..2d6c296 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -8,7 +8,7 @@ import SwiftUI struct TournamentView: View { - @State var tournament: Tournament + @Environment(Tournament.self) var tournament: Tournament var presentationContext: PresentationContext = .agenda var body: some View { @@ -16,6 +16,8 @@ struct TournamentView: View { switch tournament.state() { case .initial: TournamentInitView() + case .build: + TournamentRunningView() } // InscriptionManagerRowView(tournament: tournament) // NavigationLink(value: Screen.groupStage) { @@ -34,12 +36,11 @@ struct TournamentView: View { case .inscription: InscriptionManagerView(tournament: tournament) case .groupStage: - Text("Poules \(screen.rawValue)") + GroupStagesView() } } .environment(tournament) }) - .environment(tournament) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { @@ -50,21 +51,30 @@ struct TournamentView: View { } } - if presentationContext == .agenda { - ToolbarItem(placement: .topBarTrailing) { - Menu { + ToolbarItem(placement: .topBarTrailing) { + Menu { + if presentationContext == .agenda { Button { } label: { Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") } - } label: { - Label("Options", systemImage: "ellipsis.circle") } + + Divider() + if tournament.state() == .build { + NavigationLink(value: Screen.settings) { + LabelSettings() + } + NavigationLink(value: Screen.structure) { + LabelStructure() + } + } + } label: { + LabelOptions() } } } - } struct InscriptionManagerRowView: View { @@ -80,6 +90,7 @@ struct TournamentView: View { #Preview { NavigationStack { - TournamentView(tournament: .mock(), presentationContext: .agenda) + TournamentView(presentationContext: .agenda) + .environment(Tournament.mock()) } }