diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index d0feadc..76db047 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -126,6 +126,21 @@ FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5582BAB767000FD8220 /* Tips.swift */; }; FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */; }; FF1DF49B2BD8D23900822FA0 /* BarButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */; }; + FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; + FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */; }; + FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B732BFA00FC000B4573 /* HtmlService.swift */; }; + FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */; }; + FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; + FF1F4B832BFA02A4000B4573 /* tournament-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7F2BFA0105000B4573 /* tournament-template.html */; }; + FF1F4B842BFA02A4000B4573 /* groupstagescore-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */; }; + FF1F4B852BFA02A4000B4573 /* player-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7E2BFA0105000B4573 /* player-template.html */; }; + FF1F4B862BFA02A4000B4573 /* groupstagerow-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */; }; + FF1F4B872BFA02A4000B4573 /* hiddenplayer-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */; }; + FF1F4B882BFA02A4000B4573 /* bracket-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B762BFA0105000B4573 /* bracket-template.html */; }; + FF1F4B892BFA02A4000B4573 /* groupstagecol-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */; }; + FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; }; + FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; }; + FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; }; FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; }; @@ -138,6 +153,7 @@ FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */; }; FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */; }; + FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */; }; FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */; }; FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB62B90EFBF0061EFF9 /* MainView.swift */; }; FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */; }; @@ -428,6 +444,21 @@ FF1DC5582BAB767000FD8220 /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = ""; }; FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = ""; }; FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonView.swift; sourceTree = ""; }; + FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBuildView.swift; sourceTree = ""; }; + FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentInscriptionView.swift; sourceTree = ""; }; + FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlGenerator.swift; sourceTree = ""; }; + FF1F4B732BFA00FC000B4573 /* HtmlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlService.swift; sourceTree = ""; }; + FF1F4B762BFA0105000B4573 /* bracket-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "bracket-template.html"; sourceTree = ""; }; + FF1F4B772BFA0105000B4573 /* groupstage-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstage-template.html"; sourceTree = ""; }; + FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagecol-template.html"; sourceTree = ""; }; + FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstageentrant-template.html"; sourceTree = ""; }; + FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagerow-template.html"; sourceTree = ""; }; + FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagescore-template.html"; sourceTree = ""; }; + FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "hiddenplayer-template.html"; sourceTree = ""; }; + FF1F4B7D2BFA0105000B4573 /* match-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "match-template.html"; sourceTree = ""; }; + FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = ""; }; + FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = ""; }; + FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = ""; }; FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; @@ -439,6 +470,7 @@ FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = ""; }; FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = ""; }; FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = ""; }; + FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubCourtSetupView.swift; sourceTree = ""; }; FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = ""; }; FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = ""; }; @@ -642,6 +674,7 @@ C425D4042B6D249E002A7B48 /* Assets.xcassets */, FFF024192BF48AEE001F14B4 /* Localization */, FF0EC54D2BB195CA0056B6D1 /* CSV */, + FF1F4B802BFA0105000B4573 /* HTML Templates */, C425D4062B6D249E002A7B48 /* Preview Content */, ); path = PadelClub; @@ -896,10 +929,28 @@ FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */, FF5D0D882BB4935C005CB568 /* ClubRowView.swift */, FFC91B022BD85E2400B29808 /* CourtView.swift */, + FF53FBB62BFB301A0051D4C3 /* Shared */, ); path = Club; sourceTree = ""; }; + FF1F4B802BFA0105000B4573 /* HTML Templates */ = { + isa = PBXGroup; + children = ( + FF1F4B762BFA0105000B4573 /* bracket-template.html */, + FF1F4B772BFA0105000B4573 /* groupstage-template.html */, + FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */, + FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */, + FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */, + FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */, + FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */, + FF1F4B7D2BFA0105000B4573 /* match-template.html */, + FF1F4B7E2BFA0105000B4573 /* player-template.html */, + FF1F4B7F2BFA0105000B4573 /* tournament-template.html */, + ); + path = "HTML Templates"; + sourceTree = ""; + }; FF39719B2B8DE04B004C4E75 /* Navigation */ = { isa = PBXGroup; children = ( @@ -918,6 +969,8 @@ children = ( FF70916B2B91005400AB08DA /* TournamentView.swift */, FF8F26402BADFC8700650388 /* TournamentInitView.swift */, + FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */, + FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */, FF967CF52BAED51600A9A3BD /* TournamentRunningView.swift */, FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */, FF3F74F92B91A018004CFE0E /* Screen */, @@ -947,6 +1000,7 @@ FF1162802BCF945C000C4809 /* TournamentCashierView.swift */, FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */, FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */, + FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */, FF8F26522BAE0E4E00650388 /* Components */, ); path = Screen; @@ -1004,6 +1058,14 @@ path = ViewModel; sourceTree = ""; }; + FF53FBB62BFB301A0051D4C3 /* Shared */ = { + isa = PBXGroup; + children = ( + FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */, + ); + path = Shared; + sourceTree = ""; + }; FF5D30542BD95AF600F2B93D /* Ongoing */ = { isa = PBXGroup; children = ( @@ -1232,6 +1294,8 @@ C49EF0432BE286780077B5AA /* Key.swift */, FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */, FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */, + FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */, + FF1F4B732BFA00FC000B4573 /* HtmlService.swift */, FF8F26352BAD523300650388 /* PadelRule.swift */, FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, @@ -1396,6 +1460,16 @@ FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */, FF0EC54F2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-08-2022.csv in Resources */, FF0EC5502BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-12-2022.csv in Resources */, + FF1F4B832BFA02A4000B4573 /* tournament-template.html in Resources */, + FF1F4B842BFA02A4000B4573 /* groupstagescore-template.html in Resources */, + FF1F4B852BFA02A4000B4573 /* player-template.html in Resources */, + FF1F4B862BFA02A4000B4573 /* groupstagerow-template.html in Resources */, + FF1F4B872BFA02A4000B4573 /* hiddenplayer-template.html in Resources */, + FF1F4B882BFA02A4000B4573 /* bracket-template.html in Resources */, + FF1F4B892BFA02A4000B4573 /* groupstagecol-template.html in Resources */, + FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */, + FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */, + FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */, FF0EC5512BB195E20056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv in Resources */, FF0EC5522BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-02-2023.csv in Resources */, FF0EC5532BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-09-2022.csv in Resources */, @@ -1470,6 +1544,7 @@ FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */, FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, + FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */, FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */, @@ -1530,6 +1605,7 @@ FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */, FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, + FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, @@ -1595,11 +1671,13 @@ FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, + FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */, + FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */, FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, @@ -1628,6 +1706,7 @@ FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStageSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, + FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, @@ -1659,6 +1738,7 @@ FFC91B012BD85C2F00B29808 /* Court.swift in Sources */, FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */, FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */, + FF1F4B712BF9EFE9000B4573 /* TournamentInscriptionView.swift in Sources */, FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */, FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */, FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */, @@ -1847,7 +1927,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1885,7 +1965,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index a6bcc5b..52d33f8 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -35,9 +35,9 @@ class Club : ModelObject, Storable, Hashable { var zipCode: String? var latitude: Double? var longitude: Double? - //var courtCount: Int? + var courtCount: Int = 2 - internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil) { + internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2) { self.creator = creator self.name = name self.acronym = acronym ?? name.acronym() @@ -48,6 +48,7 @@ class Club : ModelObject, Storable, Hashable { self.zipCode = zipCode self.latitude = latitude self.longitude = longitude + self.courtCount = courtCount } func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String { @@ -63,12 +64,12 @@ class Club : ModelObject, Storable, Hashable { return URLs.main.url.appending(path: "?club=\(id)") } - var courts: [Court] { + var customizedCourts: [Court] { Store.main.filter { $0.club == self.id }.sorted(by: \.index) } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.courts) + try Store.main.deleteDependencies(items: self.customizedCourts) } enum CodingKeys: String, CodingKey { @@ -83,6 +84,7 @@ class Club : ModelObject, Storable, Hashable { case _zipCode = "zipCode" case _latitude = "latitude" case _longitude = "longitude" + case _courtCount = "courtCount" } } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index de2babd..78f349d 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -149,6 +149,18 @@ class GroupStage: ModelObject, Storable { return _matches().filter { matchIndexes.contains($0.index) } } + func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? { + if groupStagePosition == againstPosition { return nil } + let combos = Array((0.. [Match] { let runningMatches = runningMatches() return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ab0c4a3..ad87cfd 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -124,6 +124,30 @@ class Match: ModelObject, Storable { return startDate?.addingTimeInterval(minutesToAdd * 60.0) } + func winner() -> TeamRegistration? { + guard let winningTeamId else { return nil } + return Store.main.findById(winningTeamId) + } + + func localizedStartDate() -> String { + if let startDate { + return startDate.formatted(date: .abbreviated, time: .shortened) + } else { + return "" + } + } + + func scoreLabel() -> String { + if hasWalkoutTeam() == true { + return "WO" + } + let scoreOne = teamScore(.one)?.score?.components(separatedBy: ",") ?? [] + let scoreTwo = teamScore(.two)?.score?.components(separatedBy: ",") ?? [] + let tuples = zip(scoreOne, scoreTwo).map { ($0, $1) } + let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ") + return scores + } + func resetMatch() { losingTeamId = nil winningTeamId = nil @@ -541,6 +565,11 @@ class Match: ModelObject, Storable { guard let winningTeamId else { return false } return winningTeamId == team?.id } + + func teamWon(atPosition teamPosition: TeamPosition) -> Bool { + guard let winningTeamId else { return false } + return winningTeamId == team(teamPosition)?.id + } func team(_ team: TeamPosition) -> TeamRegistration? { if groupStage != nil { diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift index 9e838fd..01266d7 100644 --- a/PadelClub/Data/MockData.swift +++ b/PadelClub/Data/MockData.swift @@ -25,7 +25,7 @@ extension Club { } static func newEmptyInstance() -> Club { - Club(name: "", acronym: "") + Club(creator: DataStore.shared.user.id, name: "", acronym: "") } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 21c5351..9a7a5f8 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -290,7 +290,9 @@ class Tournament : ModelObject, Storable { enum State { case initial case build + case running case canceled + case finished } func publishedTeamsDate() -> Date { @@ -400,8 +402,15 @@ class Tournament : ModelObject, Storable { if self.isCanceled == true { return .canceled } - if (groupStageCount > 0 && groupStages().isEmpty == false) - || rounds().isEmpty == false { + + if self.hasEnded() { return .finished } + + let isBuild = (groupStageCount > 0 && groupStages().isEmpty == false) + || rounds().isEmpty == false + + if isBuild && startDate <= Date() { return .running } + + if isBuild { return .build } return .initial @@ -1411,7 +1420,7 @@ class Tournament : ModelObject, Storable { } func courtNameIfAvailable(atIndex courtIndex: Int) -> String? { - club()?.courts.first(where: { $0.index == courtIndex })?.name + club()?.customizedCourts.first(where: { $0.index == courtIndex })?.name } func courtName(atIndex courtIndex: Int) -> String { diff --git a/PadelClub/HTML Templates/bracket-template.html b/PadelClub/HTML Templates/bracket-template.html new file mode 100644 index 0000000..c344123 --- /dev/null +++ b/PadelClub/HTML Templates/bracket-template.html @@ -0,0 +1,4 @@ +
    +
  •  {{roundLabel}}
  • + {{match-template}} +
diff --git a/PadelClub/HTML Templates/groupstage-template.html b/PadelClub/HTML Templates/groupstage-template.html new file mode 100644 index 0000000..6e203b9 --- /dev/null +++ b/PadelClub/HTML Templates/groupstage-template.html @@ -0,0 +1,95 @@ + + + + + + + + +
+ + + + + {{teamsCol}} + + {{teamsRow}} +
+

{{bracketTitle}}

+

{{bracketStartDate}}

+
+ +
+ + diff --git a/PadelClub/HTML Templates/groupstagecol-template.html b/PadelClub/HTML Templates/groupstagecol-template.html new file mode 100644 index 0000000..b7b017e --- /dev/null +++ b/PadelClub/HTML Templates/groupstagecol-template.html @@ -0,0 +1,4 @@ + + {{team}} + + diff --git a/PadelClub/HTML Templates/groupstageentrant-template.html b/PadelClub/HTML Templates/groupstageentrant-template.html new file mode 100644 index 0000000..d7f2cdb --- /dev/null +++ b/PadelClub/HTML Templates/groupstageentrant-template.html @@ -0,0 +1,4 @@ +
{{playerOne}}
+
{{weightOne}}
+
{{playerTwo}}
+
{{weightTwo}}
diff --git a/PadelClub/HTML Templates/groupstagerow-template.html b/PadelClub/HTML Templates/groupstagerow-template.html new file mode 100644 index 0000000..661386d --- /dev/null +++ b/PadelClub/HTML Templates/groupstagerow-template.html @@ -0,0 +1,4 @@ + + {{team}} + {{scores}} + diff --git a/PadelClub/HTML Templates/groupstagescore-template.html b/PadelClub/HTML Templates/groupstagescore-template.html new file mode 100644 index 0000000..a9d7f7c --- /dev/null +++ b/PadelClub/HTML Templates/groupstagescore-template.html @@ -0,0 +1,5 @@ + +
{{winner}}
+
{{score}}
+ + diff --git a/PadelClub/HTML Templates/hiddenplayer-template.html b/PadelClub/HTML Templates/hiddenplayer-template.html new file mode 100644 index 0000000..054e440 --- /dev/null +++ b/PadelClub/HTML Templates/hiddenplayer-template.html @@ -0,0 +1,2 @@ + +
diff --git a/PadelClub/HTML Templates/match-template.html b/PadelClub/HTML Templates/match-template.html new file mode 100644 index 0000000..aba166a --- /dev/null +++ b/PadelClub/HTML Templates/match-template.html @@ -0,0 +1,8 @@ +
  • + {{entrantOne}} +
  • +
  • {{matchDescription}}
  • +
  • + {{entrantTwo}} +
  • +
  •  
  • diff --git a/PadelClub/HTML Templates/player-template.html b/PadelClub/HTML Templates/player-template.html new file mode 100644 index 0000000..38579d9 --- /dev/null +++ b/PadelClub/HTML Templates/player-template.html @@ -0,0 +1,3 @@ + +
    {{playerOne}}{{weightOne}}
    +
    {{playerTwo}}{{weightTwo}}
    diff --git a/PadelClub/HTML Templates/tournament-template.html b/PadelClub/HTML Templates/tournament-template.html new file mode 100644 index 0000000..9bcb606 --- /dev/null +++ b/PadelClub/HTML Templates/tournament-template.html @@ -0,0 +1,103 @@ + + + + + + + + +

    {{tournamentTitle}}

    +
    + {{brackets}} +
    + + diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index 29187ed..704454f 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -11,6 +11,7 @@ enum DisplayContext { case addition case edition case lockedForEditing + case selection } enum DisplayStyle { diff --git a/PadelClub/Utils/HtmlGenerator.swift b/PadelClub/Utils/HtmlGenerator.swift new file mode 100644 index 0000000..c8e8d79 --- /dev/null +++ b/PadelClub/Utils/HtmlGenerator.swift @@ -0,0 +1,198 @@ +// +// HtmlGenerator.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 23/10/2023. +// + +import Foundation +import UIKit +import WebKit +import PDFKit + +class HtmlGenerator: ObservableObject { + + init(tournament: Tournament) { + self.tournament = tournament + } + + let tournament: Tournament + @Published var zoomLevel: CGFloat? = 2.0 + @Published var includeBracket: Bool = true + @Published var includeGroupStage: Bool = true + @Published var includeLoserBracket: Bool = false + @Published var displayHeads: Bool = false + @Published var groupStageIsReady: Bool = false + @Published var displayRank: Bool = false + private var pdfDocument: PDFDocument = PDFDocument() + private var rects: [CGRect] = [] + private var completionHandler: ((Result) -> ())? + @Published var width: CGFloat = 0 + @Published var height: CGFloat = 0 + private var webView: WKWebView = WKWebView() + private var groupStageDone: Int = 0 + + var estimatedPageCount: Int { + if let zoomLevel { + let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let numberOfPageInWidth = Int(width / pageSize.width) + 1 + let numberOfPageInHeight = Int(height / pageSize.height) + 1 + return numberOfPageInWidth * numberOfPageInHeight + } else { + return 1 + } + } + + func preparePDF(completionHandler: @escaping ((Result) -> ())) { + self.completionHandler = completionHandler + } + + func generateWebView(webView: WKWebView) { + self.webView = webView + self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in + if complete != nil { + self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in + self.height = height as! CGFloat + }) + self.webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in + self.width = width as! CGFloat + }) + } + }) + } + + func generateGroupStage(webView: WKWebView) { + webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in + if complete != nil { + webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in + let height = height as! CGFloat + webView.evaluateJavaScript("document.documentElement.scrollWidth", completionHandler: { (width, error) in + let width = width as! CGFloat + + print("bracket", width, height) + let config = WKPDFConfiguration() + config.rect = CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(width))) + webView.createPDF(configuration: config){ result in + switch result{ + case .success(let data): + let newPage = PDFDocument(data: data)! + let page = newPage.page(at: 0)! + let copiedPage = page.copy() as! PDFPage + self.pdfDocument.insert(copiedPage, at: self.pdfDocument.pageCount) + + DispatchQueue.main.async { + self.groupStageDone += 1 + if self.groupStageDone == self.tournament.groupStages().count { + self.groupStageIsReady = true + self.completionHandler?(.success(self.savePDF())) + } + } + case .failure(let error): + self.completionHandler?(.failure(error)) + } + } + + }) + }) + } + }) + + } + + func buildPDF() { + groupStageDone = 0 + groupStageIsReady = false + pdfDocument = PDFDocument() + try? FileManager.default.removeItem(at: pdfURL!) + print("buildPDF", width, height, zoomLevel ?? 0) + if let zoomLevel { + let pageSize = CGSize(width: 595 * (1 + zoomLevel), height: 812 * (1 + zoomLevel)) + let numberOfPageInWidth = Int(width / pageSize.width) + 1 + let numberOfPageInHeight = Int(height / pageSize.height) + 1 + for w in 0.. String { + //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() + HtmlService.template(tournament: tournament).html(headName: displayHeads, withRank: displayRank, withScore: false) + } + + var pdfURL: URL? { + guard let pdfFolderURL = getFilePath() else { + return nil + } + let date = tournament.startDate + let stringDate = date.formatted(.iso8601 + .year() + .month() + .day() + .dateSeparator(.dash)) + + let name = tournament.tournamentLevel.localizedLabel() + "-" + tournament.tournamentCategory.importingRawValue + return pdfFolderURL.appendingPathComponent(stringDate + "-" + name + ".pdf") + } + + func savePDF() -> Bool { + pdfDocument.write(to: pdfURL!) + } + + var isReady: Bool { + FileManager.default.fileExists(atPath: pdfURL!.path()) + } + + func getFilePath() -> URL? { + if FileManager.default.fileExists(atPath: pdfFolderURL.path) { + return pdfFolderURL + } else { + do { + try FileManager.default.createDirectory(at: pdfFolderURL, withIntermediateDirectories: true, attributes: nil) + return pdfFolderURL + } catch { + print("getFilePath", error.localizedDescription) + return nil + } + } + } + + var pdfFolderURL: URL { + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + return URL(fileURLWithPath: documentsPath.appending("/pdfs")) + } +} diff --git a/PadelClub/Utils/HtmlService.swift b/PadelClub/Utils/HtmlService.swift new file mode 100644 index 0000000..8ab63ef --- /dev/null +++ b/PadelClub/Utils/HtmlService.swift @@ -0,0 +1,220 @@ +// +// HtmlService.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 25/10/2023. +// + +import Foundation + +enum HtmlService { + + case template(tournament: Tournament) + case bracket(tournament: Tournament, roundIndex: Int) + case match(match: Match) + case player(entrant: TeamRegistration) + case hiddenPlayer + case groupstage(groupStage: GroupStage) + case groupstageEntrant(entrant: TeamRegistration) + case groupstageColumn(entrant: TeamRegistration, position: String) + case groupstageRow(entrant: TeamRegistration, teamsPerBracket: Int) + case groupstageScore(score: Match?, shouldHide: Bool) + + var url: URL { + return URL(fileURLWithPath: "\(self.fileName)") + } + + var fileName: String { + switch self { + case .template: + return "tournament-template" + case .bracket: + return "bracket-template" + case .match: + return "match-template" + case .player: + return "player-template" + case .hiddenPlayer: + return "hiddenplayer-template" + case .groupstage: + return "groupstage-template" + case .groupstageEntrant: + return "groupstageentrant-template" + case .groupstageRow: + return "groupstagerow-template" + case .groupstageColumn: + return "groupstagecol-template" + case .groupstageScore: + return "groupstagescore-template" + } + } + + func html(headName: Bool, withRank: Bool, withScore: Bool) -> String { + guard let file = Bundle.main.path(forResource: self.fileName, ofType: "html") else { + fatalError() + } + + guard let html = try? String(contentsOfFile: file, encoding: String.Encoding.utf8) else { + fatalError() + } + + switch self { + case .groupstage(let bracket): + var template = html + if let startDate = bracket.startDate { + template = template.replacingOccurrences(of: "{{bracketStartDate}}", with: startDate.formatted()) + } else { + template = template.replacingOccurrences(of: "{{bracketStartDate}}", with: "") + } + template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle()) + template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle()) + + var col = "" + var row = "" + bracket.teams().forEach { entrant in + col = col.appending(HtmlService.groupstageColumn(entrant: entrant, position: "col").html(headName: headName, withRank: withRank, withScore: withScore)) + row = row.appending(HtmlService.groupstageRow(entrant: entrant, teamsPerBracket: bracket.size).html(headName: headName, withRank: withRank, withScore: withScore)) + } + template = template.replacingOccurrences(of: "{{teamsCol}}", with: col) + template = template.replacingOccurrences(of: "{{teamsRow}}", with: row) + + return template + case .groupstageEntrant(let entrant): + var template = html + if let playerOne = entrant.players()[safe: 0] { + template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel()) + if withRank { + template = template.replacingOccurrences(of: "{{weightOne}}", with: "(\(playerOne.formattedRank())") + } else { + template = template.replacingOccurrences(of: "{{weightOne}}", with: "") + } + } + + if let playerTwo = entrant.players()[safe: 1] { + template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel()) + if withRank { + template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank())") + } else { + template = template.replacingOccurrences(of: "{{weightTwo}}", with: "") + } + } + return template + case .groupstageRow(let entrant, let teamsPerBracket): + var template = html + template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageColumn(entrant: entrant, position: "row").html(headName: headName, withRank: withRank, withScore: withScore)) + + var scores = "" + (0.. +
  •  
  • +
  • \(winnerName)
  • +
  •  
  • + + + """ + brackets = brackets.appending(winner) + + template = template.replacingOccurrences(of: "{{brackets}}", with: brackets) + return template + } + } +} diff --git a/PadelClub/Utils/LocationManager.swift b/PadelClub/Utils/LocationManager.swift index 50afc59..3961bf6 100644 --- a/PadelClub/Utils/LocationManager.swift +++ b/PadelClub/Utils/LocationManager.swift @@ -53,7 +53,7 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { } func geocodeCity(cityOrZipcode: String, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) { - CLGeocoder().geocodeAddressString(cityOrZipcode, in: nil, completionHandler: completion) + CLGeocoder().geocodeAddressString(cityOrZipcode, completionHandler: completion) } } diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index 570fbf8..9e0c266 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -45,11 +45,11 @@ class SourceFileManager { } func fetchData() async { - if let mostRecent = mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent { - await fetchData(fromDate: current) - } else { - await fetchData(fromDate: Date()) - } + await fetchData(fromDate: Date()) +// if let mostRecent = mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent { +// await fetchData(fromDate: current) +// } else { +// } } func _removeAllData(fromDate current: Date) { @@ -96,11 +96,11 @@ class SourceFileManager { try await group.waitForAll() } - if current < Date() { - if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) { - await fetchData(fromDate: nextCurrent) - } - } +// if current < Date() { +// if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) { +// await fetchData(fromDate: nextCurrent) +// } +// } } catch { print("downloadRankingData", error) diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index 78e24fb..07e4d32 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -304,20 +304,12 @@ struct MultiTournamentsEventTip: Tip { } var message: Text? { - Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame par le même week-end par exemple.") + Text("Padel Club permet de gérer plusieurs tournois ayant lieu en même temps. Un P100 homme et dame le même week-end par exemple.") } var image: Image? { Image(systemName: "trophy.circle") } - - var actions: [Action] { - Action(id: ActionKey.addEvent.rawValue, title: "Ajoutez une épreuve") - } - - enum ActionKey: String { - case addEvent = "add-event" - } } struct NotFoundAreWalkOutTip: Tip { @@ -393,6 +385,33 @@ struct TournamentSelectionTip: Tip { } } +struct TournamentRunningTip: Tip { + @Parameter + static var isRunning: Bool = false + + var rules: [Rule] { + [ + // Define a rule based on the app state. + #Rule(Self.$isRunning) { + // Set the conditions for when the tip displays. + return $0 + } + ] + } + + var title: Text { + Text("Tournoi en cours") + } + + var message: Text? { + return Text("Le tournoi a commencé, les options utiles surtout à sa préparation sont maintenant accessibles dans le menu en haut à droite.") + } + + var image: Image? { + Image(systemName: "ellipsis.circle") + } +} + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index f62d8d4..194bb8a 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -12,6 +12,7 @@ enum URLs: String, Identifiable { case main = "https://xlr.alwaysdata.net/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" //case padelClub = "https://padelclub.app" + case padelRules = "https://fft-site.cdn.prismic.io/fft-site/ZgLn3McYqOFdyF7n_LEGUIDEDELACOMPETITIONDEPADEL-MAJDECEMBRE2023.pdf" var id: String { return self.rawValue } diff --git a/PadelClub/ViewModel/Screen.swift b/PadelClub/ViewModel/Screen.swift index ec0774a..cb4d0d6 100644 --- a/PadelClub/ViewModel/Screen.swift +++ b/PadelClub/ViewModel/Screen.swift @@ -19,4 +19,5 @@ enum Screen: String, Codable { case rankings case broadcast case event + case print } diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 9e3ed71..5db94ba 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -192,7 +192,8 @@ struct CallMessageCustomizationView: View { if let eventClub = tournament.eventObject()?.clubObject() { let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: dataStore.user.id) Section { - TextField("Nom du club", text: $customClubName) + TextField("Nom du club", text: $customClubName, axis: .vertical) + .lineLimit(2) .autocorrectionDisabled() .focused($focusedField, equals: .clubName) .onSubmit { @@ -204,8 +205,6 @@ struct CallMessageCustomizationView: View { } } .disabled(hasBeenCreated == false) - } header: { - Text("Nom du club") } footer: { if hasBeenCreated == false { Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index 97dfcd0..6b9d1a2 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -21,9 +21,12 @@ struct EventSettingsView: View { var body: some View { Form { Section { - TextField("Nom de l'événement", text: $eventName) + TextField("Nom de l'événement", text: $eventName, axis: .vertical) + .lineLimit(2) .autocorrectionDisabled() .keyboardType(.alphabet) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity) .onSubmit { if eventName.trimmed.isEmpty == false { event.name = eventName.trimmed @@ -32,6 +35,8 @@ struct EventSettingsView: View { } _save() } + } header: { + Text("Nom de l'événement") } footer: { if eventName.isEmpty == false { FooterButtonView("effacer le nom") { diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index be2b761..85ddfbc 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -9,17 +9,21 @@ import SwiftUI import LeStorage struct ClubDetailView: View { - @Bindable var club: Club - var displayContext: DisplayContext @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) var dismiss @FocusState var focusedField: Club.CodingKeys? @State private var acronymMode: Club.AcronymMode = .automatic @State private var city: String @State private var zipCode: String + @State private var selectedCourt: Court? + @Bindable var club: Club + var displayContext: DisplayContext + var selection: ((Club) -> ())? = nil - init(club: Club, displayContext: DisplayContext) { + init(club: Club, displayContext: DisplayContext, selection: ((Club) -> ())? = nil) { _club = Bindable(club) self.displayContext = displayContext + self.selection = selection _acronymMode = State(wrappedValue: club.shortNameMode()) _city = State(wrappedValue: club.city ?? "") _zipCode = State(wrappedValue: club.zipCode ?? "") @@ -27,43 +31,26 @@ 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 { - LabeledContent { - TextField("Nom du club", text: $club.name) - .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 - } + TextField("Nom du club", text: $club.name, axis: .vertical) + .lineLimit(2) + .autocorrectionDisabled() + .keyboardType(.alphabet) + .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 } - } label: { - Text("Nom du club") - } - .onTapGesture { - focusedField = ._name - } + if displayContext == .addition { + focusedField = ._acronym + } + } + LabeledContent { - if acronymMode == .automatic { + if acronymMode == .automatic || displayContext == .lockedForEditing { Text(club.acronym) } else { TextField("Nom court", text: $club.acronym) @@ -98,6 +85,7 @@ struct ClubDetailView: View { } label: { Text(acronymMode.rawValue) } + .disabled(displayContext == .lockedForEditing) } } .onChange(of: acronymMode) { @@ -106,8 +94,16 @@ struct ClubDetailView: View { club.acronym = "" } } - - if club.code == nil { + } footer: { + if displayContext == .lockedForEditing { + Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) + } else { + Text("Vous pouvez personaliser le nom court ou laisser celui généré par défaut.") + } + } + + if club.code == nil { + Section { LabeledContent { TextField("Ville", text: $city) .autocorrectionDisabled() @@ -146,18 +142,16 @@ struct ClubDetailView: View { .onTapGesture { focusedField = ._zipCode } + } footer: { + if displayContext == .lockedForEditing { + Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) + } } - - } footer: { - if displayContext == .lockedForEditing { - Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) - } else { - Text("Vous pouvez personaliser le nom court ou laisser celui généré par défaut.") - } + .disabled(displayContext == .lockedForEditing) } - .disabled(displayContext == .lockedForEditing) - + ClubCourtSetupView(club: club, displayContext: displayContext, selectedCourt: $selectedCourt) + if let federalLink = club.federalLink() { Section { LabeledContent("Code Club") { @@ -167,11 +161,51 @@ struct ClubDetailView: View { Text(club.city ?? "") } Link(destination: federalLink) { - Text("Fiche du club sur tenup") + Text("Voir la fiche du club sur tenup") + } + } + } + + if displayContext == .addition { + Section { + } header: { + HStack { + VStack { + Divider() + } + Text("ou") + VStack { + Divider() + } + } + } + + 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.") + } + } + + if displayContext == .edition || displayContext == .lockedForEditing { + let isFavorite = club.isFavorite() + Section { + RowButtonView(isFavorite ? "Retirer des favoris" : "Mettre en favori", role: isFavorite ? .destructive : nil) { + if isFavorite { + dataStore.user.clubs.removeAll(where: { $0 == club.id }) + } else { + dataStore.user.clubs.append(club.id) + } + self.dataStore.saveUser() } } } - + + if displayContext == .edition { Section { RowButtonView("Supprimer ce club", role: .destructive) { @@ -192,22 +226,10 @@ struct ClubDetailView: View { .navigationTitle(displayContext == .addition ? "Nouveau club" : "Détail du club") .navigationBarTitleDisplayMode(.inline) .toolbar(.visible, for: .navigationBar) + .headerProminence(.increased) .toolbarBackground(.visible, for: .navigationBar) - .toolbar { - if displayContext == .edition || displayContext == .lockedForEditing { - let isFavorite = club.isFavorite() - ToolbarItem(placement: .topBarTrailing) { - BarButtonView("Favori", icon: isFavorite ? "star.fill" : "star") { - if isFavorite { - dataStore.user.clubs.removeAll(where: { $0 == club.id }) - } else { - dataStore.user.clubs.append(club.id) - } - self.dataStore.saveUser() - } - .tint(isFavorite ? .green : .logoRed) - } - } + .navigationDestination(item: $selectedCourt) { court in + CourtView(court: court) } .onDisappear { if displayContext == .edition { diff --git a/PadelClub/Views/Club/ClubImportView.swift b/PadelClub/Views/Club/ClubImportView.swift index 293983d..8a80052 100644 --- a/PadelClub/Views/Club/ClubImportView.swift +++ b/PadelClub/Views/Club/ClubImportView.swift @@ -9,10 +9,11 @@ import SwiftUI struct ClubImportView: View { @Environment(\.dismiss) var dismiss + var selection: ((Club) -> ())? = nil var body: some View { NavigationStack { - ClubSearchView(displayContext: .addition) + ClubSearchView(displayContext: .addition, selection: selection) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Annuler", role: .cancel) { diff --git a/PadelClub/Views/Club/ClubRowView.swift b/PadelClub/Views/Club/ClubRowView.swift index f9d5a89..3a3e2dc 100644 --- a/PadelClub/Views/Club/ClubRowView.swift +++ b/PadelClub/Views/Club/ClubRowView.swift @@ -9,11 +9,14 @@ import SwiftUI struct ClubRowView: View { let club: Club + var displayContext: DisplayContext = .edition var body: some View { LabeledContent { - Image(systemName: club.isFavorite() ? "star.fill" : "star") - .foregroundStyle(club.isFavorite() ? .green : .logoRed) +// if displayContext == .edition { +// Image(systemName: club.isFavorite() ? "star.fill" : "star") +// .foregroundStyle(club.isFavorite() ? .green : .logoRed) +// } } label: { Text(club.name) Text(club.acronym) diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift index 4d6c14e..f94cab0 100644 --- a/PadelClub/Views/Club/ClubSearchView.swift +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -26,11 +26,21 @@ struct ClubSearchView: View { @State private var getForwardCityList: [CLPlacemark] = [] @State private var searchPresented: Bool = false @State private var showingSettingsAlert = false - @State private var presentClubCreationView: Bool = false + @State private var newClub: Club? + + var presentClubCreationView: Binding { Binding( + get: { newClub != nil }, + set: { isPresented in + if isPresented == false { + newClub = nil + } + } + )} var displayContext: DisplayContext = .edition var club: Club? - + var selection: ((Club) -> ())? = nil + fileprivate class DebouncableViewModel: ObservableObject { @Published var debouncableText: String = "" var debounceTrigger: Double = 0.15 @@ -162,7 +172,7 @@ struct ClubSearchView: View { if searchAttempted { RowButtonView("Créer un club manuellement") { - presentClubCreationView = true + newClub = club ?? Club.newEmptyInstance() } } } @@ -171,9 +181,14 @@ struct ClubSearchView: View { ContentUnavailableView("recherche en cours", systemImage: "mappin.and.ellipse", description: Text("recherche des clubs autour de vous")) } } - .sheet(isPresented: $presentClubCreationView) { - CreateClubView() + .sheet(isPresented: presentClubCreationView) { + if let newClub { + CreateClubView(club: newClub) { club in + selection?(club) + dismiss() + } .tint(.master) + } } .alert(isPresented: $showingSettingsAlert) { Alert( @@ -295,8 +310,8 @@ struct ClubSearchView: View { private func _importClub(clubToEdit: Club, clubMarker: ClubMarker) { if clubToEdit.creator == dataStore.user.id { if clubToEdit.name.isEmpty { - clubToEdit.name = clubMarker.nom - clubToEdit.acronym = clubToEdit.automaticShortName() + clubToEdit.name = clubMarker.nom.capitalized + clubToEdit.acronym = clubToEdit.automaticShortName().capitalized } clubToEdit.code = clubMarker.clubID clubToEdit.latitude = clubMarker.lat @@ -316,6 +331,7 @@ struct ClubSearchView: View { } } dismiss() + selection?(clubToEdit) } private func _filteredClubs() -> [ClubMarker] { diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift index 11d3f2f..6722175 100644 --- a/PadelClub/Views/Club/ClubsView.swift +++ b/PadelClub/Views/Club/ClubsView.swift @@ -12,20 +12,21 @@ import LeStorage struct ClubsView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss - @State private var presentClubCreationView: Bool = false @State private var presentClubSearchView: Bool = false - let tip = SlideToDeleteTip() + @State private var newClub: Club? var selection: ((Club) -> ())? = nil + var presentClubCreationView: Binding { Binding( + get: { newClub != nil }, + set: { isPresented in + if isPresented == false { + newClub = nil + } + } + )} + var body: some View { List { -// -// if dataStore.clubs.isEmpty == false && selection == nil { -// Section { -// TipView(tip) -// .tipStyle(tint: nil) -// } -// } let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: true) ForEach(clubs) { club in if let selection { @@ -33,7 +34,7 @@ struct ClubsView: View { selection(club) dismiss() } label: { - ClubRowView(club: club) + ClubRowView(club: club, displayContext: .selection) .frame(maxWidth: .infinity) } .contentShape(Rectangle()) @@ -62,7 +63,7 @@ struct ClubsView: View { Text("Texte décrivant l'utilité d'un club et les features que cela apporte") } actions: { RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") { - presentClubCreationView = true + newClub = Club.newEmptyInstance() } RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") { presentClubSearchView = true @@ -71,13 +72,27 @@ struct ClubsView: View { } } .navigationTitle(selection == nil ? "Mes clubs" : "Choisir un club") - .sheet(isPresented: $presentClubCreationView) { - CreateClubView() + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .sheet(isPresented: presentClubCreationView) { + if let newClub { + CreateClubView(club: newClub) { club in + if let selection { + selection(club) + dismiss() + } + } .tint(.master) + } } .sheet(isPresented: $presentClubSearchView) { - ClubImportView() - .tint(.master) + ClubImportView() { club in + if let selection { + selection(club) + dismiss() + } + } + .tint(.master) } .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { @@ -91,7 +106,7 @@ struct ClubsView: View { } Button { - presentClubCreationView = true + newClub = Club.newEmptyInstance() } label: { Image(systemName: "plus.circle.fill") .resizable() diff --git a/PadelClub/Views/Club/CreateClubView.swift b/PadelClub/Views/Club/CreateClubView.swift index 5f8f326..fbd1cda 100644 --- a/PadelClub/Views/Club/CreateClubView.swift +++ b/PadelClub/Views/Club/CreateClubView.swift @@ -9,18 +9,14 @@ import SwiftUI import LeStorage struct CreateClubView: View { - @Bindable var club: Club @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) var dismiss - - init() { - self.club = Club.newEmptyInstance() - } - + var club: Club + var selection: ((Club) -> ())? = nil var body: some View { NavigationStack { - ClubDetailView(club: club, displayContext: .addition) + ClubDetailView(club: club, displayContext: .addition, selection: selection) .task { do { try await dataStore.clubs.loadDataFromServerIfAllowed() @@ -55,6 +51,7 @@ struct CreateClubView: View { self.dataStore.saveUser() } dismiss() + selection?(existingOrCreatedClub) } .disabled(club.isValid == false) } @@ -64,6 +61,6 @@ struct CreateClubView: View { } #Preview { - CreateClubView() + CreateClubView(club: Club.mock()) .environmentObject(DataStore.shared) } diff --git a/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift new file mode 100644 index 0000000..76ba387 --- /dev/null +++ b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift @@ -0,0 +1,83 @@ +// +// ClubCourtSetupView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/05/2024. +// + +import SwiftUI +import LeStorage + +struct ClubCourtSetupView: View { + @EnvironmentObject var dataStore: DataStore + @Bindable var club: Club + let displayContext: DisplayContext + @Binding var selectedCourt: Court? + + @ViewBuilder + var body: some View { + Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains", count: $club.courtCount) + .disabled(displayContext == .lockedForEditing) + .onChange(of: club.courtCount) { + if displayContext != .addition { + do { + try dataStore.clubs.addOrUpdate(instance: club) + } catch { + Logger.error(error) + } + } + } + } footer: { + if displayContext == .lockedForEditing { + Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) + } + } + + Section { + ForEach((0.. some View { + let court = tournamentClub.customizedCourts.first(where: { $0.index == index }) + LabeledContent { + if displayContext == .edition { + FooterButtonView("personnaliser") { + if let court { + selectedCourt = court + } else { + let newCourt = Court(index: index, club: tournamentClub.id) + do { + try dataStore.courts.addOrUpdate(instance: newCourt) + } catch { + Logger.error(error) + } + selectedCourt = newCourt + } + } + } + } label: { + if let court { + Text(court.courtTitle()) + HStack { + if court.indoor { + Text("Couvert") + } + if court.exitAllowed { + Text("Sortie autorisée") + } + } + } else { + Text(_courtName(atIndex: index)) + } + } + } + + private func _courtName(atIndex index: Int) -> String { + Court.courtIndexedTitle(atIndex: index) + } +} diff --git a/PadelClub/Views/Components/BarButtonView.swift b/PadelClub/Views/Components/BarButtonView.swift index 7d52007..b92c01e 100644 --- a/PadelClub/Views/Components/BarButtonView.swift +++ b/PadelClub/Views/Components/BarButtonView.swift @@ -22,15 +22,23 @@ struct BarButtonView: View { Button(action: { action() }) { - Label { - Text(accessibilityLabel) - } icon: { - Image(systemName: icon) - .resizable() - .scaledToFit() - .frame(minHeight: 36) - } - .labelStyle(.iconOnly) + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(minHeight: 28) + + /* + Label { + Text(accessibilityLabel) + } icon: { + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(minHeight: 36) + } + .labelStyle(.iconOnly) + //todo: resizing not working when label used + */ } } } diff --git a/PadelClub/Views/Event/EventCreationView.swift b/PadelClub/Views/Event/EventCreationView.swift index e958b8f..eca218f 100644 --- a/PadelClub/Views/Event/EventCreationView.swift +++ b/PadelClub/Views/Event/EventCreationView.swift @@ -20,7 +20,7 @@ struct EventCreationView: View { @State private var eventName: String = "" @State var tournaments: [Tournament] = [] @State var selectedClub: Club? - + let multiTournamentsEventTip = MultiTournamentsEventTip() var body: some View { @@ -57,33 +57,29 @@ struct EventCreationView: View { } } label: { if let selectedClub { - ClubRowView(club: selectedClub) + ClubRowView(club: selectedClub, displayContext: .selection) } else { Text("Choisir un club") } } - TextField("Nom de l'événement", text: $eventName) + TextField("Nom de l'événement", text: $eventName, axis: .vertical) + .lineLimit(2) .autocorrectionDisabled() .keyboardType(.alphabet) - + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity) + LabeledContent { Text(tournaments.count.formatted()) } label: { - Text("Nombre d'épreuves") + Text("Nombre d'épreuve") } + } header: { + Text("") } - Section { - TipView(multiTournamentsEventTip) { action in - let tournament = Tournament.newEmptyInstance() - self.tournaments.append(tournament) - } - .tipStyle(tint: .orange) - } - - switch eventType { case .approvedTournament: approvedTournamentEditorView @@ -110,7 +106,7 @@ struct EventCreationView: View { } tournaments.forEach { tournament in - tournament.courtCount = selectedClub?.courts.count ?? 2 + tournament.courtCount = selectedClub?.courtCount ?? 2 tournament.startDate = startingDate tournament.dayDuration = duration tournament.setupFederalSettings() @@ -134,10 +130,23 @@ struct EventCreationView: View { dismiss() } } + + ToolbarItem(placement: .topBarTrailing) { + BarButtonView("Ajouter une épreuve", icon: "plus.circle.fill") { + let tournament = Tournament.newEmptyInstance() + self.tournaments.append(tournament) + } + .popoverTip(multiTournamentsEventTip) + } } .navigationTitle("Nouvel événement") - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) + .onAppear { + if selectedClub == nil { + selectedClub = dataStore.user.clubsObjects().first + } + } } } diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index eabfa53..8b1e540 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -66,7 +66,6 @@ struct MainView: View { .environmentObject(dataStore) .task { await self._checkSourceFileAvailability() - await self._downloadPreviousDate() } // .refreshable { // Task { @@ -99,8 +98,8 @@ struct MainView: View { } } } - } else if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable { - if mostRecentDateAvailable > SourceFileManager.shared.lastDataSourceDate() ?? .distantPast { + } else if let mostRecentDateAvailable = SourceFileManager.shared.mostRecentDateAvailable, let lastDataSourceDate = SourceFileManager.shared.lastDataSourceDate() { + if mostRecentDateAvailable > lastDataSourceDate { Label(mostRecentDateAvailable.monthYearFormatted + " disponible", systemImage: "exclamationmark.triangle") .labelStyle(.titleAndIcon) } else { diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 72e65be..7bcaed7 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -10,9 +10,13 @@ import SwiftUI struct OngoingView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @EnvironmentObject var dataStore: DataStore - + @State private var sortByField: Bool = false + let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndex!), .keyPath(\Match.startDate!)] + let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndex!)] + var matches: [Match] { - dataStore.matches.filter({ $0.startDate != nil && $0.endDate == nil }).sorted(by: \.startDate!) + let sorting = sortByField ? fieldSorting : defaultSorting + return dataStore.matches.filter({ $0.startDate != nil && $0.endDate == nil && $0.courtIndex != nil }).sorted(using: sorting, order: .ascending) } var body: some View { @@ -54,10 +58,10 @@ struct OngoingView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { Menu { - Button("Par terrain") { - - } - Button("Par date") { + Picker(selection: $sortByField) { + Text("Trier par date").tag(false) + Text("Trier par terrain").tag(true) + } label: { } //todo @@ -72,7 +76,7 @@ struct OngoingView: View { } ToolbarItem(placement: .status) { if matches.isEmpty == false { - Text("\(matches.count) matche" + matches.count.pluralSuffix) + Text("\(matches.count) match" + matches.count.pluralSuffix) } } } diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 91cf3cc..6e960e3 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -44,6 +44,10 @@ struct ToolboxView: View { } footer: { Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.") } + + Section { + Link("Accéder au guide de la compétition", destination: URLs.padelRules.url) + } } .navigationTitle(TabDestination.toolbox.title) } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index fde3f8c..0ee81ed 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -34,24 +34,18 @@ struct PlanningSettingsView: View { SubscriptionInfoView() Section { - DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) + DatePicker(selection: $tournament.startDate) { + Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized) + } LabeledContent { StepperView(count: $tournament.dayDuration, minimum: 1) } label: { Text("Durée") Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) } - } header: { - Text("Démarrage et durée du tournoi") - } - - Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) - if tournament.groupStages().isEmpty == false { - TournamentFieldsManagerView(localizedStringKey: "Nombre de poule en même temps", count: $groupStageChunkCount, max: tournament.groupStageCount) - } - if let event = tournament.eventObject() { NavigationLink { CourtAvailabilitySettingsView(event: event) @@ -61,13 +55,43 @@ struct PlanningSettingsView: View { } } } footer: { - FooterButtonView((showOptions ? "masquer" : "voir") + " les réglages avancées") { - showOptions.toggle() + if let club = tournament.club() { + if tournament.courtCount < club.courtCount { + let plural = tournament.courtCount.pluralSuffix + let verb = tournament.courtCount > 1 ? "seront" : "sera" + Text("En réduisant les terrains maximum, seul\(plural) le\(plural) \(tournament.courtCount) premier\(plural) terrain\(plural) \(verb) utilisé\(plural)") + Text(", par contre, si vous augmentez le nombre de terrains, vous pourrez plutôt préciser quel terrain n'est pas disponible.") + } else if tournament.courtCount > club.courtCount { + let isCreatedByUser = club.hasBeenCreated(by: dataStore.user.id) + Button { + do { + club.courtCount = tournament.courtCount + try dataStore.clubs.addOrUpdate(instance: club) + } catch { + Logger.error(error) + } + } label: { + if isCreatedByUser { + Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") + + Text("Mettre à jour le club ?").underline().foregroundStyle(.master) + } else { + Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) + } + } + .buttonStyle(.plain) + .disabled(isCreatedByUser == false) + } } } - if showOptions { - _optionsView() + NavigationLink { + List { + _optionsView() + } + .navigationTitle("Options avancées") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } label: { + Text("Voir les options avancées") } Section { @@ -132,6 +156,13 @@ struct PlanningSettingsView: View { @ViewBuilder private func _optionsView() -> some View { + if tournament.groupStages().isEmpty == false { + Section { + TournamentFieldsManagerView(localizedStringKey: "Poule en parallèle", count: $groupStageChunkCount, max: tournament.groupStageCount) + } footer: { + Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") + } + } Section { Toggle(isOn: $matchScheduler.randomizeCourts) { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift index 58eda9f..29e7072 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift @@ -11,109 +11,63 @@ import LeStorage struct TournamentClubSettingsView: View { @Environment(Tournament.self) private var tournament: Tournament @EnvironmentObject var dataStore: DataStore - @State var selectedCourt: Court? - + @State private var selectedCourt: Court? + @State private var showClubDetail: Club? + var body: some View { @Bindable var tournament = tournament List { let event = tournament.eventObject() let selectedClub = event?.clubObject() Section { - if let selectedClub { - NavigationLink { - ClubDetailView(club: selectedClub, displayContext: selectedClub.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing) - } label: { - ClubRowView(club: selectedClub) - } - } else { - NavigationLink { - ClubsView() { club in - if let event { - event.club = club.id - do { - try dataStore.events.addOrUpdate(instance: event) - } catch { - Logger.error(error) - } + NavigationLink { + ClubsView() { club in + if let event { + event.club = club.id + do { + try dataStore.events.addOrUpdate(instance: event) + } catch { + Logger.error(error) } } - } label: { + } + } label: { + if let selectedClub { + ClubRowView(club: selectedClub) + } else { Text("Choisir un club") } } } header: { Text("Lieu du tournoi") } footer: { - if let event, selectedClub != nil { - HStack { - Spacer() - Button("modifier", role: .destructive) { - event.club = nil - try? dataStore.events.addOrUpdate(instance: event) - } + HStack { + Spacer() + FooterButtonView("détails du club") { + showClubDetail = selectedClub } } } - - Section { - TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) - } if let selectedClub { - Section { - ForEach((0.. some View { - if let court = tournamentClub.courts.first(where: { $0.index == index }) { - LabeledContent { - FooterButtonView("personnaliser") { - selectedCourt = court - } - } label: { - Text(court.courtTitle()) - HStack { - if court.indoor { - Text("Couvert") - } - if court.exitAllowed { - Text("Sortie autorisée") - } - } - } - } else { - LabeledContent { - FooterButtonView("personnaliser") { - let court = Court(index: index, club: tournamentClub.id) - try? dataStore.courts.addOrUpdate(instance: court) - selectedCourt = court - } - } label: { - Text(_courtName(atIndex: index)) - } - } - } - - private func _courtName(atIndex index: Int) -> String { - Court.courtIndexedTitle(atIndex: index) - } - } #Preview { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 56997ce..2a3528d 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -63,8 +63,12 @@ struct TournamentGeneralSettingsView: View { .toolbar { if textFieldIsFocus { ToolbarItem(placement: .keyboard) { - Button("Valider") { - textFieldIsFocus = false + HStack { + Spacer() + Button("Valider") { + textFieldIsFocus = false + } + .buttonStyle(.bordered) } } } diff --git a/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift new file mode 100644 index 0000000..7aa4216 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/PrintSettingsView.swift @@ -0,0 +1,244 @@ +// +// PrintSettingsView.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 23/10/2023. +// + +import SwiftUI +import WebKit + +struct PrintSettingsView: View { + let tournament: Tournament + @StateObject var generator: HtmlGenerator + @State private var presentShareView: Bool = false + @State private var prepareGroupStage: Bool = false + + init(tournament: Tournament) { + self.tournament = tournament + _generator = StateObject(wrappedValue: HtmlGenerator(tournament: tournament)) + } + + var body: some View { + List { + Section { +// Toggle(isOn: $generator.displayHeads, label: { +// Text("Afficher les têtes de séries") +// }) + + Toggle(isOn: $generator.displayRank, label: { + Text("Afficher le classement du joueur") + }) + + Toggle(isOn: $generator.includeBracket, label: { + Text("Tableau") + }) + +// Toggle(isOn: $generator.includeLoserBracket, label: { +// Text("Tableau des matchs de classements") +// }) + + if tournament.groupStages().isEmpty == false { + Toggle(isOn: $generator.includeGroupStage, label: { + Text("Poules") + }) + } + } + + if generator.includeBracket { + Section { + Picker(selection: $generator.zoomLevel) { + Text("1 page").tag(nil as Optional) + Text("50%").tag(2.0 as Optional) + Text("100%").tag(1.0 as Optional) + } label: { + Text("Zoom") + } + + HStack { + Text("Nombre de page A4 à imprimer") + Spacer() + Text(generator.estimatedPageCount.formatted()) + } + } header: { + Text("Tableau principal") + } + } + + Section { + NavigationLink { + WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in + }) + } label: { + Text("Aperçu du tableau") + } + } + + ForEach(tournament.groupStages()) { groupStage in + Section { + NavigationLink { + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateGroupStage(webView: webView) + } else { + print("preparePDF", "is loading") + } + }) + } label: { + Text("Aperçu de la \(groupStage.groupStageTitle())") + } + } + } + } + .background { + WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateWebView(webView: webView) + } else { + print("preparePDF", "is loading") + } + }).opacity(0) + + if prepareGroupStage { + ForEach(tournament.groupStages()) { groupStage in + WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in + if let error { + print("preparePDF", error) + } else if loaded == false { + generator.generateGroupStage(webView: webView) + } else { + print("preparePDF", "is loading") + } + }).opacity(0) + } + } + } + .navigationTitle("Imprimer") + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.visible, for: .bottomBar) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .bottomBar) { + Button { + generator.preparePDF { result in + switch result { + case .success(true): + if generator.includeGroupStage && generator.groupStageIsReady == false { + self.prepareGroupStage = true + } else { + self.presentShareView = true + } + case .success(false): + print("didn't save pdf") + break + case .failure(let error): + print(error) + break + } + } + + self.prepareGroupStage = false + self.generator.buildPDF() + + } label: { + Text("Obtenir le PDF") + } + .disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false) + .buttonStyle(.borderedProminent) + } + ToolbarItem(placement: .topBarTrailing) { + Menu { + Section { + ShareLink(item: generator.generateHtml()) { + Text("Tableau") + } + + if let groupStage = tournament.groupStages().first { + ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) { + Text("Poule") + } + } + } header: { + Text("Partager le code source HTML") + } + } label: { + Label("Options", systemImage: "ellipsis.circle") + } + } + } + .sheet(isPresented: $presentShareView) { + if let pdfURL = generator.pdfURL { + ShareSheet(urls: [pdfURL]) + } + } + } +} + +// MARK: Share Sheet +struct ShareSheet: UIViewControllerRepresentable{ + + var urls: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: urls, applicationActivities: nil) + + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + + } +} + +struct WebView: UIViewRepresentable { + var htmlRawData: String? = nil + var url: URL? = nil + var loadStatusChanged: ((Bool, Error?, WKWebView) -> Void)? = nil + + func makeCoordinator() -> WebView.Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> WKWebView { + let view = WKWebView() + view.navigationDelegate = context.coordinator + + if let htmlRawData { + view.loadHTMLString(htmlRawData, baseURL: nil) + } + if let url { + view.loadFileURL(url, allowingReadAccessTo: url) + } + return view + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + // you can access environment via context.environment here + // Note that this method will be called A LOT + } + + class Coordinator: NSObject, WKNavigationDelegate { + let parent: WebView + + init(_ parent: WebView) { + self.parent = parent + } + + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + parent.loadStatusChanged?(true, nil, webView) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + parent.loadStatusChanged?(false, nil, webView) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + parent.loadStatusChanged?(false, error, webView) + } + } +} + diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 73c09f6..3986974 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import LeStorage struct TournamentCellView: View { @EnvironmentObject var dataStore: DataStore @@ -123,7 +124,11 @@ struct TournamentCellView: View { newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) newTournament.setupFederalSettings() - try? dataStore.tournaments.addOrUpdate(instance: newTournament) + do { + try dataStore.tournaments.addOrUpdate(instance: newTournament) + } catch { + Logger.error(error) + } } } } diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift new file mode 100644 index 0000000..f040181 --- /dev/null +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -0,0 +1,74 @@ +// +// TournamentBuildView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/05/2024. +// + +import SwiftUI + +struct TournamentBuildView: View { + var tournament: Tournament + + @ViewBuilder + var body: some View { + Section { + if tournament.state() != .finished { + NavigationLink(value: Screen.schedule) { + let tournamentStatus = tournament.scheduleStatus() + LabeledContent { + Text(tournamentStatus.completion) + } label: { + Text("Horaires") + Text(tournamentStatus.label) + } + } + + NavigationLink(value: Screen.call) { + let tournamentStatus = tournament.callStatus() + LabeledContent { + Text(tournamentStatus.completion) + } label: { + Text("Convocations") + Text(tournamentStatus.label) + } + } + } + NavigationLink(value: Screen.cashier) { + let tournamentStatus = tournament.cashierStatus() + LabeledContent { + Text(tournamentStatus.completion) + } label: { + Text("Encaissement") + Text(tournamentStatus.label) + } + } + } + + Section { + if tournament.groupStages().isEmpty == false { + NavigationLink(value: Screen.groupStage) { + LabeledContent { + Text(tournament.groupStageStatus()) + } label: { + Text("Poules") + } + } + } + + if tournament.rounds().isEmpty == false { + NavigationLink(value: Screen.round) { + LabeledContent { + Text(tournament.bracketStatus()) + } label: { + Text("Tableau") + } + } + } + } + } +} + +#Preview { + TournamentBuildView(tournament: Tournament.mock()) +} diff --git a/PadelClub/Views/Tournament/TournamentInitView.swift b/PadelClub/Views/Tournament/TournamentInitView.swift index 78584a5..c6342c7 100644 --- a/PadelClub/Views/Tournament/TournamentInitView.swift +++ b/PadelClub/Views/Tournament/TournamentInitView.swift @@ -12,10 +12,17 @@ struct TournamentInitView: View { @ViewBuilder var body: some View { - - if let event = tournament.eventObject() { - let tournaments = event.tournaments - Section { + Section { + NavigationLink(value: Screen.broadcast) { + LabeledContent { + // Text(tournament.isPrivate ? "privée" : "publique") + } label: { + Text("Publication") + } + } + + if let event = tournament.eventObject() { + let tournaments = event.tournaments NavigationLink(value: Screen.event) { LabeledContent { Text(tournaments.count.formatted() + " épreuve" + tournaments.count.pluralSuffix) @@ -24,9 +31,6 @@ struct TournamentInitView: View { } } } - } - - Section { NavigationLink(value: Screen.settings) { LabeledContent { Text(tournament.settingsDescriptionLocalizedLabel()) @@ -38,29 +42,6 @@ struct TournamentInitView: View { } footer: { Text("La date, la catégorie, le niveau, le nombre de terrain, les formats, etc.") } - - Section { - NavigationLink(value: Screen.broadcast) { - LabeledContent { -// Text(tournament.isPrivate ? "privée" : "publique") - } label: { - Text("Publication") - } - } - } - - Section { - NavigationLink(value: Screen.structure) { - LabeledContent { - Text(tournament.structureDescriptionLocalizedLabel()) - .tint(.master) - } label: { - LabelStructure() - } - } - } footer: { - Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.") - } } } diff --git a/PadelClub/Views/Tournament/TournamentInscriptionView.swift b/PadelClub/Views/Tournament/TournamentInscriptionView.swift new file mode 100644 index 0000000..9d55c77 --- /dev/null +++ b/PadelClub/Views/Tournament/TournamentInscriptionView.swift @@ -0,0 +1,70 @@ +// +// TournamentInscriptionView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/05/2024. +// + +import SwiftUI +import LeStorage + +struct TournamentInscriptionView: View { + @EnvironmentObject var dataStore: DataStore + var tournament: Tournament + + @ViewBuilder + var body: some View { + Section { + NavigationLink(value: Screen.inscription) { + LabeledContent { + Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) + } label: { + Text("Gestion des inscriptions") + if let closedRegistrationDate = tournament.closedRegistrationDate { + Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) + } + } + } + if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { + LabeledContent { + Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) + } label: { + Text("Date limite") + } + } + + if tournament.state() != .running { + NavigationLink(value: Screen.structure) { + LabeledContent { + Text(tournament.structureDescriptionLocalizedLabel()) + .tint(.master) + } label: { + LabelStructure() + } + } + } + } footer: { + if tournament.inscriptionClosed() == false && tournament.state() == .build && tournament.unsortedTeams().isEmpty == false && tournament.hasStarted() == false { + Button { + tournament.lockRegistration() + _save() + } label: { + Text("clôturer les inscriptions") + .underline() + } + .buttonStyle(.borderless) + } else if tournament.state() != .running { + Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.") + } + } + } + + private func _save() { + do { + try dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + } + +} diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 3b13623..f0e4ec1 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -17,61 +17,7 @@ struct TournamentRunningView: View { } @ViewBuilder - var body: some View { - Section { - NavigationLink(value: Screen.schedule) { - let tournamentStatus = tournament.scheduleStatus() - LabeledContent { - Text(tournamentStatus.completion) - } label: { - Text("Horaires") - Text(tournamentStatus.label) - } - } - - NavigationLink(value: Screen.call) { - let tournamentStatus = tournament.callStatus() - LabeledContent { - Text(tournamentStatus.completion) - } label: { - Text("Convocations") - Text(tournamentStatus.label) - } - } - - NavigationLink(value: Screen.cashier) { - let tournamentStatus = tournament.cashierStatus() - LabeledContent { - Text(tournamentStatus.completion) - } label: { - Text("Encaissement") - Text(tournamentStatus.label) - } - } - } - - Section { - if tournament.groupStages().isEmpty == false { - NavigationLink(value: Screen.groupStage) { - LabeledContent { - Text(tournament.groupStageStatus()) - } label: { - Text("Poules") - } - } - } - - if tournament.rounds().isEmpty == false { - NavigationLink(value: Screen.round) { - LabeledContent { - Text(tournament.bracketStatus()) - } label: { - Text("Tableau") - } - } - } - } - + var body: some View { MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches)) MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false) MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index 8987448..5619109 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -16,6 +16,7 @@ struct TournamentView: View { var presentationContext: PresentationContext = .agenda let tournamentSelectionTip = TournamentSelectionTip() + let tournamentRunningTip = TournamentRunningTip() var selectedTournamentId: Binding { Binding( get: { tournament.id }, @@ -38,41 +39,13 @@ struct TournamentView: View { var body: some View { VStack(spacing: 0.0) { List { - SubscriptionInfoView() + TipView(tournamentRunningTip) + .tipStyle(tint: nil) - if tournament.state() != .canceled { - Section { - NavigationLink(value: Screen.inscription) { - LabeledContent { - Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted()) - } label: { - Text("Gestion des inscriptions") - if let closedRegistrationDate = tournament.closedRegistrationDate { - Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened)) - } - } - } - if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false { - LabeledContent { - Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened)) - } label: { - Text("Date limite") - } - } - } footer: { - if tournament.inscriptionClosed() == false && tournament.state() == .build && tournament.unsortedTeams().isEmpty == false && tournament.hasStarted() == false { - Button { - tournament.lockRegistration() - _save() - } label: { - Text("clôturer les inscriptions") - .underline() - } - .buttonStyle(.borderless) - } - } + if tournament.state() != .finished { + SubscriptionInfoView() } - + switch tournament.state() { case .canceled: Section { @@ -84,15 +57,28 @@ struct TournamentView: View { Text("todo expliquer cet état") } case .initial: + TournamentInscriptionView(tournament: tournament) TournamentInitView(tournament: tournament) case .build: - TournamentRunningView(tournament: tournament) - + TournamentInscriptionView(tournament: tournament) + TournamentInitView(tournament: tournament) + Section { + NavigationLink(value: Screen.print) { + Label("Imprimer", systemImage: "printer") + } + } + TournamentBuildView(tournament: tournament) + case .running, .finished: + TournamentInscriptionView(tournament: tournament) + TournamentBuildView(tournament: tournament) if tournament.hasEnded() { - NavigationLink(value: Screen.rankings) { - Text("Classement") + Section { + NavigationLink(value: Screen.rankings) { + Text("Classement") + } } } + TournamentRunningView(tournament: tournament) } } } @@ -124,6 +110,8 @@ struct TournamentView: View { if let event = tournament.eventObject() { EventView(event: event) } + case .print: + PrintSettingsView(tournament: tournament) } } .environment(tournament) @@ -154,42 +142,48 @@ struct TournamentView: View { TournamentSelectionTip.tournamentCount = tournament.eventObject()?.tournaments.count } } - - ToolbarItem(placement: .topBarTrailing) { - Menu { - if presentationContext == .agenda { - Button { - navigation.openTournamentInOrganizer(tournament) - } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") - } - } - - Divider() - if tournament.state() == .build { - NavigationLink(value: Screen.event) { - Text("Gestion de l'événement") + + if presentationContext == .agenda || tournament.state() == .running { + ToolbarItem(placement: .topBarTrailing) { + Menu { + if presentationContext == .agenda { + Button { + navigation.openTournamentInOrganizer(tournament) + } label: { + Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + } } + + Divider() + if tournament.state() == .running { + NavigationLink(value: Screen.event) { + Text("Gestion de l'événement") + } + + NavigationLink(value: Screen.settings) { + LabelSettings() + } + NavigationLink(value: Screen.structure) { + LabelStructure() + } + + NavigationLink(value: Screen.broadcast) { + Text("Publication") + } + + NavigationLink(value: Screen.print) { + Label("Imprimer", systemImage: "printer") + } - NavigationLink(value: Screen.settings) { - LabelSettings() - } - NavigationLink(value: Screen.structure) { - LabelStructure() - } - NavigationLink(value: Screen.rankings) { - Text("Classement") - } - NavigationLink(value: Screen.broadcast) { - Text("Publication") } + } label: { + LabelOptions() } - } label: { - LabelOptions() } } } .onAppear { + TournamentRunningTip.isRunning = tournament.state() == .running Logger.log("Payment = \(String(describing: self.tournament.payment)), canceled = \(self.tournament.isCanceled)") } }