Laurent 1 year ago
commit 30affc2de9
  1. 84
      PadelClub.xcodeproj/project.pbxproj
  2. 10
      PadelClub/Data/Club.swift
  3. 12
      PadelClub/Data/GroupStage.swift
  4. 29
      PadelClub/Data/Match.swift
  5. 2
      PadelClub/Data/MockData.swift
  6. 15
      PadelClub/Data/Tournament.swift
  7. 4
      PadelClub/HTML Templates/bracket-template.html
  8. 95
      PadelClub/HTML Templates/groupstage-template.html
  9. 4
      PadelClub/HTML Templates/groupstagecol-template.html
  10. 4
      PadelClub/HTML Templates/groupstageentrant-template.html
  11. 4
      PadelClub/HTML Templates/groupstagerow-template.html
  12. 5
      PadelClub/HTML Templates/groupstagescore-template.html
  13. 2
      PadelClub/HTML Templates/hiddenplayer-template.html
  14. 8
      PadelClub/HTML Templates/match-template.html
  15. 3
      PadelClub/HTML Templates/player-template.html
  16. 103
      PadelClub/HTML Templates/tournament-template.html
  17. 1
      PadelClub/Utils/DisplayContext.swift
  18. 198
      PadelClub/Utils/HtmlGenerator.swift
  19. 220
      PadelClub/Utils/HtmlService.swift
  20. 2
      PadelClub/Utils/LocationManager.swift
  21. 20
      PadelClub/Utils/SourceFileManager.swift
  22. 37
      PadelClub/Utils/Tips.swift
  23. 1
      PadelClub/Utils/URLs.swift
  24. 1
      PadelClub/ViewModel/Screen.swift
  25. 5
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  26. 7
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  27. 146
      PadelClub/Views/Club/ClubDetailView.swift
  28. 3
      PadelClub/Views/Club/ClubImportView.swift
  29. 7
      PadelClub/Views/Club/ClubRowView.swift
  30. 28
      PadelClub/Views/Club/ClubSearchView.swift
  31. 47
      PadelClub/Views/Club/ClubsView.swift
  32. 13
      PadelClub/Views/Club/CreateClubView.swift
  33. 83
      PadelClub/Views/Club/Shared/ClubCourtSetupView.swift
  34. 26
      PadelClub/Views/Components/BarButtonView.swift
  35. 37
      PadelClub/Views/Event/EventCreationView.swift
  36. 5
      PadelClub/Views/Navigation/MainView.swift
  37. 16
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  38. 4
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  39. 57
      PadelClub/Views/Planning/PlanningSettingsView.swift
  40. 104
      PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift
  41. 8
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  42. 244
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  43. 7
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  44. 74
      PadelClub/Views/Tournament/TournamentBuildView.swift
  45. 39
      PadelClub/Views/Tournament/TournamentInitView.swift
  46. 70
      PadelClub/Views/Tournament/TournamentInscriptionView.swift
  47. 54
      PadelClub/Views/Tournament/TournamentRunningView.swift
  48. 118
      PadelClub/Views/Tournament/TournamentView.swift

@ -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 = "<group>"; };
FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = "<group>"; };
FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonView.swift; sourceTree = "<group>"; };
FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBuildView.swift; sourceTree = "<group>"; };
FF1F4B702BF9EFE9000B4573 /* TournamentInscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentInscriptionView.swift; sourceTree = "<group>"; };
FF1F4B722BFA00FB000B4573 /* HtmlGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlGenerator.swift; sourceTree = "<group>"; };
FF1F4B732BFA00FC000B4573 /* HtmlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlService.swift; sourceTree = "<group>"; };
FF1F4B762BFA0105000B4573 /* bracket-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "bracket-template.html"; sourceTree = "<group>"; };
FF1F4B772BFA0105000B4573 /* groupstage-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstage-template.html"; sourceTree = "<group>"; };
FF1F4B782BFA0105000B4573 /* groupstagecol-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagecol-template.html"; sourceTree = "<group>"; };
FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstageentrant-template.html"; sourceTree = "<group>"; };
FF1F4B7A2BFA0105000B4573 /* groupstagerow-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagerow-template.html"; sourceTree = "<group>"; };
FF1F4B7B2BFA0105000B4573 /* groupstagescore-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "groupstagescore-template.html"; sourceTree = "<group>"; };
FF1F4B7C2BFA0105000B4573 /* hiddenplayer-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "hiddenplayer-template.html"; sourceTree = "<group>"; };
FF1F4B7D2BFA0105000B4573 /* match-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "match-template.html"; sourceTree = "<group>"; };
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
@ -439,6 +470,7 @@
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectablePlayerListView.swift; sourceTree = "<group>"; };
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedPlayerView.swift; sourceTree = "<group>"; };
FF4C7F012BBBD7150031B6A3 /* TabItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemModifier.swift; sourceTree = "<group>"; };
FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubCourtSetupView.swift; sourceTree = "<group>"; };
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolboxView.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
FF53FBB62BFB301A0051D4C3 /* Shared */ = {
isa = PBXGroup;
children = (
FF53FBB72BFB302B0051D4C3 /* ClubCourtSetupView.swift */,
);
path = Shared;
sourceTree = "<group>";
};
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;

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

@ -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..<size).combinations(ofCount: 2))
var matchIndexes = [Int]()
for (index, combo) in combos.enumerated() {
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
matchIndexes.append(index)
}
}
return _matches().first(where: { matchIndexes.contains($0.index) })
}
func availableToStart() -> [Match] {
let runningMatches = runningMatches()
return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })

@ -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
@ -542,6 +566,11 @@ class Match: ModelObject, Storable {
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 {
switch team {

@ -25,7 +25,7 @@ extension Club {
}
static func newEmptyInstance() -> Club {
Club(name: "", acronym: "")
Club(creator: DataStore.shared.user.id, name: "", acronym: "")
}
}

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

@ -0,0 +1,4 @@
<ul class="round">
<li class="spacer">&nbsp;{{roundLabel}}</li>
{{match-template}}
</ul>

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
main{
display:flex;
display: inline-block;
padding: 1%;
}
/*
* General Styles
*/
body{
font-family:sans-serif;
}
td,
th {
border: 1px solid rgb(190, 190, 190);
padding: 10px;
text-align: left;
height: 4rem;
overflow: hidden;
}
td {
text-align: center;
}
td[scope='hide'] {
background-color: #a9a9a9;
text-align: left;
}
tr {
background-color: #fff;
text-align: left;
}
th[scope='col'] {
background-color: #d7d9f2;
text-align: left;
}
th[scope='row'] {
background-color: #d7d9f2;
text-align: left;
}
caption {
padding: 10px;
caption-side: bottom;
}
table {
border-collapse: collapse;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
table-layout: fixed;
width: 100%;
}
.score {
text-align: center;
}
.player {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<main id="tournament">
<table>
<caption>
<h2>{{bracketTitle}}</h2>
<h3>{{bracketStartDate}}</h3>
</caption>
<tr>
<th scope="col" style="visibility:hidden"></th>
{{teamsCol}}
</tr>
{{teamsRow}}
</table>
</main>
</body>
</html>

@ -0,0 +1,4 @@
<th scope="{{tablePosition}}">
{{team}}
</th>

@ -0,0 +1,4 @@
<div class="player">{{playerOne}}</div>
<div class="player">{{weightOne}}</div>
<div class="player">{{playerTwo}}</div>
<div class="player">{{weightTwo}}</div>

@ -0,0 +1,4 @@
<tr>
{{team}}
{{scores}}
</tr>

@ -0,0 +1,5 @@
<td scope="{{hide}}">
<div class="score">{{winner}}</div>
<div class="score">{{score}}</div>
</td>

@ -0,0 +1,2 @@
<div class="hiddenPlayer"> </div>

@ -0,0 +1,8 @@
<li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}">
{{entrantOne}}
</li>
<li class="game game-spacer" style="visibility:{{hidden}}"><div class="multiline">{{matchDescription}}</div></li>
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}">
{{entrantTwo}}
</li>
<li class="spacer">&nbsp;</li>

@ -0,0 +1,3 @@
<div class="player">{{playerOne}}<span>{{weightOne}}</span></div>
<div class="player">{{playerTwo}}<span>{{weightTwo}}</span></div>

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
main{
display:flex;
flex-direction:row;
padding: 1%;
}
.round{
display:flex;
flex-direction:column;
justify-content:center;
min-width: 400px;
list-style:none;
padding:0;
border-right: 1px dashed #ccc;
}
.round[scope='last'] {
border-right: 0px;
}
.round .spacer{ flex-grow:1;
font-size:24px;
text-align: center;
color: #bbb;
font-style:italic;
}
.round .spacer:first-child,
.round .spacer:last-child{ flex-grow:.5; }
.round .game-spacer{
flex-grow:1;
}
/*
* General Styles
*/
body{
font-family:sans-serif;
font-size:32px;
padding:10px;
line-height:32px;
}
li.game{
padding-left:20px;
}
li.game.winner{
font-weight:bold;
}
li.game span{
float:right;
margin-right:5px;
}
li.game-top{
border-bottom:2px solid #4f7a38;
}
li.game-spacer{
border-right:2px solid #4f7a38;
min-height:156px;
text-align: right;
display : flex;
justify-content: center;
align-items : center;
}
.multiline {
white-space: pre-wrap;
}
li.game-bottom{
border-top:2px solid #4f7a38;
}
.player {
font-size:28px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hiddenPlayer {
font-size:28px;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<h1>{{tournamentTitle}}</h1>
<main id="tournament">
{{brackets}}
</main>
</body>
</html>

@ -11,6 +11,7 @@ enum DisplayContext {
case addition
case edition
case lockedForEditing
case selection
}
enum DisplayStyle {

@ -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<Bool, Error>) -> ())?
@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<Bool, Error>) -> ())) {
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..<numberOfPageInWidth {
for h in 0..<numberOfPageInHeight {
let rect = CGRect(x: CGFloat(w) * pageSize.width, y: CGFloat(h) * pageSize.height, width: pageSize.width, height: pageSize.height)
rects.append(rect)
}
}
} else {
rects = [CGRect(origin: .zero, size: CGSize(width: Int(width), height: Int(height)))]
}
if includeBracket {
createPage()
} else {
DispatchQueue.main.async {
self.completionHandler?(.success(true))
}
}
}
func createPage() {
let config = WKPDFConfiguration()
config.rect = rects[pdfDocument.pageCount]
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)
if self.pdfDocument.pageCount < self.rects.count {
self.createPage()
} else {
self.completionHandler?(.success(self.savePDF()))
}
case .failure(let error):
self.completionHandler?(.failure(error))
}
}
}
func generateHtml() -> 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"))
}
}

@ -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..<teamsPerBracket).forEach { index in
let shouldHide = entrant.groupStagePosition! == index
var match: Match? = nil
if shouldHide == false {
match = entrant.groupStageObject()?.matchPlayed(by: entrant.groupStagePosition!, againstPosition: index)
}
scores.append(HtmlService.groupstageScore(score: match, shouldHide: shouldHide).html(headName: headName, withRank: withRank, withScore: withScore))
}
template = template.replacingOccurrences(of: "{{scores}}", with: scores)
return template
case .groupstageColumn(let entrant, let position):
var template = html
template = template.replacingOccurrences(of: "{{tablePosition}}", with: position)
template = template.replacingOccurrences(of: "{{team}}", with: HtmlService.groupstageEntrant(entrant: entrant).html(headName: headName, withRank: withRank, withScore: withScore))
return template
case .groupstageScore(let match, let shouldHide):
var template = html
if match == nil || withScore == false {
template = template.replacingOccurrences(of: "{{winner}}", with: "")
template = template.replacingOccurrences(of: "{{score}}", with: "")
} else {
template = template.replacingOccurrences(of: "{{winner}}", with: match!.winner()!.teamLabel())
template = template.replacingOccurrences(of: "{{score}}", with: match!.scoreLabel())
}
template = template.replacingOccurrences(of: "{{hide}}", with: shouldHide ? "hide" : "")
return template
case .player(let entrant):
var template = html
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel(.short))
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(.short))
if withRank {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank())")
} else {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "")
}
}
return template
case .hiddenPlayer:
return html + html
case .match(let match):
var template = html
if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
}
if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withScore: withScore))
} else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withScore: withScore))
}
if match.disabled {
template = template.replacingOccurrences(of: "{{hidden}}", with: "hidden")
} else {
template = template.replacingOccurrences(of: "{{hidden}}", with: "")
}
if match.hasEnded() {
if match.teamWon(atPosition: .one) == true {
template = template.replacingOccurrences(of: "{{entrantOneWon}}", with: "winner")
} else if match.teamWon(atPosition: .two) == true {
template = template.replacingOccurrences(of: "{{entrantTwoWon}}", with: "winner")
}
template = template.replacingOccurrences(of: "{{matchDescription}}", with: [match.localizedStartDate(), match.scoreLabel()].joined(separator: "\n"))
}
template = template.replacingOccurrences(of: "{{matchDescription}}", with: "")
return template
case .bracket(let tournament, let roundIndex):
var template = ""
var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
for (_, match) in round.playedMatches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
}
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
}
return bracket
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle())
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore))
}
var winnerName = ""
// if let tournamentWinner = tournament.winnerEntrant {
// winnerName = HtmlService.player(entrant: tournamentWinner).html(headName: headName, withRank: withRank, withScore: withScore)
// }
let winner = """
<ul class="round" scope="last">
<li class="spacer">&nbsp;</li>
<li class="game game-top winner">\(winnerName)</li>
<li class="spacer">&nbsp;</li>
</ul>
<ul class="main" style="visibility:hidden">
</ul>
"""
brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
return template
}
}
}

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

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

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

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

@ -19,4 +19,5 @@ enum Screen: String, Codable {
case rankings
case broadcast
case event
case print
}

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

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

@ -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 = ""
}
}
} 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 {
if club.code == nil {
Section {
LabeledContent {
TextField("Ville", text: $city)
.autocorrectionDisabled()
@ -146,17 +142,15 @@ 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 {
@ -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 {

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

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

@ -26,10 +26,20 @@ 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<Bool> { 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 = ""
@ -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] {

@ -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<Bool> { 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()

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

@ -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..<club.courtCount), id: \.self) { courtIndex in
_courtView(atIndex: courtIndex, tournamentClub: club)
}
}
}
@ViewBuilder
private func _courtView(atIndex index: Int, tournamentClub: Club) -> 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)
}
}

@ -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
*/
}
}
}

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

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

@ -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<Match>] = [.keyPath(\Match.courtIndex!), .keyPath(\Match.startDate!)]
let defaultSorting : [MySortDescriptor<Match>] = [.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)
}
}
}

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

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

@ -11,7 +11,8 @@ 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
@ -19,101 +20,54 @@ struct TournamentClubSettingsView: View {
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..<tournament.courtCount), id: \.self) { courtIndex in
_courtView(atIndex: courtIndex, tournamentClub: selectedClub)
ClubCourtSetupView(club: selectedClub, displayContext: selectedClub.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing, selectedCourt: $selectedCourt)
.onChange(of: selectedClub.courtCount) {
tournament.courtCount = max(tournament.courtCount, selectedClub.courtCount)
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
}
}
.onChange(of: tournament.courtCount) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
.navigationDestination(item: $showClubDetail) { club in
ClubDetailView(club: club, displayContext: club.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing)
}
.navigationDestination(item: $selectedCourt) { court in
CourtView(court: court)
}
}
@ViewBuilder
private func _courtView(atIndex index: Int, tournamentClub: Club) -> 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 {

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

@ -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<CGFloat>)
Text("50%").tag(2.0 as Optional<CGFloat>)
Text("100%").tag(1.0 as Optional<CGFloat>)
} 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)
}
}
}

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

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

@ -12,10 +12,17 @@ struct TournamentInitView: View {
@ViewBuilder
var body: some View {
Section {
NavigationLink(value: Screen.broadcast) {
LabeledContent {
// Text(tournament.isPrivate ? "privée" : "publique")
} label: {
Text("Publication")
}
}
if let event = tournament.eventObject() {
let tournaments = event.tournaments
Section {
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.")
}
}
}

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

@ -18,60 +18,6 @@ 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")
}
}
}
}
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)

@ -16,6 +16,7 @@ struct TournamentView: View {
var presentationContext: PresentationContext = .agenda
let tournamentSelectionTip = TournamentSelectionTip()
let tournamentRunningTip = TournamentRunningTip()
var selectedTournamentId: Binding<String> { Binding(
get: { tournament.id },
@ -38,39 +39,11 @@ 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() {
@ -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)
@ -155,41 +143,47 @@ struct TournamentView: View {
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
if presentationContext == .agenda {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
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() == .build {
NavigationLink(value: Screen.event) {
Text("Gestion de l'événement")
}
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)")
}
}

Loading…
Cancel
Save